# defineHook

Define lifecycle hooks for agent execution

## Overview

`defineHook` creates a hook that runs at specific points during agent execution. Each hook has a unique `id`. A hook runs when a prompt, an agent, or an enabled direct child's `parentHooks` declaration references it.

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

export default defineHook({
  hook: 'after_create_message',
  id: 'log_message_creation',
  execute: async (state, message) => {
    console.log('Message created:', message.id);
  },
});
```

## Type Definition

```typescript
function defineHook<K extends HookName>(
  options: HookDefinitionOptions<K>
): HookDefinitionResult<K>;

interface HookDefinitionOptions<K extends HookName> {
  hook: K;
  id: string;
  execute: HookSignatures[K];
}

interface HookDefinitionResult<K extends HookName> {
  hook: K;
  id: string;
  execute: HookSignatures[K];
}

type HookName =
  | 'after_thread_created'
  | 'after_subagent_created'
  | 'after_system_message'
  | 'filter_messages'
  | 'prefilter_llm_history'
  | 'before_create_message'
  | 'before_update_message'
  | 'after_create_message'
  | 'after_update_message'
  | 'before_store_tool_result'
  | 'after_tool_call_success'
  | 'after_tool_call_failure'
  | 'tool_call_started'
  | 'tool_call_done'
  | 'user_client_attached'
  | 'user_client_detached'
  | 'user_value_changed';
```

The last three names are user-scope hooks. They receive a `UserState` in place of a `ThreadState`. See [User lifecycle hooks](/docs/core-concepts/users#user-lifecycle-hooks).

## Parameters

- hook (HookName), required

  The hook type. Determines when the hook runs and what parameters `execute` receives.

* id (string), required

  Unique identifier for this hook. Must be snake_case (lowercase letters, numbers, underscores, starting with a letter). Referenced by prompts and agents in their `hooks` array.

- execute (Function), required

  Async function that executes when the hook is triggered. Parameters are automatically typed based on the `hook` type.

## Hook Scoping

A prompt or agent references hooks by ID:

```typescript
// Prompt references hooks by ID
definePrompt({
  name: 'customer_support',
  hooks: ['log_message_creation', 'limit_to_20_messages'],
  // ...
});

// Agent hooks apply when the prompt's hooks array is omitted or empty
defineAgent({
  name: 'support_agent',
  hooks: ['inject_context'],
  // ...
});
```

**Resolution:** A non-empty prompt `hooks` array selects the parent base set. An omitted or empty array uses the agent's hooks. Required child hooks run before or after that base set according to their stage. At rest, when no prompt is running, the [Standard Agent Specification](https://standardagentspec.org/0.2.0/specification/hooks) resolves lifecycle hooks from the agent's `hooks` array.

> **Note:**
>
> Standard Agents Builder does not follow this rule yet. At rest, it uses the side A prompt's hooks when that prompt has a non-empty `hooks` array, and the agent's hooks otherwise. For lifecycle hooks that must run at rest in every runtime, put them in the agent's `hooks` array and leave the side A prompt's `hooks` array omitted or empty. A non-empty side A prompt list replaces the agent's hooks in Standard Agents Builder.

Each thread hook receives `HookExecutionContext` as its final argument.
`context.source` identifies a prompt, an agent, or a child relationship.

## Hook Types

### Transformation Hooks

These hooks receive data, can modify it, and return the modified version.

#### filter_messages

Runs exactly once per model request on a stable snapshot of eligible persisted
history. Its result is request-local and does not replace
`state.execution.messageHistory`. Messages injected while it runs enter the
following snapshot.

```typescript
defineHook({
  hook: 'filter_messages',
  id: 'keep_completed',
  execute: async (state, rows) => {
    return rows.filter(row => row.status === 'completed');
  },
});
```

- state (ThreadState)

  Current execution context

* rows (Message\[])

  Messages from storage

**Returns:** `Message[]` - Filtered/modified messages

#### prefilter_llm_history

Runs after provider-message transformation and before dispatch. Its changes are
request-local. Messages injected while it runs enter the following snapshot.

```typescript
defineHook({
  hook: 'prefilter_llm_history',
  id: 'add_dynamic_context',
  execute: async (state, messages) => {
    return [
      { role: 'system', content: `Current time: ${new Date().toISOString()}` },
      ...messages,
    ];
  },
});
```

- state (ThreadState)

  Current execution context

* messages (LLMMessage\[])

  Messages about to be sent to LLM

**Returns:** `LLMMessage[]` - Modified messages

#### before_create_message

Runs before a message is inserted into the database.

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

- state (ThreadState)

  Current execution context

* message (Message)

  Message about to be created

**Returns:** `Message` - Modified message

#### before_update_message

Runs before a message is updated in the database.

```typescript
defineHook({
  hook: 'before_update_message',
  id: 'tag_updated_message',
  execute: async (state, messageId, updates) => {
    return {
      ...updates,
      metadata: { ...updates.metadata as Record<string, unknown>, updated_at: Date.now() },
    };
  },
});
```

- state (ThreadState)

  Current execution context

* messageId (string)

  ID of message being updated

- updates (Record\<string, unknown>)

  Updates being applied

**Returns:** `Record<string, unknown>` - Modified updates

#### before_store_tool_result

Runs before a tool result is stored in the database.

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

- state (ThreadState)

  Current execution context

* toolCall (HookToolCall)

  The tool call that was executed

- toolResult (HookToolResult)

  The result to be stored

**Returns:** `HookToolResult` - Modified tool result

#### after_tool_call_success

Runs after a tool executes successfully. Can modify the result, or add messages beside it. Returning `null` keeps the result the hook was given; the tool call is never removed from history.

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

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; // The tool call and its result stay in history
  },
});
```

