Skip to main content

What are Hooks?

Hooks are optional functions that run automatically at specific points during agent execution. They allow you to modify data, perform side effects, and integrate with external systems without modifying the framework code.

How Hooks Work

Hooks are functions that receive execution context (ThreadState) and data, then either:
  1. Transform data and return modified version (transformation hooks)
  2. Perform side effects without returning anything (event hooks)
All hooks are wrapped in error handling - if a hook throws an error, it’s logged but execution continues with original data.

Defining Hooks

Hooks are defined using defineHook with a unique identifier:
import { defineHook } from '@standardagents/spec';

export default defineHook({
  hook: 'filter_messages',
  id: 'remove_failed_tools',
  execute: async (state, messages) => {
    return messages.filter(m => m.role !== 'tool' || m.tool_status !== 'error');
  },
});
Each hook requires three properties:
PropertyTypeDescription
hookHookNameThe hook type (e.g., 'filter_messages')
idstringUnique identifier for this hook (snake_case)
executeFunctionThe hook implementation, typed based on hook

Hook ID Requirements

  • IDs must be unique across all hooks in the project
  • IDs must be snake_case (lowercase letters, numbers, underscores, starting with a letter)
  • IDs should be descriptive of what the hook does
Benefits of defineHook:
  • Strict typing: Parameters automatically typed based on hook type
  • IntelliSense support: Full autocomplete in your editor
  • Type checking: Catch errors at compile time

Hook Scoping

Hooks must be explicitly declared on prompts or agents to execute. Undeclared hooks do not run.

Prompt-Level Hooks

import { definePrompt } from '@standardagents/spec';

export default definePrompt({
  name: 'customer_support',
  // ... other config ...
  hooks: ['remove_failed_tools', 'log_tool_calls'],
});
Prompt hooks execute when that prompt is the active execution context.

Agent-Level Hooks

import { defineAgent } from '@standardagents/spec';

export default defineAgent({
  name: 'support_agent',
  // ... other config ...
  hooks: ['inject_context', 'sanitize_output'],
});
Agent hooks are a fallback — they execute when the active prompt has no hooks defined.

Resolution Priority

  1. If the current prompt declares hooks, only those hooks run
  2. If the prompt has no hooks but the agent does, agent hooks run
  3. If neither declares hooks, no hooks execute
This means hooks in agents/hooks/ that aren’t referenced by any prompt or agent will never run. Always add hook IDs to the relevant prompt or agent.

Multiple Hooks of Same Type

You can define multiple hooks of the same hook type with different IDs:
// agents/hooks/limit_messages.ts
export default defineHook({
  hook: 'filter_messages',
  id: 'limit_to_20_messages',
  execute: async (state, messages) => messages.slice(-20),
});

// agents/hooks/remove_system.ts
export default defineHook({
  hook: 'filter_messages',
  id: 'remove_system_messages',
  execute: async (state, messages) => messages.filter(m => m.role !== 'system'),
});
When both are included in a prompt’s hooks array, they execute in the order listed:
hooks: ['limit_to_20_messages', 'remove_system_messages']

Hook Types

Transformation Hooks

These hooks receive data, modify it, and return the modified version:
import { defineHook } from '@standardagents/spec';

export default defineHook({
  hook: 'filter_messages',
  id: 'keep_completed_only',
  execute: async (state, rows) => {
    return rows.filter(row => row.status === 'completed');
  },
});
Available transformation hooks:
  • filter_messages - Filter message rows before transformation
  • prefilter_llm_history - Modify messages before sending to LLM
  • before_create_message - Modify message before database insert
  • before_update_message - Modify updates before applying
  • before_store_tool_result - Modify tool result before storage
  • after_tool_call_success - Modify or remove successful tool results
  • after_tool_call_failure - Modify or remove failed tool results

Event Hooks

These hooks run after an event occurs and don’t return anything:
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);
  },
});
Available event hooks:
  • after_create_message - After message inserted
  • after_update_message - After message updated

Common Use Cases

Message Filtering

Filter out unwanted messages before they’re sent to the LLM:
import { defineHook } from '@standardagents/spec';

export default defineHook({
  hook: 'filter_messages',
  id: 'remove_failed_tool_messages',
  execute: async (state, rows) => {
    return rows.filter(row => {
      if (row.role === 'tool' && row.tool_status === 'error') {
        return false;
      }
      return true;
    });
  },
});

External Logging

Send events to external analytics services:
import { defineHook } from '@standardagents/spec';

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

