Skip to main content

What Are Tools?

Tools are callable capabilities that extend what AI agents can do beyond generating text. They allow agents to:
  • Execute code: Run custom business logic
  • Access data: Query databases and APIs
  • Perform actions: Create records, send emails, trigger workflows
  • Call other AI: Invoke sub-prompts or hand off to specialized agents
In AgentBuilder, “tools” includes function tools, prompt tools, and agent tools. Callable dual_ai agent tools can execute as autonomous subagents.

Quick Start

Create your first tool in agents/tools/search_database.ts:
import { defineTool } from '@standardagents/spec';
import { z } from 'zod';

export default defineTool({
  description: 'Search the customer database for matching records',
  args: z.object({
    query: z.string().describe('Search query'),
    limit: z.number().optional().default(10).describe('Maximum results'),
  }),
  execute: async (state, args) => {
    const results = await searchDatabase(args.query, args.limit);

    return {
      status: 'success',
      result: JSON.stringify(results),
    };
  },
});
That’s it! Your tool is now available to any prompt that includes it in its tools array. The framework auto-discovers it from the agents/tools/ directory.

Tool Types

AgentBuilder supports three types of tools that all appear the same to LLMs:

Function Tools

Custom TypeScript functionsDefined in: agents/tools/Best for:
  • Database queries
  • API calls
  • File operations
  • Business logic

Prompt Tools

Other prompts called as sub-promptsDefined in: agents/prompts/ with exposeAsTool: trueBest for:
  • Specialized analysis
  • Content generation
  • Data summarization
  • Validation tasks

Agent Tools

Full agents for handoffsDefined in: agents/agents/ with exposeAsTool: trueBest for:
  • Specialist routing
  • Complex workflows
  • Domain expertise
  • Multi-turn tasks

Runtime Subagent Lifecycle Tools

When a prompt defines resumable subagent relationships, AgentBuilder injects runtime lifecycle tools:
  • subagent_create
  • subagent_message
These tools are runtime-provided and scoped to the prompt/thread state. They are not user-defined files in agents/tools/. subagent_create requires a non-empty name for the spawned child instance. Non-resumable subagents still behave like regular tool calls.

How They Work Together

// Function tool: agents/tools/lookup_customer.ts
export default defineTool({
  description: 'Look up customer by ID',
  args: z.object({ customerId: z.string() }),
  execute: async (state, args) => {
    const customer = await db.getCustomer(args.customerId);
    return { status: 'success', result: JSON.stringify(customer) };
  },
});

// Prompt tool: agents/prompts/summarizer.ts
export default definePrompt({
  name: 'summarizer',
  exposeAsTool: true,
  toolDescription: 'Summarize text into key points',
  prompt: 'You are an expert summarizer...',
  model: 'gpt-5.4',
  requiredSchema: z.object({
    text: z.string().describe('Text to summarize'),
  }),
});

// Agent tool: agents/agents/billing_specialist.ts
export default defineAgent({
  name: 'billing_specialist',
  exposeAsTool: true,
  toolDescription: 'Hand off billing issues to specialist',
  sideA: { prompt: 'billing_expert' },
});

// Use all three in a prompt
export default definePrompt({
  name: 'support_router',
  tools: [
    'lookup_customer',      // Function tool
    'summarizer',           // Prompt tool
    'billing_specialist',   // Agent tool
  ],
  // ...
});

ThreadState Context

Every tool receives a ThreadState object providing execution context:
defineTool({
  description: 'My tool',
  args: z.object({ input: z.string() }),
  execute: async (state, args) => {
    // Access execution context
    console.log('Thread ID:', state.threadId);
    console.log('Turn:', state.turnCount);
    console.log('Agent:', state.agentConfig.name);

    // Access storage
    const result = await state.storage.sql.exec('SELECT * FROM data WHERE id = ?', args.input);

    // Access environment
    const apiKey = await state.env('MY_API_KEY');

    return { status: 'success', result: 'Done' };
  },
});
ThreadState gives tools access to thread storage, message operations, file system, and execution state. See the ThreadState documentation for details.

Sandboxed Code Execution

Use state.runCode() when a tool needs to evaluate model- or user-authored JavaScript/TypeScript. The code runs in a Dynamic Worker sandbox with no implicit host capabilities and is loaded by stable content ID when possible. Bridge only the functions and data you want the code to access. Use modules when the entry source imports local relative modules, and execute when you want to run a named export or pass arguments.
const run = state.runCode(code, {
  execute: { fn: 'main', args: ['notes.txt'] },
  imports: {
    fs: {
      readFile: async (path: string) => {
        const data = await state.readFile(path);
        return data ? new TextDecoder().decode(data) : null;
      },
    },
  },
  report: (value) => console.log('sandbox report', value),
});

