defineTool
Define custom tools that agents can call
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,
>(
options: DefineToolOptions<State, Args>
): ToolDefinition<State, Args>;
interface DefineToolOptions<
State = ThreadState,
Args extends ToolArgs | null = null,
> {
description: string;
args?: Args;
execute: Tool<State, Args>;
variables?: VariableDefinition[];
executionMode?: 'local' | 'provider';
executionProvider?: string;
annotations?: Record<string, unknown>;
uses?: readonly string[];
progressArgument?: string;
}
interface ToolDefinition<
State = ThreadState,
Args extends ToolArgs | null = null,
> {
description: string;
args: Args;
execute: Tool<State, Args>;
variables: VariableDefinition[];
executionMode?: 'local' | 'provider';
executionProvider?: string;
}
interface VariableDefinition {
name: string;
type: 'text' | 'secret';
required: boolean;
scoped?: boolean;
description: 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>>;
// ToolArgsNode supports string, number, boolean, null, literal, enum, array,
// object, record, union, optional, nullable, and default schemas.
// Prefer explicit, bounded shapes for provider-visible tool args. For arbitrary
// JSON payloads, accept a JSON string and parse it inside execute; open-ended
// z.unknown(), z.any(), or catchall schemas can be rejected by strict providers.
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;
}The type block is abbreviated.
The canonical types also include the deprecated Tenvs generic and tenvs field.
The Uses generic constrains the execute context's callable names.
Parameters
descriptionstringrequiredHuman-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'argsz.ZodObjectZod 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>requiredAsync function that executes when the tool is called.
- First parameter:
ThreadState(execution context) - Second parameter: Validated arguments (only if
argsis defined)
execute: async (state, args) => {
const result = await doSomething(args.query);
return { status: 'success', result: JSON.stringify(result) };
}variablesVariableDefinition[]Thread environment variables that the tool requires or optionally uses.
variables: [
{
name: 'VECTOR_STORE_ID',
type: 'secret',
required: true,
description: 'OpenAI Vector Store ID',
},
]The runtime validates variables when the tool runs. A required variable that state.env(name) cannot resolve causes an error before execute is called. The earlier tenvs Zod schema option is deprecated.
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
executionProviderstringWhich provider executes this tool (when executionMode='provider'). e.g., 'openai', 'anthropic'
usesreadonly string[]Declares callables that this tool can invoke or queue.
Use as const for compile-time name checks.
Runtime calls to unlisted names fail.
Packed tools need this declaration for callable dependencies.
annotationsRecord<string, unknown>Tool metadata for consumers.
progressArgumentstringNames an argument that describes the operation in progress.
tool_call_started will wait for that argument to finish streaming.
If it never arrives, the event will fire when argument streaming ends.
Execute Parameters
ThreadState (first parameter)
The execution context providing access to thread data and environment:
interface ThreadState {
readonly threadId: string;
readonly agentId: string;
readonly userId: string | null;
readonly arguments: Record<string, unknown>;
execution: ExecutionState | null;
context: Record<string, unknown>;
getMessages(options?: GetMessagesOptions): Promise<MessagesResult>;
injectMessage(message: InjectMessageInput): Promise<Message>;
queueTool(toolName: string, args: Record<string, unknown>): void;
invokeTool(toolName: string, args: Record<string, unknown>): Promise<ToolResult>;
emit(event: string, data: unknown): void;
getValue<T = unknown>(key: string): Promise<T | null>;
setValue(key: string, value: unknown): Promise<void>;
env(propertyName: string): Promise<string>;
envType(propertyName: string): Promise<'text' | 'secret'>;
setEnv(
propertyName: string,
value: string,
options?: { type?: 'text' | 'secret' }
): Promise<void>;
runCode(source: string, options?: CodeExecutionOptions): CodeExecution;
}state.execution holds flowId, currentSide, stepCount, stopped, and messageHistory for the active execution. Its value is null when the thread is at rest.
See ThreadState for full documentation.
state.envType(name) defaults to secret. Use state.setEnv(name, value, { type: 'text' }) only when the value is safe to show in tool output and errors; use { type: 'secret' } for redacted values.
Use state.arguments for thread invocation data and subagent creation arguments. Use state.context only for mutable scratch data during the current execution.
CodeExecutionOptions.execute selects the export to run and optional args, defaulting to { fn: 'default', args: [] }. CodeExecutionOptions.modules can provide local relative ES modules to sandboxed code, such as { './helpers.js': 'export const ok = true' }.
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'requiredWhether the tool executed successfully.
resultstringThe result to return to the LLM. For complex data, use JSON.stringify().
errorstringError message (when status is 'error').
stackstringStack trace for debugging (when status is 'error').
attachmentsArray<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) => {
// Read an environment value (thread, agent, prompt, user account, then instance)
const databaseUrl = await state.env('DATABASE_URL');
// Access thread invocation or subagent creation arguments
const userId = state.arguments.userId;
// Read the thread's durable key-value store
const cached = await state.getValue<string>(`customer:${args.customerId}`);
if (cached !== null) {
return { status: 'success', result: cached };
}
const customer = await lookupCustomer(databaseUrl, args.customerId);
return {
status: 'success',
result: JSON.stringify(customer),
};
},
});With Thread Environment Variables
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 = await state.env('VECTOR_STORE_ID');
const results = await searchVectorStore(vectorStoreId, args.query, args.maxResults);
return { status: 'success', result: JSON.stringify(results) };
},
variables: [
{
name: 'VECTOR_STORE_ID',
type: 'secret',
required: true,
description: 'OpenAI Vector Store ID',
},
],
});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 instanceof Error ? error.message : String(error)}`,
};
}
},
});Queueing Tools and Emitting Events
import { defineTool } from '@standardagents/spec';
import { z } from 'zod';
export default defineTool({
description: 'Start a multi-step workflow',
uses: ['execute_step'] as const,
args: z.object({
workflowId: z.string(),
}),
execute: async (state, args) => {
// Queue another tool to run next
state.queueTool('execute_step', {
workflowId: args.workflowId,
step: 1,
});
// Emit event to frontend
state.emit('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
import { defineModel } from '@standardagents/spec';
import { openai } from '@standardagents/openai';
export default defineModel({
name: 'image_tools',
provider: openai,
model: 'gpt-6.1-sol',
providerTools: ['image_generation'],
});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.tsRequirements:
- 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 require Zod 4 (zod@^4.0.0). Zod 3 schemas are unsupported.
They support the following Zod types:
Primitives:
-
z.string() -
z.number() -
z.boolean() -
z.null() -
z.literal(value) -
z.unknown()for an arbitrary JSON 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
Write Clear Descriptions
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'Describe Schema Fields
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 Structured Results
Return JSON strings for complex data:
return {
status: 'success',
result: JSON.stringify({
articles: results,
totalCount: count,
hasMore: offset + limit < count,
}),
};Handle Errors Gracefully
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 instanceof Error ? error.message : String(error)}`,
};
}Queue Tools Through ThreadState
Use state.queueTool() to queue a tool from the current execution context:
state.queueTool('next_tool', args);