Skip to content

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.

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

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.

Parameters

hookHookNamerequired

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

idstringrequired

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.

executeFunctionrequired

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:

// 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 resolves lifecycle hooks from the agent's hooks array.

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.

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

Current execution context

rowsMessage[]

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.

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

Current execution context

messagesLLMMessage[]

Messages about to be sent to LLM

Returns: LLMMessage[] - Modified messages

before_create_message

Runs before a message is inserted into the database.

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

Current execution context

messageMessage

Message about to be created

Returns: Message - Modified message

before_update_message

Runs before a message is updated in the database.

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() },
    };
  },
});
stateThreadState

Current execution context

messageIdstring

ID of message being updated

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

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

Current execution context

toolCallHookToolCall

The tool call that was executed

toolResultHookToolResult

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.

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
  },
});
stateThreadState

Current execution context

callToolCall

The tool call that was executed

resultToolResult

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.

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.`,
    };
  },
});
stateThreadState

Current execution context

callToolCall

The tool call that failed

errorToolResult

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.

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,
      }),
    });
  },
});
stateThreadState

Current execution context

messageMessage

The created message

Returns: void

after_update_message

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

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

Current execution context

messageMessage

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.

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

Current execution context

toolCallHookToolCall

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

progressstring | undefined

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

contextHookExecutionContext

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

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

Current execution context

toolCallHookToolCall

The tool call that executed

toolResultHookToolResult

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

contextHookExecutionContext

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