Skip to content

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

descriptionstringrequired

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'
argsz.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) };
}
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
executionProviderstring

Which 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.

progressArgumentstring

Names 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'required

Whether the tool executed successfully.

resultstring

The result to return to the LLM. For complex data, use JSON.stringify().

errorstring

Error message (when status is 'error').

stackstring

Stack 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.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 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);