# Hooks API Reference

Complete API documentation for all lifecycle hooks

## Overview

Hooks are optional functions that run automatically at specific points during agent execution. This reference documents all available hooks, their signatures, parameters, and usage patterns.

> **Note:**
>
> Thread hooks are selected from prompt hooks, agent hooks, and required hooks from enabled direct child relationships. See [Hook Scoping](/docs/core-concepts/hooks#hook-scoping) for details.

## Using defineHook

```typescript
import { defineHook } from '@standardagents/spec';

export default defineHook({
  hook: 'filter_messages',
  id: 'my_filter',
  execute: async (state, rows) => {
    return rows;
  },
});
```

| Property  | Type       | Required | Description                           |
| --------- | ---------- | -------- | ------------------------------------- |
| `hook`    | `HookName` | Yes      | The hook type                         |
| `id`      | `string`   | Yes      | Unique identifier (snake_case)        |
| `execute` | `Function` | Yes      | Implementation, typed based on `hook` |

Every thread hook receives a final `HookExecutionContext` argument. Its
`source` identifies the prompt, agent, or child relationship that selected the
hook. User-scope hooks have no context argument.

## Available Hooks

### after_thread_created

Runs after a thread is created and before execution starts.

- Signature (function)

  ```typescript
  defineHook({
    hook: 'after_thread_created',
    id: 'init_thread',
    execute: async (state) => {
      await state.setValue('initialized', true);
    },
  })
  ```

### after_subagent_created

Runs on the parent thread immediately after a subagent child thread is created.

- Signature (function)

  ```typescript
  defineHook({
    hook: 'after_subagent_created',
    id: 'track_child',
    execute: async (state, childState) => {
      await state.setValue('last_child_thread_id', childState.threadId);
    },
  })
  ```

### after_system_message

Runs after the system message is rendered for a request. Return a string to replace it, or `null` / `undefined` to keep the original.

- Signature (function)

  ```typescript
  defineHook({
    hook: 'after_system_message',
    id: 'dynamic_system',
    execute: async (state, systemMessage) => {
      return `${systemMessage}\nCurrent thread: ${state.threadId}`;
    },
  })
  ```

### filter_messages

Builds the request context from one stable snapshot of eligible persisted history for the current model
request. The request context does not replace live execution history.

- Signature (function)

  ```typescript
  defineHook({
    hook: 'filter_messages',
    id: 'my_filter',
    execute: async (state, rows) => {
      return rows;
    },
  })
  ```

* state (ThreadState)

  Full execution context

- rows (Message\[])

  Array of messages from storage

* Returns (Promise\<Message\[]>)

  The filtered/modified message array

#### When It Runs

- **Timing**: After `after_system_message` and snapshot creation, before transformation
- **Frequency**: Exactly once per model-context assembly
- **Injection boundary**: Messages injected here enter the following snapshot

#### Examples

### Filter Out Failed Tools

```typescript
import { defineHook } from '@standardagents/spec';

export default defineHook({
  hook: 'filter_messages',
  id: 'remove_failed_tools',
  execute: async (state, rows) => {
    const failedIds = new Set(rows
      .filter(row => row.role === 'tool' && row.tool_status === 'error')
      .map(row => row.tool_call_id));
    const removedIds = new Set<string>();
    for (const row of rows) {
      const calls = JSON.parse(row.tool_calls || '[]') as { id: string }[];
      if (calls.some(call => failedIds.has(call.id))) {
        for (const call of calls) removedIds.add(call.id);
      }
    }
    return rows.filter(row => {
      const calls = JSON.parse(row.tool_calls || '[]') as { id: string }[];
      return !calls.some(call => removedIds.has(call.id)) &&
        !(row.role === 'tool' && row.tool_call_id && removedIds.has(row.tool_call_id));
    });
  },
});
```

### Time-Based Filtering

```typescript
import { defineHook } from '@standardagents/spec';

export default defineHook({
  hook: 'filter_messages',
  id: 'filter_last_24h',
  execute: async (state, rows) => {
    const dayAgo = (Date.now() - 24 * 60 * 60 * 1000) * 1000; // microseconds
    return rows.filter(row => row.created_at >= dayAgo);
  },
});
```

---

### prefilter_llm_history

Modifies transformed provider messages before dispatch. Its changes stay within
the current request. Messages injected here enter the following snapshot.

- Signature (function)

  ```typescript
  defineHook({
    hook: 'prefilter_llm_history',
    id: 'my_prefilter',
    execute: async (state, messages) => {
      return messages;
    },
  })
  ```

* state (ThreadState)

  Full execution context

- messages (LLMMessage\[])

  Array of messages about to be sent to LLM

* Returns (Promise\<LLMMessage\[]>)

  The filtered/modified message array

#### LLMMessage Structure

```typescript
import type { LLMMessage } from '@standardagents/spec';

interface LLMMessage {
  role: string;
  content?: string | null | unknown[]; // string for text, array for multimodal parts
  tool_calls?: unknown;
  tool_call_id?: string;
  name?: string;
  reasoning_content?: string;
  [key: string]: unknown; // provider-specific fields
}
```

#### When It Runs

- **Timing**: Immediately before sending messages to LLM
- **Frequency**: Every turn that involves an LLM request
- **Context**: After message history assembled but before API call

#### Examples

### Limit Message History

```typescript
import { defineHook } from '@standardagents/spec';

export default defineHook({
  hook: 'prefilter_llm_history',
  id: 'limit_to_20',
  execute: async (state, messages) => {
    return messages.slice(-20);
  },
});
```

### Add Dynamic Context

```typescript
import { defineHook } from '@standardagents/spec';

export default defineHook({
  hook: 'prefilter_llm_history',
  id: 'inject_turn_count',
  execute: async (state, messages) => {
    const systemMsg = messages.find(m => m.role === 'system');
    if (systemMsg && typeof systemMsg.content === 'string') {
      systemMsg.content += `\n\nCurrent time: ${new Date().toISOString()}`;
    }
    return messages;
  },
});
```

---

### before_create_message

Modifies any message before it's inserted into the database. A hook that
throws prevents the insertion. `injectMessage()` rejects with the hook error.
The message of a model step and a tool result message are still inserted with
the last good value. See [Error Handling](#error-handling) for each write.

- Signature (function)

  ```typescript
  defineHook({
    hook: 'before_create_message',
    id: 'my_transform',
    execute: async (state, message) => {
      return message;
    },
  })
  ```

* state (ThreadState)

  Full execution context

- message (HookMessage)

  Message about to be created

* Returns (Promise\<HookMessage>)

  The modified message

#### When It Runs

- **Timing**: Immediately before INSERT INTO messages
- **Frequency**: Every time a message is created
- **Scope**: ALL message types (user, assistant, tool, system)

#### Examples

### Add Agent Name

```typescript
import { defineHook } from '@standardagents/spec';

export default defineHook({
  hook: 'before_create_message',
  id: 'tag_with_agent_name',
  execute: async (state, message) => {
    if (message.role === 'assistant') {
      message.name = state.agentId;
    }
    return message;
  },
});
```

### Add Content Prefix

```typescript
import { defineHook } from '@standardagents/spec';

export default defineHook({
  hook: 'before_create_message',
  id: 'prefix_user_messages',
  execute: async (state, message) => {
    if (message.content && message.role === 'user') {
      message.content = `[User] ${message.content}`;
    }
    return message;
  },
});
```

---

### after_create_message

Runs after a message is successfully inserted into the database.

- Signature (function)

  ```typescript
  defineHook({
    hook: 'after_create_message',
    id: 'my_logger',
    execute: async (state, message) => {
      // No return value
    },
  })
  ```

* state (ThreadState)

  Full execution context

- message (HookMessage)

  The message that was just created

* Returns (Promise\<void>)

  No return value

#### When It Runs

- **Timing**: Immediately after successful INSERT INTO messages
- **Frequency**: Every time a message is created
- **Scope**: ALL message types

#### Examples

### External Logging

```typescript
import { defineHook } from '@standardagents/spec';

export default defineHook({
  hook: 'after_create_message',
  id: 'analytics_logger',
  execute: async (state, message) => {
    if (message.role === 'assistant' && message.content) {
      try {
        await fetch('https://analytics.example.com/events', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({
            event: 'message_created',
            thread_id: state.threadId,
            agent_id: state.agentId,
          }),
        });
      } catch (error) {
        console.error('Analytics failed:', error);
      }
    }
  },
});
```

---

### before_update_message

Modifies message updates before they're applied to the database.

- Signature (function)

  ```typescript
  defineHook({
    hook: 'before_update_message',
    id: 'my_update_hook',
    execute: async (state, messageId, updates) => {
      return updates;
    },
  })
  ```

* state (ThreadState)

  Full execution context

- messageId (string)

  ID of message being updated

* updates (Record\<string, unknown>)

  Fields being updated

- Returns (Promise\<Record\<string, unknown>>)

  The modified updates object

> **Warning:**
>
> Updates object only contains fields being changed, not the full message.

A hook that throws prevents the update, and `state.updateMessage()` rejects
with the hook error. The updates that a model step makes to its own message
still apply with the last good value. See [Error Handling](#error-handling).

#### When It Runs

- **Timing**: Immediately before UPDATE messages SET ...
- **Frequency**: Every time a message is updated
- **Common Triggers**: Status changes, content streaming completion, adding tool_calls

#### Examples

### Add Completion Timestamp

```typescript
import { defineHook } from '@standardagents/spec';

export default defineHook({
  hook: 'before_update_message',
  id: 'add_completion_timestamp',
  execute: async (state, messageId, updates) => {
    if (updates.status === 'completed' && !updates.response_completed_at) {
      updates.response_completed_at = Date.now() * 1000;
    }
    return updates;
  },
});
```

---

### after_update_message

Runs after a message is successfully updated in the database.

- Signature (function)

  ```typescript
  defineHook({
    hook: 'after_update_message',
    id: 'my_update_logger',
    execute: async (state, message) => {
      // No return value
    },
  })
  ```

* state (ThreadState)

  Full execution context

- message (Message)

  The updated message

* Returns (Promise\<void>)

  No return value

#### When It Runs

- **Timing**: Immediately after successful UPDATE messages
- **Frequency**: Every time a message is updated
- **Scope**: All message update operations

#### Examples

### Track Status Changes

```typescript
import { defineHook } from '@standardagents/spec';

export default defineHook({
  hook: 'after_update_message',
  id: 'track_status_changes',
  execute: async (state, message) => {
    console.log(`[Status Change] Message ${message.id} updated`);
  },
});
```

---

### before_store_tool_result

Modifies tool results before they are stored in the database.

- Signature (function)

  ```typescript
  defineHook({
    hook: 'before_store_tool_result',
    id: 'my_result_transform',
    execute: async (state, toolCall, toolResult) => {
      return toolResult;
    },
  })
  ```

* state (ThreadState)

  Full execution context

- toolCall (HookToolCall)

  The tool call that was executed

* toolResult (HookToolResult)

  The result from the tool

- Returns (Promise\<HookToolResult>)

  The modified tool result

#### When It Runs

- **Timing**: After tool execution, before result is stored
- **Frequency**: Every time a tool call produces a result
- **Scope**: All tool types (native, prompt, agent)

#### Examples

### Sanitize Sensitive Data

```typescript
import { defineHook } from '@standardagents/spec';

export default defineHook({
  hook: 'before_store_tool_result',
  id: 'sanitize_pii',
  execute: async (state, toolCall, toolResult) => {
    if (toolResult.result && typeof toolResult.result === 'string') {
      toolResult.result = toolResult.result.replace(
        /\b\d{3}-\d{2}-\d{4}\b/g, '***-**-****'
      );
    }
    return toolResult;
  },
});
```

### Add Metadata

```typescript
import { defineHook } from '@standardagents/spec';

export default defineHook({
  hook: 'before_store_tool_result',
  id: 'add_result_metadata',
  execute: async (state, toolCall, toolResult) => {
    await state.setValue(`tool_result:${toolCall.id}`, {
      stored_at: Date.now(),
      thread_id: state.threadId,
    });
    return toolResult;
  },
});
```

---

### after_tool_call_success

Intercepts successful tool executions before results are stored in the database.

- Signature (function)

  ```typescript
  defineHook({
    hook: 'after_tool_call_success',
    id: 'my_success_hook',
    execute: async (state, call, result) => {
      return result;  // or null to keep the original result
    },
  })
  ```

* state (ThreadState)

  Full execution context

- call (ToolCall)

  The tool call that was executed

* result (ToolResult)

  The successful result from the tool

- Returns (Promise\<ToolResult | null>)

  Modified result, or `null` to keep the original result

#### ToolCall Structure

```typescript
interface ToolCall {
  id: string;
  type: "function";
  function: {
    name: string;
    arguments: string; // JSON string
  };
}
```

#### ToolResult Structure

```typescript
interface ToolResult {
  status: "success";
  result?: string;
}
```

#### When It Runs

- **Timing**: Immediately after successful tool execution, before storage
- **Frequency**: Every time a tool executes successfully
- **Scope**: All tool types (native, prompt, agent)

#### Returning `null`

When the hook returns `null`, the result it was given is kept:

1. The tool call message stays in message history
2. The tool result message is stored unchanged
3. The next hook in the chain receives the same result

A hook cannot remove a tool call or its result. To change what is stored,
return a modified result. Messages added with `injectMessage()` are stored in
addition to the tool result.

#### Examples

### Add a User Message from a Tool Result

```typescript
import { defineHook } from "@standardagents/spec";

export default defineHook({
  hook: 'after_tool_call_success',
  id: 'convert_user_input',
  execute: async (state, call, result) => {
    if (call.function.name === "get_user_input") {
      await state.injectMessage({
        role: "user",
        content: result.result || "",
      });
    }
    return result;
  },
});
```

### Filter Sensitive Data

```typescript
import { defineHook } from "@standardagents/spec";

export default defineHook({
  hook: 'after_tool_call_success',
  id: 'filter_sensitive_results',
  execute: async (state, call, result) => {
    if (call.function.name === "lookup_user" && result.result) {
      const data = JSON.parse(result.result);
      delete data.ssn;
      delete data.password;
      result.result = JSON.stringify(data);
    }
    return result;
  },
});
```

---

### after_tool_call_failure

Intercepts failed tool executions before errors are stored in the database.

- Signature (function)

  ```typescript
  defineHook({
    hook: 'after_tool_call_failure',
    id: 'my_failure_hook',
    execute: async (state, call, result) => {
      return result;  // or null to keep the original result
    },
  })
  ```

* state (ThreadState)

  Full execution context

- call (ToolCall)

  The tool call that failed

* result (ToolResult)

  The error result from the tool

- Returns (Promise\<ToolResult | null>)

  Modified error result, or `null` to keep the original error

#### ToolResult Structure (Error)

```typescript
interface ToolResult {
  status: "error";
  error?: string;
  stack?: string;
}
```

#### When It Runs

- **Timing**: Immediately after tool failure, before storage
- **Frequency**: Every time a tool fails or throws an error
- **Scope**: All tool types (native, prompt, agent)

#### Returning `null`

When the hook returns `null`, the error result it was given is kept: the tool
call stays in message history, the error result is stored unchanged, and the
next hook in the chain receives the same result. A hook cannot remove a failed
tool call. To change what is stored, return a modified result.

#### Examples

### Replace Specific Errors

```typescript
import { defineHook } from "@standardagents/spec";

export default defineHook({
  hook: 'after_tool_call_failure',
  id: 'explain_not_found',
  execute: async (state, call, result) => {
    if (result.error?.includes("not found")) {
      return { ...result, error: "The requested item was not found." };
    }
    return result;
  },
});
```

### Send Error Notifications

```typescript
import { defineHook } from "@standardagents/spec";

export default defineHook({
  hook: 'after_tool_call_failure',
  id: 'alert_critical_errors',
  execute: async (state, call, result) => {
    if (call.function.name === "payment_processing") {
      try {
        await fetch("https://alerts.example.com/error", {
          method: "POST",
          body: JSON.stringify({
            tool: call.function.name,
            error: result.error,
            threadId: state.threadId,
          }),
        });
      } catch (err) {
        console.error("Failed to send error notification:", err);
      }
    }
    return result;
  },
});
```

---

### tool_call_started

Observes a tool call when it starts to stream from the model, before the tool executes. The return value is ignored.

- Signature (function)

  ```typescript
  defineHook({
    hook: 'tool_call_started',
    id: 'my_start_observer',
    execute: async (state, toolCall, progress, context) => {
      // No return value
    },
  })
  ```

* state (ThreadState)

  Full execution context

- toolCall (HookToolCall)

  The tool call that starts. `function.name` is known. `function.arguments` can be empty or an incomplete JSON fragment

* progress (string | undefined)

  The value of the tool's `progressArgument`, when the tool declares one and the model has sent it

- context (HookExecutionContext)

  The reason the hook is active: `{ source: { kind: 'prompt' | 'agent' | 'child', ... } }`

* Returns (Promise\<void>)

  No return value

#### When It Runs

- **Frequency**: Once for each tool call in a model response
- **Without `progressArgument`**: When the tool call appears in the model stream. `progress` is `undefined`
- **With `progressArgument`**: When that one argument has finished streaming. `progress` holds its string value
- **Late or absent progress argument**: Standard Agents Builder scans the first 8,192 characters of the streamed arguments for the progress argument. When the argument completes after that point, or the model does not send it, the hook runs after the model response ends. `progress` then holds the value if the complete arguments contain it as a string, and is `undefined` otherwise
- **Scope**: Tool calls that a model response contains. A tool call that code starts with `state.queueTool()` or `state.invokeTool()` does not run this hook

Set `progressArgument` in `defineTool` to the name of a short string argument. Put that argument first in the `args` schema so that it completes early:

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

export default defineTool({
  description: 'Write a file to the workspace',
  progressArgument: 'description',
  args: z.object({
    description: z.string().describe('For example "writing index.html"'),
    path: z.string(),
    content: z.string(),
  }),
  execute: async (state, args) => {
    await state.writeFile(args.path, args.content, 'text/plain');
    return { status: 'success', result: `Wrote ${args.path}` };
  },
});
```

#### Thread event

Standard Agents Builder sends a `tool_call_started` thread event to connected clients before it runs the hooks. The event data is `{ id, name, progress }`. `id` is the tool call ID. The event is sent when no `tool_call_started` hook is selected too.

#### Errors and time limit

- An error that the hook throws is logged with the `[Hooks] ✗` prefix. The remaining `tool_call_started` hooks run, and the tool call continues.
- The model stream waits for the `tool_call_started` hooks of one tool call for 30 seconds at most. After 30 seconds the stream continues, and the hook is not cancelled.

#### Examples

### Log Tool Starts

```typescript
import { defineHook } from '@standardagents/spec';

export default defineHook({
  hook: 'tool_call_started',
  id: 'log_tool_start',
  execute: async (state, toolCall, progress) => {
    console.log(`[${state.threadId}] Started ${toolCall.function.name}`, progress ?? '');
  },
});
```

---

### tool_call_done

Observes a tool call after it has executed. The return value is ignored.

- Signature (function)

  ```typescript
  defineHook({
    hook: 'tool_call_done',
    id: 'my_done_observer',
    execute: async (state, toolCall, toolResult, context) => {
      // No return value
    },
  })
  ```

* state (ThreadState)

  Full execution context

- toolCall (HookToolCall)

  The tool call that executed, with its complete arguments

* toolResult (HookToolResult)

  The result that the tool returned. `status` is `'success'` or `'error'`

- context (HookExecutionContext)

  The reason the hook is active: `{ source: { kind: 'prompt' | 'agent' | 'child', ... } }`

* Returns (Promise\<void>)

  No return value

#### When It Runs

- **Timing**: After the tool has executed, before `after_tool_call_success` or `after_tool_call_failure`, and before `before_store_tool_result`
- **Frequency**: Once for each tool call that executed, for a success result and for an error result
- **Result**: The result that the tool returned. Changes that later hooks make are not visible here
- **Scope**: Tool calls from a model response and tool calls that code queues with `state.queueTool()`. A tool that code runs with `state.invokeTool()` returns its result to the caller and does not run this hook
- **Exception**: A tool call that the active prompt does not make available gets an error result without execution. This hook does not run for that call

#### Thread event

Standard Agents Builder sends a `tool_call_done` thread event to connected clients before it runs the hooks. The event data is `{ id, name, status }`. `id` matches the `id` of the `tool_call_started` event for the same call. The event is sent when no `tool_call_done` hook is selected too.

#### Errors

An error that the hook throws is logged with the `[Hooks] ✗` prefix. The remaining `tool_call_done` hooks run, and the tool result is stored unchanged by the error. The next tool call waits until the `tool_call_done` hooks have finished.

#### Examples

### Log Tool Results

```typescript
import { defineHook } from '@standardagents/spec';

export default defineHook({
  hook: 'tool_call_done',
  id: 'log_tool_done',
  execute: async (state, toolCall, toolResult) => {
    if (toolResult.status === 'error') {
      console.error(`${toolCall.function.name} failed: ${toolResult.error}`);
      return;
    }
    console.log(`${toolCall.function.name} succeeded`);
  },
});
```

---

### User-scope hooks

`user_client_attached`, `user_client_detached`, and `user_value_changed` run in the user scope. They receive a `UserState` in place of a `ThreadState`, and they take no `context` argument. See [User lifecycle hooks](/docs/core-concepts/users#user-lifecycle-hooks) for their signatures and behavior.

## Common Types

### ThreadState

Thread hooks receive the following execution context. User-scope hooks receive
`UserState`.

```typescript
interface ThreadState {
  // Identity (readonly)
  readonly threadId: string;
  readonly agentId: string;
  readonly userId: string | null;
  readonly createdAt: number;
  readonly arguments: Record<string, unknown>;

  // Execution State (null at rest)
  execution: ExecutionState | null;

  // Context Storage (current execution only)
  context: Record<string, unknown>;

  // Methods
  getMessages(options?): Promise<MessagesResult>;
  injectMessage(input): Promise<Message>;
  queueTool(toolName, args): void;
  emit(event, data): void;
}
```

## Error Handling

Hook error behavior depends on where the hook runs:

- A `before_create_message` or `before_update_message` hook that throws prevents the write. The remaining hooks of that type do not run for that write.
- `state.injectMessage()` and `state.updateMessage()` reject with the hook error. Nothing is persisted.
- A sent message is not stored and no turn starts. The thread status becomes `failed` with reason `message_hook_error`. A client tool result that is ready for the same side is still continued.
- A queued message is not stored and leaves the queue. The run continues with the other queued messages. A run that started only for queued messages starts no turn when every one is prevented, and the thread status becomes `failed` with reason `message_hook_error`.
- A tool call queued on an idle thread with `state.queueTool()` is not stored. The tool does not run, and the run settles as `failed`.
- An agent-handoff user message, a generated-asset path message, and the repeated tool error notice are not stored. The handoff and the stop still happen.
- Three writes continue when a pre-write hook throws: the message of a model step, the failed status of a model step, and a tool result message. An execution cannot go on without these rows. Standard Agents Builder logs the error with the `[Hooks] ✗` prefix and continues with the last good value. Return a modified value to change these messages.
- A `prefilter_llm_history` hook can stop the current step by throwing an error with `block: true`. Standard Agents Builder stores its message as an assistant message and stops with reason `hook_blocked`. The provider request does not run.
- Other hook errors are logged. Execution continues with the last successful result in the hook chain.
- Post-write hook failures leave the stored message intact.

### Example Error Flow

```typescript
// Your hook throws an error
export default defineHook({
  hook: 'before_create_message',
  id: 'buggy_hook',
  execute: async (state, message) => {
    throw new Error('Something went wrong!');
  },
});

// In a tool that runs while this hook is selected:
try {
  await state.injectMessage({ role: 'system', content: 'Context loaded' });
} catch (error) {
  // The injection rejected. No message was persisted.
}
```

## File Structure

```
agents/
└── hooks/
    ├── limit_messages.ts
    ├── remove_failed_tools.ts
    ├── log_analytics.ts
    ├── sanitize_pii.ts
    └── alert_errors.ts
```

**Requirements:**

- File names can be anything (hook is identified by its `id`)
- Default export required
- Use `defineHook` for type safety
- Multiple hooks of the same type are supported (each with a unique `id`)

## Related Documentation

- [Hooks Overview](/docs/core-concepts/hooks)

  High-level hooks concepts and scoping

* [ThreadState](/docs/core-concepts/flowstate)

  ThreadState utility methods

- [Tools](/docs/api-reference/tools)

  Create custom function tools