const result = await run;
The handle is awaitable, exposes live reports, and can be stopped with run.terminate(reason) from a caller-owned timeout.

Input Validation with Zod

Tools use Zod schemas to validate arguments:
const argsSchema = z.object({
  // Required string
  query: z.string().describe('Search query'),

  // Optional with default
  limit: z.number().optional().default(10).describe('Max results'),

  // Enum
  format: z.enum(['json', 'text', 'markdown']).describe('Output format'),

  // Nested object
  filters: z.object({
    category: z.string().optional(),
    minPrice: z.number().optional(),
  }).optional().describe('Filter options'),

  // Array
  tags: z.array(z.string()).optional().describe('Filter by tags'),
});
Always use .describe() on parameters! These descriptions help LLMs understand how to use your tools correctly.

Tool Results

Tools return a ToolResult object:
interface ToolResult {
  status: 'success' | 'error';
  result?: string;
  error?: string;
  stack?: string;
  attachments?: Array<ToolAttachment | AttachmentRef>;
}

interface ToolAttachment {
  name: string;           // Filename (e.g., "generated.png")
  mimeType: string;       // MIME type
  data: string;           // Base64-encoded file data
  width?: number;         // Image width in pixels
  height?: number;        // Image height in pixels
}
When tool-driven subagent communication crosses threads, attachment paths are copied and rewritten to destination-local paths before delivery.

Common Patterns

Text Result
return {
  status: 'success',
  result: 'Operation completed successfully',
};
JSON Data
return {
  status: 'success',
  result: JSON.stringify({ id: '123', name: 'John' }),
};
Error Handling
return {
  status: 'error',
  error: 'Failed to find user: User not found',
};

Thread Environment Variables

Tools can declare required thread environment variables using the variables option:
export default defineTool({
  description: 'Search through uploaded files using vector embeddings',
  args: z.object({
    query: z.string().describe('Search query'),
  }),
  execute: async (state, args) => {
    const vectorStoreId = await state.env('VECTOR_STORE_ID');
    const results = await searchVectorStore(vectorStoreId, args.query);
    return { status: 'success', result: JSON.stringify(results) };
  },
  variables: [
    {
      name: 'VECTOR_STORE_ID',
      type: 'secret',
      required: true,
      description: 'OpenAI Vector Store ID',
    },
  ],
});
Variables are validated at runtime. If required variables are missing, the tool execution fails with a clear error message. Thread env values marked secret should be redacted from tool output and errors; values marked text may be shown. Tools can write thread env values with state.setEnv(name, value, { type: 'text' | 'secret' }). When a tool reads state.env(name), AgentBuilder resolves values in this order: thread, user account, AgentBuilder instance, agent definition, then prompt definition. User-account and instance-level variables can also persist their own text / secret display type, so safe defaults may preload in the UI without forcing secrets to be revealed.

Provider-Executed Tools

Some tools are executed by the LLM provider rather than locally. Provider packages usually expose these names through getTools(), and models opt in with providerTools:
export default defineModel({
  name: 'gpt-5.4-with-tools',
  provider: openai,
  model: 'gpt-5.4',
  providerTools: ['web_search', 'code_interpreter'],
});
Prompts select enabled provider tools by name in tools, just like local tools. AgentBuilder marks those definitions with executionMode: 'provider' and passes them to the provider. The local tool executor does not run provider tools; the provider package owns native request translation, execution, and reporting completed calls through the generic provider-tool log path. Legacy provider events such as web search are adapted into the same log format. Generated prompt files may store selected provider tools as provider:<toolName> so they remain distinct from local tools with the same name.

Common Use Cases

Database Query Tool

import { defineTool } from '@standardagents/spec';
import { z } from 'zod';

export default defineTool({
  description: 'Look up customer information by ID or email',
  args: z.object({
    customerId: z.string().optional().describe('Customer ID'),
    email: z.string().email().optional().describe('Customer email'),
  }),
  execute: async (state, args) => {
    const databaseUrl = await state.env('DATABASE_URL');

    let customer;
    if (args.customerId) {
      customer = await lookupCustomer(databaseUrl, { customerId: args.customerId });
    } else {
      customer = await lookupCustomer(databaseUrl, { email: args.email });
    }

    if (!customer) {
      return {
        status: 'error',
        error: 'Customer not found',
      };
    }

    return {
      status: 'success',
      result: JSON.stringify(customer),
    };
  },
});