Tool Call Transformation

Convert tool calls to user messages:
import { defineHook } from '@standardagents/spec';
import { injectMessage } from '@standardagents/builder';

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

Message Enrichment

Add context to messages before storage:
import { defineHook } from '@standardagents/spec';

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

Sanitize Tool Results

Clean sensitive data from tool results before storage:
import { defineHook } from '@standardagents/spec';

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

Available Hooks Quick Reference

HookTypeWhen It RunsUse Case
filter_messagesTransformBefore SQL → Message conversionFilter/modify message rows
prefilter_llm_historyTransformBefore sending to LLMLimit context, add dynamic data
before_create_messageTransformBefore INSERTAdd metadata, modify content
before_update_messageTransformBefore UPDATEValidate changes, add timestamps
before_store_tool_resultTransformBefore storing tool resultSanitize results, add metadata
after_create_messageEventAfter INSERTExternal logging, webhooks
after_update_messageEventAfter UPDATETrack status changes
after_tool_call_successTransformAfter successful toolModify results, convert to messages
after_tool_call_failureTransformAfter failed toolEnhance errors, suppress failures

File Organization

Hooks are auto-discovered from the agents/hooks/ directory:
agents/
└── hooks/
    ├── limit_messages.ts
    ├── remove_failed_tools.ts
    ├── log_analytics.ts
    └── sanitize_pii.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)

Error Handling

All hooks are wrapped in error handling that:
  1. Catches exceptions without breaking execution
  2. Logs errors with [Hooks] ✗ prefix
  3. Returns original data as fallback
// Your hook throws an error
export default defineHook({
  hook: 'filter_messages',
  id: 'buggy_filter',
  execute: async (state, rows) => {
    throw new Error('Something went wrong!');
  },
});

// Framework behavior:
// 1. Catches error
// 2. Logs: [Hooks] ✗ Error running filter_messages hook: Something went wrong!
// 3. Returns original rows (unmodified)
// 4. Continues execution normally
Always wrap risky operations (API calls, etc.) in try-catch blocks to handle errors gracefully.

Best Practices

Hooks run in the critical execution path. Keep them fast (< 100ms ideal):
// Good - quick operation
export default defineHook({
  hook: 'filter_messages',
  id: 'quick_filter',
  execute: async (state, rows) => {
    return rows.filter(row => row.status === 'completed');
  },
});

// Avoid - slow operation
export default defineHook({
  hook: 'filter_messages',
  id: 'slow_filter',
  execute: async (state, rows) => {
    for (const row of rows) {
      await slowExternalAPI(row);
    }
    return rows;
  },
});
Wrap risky operations in try-catch:
export default defineHook({
  hook: 'after_create_message',
  id: 'safe_analytics',
  execute: async (state, message) => {
    try {
      await fetch('https://api.example.com/log', {
        method: 'POST',
        body: JSON.stringify(message),
      });
    } catch (error) {
      console.error('[Hook] External API failed:', error);
    }
  },
});
Check data structure before processing:
export default defineHook({
  hook: 'before_create_message',
  id: 'trim_content',
  execute: async (state, message) => {
    if (!message || !message.content) {
      return message;
    }
    message.content = message.content.trim();
    return message;
  },
});
Only add hooks to prompts/agents that need them:
// Prompt that needs message filtering
definePrompt({
  name: 'support_prompt',
  hooks: ['limit_to_20_messages', 'remove_failed_tools'],
  // ...
});

// Prompt that doesn't need hooks — leave hooks undefined
definePrompt({
  name: 'simple_responder',
  // No hooks property — no hooks run
  // ...
});

ThreadState Context

All hooks receive a ThreadState object containing execution context:
interface ThreadState {
  // Identity (readonly)
  readonly threadId: string;
  readonly agentId: string;
  readonly userId: string | null;
  readonly createdAt: number;

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

  // Context Storage
  context: Record<string, unknown>;

  // Methods
  getMessages(options?): Promise<MessagesResult>;
  injectMessage(input): Promise<Message>;
  queueTool(toolName, args): void;
  emit(event, data): void;
  // ... and more
}
Use ThreadState to:
  • Check which agent is executing (state.agentId)
  • Access execution state (state.execution?.stepCount)
  • Get/inject messages
  • Emit events to frontend
  • Access the file system

Next Steps

Hooks API Reference

Complete hooks specification

ThreadState

Learn about ThreadState interface

Tools

Create custom tools

Examples

See hooks in action