Skip to main content

Overview

defineTool creates a function tool that agents can call during execution. It uses an object-based API for clear, typed configuration.
import { defineTool } from '@standardagents/spec';
import { z } from 'zod';

export default defineTool({
  description: 'Search the knowledge base for relevant articles',
  args: z.object({
    query: z.string().describe('Search query'),
  }),
  execute: async (state, args) => {
    const results = await searchKnowledgeBase(args.query);
    return {
      status: 'success',
      result: JSON.stringify(results),
    };
  },
});

Type Definition

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

function defineTool<
  State = ThreadState,
  Args extends ToolArgs | null = null,
  Tenvs extends ToolTenvs | null = null,
>(
  options: DefineToolOptions<State, Args, Tenvs>
): ToolDefinition<State, Args, Tenvs>;

interface DefineToolOptions<
  State = ThreadState,
  Args extends ToolArgs | null = null,
  Tenvs extends ToolTenvs | null = null,
> {
  description: string;
  args?: Args;
  execute: Tool<State, Args>;
  tenvs?: Tenvs;
  executionMode?: 'local' | 'provider';
  executionProvider?: string;
}

interface ToolDefinition<
  State = ThreadState,
  Args extends ToolArgs | null = null,
  Tenvs extends ToolTenvs | null = null,
> {
  description: string;
  args: Args;
  execute: Tool<State, Args>;
  tenvs: Tenvs;
  executionMode?: 'local' | 'provider';
  executionProvider?: string;
}

type Tool<State, Args extends ToolArgs | null> = Args extends ToolArgs
  ? (state: State, args: z.infer<Args>) => Promise<ToolResult>
  : (state: State) => Promise<ToolResult>;

type ToolArgs = z.ZodObject<Record<string, ToolArgsNode>>;
type ToolTenvs = z.ZodObject<Record<string, ToolArgsNode>>;

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

interface ToolAttachment {
  name: string;
  mimeType: string;
  data: string;
  width?: number;
  height?: number;
}

Parameters

description
string
required
Human-readable description of what the tool does. This is sent to the LLM to help it decide when to use the tool.Good:
description: 'Search the knowledge base for product documentation, FAQs, and troubleshooting guides. Returns relevant articles sorted by relevance.'
Avoid:
description: 'Search stuff'
args
z.ZodObject
Zod object schema defining the tool’s parameters. Omit for tools with no arguments.Use .describe() on each field to help the LLM understand what to pass.
args: z.object({
  query: z.string().describe('Search query'),
  limit: z.number().optional().default(10).describe('Max results'),
})
execute
(state, args?) => Promise<ToolResult>
required
Async function that executes when the tool is called.
  • First parameter: ThreadState (execution context)
  • Second parameter: Validated arguments (only if args is defined)
execute: async (state, args) => {
  const result = await doSomething(args.query);
  return { status: 'success', result: JSON.stringify(result) };
}
tenvs
z.ZodObject
Zod object schema for thread environment variables the tool requires.
tenvs: z.object({
  vectorStoreId: z.string().describe('OpenAI Vector Store ID'),
})
Tenvs are validated at runtime. Missing required tenvs cause an error.
executionMode
'local' | 'provider'
Where this tool is executed:
  • 'local' (default): Execute locally by the execution engine
  • 'provider': Executed by the LLM provider, results come in response
executionProvider
string
Which provider executes this tool (when executionMode='provider'). e.g., 'openai', 'anthropic'

Execute Parameters

ThreadState (first parameter)

The execution context providing access to thread data and environment:
interface ThreadState {
  threadId: string;
  flowId: string;
  agentConfig: Agent;
  currentSide: 'a' | 'b';
  turnCount: number;
  stopped: boolean;
  messageHistory: Message[];
  storage: DurableObjectStorage;
  env: Env;
  context: Record<string, unknown>;
  tenvs: Record<string, unknown>;
  rootState: ThreadState;
}
See ThreadState for full documentation.

Args (second parameter)

Validated arguments matching your Zod schema:
// If args is:
args: z.object({
  query: z.string(),
  limit: z.number().optional(),
})

// Then the second parameter is typed as:
{ query: string; limit?: number }

Return Value

Tools must return a ToolResult object:
status
'success' | 'error'
required
Whether the tool executed successfully.
result
string
The result to return to the LLM. For complex data, use JSON.stringify().
error
string
Error message (when status is 'error').
stack
string
Stack trace for debugging (when status is 'error').
attachments
Array<ToolAttachment | AttachmentRef>
File attachments to include with the tool result. Attachments are stored in the thread’s file system and linked to the message.
attachments: [{
  name: 'generated.png',
  mimeType: 'image/png',
  data: base64Data,
  width: 1024,
  height: 1024,
}]

Examples

Basic Tool

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

export default defineTool({
  description: 'Get the current weather for a location',
  args: z.object({
    location: z.string().describe('City name or coordinates'),
    units: z.enum(['celsius', 'fahrenheit']).default('celsius'),
  }),
  execute: async (state, args) => {
    const weather = await fetchWeather(args.location, args.units);
    return {
      status: 'success',
      result: JSON.stringify(weather),
    };
  },
});

Tool Without Arguments

export default defineTool({
  description: 'Get the current server time',
  execute: async (state) => {
    return {
      status: 'success',
      result: new Date().toISOString(),
    };
  },
});

