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.
Using defineHook
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.
SignaturefunctiondefineHook({
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.
SignaturefunctiondefineHook({
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.
SignaturefunctiondefineHook({
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.
SignaturefunctiondefineHook({
hook: 'filter_messages',
id: 'my_filter',
execute: async (state, rows) => {
return rows;
},
})stateThreadStateFull execution context
rowsMessage[]Array of messages from storage
ReturnsPromise<Message[]>The filtered/modified message array
When It Runs
- Timing: After
after_system_messageand 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
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
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.
SignaturefunctiondefineHook({
hook: 'prefilter_llm_history',
id: 'my_prefilter',
execute: async (state, messages) => {
return messages;
},
})stateThreadStateFull execution context
messagesLLMMessage[]Array of messages about to be sent to LLM
ReturnsPromise<LLMMessage[]>The filtered/modified message array
LLMMessage Structure
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
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
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 for each write.
SignaturefunctiondefineHook({
hook: 'before_create_message',
id: 'my_transform',
execute: async (state, message) => {
return message;
},
})stateThreadStateFull execution context
messageHookMessageMessage about to be created
ReturnsPromise<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
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
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.
SignaturefunctiondefineHook({
hook: 'after_create_message',
id: 'my_logger',
execute: async (state, message) => {
// No return value
},
})stateThreadStateFull execution context
messageHookMessageThe message that was just created
ReturnsPromise<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
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.
SignaturefunctiondefineHook({
hook: 'before_update_message',
id: 'my_update_hook',
execute: async (state, messageId, updates) => {
return updates;
},
})stateThreadStateFull execution context
messageIdstringID of message being updated
updatesRecord<string, unknown>Fields being updated
ReturnsPromise<Record<string, unknown>>The modified updates object
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.
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
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.
SignaturefunctiondefineHook({
hook: 'after_update_message',
id: 'my_update_logger',
execute: async (state, message) => {
// No return value
},
})stateThreadStateFull execution context
messageMessageThe updated message
ReturnsPromise<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
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.
SignaturefunctiondefineHook({
hook: 'before_store_tool_result',
id: 'my_result_transform',
execute: async (state, toolCall, toolResult) => {
return toolResult;
},
})stateThreadStateFull execution context
toolCallHookToolCallThe tool call that was executed
toolResultHookToolResultThe result from the tool
ReturnsPromise<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
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
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.
SignaturefunctiondefineHook({
hook: 'after_tool_call_success',
id: 'my_success_hook',
execute: async (state, call, result) => {
return result; // or null to keep the original result
},
})stateThreadStateFull execution context
callToolCallThe tool call that was executed
resultToolResultThe successful result from the tool
ReturnsPromise<ToolResult | null>Modified result, or null to keep the original result
ToolCall Structure
interface ToolCall {
id: string;
type: "function";
function: {
name: string;
arguments: string; // JSON string
};
}ToolResult Structure
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:
- The tool call message stays in message history
- The tool result message is stored unchanged
- 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
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
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.
SignaturefunctiondefineHook({
hook: 'after_tool_call_failure',
id: 'my_failure_hook',
execute: async (state, call, result) => {
return result; // or null to keep the original result
},
})stateThreadStateFull execution context
callToolCallThe tool call that failed
resultToolResultThe error result from the tool
ReturnsPromise<ToolResult | null>Modified error result, or null to keep the original error
ToolResult Structure (Error)
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
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
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.
SignaturefunctiondefineHook({
hook: 'tool_call_started',
id: 'my_start_observer',
execute: async (state, toolCall, progress, context) => {
// No return value
},
})stateThreadStateFull execution context
toolCallHookToolCallThe tool call that starts. function.name is known. function.arguments can be empty or an incomplete JSON fragment
progressstring | undefinedThe value of the tool's progressArgument, when the tool declares one and the model has sent it
contextHookExecutionContextThe reason the hook is active: { source: { kind: 'prompt' | 'agent' | 'child', ... } }
ReturnsPromise<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.progressisundefined - With
progressArgument: When that one argument has finished streaming.progressholds 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.
progressthen holds the value if the complete arguments contain it as a string, and isundefinedotherwise - Scope: Tool calls that a model response contains. A tool call that code starts with
state.queueTool()orstate.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:
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 remainingtool_call_startedhooks run, and the tool call continues. - The model stream waits for the
tool_call_startedhooks 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
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.
SignaturefunctiondefineHook({
hook: 'tool_call_done',
id: 'my_done_observer',
execute: async (state, toolCall, toolResult, context) => {
// No return value
},
})stateThreadStateFull execution context
toolCallHookToolCallThe tool call that executed, with its complete arguments
toolResultHookToolResultThe result that the tool returned. status is 'success' or 'error'
contextHookExecutionContextThe reason the hook is active: { source: { kind: 'prompt' | 'agent' | 'child', ... } }
ReturnsPromise<void>No return value
When It Runs
- Timing: After the tool has executed, before
after_tool_call_successorafter_tool_call_failure, and beforebefore_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 withstate.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
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 for their signatures and behavior.
Common Types
ThreadState
Thread hooks receive the following execution context. User-scope hooks receive
UserState.
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_messageorbefore_update_messagehook that throws prevents the write. The remaining hooks of that type do not run for that write. state.injectMessage()andstate.updateMessage()reject with the hook error. Nothing is persisted.- A sent message is not stored and no turn starts. The thread status becomes
failedwith reasonmessage_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
failedwith reasonmessage_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 asfailed. - 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_historyhook can stop the current step by throwing an error withblock: true. Standard Agents Builder stores its message as an assistant message and stops with reasonhook_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
// 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.tsRequirements:
- File names can be anything (hook is identified by its
id) - Default export required
- Use
defineHookfor type safety - Multiple hooks of the same type are supported (each with a unique
id)