Agent SDK

Hooks

Lifecycle hooks for customizing agent behavior

Hooks

Hooks let you inject custom logic at key points in the agent lifecycle — before a run starts, after a tool call, when a handoff occurs, or when the run completes. They're the extension point for cross-cutting concerns like logging, analytics, rate limiting, and custom business logic.

Agent Hooks

const agent = new Agent({
  name: 'tracked',
  model: claude('claude-sonnet-4-6'),
  instructions: 'You are a helpful assistant.',
  hooks: {
    /** Called before the agent starts processing */
    onStart: async (context) => {
      console.log(`Run ${context.runId} started`)
      await analytics.track('agent_run_started', { agent: context.agentName })
    },

    /** Called before each tool execution */
    onToolCall: async (toolCall, context) => {
      console.log(`Tool: ${toolCall.name}(${JSON.stringify(toolCall.args)})`)
      // Return modified args, or throw to block the call
    },

    /** Called after each tool execution */
    onToolResult: async (toolCall, result, context) => {
      await logger.log({
        tool: toolCall.name,
        args: toolCall.args,
        result,
        duration: context.lastToolDuration,
      })
    },

    /** Called when the agent hands off to another agent */
    onHandoff: async (from, to, context) => {
      console.log(`Handoff: ${from} → ${to}`)
      await analytics.track('agent_handoff', { from, to })
    },

    /** Called when the run completes (success or failure) */
    onEnd: async (result, context) => {
      console.log(`Run ${context.runId} ended: ${result.status}`)
      await analytics.track('agent_run_completed', {
        agent: context.agentName,
        status: result.status,
        tokens: result.usage.totalTokens,
        duration: context.durationMs,
      })
    },

    /** Called when max turns is reached */
    onMaxTurns: async (context) => {
      console.log(`Agent hit max turns limit`)
      // Optionally return a custom response
      return {
        output: 'I was unable to complete the task within the allowed steps. Please try again with a simpler request.',
      }
    },

    /** Called when an error occurs */
    onError: async (error, context) => {
      await errorTracker.capture(error, { runId: context.runId })
      // Optionally return a fallback response
      return {
        output: 'I encountered an error. Please try again.',
      }
    },
  },
})

Runner Hooks

Runner-level hooks apply to all agents in a run, including sub-agents and handoff targets:

const result = await Runner.run(agent, {
  messages,
  hooks: {
    /** Called for every agent in the run (including sub-agents) */
    onAgentStart: async (agentName, context) => {
      console.log(`Agent "${agentName}" activated`)
    },

    onAgentEnd: async (agentName, result, context) => {
      console.log(`Agent "${agentName}" finished: ${result.status}`)
    },

    /** Called for every tool call across all agents */
    onToolCall: async (toolCall, agentName, context) => {
      // Global tool call logging
    },

    /** Called for every guardrail trigger across all agents */
    onGuardrailTriggered: async (guardrail, phase, result, context) => {
      await securityLog.write({
        guardrail: guardrail.name,
        phase,
        allow: result.allow,
        reason: result.reason,
      })
    },
  },
})

Hook Context

Every hook receives a context object with metadata about the current run:

interface HookContext {
  /** Unique run identifier */
  runId: string

  /** Name of the current agent */
  agentName: string

  /** Current turn number */
  turn: number

  /** Total tokens used so far */
  tokensUsed: number

  /** Run duration so far in milliseconds */
  durationMs: number

  /** Duration of the last tool call */
  lastToolDuration?: number

  /** Custom metadata attached to the run */
  metadata: Record<string, unknown>
}

Common Hook Patterns

Cost Tracking

hooks: {
  onEnd: async (result, context) => {
    const cost = calculateCost(result.usage, context.agentName)
    await billing.record(context.userId, cost)

    if (cost > 1.0) {
      await alerts.send(`High-cost run: $${cost.toFixed(2)} for ${context.agentName}`)
    }
  },
}

Retry on Failure

hooks: {
  onError: async (error, context) => {
    if (context.retryCount < 3 && isTransient(error)) {
      // Signal the runner to retry
      return { retry: true, delay: 1000 * Math.pow(2, context.retryCount) }
    }
    // Give up and return a fallback
    return { output: 'Service temporarily unavailable.' }
  },
}

Tool Call Modification

hooks: {
  onToolCall: async (toolCall, context) => {
    if (toolCall.name === 'web_search') {
      // Add a safety suffix to all searches
      return {
        ...toolCall,
        args: {
          ...toolCall.args,
          query: `${toolCall.args.query} site:trusted-sources.com`,
        },
      }
    }
  },
}

Performance Monitoring

hooks: {
  onToolCall: async (toolCall, context) => {
    context.metadata[`tool_start_${toolCall.id}`] = Date.now()
  },

  onToolResult: async (toolCall, result, context) => {
    const startTime = context.metadata[`tool_start_${toolCall.id}`]
    const duration = Date.now() - startTime

    if (duration > 5000) {
      console.warn(`Slow tool: ${toolCall.name} took ${duration}ms`)
    }

    await metrics.histogram('tool_duration', duration, {
      tool: toolCall.name,
      agent: context.agentName,
    })
  },
}

Hook Execution Order

onStart

├─ onToolCall (before tool executes)
│  └─ onToolResult (after tool returns)

├─ onToolCall ...
│  └─ onToolResult ...

├─ onGuardrailTriggered (if triggered)

├─ onHandoff (if handoff occurs)
│  └─ New agent's onStart
│     └─ ... (new agent's hooks run)

└─ onEnd (always runs, even on error)
   or
   onError → onEnd
   or
   onMaxTurns → onEnd

Best Practices

  1. Keep hooks fast — Hooks run sequentially in the agent loop. Slow hooks add latency to every interaction. Use fire-and-forget for analytics.

  2. Don't modify state in hooks — Hooks should observe and react, not mutate agent configuration. Use onToolCall return values for safe modifications.

  3. Use runner hooks for cross-cutting concerns — Logging, analytics, and security should use runner-level hooks to cover all agents uniformly.

  4. Handle errors in hooks — A crashing hook shouldn't crash the agent. Wrap hook logic in try/catch.

  5. Use hooks for observability, not business logic — If your hook is implementing core business logic, it should probably be a guardrail or a tool instead.