Skip to main content
Lifecycle hooks let you observe and control specific points in the callModel agent loop. Use them to:
  • Inspect, modify, or block tool calls before execution
  • Record successful and failed tool executions
  • Rewrite or reject user prompts
  • Decide whether approval-gated tools run
  • Override stop conditions
  • Track sessions, model calls, token usage, latency, and cost
  • Run custom, type-safe application events
Lifecycle hooks are different from transport hooks such as SDKHooks and BeforeRequestHook. Transport hooks intercept HTTP requests. Lifecycle hooks fire on events within the agent loop.

Quick start

Pass built-in hooks directly to callModel with the hooks option:
Inline configuration supports all built-in hooks. Use a HooksManager when you need custom hooks, dynamic registration, programmatic emission, or explicit lifecycle control.

HooksManager

Create a manager and pass it to one or more callModel calls:
HooksManager is available from @openrouter/agent and from the focused @openrouter/agent/hooks-manager subpath. Every handler receives a validated payload and a context object:
The SDK supplies the current run’s session ID for each lifecycle event. A single manager can therefore be shared by concurrent callModel runs without mixing their session IDs.

Built-in hooks

SessionStart

SessionStart fires once before the initial model request. Its optional config summarizes the run without exposing the full request: The exported payload type allows an extensible config record:
The agent loop currently emits this config:
Use it to initialize tracing, audit records, or session-scoped resources. Handlers are observational and do not return a result.

UserPromptSubmit

UserPromptSubmit fires before the initial user prompt is sent:
mutatedPrompt replaces the prompt. reject: true aborts with a default error, while a non-empty string becomes the rejection reason. For structured input, the SDK uses the latest user-role text. If it cannot extract user text, it skips this hook.

PostModelCall

PostModelCall fires once for every completed model response, including:
  • The initial request
  • Approval or HITL resume requests
  • Follow-up requests after tool rounds
  • The final request created by allowFinalResponse
  • Empty-final retries
responseId is the OpenRouter generation ID. durationMs measures from request dispatch until the full response is materialized, including stream consumption. The hook is observational and is suitable for one tracing span per model call.
A failed stream that never produces a complete response does not emit PostModelCall. A materialized response.incomplete response does emit it. usage is omitted when the server does not report usage, and usage.cost is only present when server-side usage accounting reports cost. On a no-tools streaming path, the response becomes complete only after stream consumption. The hook therefore fires during session teardown, immediately before SessionEnd.

PermissionRequest

PermissionRequest fires when a tool requires approval, before the SDK pauses for a user decision:
  • allow skips the approval gate and runs the tool through the normal loop.
  • deny does not run the tool and sends a rejected result to the model.
  • ask_user continues into the normal approval flow.
When multiple handlers return decisions, the last returned decision wins. The SDK derives riskLevel from the approval rule: callback-based rules are high, blanket true rules are medium, and other cases are low.

PreToolUse

PreToolUse fires before client tool execution, including normal tool rounds and resumed approval flows:
mutatedInput replaces the arguments before schema validation and execution. block: true skips execution with a default reason. A non-empty string skips execution and is sent to the model as the reason. If the model supplied invalid JSON arguments, the SDK produces a parse error without emitting the tool hooks because no valid tool input exists.

PostToolUse

PostToolUse fires after successful tool execution:
It is observational and has no result. toolInput contains the effective arguments after any PreToolUse mutation.

PostToolUseFailure

PostToolUseFailure fires when a tool executes and throws or returns an error:
It does not fire when the tool never ran, including a PermissionRequest denial, user rejection, PreToolUse block, invalid JSON arguments, or an HITL tool pausing for later input. Observe those outcomes in the corresponding gating hook. The post-use hook fires after a paused HITL tool is resumed and successfully completes.

Stop