Using ThreadState

export default defineTool({
  description: 'Look up customer information',
  args: z.object({
    customerId: z.string().describe('Customer ID'),
  }),
  execute: async (state, args) => {
    // Access environment bindings
    const db = state.env.DB;

    // Access custom context
    const userId = state.context.userId;

    // Access storage
    const cached = await state.storage.get(`customer:${args.customerId}`);
    if (cached) {
      return { status: 'success', result: cached };
    }

    const customer = await db.prepare(
      'SELECT * FROM customers WHERE id = ?'
    ).bind(args.customerId).first();

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

With Thread Environment Variables (tenvs)

export default defineTool({
  description: 'Search through uploaded files using vector embeddings',
  args: z.object({
    query: z.string().describe('Search query'),
    maxResults: z.number().optional().default(5),
  }),
  execute: async (state, args) => {
    const vectorStoreId = state.tenvs.vectorStoreId;
    const results = await searchVectorStore(vectorStoreId, args.query, args.maxResults);
    return { status: 'success', result: JSON.stringify(results) };
  },
  tenvs: z.object({
    vectorStoreId: z.string().describe('OpenAI Vector Store ID'),
    userLocation: z.string().optional().describe('User location for relevance'),
  }),
});

Error Handling

export default defineTool({
  description: 'Process payment',
  args: z.object({
    amount: z.number().positive(),
    currency: z.string().length(3),
  }),
  execute: async (state, args) => {
    try {
      const result = await processPayment(args.amount, args.currency);
      return {
        status: 'success',
        result: `Payment processed: ${result.transactionId}`,
      };
    } catch (error) {
      return {
        status: 'error',
        error: `Payment failed: ${error.message}`,
      };
    }
  },
});

Using Utilities

import { defineTool } from '@standardagents/spec';
import { queueTool, emitThreadEvent } from '@standardagents/builder';
import { z } from 'zod';

export default defineTool({
  description: 'Start a multi-step workflow',
  args: z.object({
    workflowId: z.string(),
  }),
  execute: async (state, args) => {
    // Queue another tool to run next
    queueTool(state.rootState, 'execute_step', {
      workflowId: args.workflowId,
      step: 1,
    });

    // Emit event to frontend
    await emitThreadEvent(state, 'workflow_started', {
      workflowId: args.workflowId,
    });

    return {
      status: 'success',
      result: 'Workflow initiated',
    };
  },
});

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,
      }],
    };
  },
});
Attachments are automatically stored in the thread’s /attachments/ directory and linked to the tool result message. Large files (>1.75MB) are automatically chunked.

Provider-Executed Tool

export default defineTool({
  description: 'Generate images using DALL-E',
  args: z.object({
    prompt: z.string().describe('Image description'),
  }),
  execute: async () => {
    // This won't be called - the provider handles execution
    return { status: 'success', result: 'Handled by provider' };
  },
  executionMode: 'provider',
  executionProvider: 'openai',
});

Complex Schema

export default defineTool({
  description: 'Create a support ticket',
  args: z.object({
    title: z.string().min(5).max(200).describe('Ticket title'),
    description: z.string().describe('Detailed description'),
    priority: z.enum(['low', 'medium', 'high', 'urgent']).default('medium'),
    category: z.string().describe('Ticket category'),
    tags: z.array(z.string()).optional().describe('Optional tags'),
    assignee: z.string().optional().describe('User ID to assign to'),
  }),
  execute: async (state, args) => {
    const ticket = await createTicket(args);
    return {
      status: 'success',
      result: `Created ticket #${ticket.id}: ${ticket.title}`,
    };
  },
});

File Location

Tools are auto-discovered from agents/tools/:
agents/
└── tools/
    ├── search_knowledge_base.ts
    ├── lookup_customer.ts
    └── create_ticket.ts
Requirements:
  • Use snake_case for file names
  • Tool name is derived from file name
  • One tool per file
  • Default export required

Supported Zod Types

Tool argument schemas support the following Zod types: Primitives:
  • z.string()
  • z.number()
  • z.boolean()
  • z.null()
  • z.literal(value)
Enums:
  • z.enum(['a', 'b', 'c'])
Wrappers:
  • z.optional(...)
  • z.nullable(...)
  • z.default(...)
Collections:
  • z.array(...)
  • z.object({ ... })
  • z.record(z.string(), ...)
Unions:
  • z.union([...])
Nested schemas are supported up to 7 levels deep.

Best Practices

The description helps the LLM decide when to use the tool:Good:
description: 'Search the knowledge base for product documentation, FAQs, and troubleshooting guides. Returns relevant articles ranked by relevance.'
Avoid:
description: 'Search stuff'
Use .describe() on every field:
args: z.object({
  query: z.string().describe('Natural language search query'),
  limit: z.number().min(1).max(20).default(5).describe('Max results'),
  category: z.string().optional().describe('Filter by category name'),
})
Return JSON strings for complex data:
return {
  status: 'success',
  result: JSON.stringify({
    articles: results,
    totalCount: count,
    hasMore: offset + limit < count,
  }),
};
Always catch and return meaningful errors:
try {
  // ... operation
} catch (error) {
  console.error('[Tool] Operation failed:', error);
  return {
    status: 'error',
    error: `Failed to complete operation: ${error.message}`,
  };
}
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);