- state (ThreadState)

  Current execution context

* call (ToolCall)

  The tool call that was executed

- result (ToolResult)

  Result from the tool

**Returns:** `ToolResult | null` - Modified result, or null to keep the current result. Later hooks still run

#### after_tool_call_failure

Runs after a tool fails. Can modify the error result. Returning `null` keeps the error result the hook was given.

```typescript
defineHook({
  hook: 'after_tool_call_failure',
  id: 'friendly_errors',
  execute: async (state, call, error) => {
    return {
      status: 'error',
      error: `The ${call.function.name} operation is temporarily unavailable.`,
    };
  },
});
```

- state (ThreadState)

  Current execution context

* call (ToolCall)

  The tool call that failed

- error (ToolResult)

  The error result

**Returns:** `ToolResult | null` - Modified error result, or null to keep the current error result. Later hooks still run

### Event Hooks

These hooks run after an event and don't return anything.

#### after_create_message

Runs after a message is inserted. Use for logging, analytics, or webhooks.

```typescript
defineHook({
  hook: 'after_create_message',
  id: 'log_messages',
  execute: async (state, message) => {
    await fetch('https://analytics.example.com/events', {
      method: 'POST',
      body: JSON.stringify({
        event: 'message_created',
        thread_id: state.threadId,
        role: message.role,
      }),
    });
  },
});
```

- state (ThreadState)

  Current execution context

* message (Message)

  The created message

**Returns:** `void`

#### after_update_message

Runs after a message is updated. Use to track status changes.

```typescript
defineHook({
  hook: 'after_update_message',
  id: 'track_updates',
  execute: async (state, message) => {
    console.log(`Message ${message.id} updated`);
  },
});
```

- state (ThreadState)

  Current execution context

* message (Message)

  The updated message

**Returns:** `void`

#### tool_call_started

Runs once for each tool call in a model response, while the response streams and before the tool executes. The return value is ignored.

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

- state (ThreadState)

  Current execution context

* toolCall (HookToolCall)

  The tool call that starts. `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

**Returns:** `void`

A tool without `progressArgument` starts the hook when the tool call appears in the model stream. A tool with `progressArgument` starts the hook when that argument has finished streaming. See the [Hooks API Reference](/docs/api-reference/hooks#tool_call_started) for the full timing.

#### tool_call_done

Runs once after a tool call has executed, for a success result and for an error result. It runs before `after_tool_call_success` or `after_tool_call_failure`. The return value is ignored.

```typescript
defineHook({
  hook: 'tool_call_done',
  id: 'log_tool_done',
  execute: async (state, toolCall, toolResult) => {
    console.log(`Finished ${toolCall.function.name}: ${toolResult.status}`);
  },
});
```

- state (ThreadState)

  Current execution context

* toolCall (HookToolCall)

  The tool call that executed

- toolResult (HookToolResult)

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

* context (HookExecutionContext)

  The reason the hook is active

**Returns:** `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.
- An error in any other hook is logged. Execution continues with the data that the hook received.
- Post-write hook failures leave the stored message intact.

```typescript
// This hook throws. Execution continues with the original rows.
defineHook({
  hook: 'filter_messages',
  id: 'buggy_filter',
  execute: async (state, rows) => {
    throw new Error('Something went wrong!');
  },
});

// Framework behavior:
// 1. Logs: [Hooks] ✗ Error running filter_messages hook: Something went wrong!
// 2. Returns original rows (unmodified)
// 3. Continues execution normally
```

## File Location

Hooks are auto-discovered from `agents/hooks/`:

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

**Requirements:**

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

## Quick Reference

| Hook                       | Type               | When It Runs                                            |
| -------------------------- | ------------------ | ------------------------------------------------------- |
| `after_thread_created`     | Event              | After thread creation, before execution                 |
| `after_subagent_created`   | Event              | On parent after subagent thread creation                |
| `after_system_message`     | Transform          | After system message render                             |
| `filter_messages`          | Transform          | Before message transformation                           |
| `prefilter_llm_history`    | Transform          | Before sending to LLM                                   |
| `before_create_message`    | Transform          | Before INSERT                                           |
| `before_update_message`    | Transform          | Before UPDATE                                           |
| `before_store_tool_result` | Transform          | Before storing tool result                              |
| `after_create_message`     | Event              | After INSERT                                            |
| `after_update_message`     | Event              | After UPDATE                                            |
| `after_tool_call_success`  | Transform          | After successful tool                                   |
| `after_tool_call_failure`  | Transform          | After failed tool                                       |
| `tool_call_started`        | Event              | When a tool call starts to stream, before the tool runs |
| `tool_call_done`           | Event              | After a tool call has executed                          |
| `user_client_attached`     | Event (user scope) | A client connects to the user event stream              |
| `user_client_detached`     | Event (user scope) | A client disconnects from the user event stream         |
| `user_value_changed`       | Event (user scope) | A user value changes                                    |