API Integration Tool

import { defineTool } from '@standardagents/spec';
import { z } from 'zod';

export default defineTool({
  description: 'Fetch current weather for a city',
  args: z.object({
    city: z.string().describe('City name'),
    units: z.enum(['metric', 'imperial']).default('metric'),
  }),
  execute: async (state, args) => {
    const apiKey = await state.env('WEATHER_API_KEY');

    try {
      const response = await fetch(
        `https://api.weather.com/v1/current?city=${args.city}&units=${args.units}&key=${apiKey}`
      );

      if (!response.ok) {
        return {
          status: 'error',
          error: `Weather API error: ${response.status}`,
        };
      }

      const data = await response.json();

      return {
        status: 'success',
        result: `Weather in ${args.city}: ${data.temperature}°, ${data.conditions}`,
      };
    } catch (error) {
      return {
        status: 'error',
        error: `Failed to fetch weather: ${error.message}`,
      };
    }
  },
});

Tool Chaining

Use queueTool to chain multiple tools:
import { defineTool } from '@standardagents/spec';
import { queueTool } from '@standardagents/builder';
import { z } from 'zod';

export default defineTool({
  description: 'Process order and queue follow-up tasks',
  args: z.object({
    orderId: z.string().describe('Order ID'),
  }),
  execute: async (state, args) => {
    const order = await validateOrder(args.orderId);

    if (!order.valid) {
      return {
        status: 'error',
        error: 'Order validation failed',
      };
    }

    // Queue additional tools to execute after this one
    queueTool(state.rootState, 'charge_payment', {
      orderId: args.orderId,
      amount: order.total,
    });

    queueTool(state.rootState, 'send_confirmation_email', {
      userId: order.userId,
      orderId: args.orderId,
    });

    return {
      status: 'success',
      result: `Order ${args.orderId} queued for processing`,
    };
  },
});

Returning File Attachments

export default defineTool({
  description: 'Generate an image based on a text prompt',
  args: z.object({
    prompt: z.string().describe('Text description of the image'),
  }),
  execute: async (state, args) => {
    const image = await generateImage(args.prompt);

    return {
      status: 'success',
      result: `Generated image for: "${args.prompt}"`,
      attachments: [{
        name: `generated-${Date.now()}.png`,
        mimeType: 'image/png',
        data: image.base64,
        width: 1024,
        height: 1024,
      }],
    };
  },
});

ThreadState Utilities

AgentBuilder provides utility functions for tools:
Queue another tool to execute after the current one
queueTool(state.rootState, 'other_tool', { param: 'value' });
Add a message to conversation without triggering execution
await injectMessage(state, {
  role: 'system',
  content: 'Important context update',
});
Retrieve message history
const messages = await getMessages(state, 20);
Send custom events to frontend
emitThreadEvent(state, 'progress', {
  step: 3,
  total: 10,
});
See the ThreadState documentation for complete details.

Best Practices

Tool descriptions help LLMs understand when to use each tool:Good:
defineTool({
  description: 'Search the knowledge base for product documentation, FAQs, and troubleshooting guides. Returns relevant articles sorted by relevance.',
  // ...
});
Avoid:
defineTool({
  description: 'Search stuff',
  // ...
});
Use .describe() on every parameter:
args: z.object({
  query: z.string().describe('Search query - supports partial matches'),
  category: z.string().optional().describe('Product category filter'),
  minPrice: z.number().optional().describe('Minimum price in USD'),
})
Return errors as tool results, don’t throw:
try {
  const result = await riskyOperation(args);
  return {
    status: 'success',
    result: JSON.stringify(result),
  };
} catch (error) {
  return {
    status: 'error',
    error: `Operation failed: ${error.message}`,
  };
}
File names should use snake_case:Good: search_database.ts, create_ticket.tsAvoid: SearchDatabase.ts, createTicket.ts
Each tool should do one thing well:Good:
  • lookup_customer.ts - Just lookup
  • update_customer.ts - Just update
  • delete_customer.ts - Just delete
Avoid:
  • customer_manager.ts - Does everything
When queueing tools from sub-prompts, use state.rootState:
// Correct
queueTool(state.rootState, 'next_tool', args);

// Incorrect (may not work in sub-prompts)
queueTool(state, 'next_tool', args);

Next Steps

API Reference

View complete tool API specification

ThreadState

Learn about execution context and utilities

Prompts

Configure prompts that use tools

Examples

Explore real-world tool patterns