Skip to main content

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.
All hooks should use the defineHook utility for strict typing and better developer experience.

Using defineHook

import { defineHook } from '@standardagents/builder';

export default defineHook('filter_messages', async (state, rows) => {
  // TypeScript knows exactly what state and rows are!
  return rows;
});

Available Hooks

filter_messages

Filters or modifies SQL row data before it’s transformed to chat completion format.
Signature
function
defineHook('filter_messages', async (state, rows) => {
  return rows;
})
state
FlowState
Full execution context
rows
MessageRow[]
Array of SQL rows from messages table
Returns
Promise<MessageRow[]>
The filtered/modified row array

MessageRow Structure

interface MessageRow {
  id: string;
  role: 'system' | 'user' | 'assistant' | 'tool';
  content: string | null;
  name: string | null;
  tool_calls: string | null;          // JSON string of tool calls
  tool_call_id: string | null;        // For role='tool' messages
  log_id: string | null;              // Reference to logs table
  created_at: number;                 // Microseconds timestamp
  request_sent_at: number | null;
  response_completed_at: number | null;
  status: 'pending' | 'completed' | 'failed' | null;
  silent: number | null;              // 1 if hidden, 0/null otherwise
  tool_status: 'success' | 'error' | null;
}

When It Runs

  • Timing: Before SQL rows are transformed to chat completion format
  • Frequency: Every turn that loads message history
  • Context: After rows fetched from SQLite, before transformation

Examples

import { defineHook } from '@standardagents/builder';

export default defineHook('filter_messages', async (state, rows) => {
  return rows.filter(row => {
    if (row.role === 'tool' && row.tool_status === 'error') {
      return false;
    }
    return true;
  });
});
import { defineHook } from '@standardagents/builder';

export default defineHook('filter_messages', async (state, rows) => {
  // Only keep messages from last 24 hours
  const dayAgo = (Date.now() - 24 * 60 * 60 * 1000) * 1000; // microseconds
  return rows.filter(row => row.created_at >= dayAgo);
});
import { defineHook } from '@standardagents/builder';

export default defineHook('filter_messages', async (state, rows) => {
  if (state.agentConfig.title === 'Production Agent') {
    // Remove all error messages for cleaner context
    return rows.filter(row => row.tool_status !== 'error');
  }
  return rows;
});

prefilter_llm_history

Modifies the message history before it’s sent to the LLM.
Signature
function
defineHook('prefilter_llm_history', async (state, messages) => {
  return messages;
})
state
FlowState
Full execution context
messages
Message[]
Array of messages about to be sent to LLM
Returns
Promise<Message[]>
The filtered/modified message array

Message Structure

interface Message {
  role: 'system' | 'user' | 'assistant' | 'tool';
  content: string | null;
  tool_calls?: string | null;
  tool_call_id?: string | null;
  name?: string | null;
}

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

import { defineHook } from '@standardagents/builder';

export default defineHook('prefilter_llm_history', async (state, messages) => {
  // Keep only last 20 messages
  return messages.slice(-20);
});
import { defineHook } from '@standardagents/builder';

export default defineHook('prefilter_llm_history', async (state, messages) => {
  // Inject current turn count into system prompt
  const systemMsg = messages.find(m => m.role === 'system');
  if (systemMsg && systemMsg.content) {
    systemMsg.content += `\n\nCurrent turn: ${state.turnCount}/25`;
  }
  return messages;
});
import { defineHook } from '@standardagents/builder';

export default defineHook('prefilter_llm_history', async (state, messages) => {
  // Keep system messages and recent non-tool messages
  const systemMessages = messages.filter(m => m.role === 'system');
  const otherMessages = messages
    .filter(m => m.role !== 'system' && m.role !== 'tool')
    .slice(-10);

  return [...systemMessages, ...otherMessages];
});

post_process_message

Modifies assistant messages after LLM response but before storage.
This hook is defined but not yet invoked in FlowEngine. It will be activated in a future update.
Signature
function
defineHook('post_process_message', async (state, message) => {
  return message;
})
state
FlowState
Full execution context
message
Message
The assistant’s message from LLM
Returns
Promise<Message>
The modified message

Examples

import { defineHook } from '@standardagents/builder';

export default defineHook('post_process_message', async (state, message) => {
  if (message.content) {
    message.content = message.content
      .trim()
      .replace(/\n{3,}/g, '\n\n');  // Max 2 consecutive newlines
  }
  return message;
});

before_create_message

Modifies any message before it’s inserted into the database.
Signature
function
defineHook('before_create_message', async (state, message) => {
  return message;
})
state
FlowState
Full execution context
message
CreateMessage
Message about to be created
Returns
Promise<CreateMessage>
The modified message

