Agent SDK

Event Handlers

Customize agent behavior with event handlers. Intercept messages, add pre-processing, and respond to the tool execution lifecycle.


onMessage Handler

The onMessage handler runs before the LLM processes a user message. It lets you:

  • Add custom routing logic
  • Validate or transform input
  • Short-circuit with a direct response
  • Log or track messages
TypeScript
const agent = createAgent({
  name: "Support Agent",
  systemPrompt: "...",
  tools: [tools.webSearch],

  onMessage: async (message, context) => {
    // message: string — the user's message text
    // context: { messages, metadata } — conversation context

    // Return null → proceed with normal AI processing
    // Return string → skip AI, use this as the response

    // Example: Route specific keywords directly
    if (message.toLowerCase().includes("pricing")) {
      return "Our pricing starts at free! Visit /pricing for details.";
    }

    // Example: Log all messages
    console.log(`User said: ${message}`);

    // Proceed with AI processing
    return null;
  },
});

Parameters

ParameterTypeDescription
messagestringThe user's message text
contextobjectContains messages (conversation history) and metadata (arbitrary key-value pairs)

Return Value

  • null — Continue with standard AI processing
  • string — Skip the LLM entirely and return this text as the response

Tool Execution Lifecycle

When the LLM decides to call a tool, the following sequence occurs:

  1. LLM requests tool call — The model returns a function call with name and arguments
  2. Arguments validated — The tool's Zod schema validates the arguments
  3. Tool executes — The tool's execute() function runs
  4. Result sent back — The tool's return value is sent to the LLM
  5. Loop continues — Steps 1-4 repeat if the LLM needs more tool calls
  6. Final response — The LLM generates a text response incorporating all tool results

Each tool call is tracked in the AgentOutput.toolCalls array:

TypeScript
const result = await handler(input);

for (const call of result.toolCalls ?? []) {
  console.log(`Tool: ${call.name}`);
  console.log(`Args: ${JSON.stringify(call.args)}`);
  console.log(`Duration: ${call.durationMs}ms`);
  console.log(`Result: ${JSON.stringify(call.result)}`);
}

Context Messages

Pass conversation history via context.messages to give the agent memory of the conversation:

TypeScript
const result = await handler({
  taskId: "task-001",
  userId: "user-001",
  userEmail: "user@example.com",
  data: { query: "Now show me the top 5" },
  integrations: {},
  context: {
    messages: [
      { role: "user", content: "Analyze Q3 revenue by region" },
      { role: "assistant", content: "Here's the Q3 revenue breakdown..." },
      { role: "user", content: "Now show me the top 5" },
    ],
  },
});

The platform automatically includes the last 20 messages as context in chat conversations. In the Code Editor, you can manually control what context to pass.

Integration Flags

The integrations field in AgentInput tells the agent which third-party services the user has connected:

TypeScript
integrations: {
  googleDrive: true,      // Can access Google Drive files
  googleCalendar: true,   // Can read/write calendar events
  gmail: true,            // Can send/read emails
  notion: false,          // Not connected
  slack: false,           // Not connected
}

Use these flags to conditionally enable tool functionality:

TypeScript
onMessage: async (message, context) => {
  if (!input.integrations.googleCalendar) {
    return "Please connect your Google Calendar first in Settings.";
  }
  return null;
},

Error Handling

If a tool execution fails, the error is caught and sent back to the LLM, which can:

  • Try a different tool
  • Retry with different arguments
  • Report the error to the user

The AgentOutput.success field indicates whether the overall execution succeeded. Tool-level errors are recorded in individual ToolCall results.

Common Patterns

Input Validation

TypeScript
onMessage: async (message) => {
  if (message.length > 10000) {
    return "Message too long. Please keep messages under 10,000 characters.";
  }
  if (message.trim().length === 0) {
    return "Please enter a message.";
  }
  return null;
},

Feature Gating

TypeScript
onMessage: async (message, context) => {
  const isPremiumQuery = message.includes("detailed analysis");
  if (isPremiumQuery && !context.metadata?.isPremium) {
    return "Detailed analysis is a premium feature. Upgrade to access.";
  }
  return null;
},

Logging & Analytics

TypeScript
onMessage: async (message) => {
  // Track usage without modifying behavior
  await trackEvent("agent_message", { length: message.length });
  return null; // Always proceed with AI processing
},

Next Steps