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 three types: function tools (custom code), prompt tools (sub-prompts), and agent tools (handoffs). All appear to the LLM as functions it can call.

Quick Start

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

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

    return {
      status: 'success',
      content: [{ type: 'text', text: 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

How They Work Together

// Function tool: agents/tools/lookup_customer.ts
export default defineTool(
  'Look up customer by ID',
  z.object({ customerId: z.string() }),
  async (flow, args) => {
    const customer = await db.getCustomer(args.customerId);
    return { status: 'success', content: [{ type: 'text', text: 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-4o',
  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
  ],
  // ...
});

FlowState Context

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

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

    // Access environment
    const apiKey = flow.env.MY_API_KEY;

    return { status: 'success', content: [{ type: 'text', text: 'Done' }] };
  }
);
FlowState gives tools access to thread storage, environment bindings, message history, and execution state. See the FlowState documentation for details.

Input Validation with Zod

Tools use Zod schemas to validate arguments:
const args = 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 CallToolResult object:
interface CallToolResult {
  status: 'success' | 'error';
  content: Array<{
    type: 'text' | 'image' | 'document';
    text?: string;
    source?: {
      type: 'url' | 'base64';
      media_type?: string;
      data: string;
    };
  }>;
}

Common Patterns

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

Common Use Cases

Database Query Tool

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

export default defineTool(
  'Look up customer information by ID or email',
  z.object({
    customerId: z.string().optional().describe('Customer ID'),
    email: z.string().email().optional().describe('Customer email'),
  }),
  async (flow, args) => {
    const db = flow.env.DB;

    let customer;
    if (args.customerId) {
      customer = await db.prepare('SELECT * FROM customers WHERE id = ?')
        .bind(args.customerId).first();
    } else {
      customer = await db.prepare('SELECT * FROM customers WHERE email = ?')
        .bind(args.email).first();
    }

    if (!customer) {
      return {
        status: 'error',
        content: [{ type: 'text', text: 'Customer not found' }],
      };
    }

    return {
      status: 'success',
      content: [{ type: 'text', text: JSON.stringify(customer) }],
    };
  }
);

API Integration Tool

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

export default defineTool(
  'Fetch current weather for a city',
  z.object({
    city: z.string().describe('City name'),
    units: z.enum(['metric', 'imperial']).default('metric'),
  }),
  async (flow, args) => {
    const apiKey = flow.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',
          content: [{ type: 'text', text: `Weather API error: ${response.status}` }],
        };
      }

      const data = await response.json();

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

Tool Chaining

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

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

    if (!order.valid) {
      return {
        status: 'error',
        content: [{ type: 'text', text: 'Order validation failed' }],
      };
    }

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

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

    return {
      status: 'success',
      content: [{ type: 'text', text: `Order ${args.orderId} queued for processing` }],
    };
  }
);

FlowState Utilities

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

Best Practices

Tool descriptions help LLMs understand when to use each tool:Good:
defineTool(
  'Search the knowledge base for product documentation, FAQs, and troubleshooting guides. Returns relevant articles sorted by relevance.',
  // ...
);
Avoid:
defineTool('Search stuff', /* ... */);
Use .describe() on every parameter:
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',
    content: [{ type: 'text', text: JSON.stringify(result) }],
  };
} catch (error) {
  return {
    status: 'error',
    content: [{ type: 'text', text: `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 flow.rootState:
// Correct
queueTool(flow.rootState, 'next_tool', args);

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

Next Steps