Stop fires when a stopWhen condition halts the loop:
appendPrompt injects a user message, but does not by itself continue the tool loop. Prompts from multiple handlers are joined with newlines. If any handler returns forceResume: true, the loop continues, subject to a limit of three consecutive overrides without progress. A tool output or a fresh model response resets that counter. Blocked and denied tool outputs also count as progress because the model receives the feedback. A bare forceResume normally triggers the same stop condition again and uses the override limit. Pair it with appendPrompt or a stop condition whose external state can change.

SessionEnd

SessionEnd fires once when a started session exits:
totalUsage is present when at least one model response completed. Token fields are summed across calls. cost is included when at least one response reported it. When SessionStart succeeds, SessionEnd fires exactly once for that run on completion, approval pause, interruption, error, and no-tools streaming teardown. An approval pause currently ends with reason: 'complete'. Approval and HITL resume calls skip SessionStart and SessionEnd, but tool hooks still fire and detached work is still drained. A teardown error is logged and never replaces the run’s original error.

Matchers and filters

Use matcher to register handlers for selected tool names:
A string matches exactly. A regular expression or predicate can match a set of tools. A matcher-scoped handler is skipped when an event has no tool name. filter receives the hook payload and can add any further condition. Matchers are intended for tool-scoped hooks. Use filter by itself for session, prompt, model-call, and stop hooks.

Handler chain behavior

Handlers run sequentially in registration order.
  1. The SDK checks the handler’s matcher and filter.
  2. It calls the handler with the latest payload.
  3. A mutation replaces the corresponding payload field for later handlers.
  4. A block or rejection stops the remaining handler chain.
Mutations therefore pipe through the chain:
A blocking handler’s mutation is still applied before the chain stops. Empty strings do not block or reject. Return mutatedInput or mutatedPrompt instead of mutating nested payload values in place. By default, a throwing handler, matcher, or filter is logged and skipped. Use strict mode to propagate these failures, which is useful in tests:
Payload and result schema failures follow the same error policy.

Asynchronous handlers

Handlers may return a promise. The chain waits for a normal asynchronous handler before running the next handler:
To detach telemetry or audit work without delaying the loop, return an AsyncOutput signal:
The asynchronous signal has this exact shape:
The default timeout is 30 seconds. On timeout, the handler context’s signal is aborted and the timeout is logged. JavaScript promises cannot be forcibly cancelled, so background work should observe context.signal. The manager tracks detached work. Use its lifecycle methods during shutdown:
abortInflight() aborts the signals for active handler work. drain() waits for all detached work to settle. The agent loop also drains pending hook work when a run ends. Use the exported isAsyncOutput(value) type guard when handling hook return values yourself. Objects with extra fields are not asynchronous signals, so a mutation cannot be accidentally discarded.

Custom hooks

Define custom hooks with Zod payload and result schemas:
Payloads and non-void results are validated on every emit(). Zod transforms, defaults, and coercion are applied before handlers run, so handlers receive the schema output type. Custom hook names must be non-empty and cannot collide with built-in names. Custom hooks do not inherit built-in mutation or blocking behavior. Inline configuration only supports built-in hooks. When directly emitting from a manager shared by concurrent operations, pass a per-emit session ID:
setSessionId() sets a manager-wide default for direct emissions. It is last-writer-wins, so prefer the per-emit value when operations may overlap.

HooksManager API

constructor

  • customHooks maps custom names to Zod payload and result schemas.
  • options.throwOnHandlerError propagates hook errors when true.

on

Registers a typed handler and returns an unsubscribe function. An entry has a handler and optional matcher and filter.

off

Removes a specific handler. Returns whether it was found.

removeAll

Removes handlers for one hook, or every handler when no name is supplied.

setSessionId

Sets the default context.sessionId for direct emit() calls. This mutable default is last-writer-wins. Use the per-emit override for concurrent work.

emit

Validates the payload, runs the chain, and returns:
toolName supplies the value used by matchers. sessionId overrides the manager default for this emission.

hasHandlers

Returns whether the hook has any registered handlers.

abortInflight and drain

Abort active handler signals, then await detached work during graceful shutdown.

Next steps