CreateMessage Structure

interface CreateMessage {
  id: string;
  role: 'system' | 'user' | 'assistant' | 'tool';
  content: string | null;
  tool_calls?: string | null;
  tool_call_id?: string | null;
  name?: string | null;
  created_at: number;
  status?: 'pending' | 'completed' | 'failed';
  silent?: boolean;
}

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

import { defineHook } from '@standardagents/builder';

export default defineHook('before_create_message', async (state, message) => {
  // Tag messages with agent name
  if (message.role === 'assistant') {
    message.name = state.agentConfig.title;
  }
  return message;
});
import { defineHook } from '@standardagents/builder';

export default defineHook('before_create_message', async (state, message) => {
  // Add prefix based on role
  if (message.content && message.role === 'user') {
    message.content = `[User] ${message.content}`;
  }
  return message;
});
import { defineHook } from '@standardagents/builder';

export default defineHook('before_create_message', async (state, message) => {
  // Only modify for specific agent
  if (state.agentConfig.title === 'Customer Support') {
    if (message.role === 'assistant' && message.content) {
      // Add signature to all responses
      message.content += '\n\nBest regards,\nSupport Team';
    }
  }
  return message;
});

after_create_message

Runs after a message is successfully inserted into the database.
Signature
function
defineHook('after_create_message', async (state, message) => {
  // No return value
})
state
FlowState
Full execution context
message
CreateMessage
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

import { defineHook } from '@standardagents/builder';

export default defineHook('after_create_message', async (state, message) => {
  // Send to analytics service
  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.agentConfig.id,
          message_id: message.id,
          content_length: message.content.length,
        }),
      });
    } catch (error) {
      console.error('Analytics failed:', error);
    }
  }
});
import { defineHook } from '@standardagents/builder';

export default defineHook('after_create_message', async (state, message) => {
  // Notify webhook on user messages
  if (message.role === 'user') {
    await fetch('https://webhook.example.com/new-message', {
      method: 'POST',
      body: JSON.stringify({
        threadId: state.threadId,
        content: message.content,
      }),
    });
  }
});

before_update_message

Modifies message updates before they’re applied to the database.
Signature
function
defineHook('before_update_message', async (state, messageId, updates) => {
  return updates;
})
state
FlowState
Full execution context
messageId
string
ID of message being updated
updates
MessageUpdates
Fields being updated
Returns
Promise<MessageUpdates>
The modified updates object

MessageUpdates Structure

interface MessageUpdates {
  content?: string | null;
  tool_calls?: string | null;
  log_id?: string | null;
  response_completed_at?: number | null;
  status?: 'pending' | 'completed' | 'failed';
}
Updates object only contains fields being changed, not the full message.

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

import { defineHook } from '@standardagents/builder';

export default defineHook('before_update_message', async (state, messageId, updates) => {
  // Ensure completed messages have timestamp
  if (updates.status === 'completed' && !updates.response_completed_at) {
    updates.response_completed_at = Date.now() * 1000; // microseconds
  }
  return updates;
});
import { defineHook } from '@standardagents/builder';

export default defineHook('before_update_message', async (state, messageId, updates) => {
  // Clean up content on updates
  if (updates.content && typeof updates.content === 'string') {
    updates.content = updates.content.trim();
  }
  return updates;
});

after_update_message

Runs after a message is successfully updated in the database.
Signature
function
defineHook('after_update_message', async (state, messageId, updates) => {
  // No return value
})
state
FlowState
Full execution context
messageId
string
ID of message that was updated
updates
MessageUpdates
Fields that were updated
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

import { defineHook } from '@standardagents/builder';

export default defineHook('after_update_message', async (state, messageId, updates) => {
  // Log status transitions
  if (updates.status) {
    console.log(`[Status Change] Message ${messageId}: ${updates.status}`);

    // Send to monitoring
    await fetch('https://monitor.example.com/status', {
      method: 'POST',
      body: JSON.stringify({
        messageId,
        status: updates.status,
        threadId: state.threadId,
      }),
    });
  }
});

after_tool_call_success

Intercepts successful tool executions before results are stored in the database.
Signature
function
defineHook('after_tool_call_success', async (state, call, result) => {
  return result;  // or null to remove from history
})
state
FlowState
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 remove the tool call from history

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:
  1. The tool call message is removed from message history
  2. The tool result message is not stored
  3. You can inject custom messages using injectMessage()

Examples

import { defineHook, injectMessage } from "@standardagents/builder";

