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

```typescript
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

```typescript
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

- 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:**

  ```typescript
  description: 'Search the knowledge base for product documentation, FAQs, and troubleshooting guides. Returns relevant articles sorted by relevance.'
  ```

  **Avoid:**

  ```typescript
  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.

  ```typescript
  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)

  ```typescript
  execute: async (state, args) => {
    const result = await doSomething(args.query);
    return { status: 'success', result: JSON.stringify(result) };
  }
  ```

* variables (VariableDefinition\[])

  Thread environment variables that the tool requires or optionally uses.

  ```typescript
  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

* executionProvider (string)

  Which provider executes this tool (when `executionMode='provider'`). e.g., `'openai'`, `'anthropic'`

- uses (readonly 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.

* annotations (Record\<string, unknown>)

  Tool metadata for consumers.

- progressArgument (string)

  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:

```typescript
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](/docs/core-concepts/flowstate) 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:

```typescript
// 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.

  ```typescript
  attachments: [{
    name: 'generated.png',
    mimeType: 'image/png',
    data: base64Data,
    width: 1024,
    height: 1024,
  }]
  ```

## Examples

### Basic Tool

```typescript
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

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

### Using ThreadState

```typescript
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

```typescript
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

```typescript
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

```typescript
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

```typescript
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

```typescript
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

```typescript
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:**

```typescript
description: 'Search the knowledge base for product documentation, FAQs, and troubleshooting guides. Returns relevant articles ranked by relevance.'
```

**Avoid:**

```typescript
description: 'Search stuff'
```

### Describe Schema Fields

Use `.describe()` on every field:

```typescript
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:

```typescript
return {
  status: 'success',
  result: JSON.stringify({
    articles: results,
    totalCount: count,
    hasMore: offset + limit < count,
  }),
};
```

### Handle Errors Gracefully

Always catch and return meaningful errors:

```typescript
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:

```typescript
state.queueTool('next_tool', args);
```