export default defineHook('after_tool_call_success', async (state, call, result) => {
  // Convert specific tool calls into user messages
  if (call.function.name === "get_user_input") {
    // Inject a user message with the tool's result
    await injectMessage(state, {
      role: "user",
      content: result.result || "",
    });

    // Return null to remove the tool call from history
    return null;
  }

  return result; // Pass through unchanged
});
import { defineHook } from "@standardagents/builder";

export default defineHook('after_tool_call_success', async (state, call, result) => {
  // Add metadata to all tool results
  if (result.result) {
    result.result = `[Tool: ${call.function.name}]\n${result.result}`;
  }
  return result;
});
import { defineHook } from "@standardagents/builder";

export default defineHook('after_tool_call_success', async (state, call, result) => {
  // Remove sensitive data from results
  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
defineHook('after_tool_call_failure', async (state, call, result) => {
  return result;  // or null to remove from history
})
state
FlowState
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 remove the tool call from history

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)

Examples

import { defineHook, injectMessage } from "@standardagents/builder";

export default defineHook('after_tool_call_failure', async (state, call, result) => {
  // Silently suppress "not found" errors
  if (result.error?.includes("not found")) {
    // Inject a user message instead
    await injectMessage(state, {
      role: "user",
      content: "The requested item was not found.",
    });

    // Remove the failed tool call
    return null;
  }

  return result; // Pass through other errors
});
import { defineHook } from "@standardagents/builder";

export default defineHook('after_tool_call_failure', async (state, call, result) => {
  // Add context to error messages
  if (result.error) {
    result.error = `[${call.function.name} failed] ${result.error}`;
  }
  return result;
});
import { defineHook } from "@standardagents/builder";

export default defineHook('after_tool_call_failure', async (state, call, result) => {
  // Notify external system of critical errors
  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;
});

Common Types

FlowState

Full execution context available to all hooks:
interface FlowState {
  // Identity
  threadId: string;
  flowId: string;

  // Configuration
  agentConfig: Agent;
  currentSide: 'a' | 'b';

  // Execution State
  turnCount: number;
  stopped: boolean;
  stoppedBy?: 'a' | 'b';

  // Message Context
  messageHistory: Message[];

  // Tool Execution
  sequence: {
    queue: ToolCall[];
    isHandling: boolean;
  };

  // Streaming & Telemetry
  stream: StreamManager;
  emitMessage?: (msg: any) => void;
  emitLog?: (log: any) => void;

  // Runtime Context
  env: Env;
  storage: DurableObjectStorage;
  context: Record<string, any>;

  // Retry Tracking
  retryCount: number;
  retryReason?: string;

  // Abort Control
  abortController?: AbortController;
}

Agent

Agent configuration from D1:
interface Agent {
  id: string;
  title: string;
  type: 'dual_ai' | 'ai_human';

  // Side A Configuration
  side_a_system_prompt: string;
  side_a_stop_option: 'returns_content' | 'tool';
  side_a_stop_tool?: string;

  // Side B Configuration (null for ai_human)
  side_b_system_prompt?: string;
  side_b_stop_option?: 'returns_content' | 'tool';
  side_b_stop_tool?: string;

  // Additional fields...
  max_session_turns?: number;
  expose_as_tool?: boolean;
  tool_description?: string;
}

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

Example Error Flow

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

// Framework behavior:
// 1. Catches error
// 2. Logs: [Hooks] ✗ Error running before_create_message hook: Something went wrong!
// 3. Returns original message (unmodified)
// 4. Continues execution normally

Best Practices for Error Handling

export default defineHook('after_create_message', 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);
    // Don't throw - let hook complete successfully
  }
});
export default defineHook('before_create_message', async (state, message) => {
  if (!message || !message.content) {
    console.warn('[Hook] Invalid message structure');
    return message;  // Return unchanged if invalid
  }

  // Safe to process
  message.content = message.content.trim();
  return message;
});
export default defineHook('before_update_message', async (state, messageId, updates) => {
  // Check if field exists before modifying
  if (updates.status && updates.status === 'completed') {
    updates.response_completed_at = Date.now() * 1000;
  }
  return updates;
});

File Structure

agents/
└── hooks/
    ├── filter_messages.ts
    ├── prefilter_llm_history.ts
    ├── before_create_message.ts
    ├── after_create_message.ts
    ├── before_update_message.ts
    ├── after_update_message.ts
    ├── after_tool_call_success.ts
    └── after_tool_call_failure.ts
Requirements:
  • File name must match hook name
  • One hook per file
  • Default export required
  • Use defineHook for type safety