Observe and control agent execution at well-defined points in the conversation loop.
Overview
Lifecycle hooks let you register handlers on an agent version that fire during a chat — for example, to log tool calls, inject context, or block a tool before it runs. Hooks are configured on the agent version alongside tools and skills.
Each hook listens for one event and dispatches to a webhook URL, task agent, or gate (human-in-the-loop interrupt). Webhook handlers can return a decision or context injection synchronously; gate hooks pause the run until an external resolver calls the Interrupt gates API.
→ Approval settings — built-in human-in-the-loop for individual tools
Configuration
Add a hooks array to the agent version when creating or updating an agent via the Agents API or SDK:
1from inferencesh import inference, lifecycle_hook, HookEvent23client = inference(api_key="inf_your_key")45client.agents.create({6 "name": "guarded-agent",7 "core_app": {"ref": "infsh/claude-sonnet-4@latest"},8 "hooks": [9 lifecycle_hook(HookEvent.TOOL_CALL)10 .webhook("https://example.com/hooks/tool-call")11 .timeout(10)12 .build(),13 lifecycle_hook(HookEvent.TURN_START)14 .task("my-team/auditor@latest")15 .async_(True)16 .build(),17 ],18})Hook fields
| Field | Type | Description |
|---|---|---|
event | string | Lifecycle event to listen for (see Events) |
type | "webhook" | "task" | "gate" | How the handler is invoked |
handler | string | Webhook URL (webhook) or agent ref (task). Omitted for gate. |
async | boolean | Run without blocking the agent loop (default false; not used for gate) |
timeout | integer | Handler timeout in seconds. Webhook default 30 (max 300). Gate default 300 (max 86400). |
default_resolution | "allow" | "deny" | Gate only — resolution applied when timeout elapses (default "allow"). See Gate expiry. |
Webhook handlers receive a POST with Content-Type: application/json and may return a JSON response body. Task handlers receive the payload as the input text of a new message to the referenced agent; task hooks do not return decisions. Gate handlers create an interrupt with reason: hook_gate, store the hook payload in meta, and suspend the run until you resolve it via POST /interrupts/:id/resolve.
Set async: true to fire-and-forget — async hooks never block execution or apply injections.
GET /agents/hook-events returns the canonical event list with can_gate flags. Requires the agents:read scope.
Events
| Event | When it fires | Event data | Webhook gate | Gate hook |
|---|---|---|---|---|
agent.start | First turn of a chat (turn count 0) | — | Blocks turn on deny or stop | Yes |
agent.turn_start | Before each LLM turn | — | Blocks turn on deny or stop | Yes |
agent.tool_call | Before a tool executes | Tool name and arguments | Blocks that tool on deny | Yes |
agent.tool_result | After an async app tool completes | Tool name, status, result | Observe only | No |
agent.turn_complete | After the assistant message, before tool dispatch | — | Observe only | No |
agent.post_compact | After context auto-compaction | — | Observe only | No |
agent.error | When the agent run fails | Error message | Observe only | No |
agent.complete | When the active run finishes | — | Observe only | No |
The following events are defined in the API but not emitted yet: agent.pre_compact (supports gate hooks when wired), agent.idle.
Request payload
Every hook handler receives the same envelope:
1{2 "event": "agent.tool_call",3 "timestamp": "2026-08-11T14:00:00Z",4 "agent_id": "ag_abc123",5 "chat_id": "ch_def456",6 "run_id": "ar_ghi789",7 "turn_count": 3,8 "data": {9 "tool": "delete_file",10 "arguments": { "path": "/tmp/old.txt" }11 }12}data shape depends on the event:
| Event | data fields |
|---|---|
agent.tool_call | tool (string), arguments (object) |
agent.tool_result | tool (string), status (string), result (string) |
agent.error | error (string) |
Response
Synchronous webhook handlers may return JSON. An empty 200 response is treated as { "decision": "allow" }.
1{2 "decision": "allow",3 "reason": "optional explanation",4 "inject": {5 "content": "Reminder: Only delete files in /tmp.",6 "role": "system",7 "ttl_turns": 5,8 "dedup_key": "policy-reminder"9 },10 "system": "Permanent system note from this handler."11}Decisions
| Decision | Effect |
|---|---|
allow | Continue (default) |
deny | Block the gated action |
stop | Same as deny for turn-level gates (agent.start, agent.turn_start) |
suspend | Pause the run until the interrupt is resolved (gate hooks only) |
Turn gates (agent.start, agent.turn_start): returning deny or stop aborts the turn. The agent receives an error such as turn blocked: <reason>. A gate hook on these events creates an interrupt and suspends the run instead.
Tool gate (agent.tool_call): returning deny skips execution for that invocation. The tool is completed with status failed and your reason (defaults to "blocked by hook" if omitted). The agent sees the failure and can try an alternative. stop does not block tool execution — use deny. A gate hook sets the tool invocation to awaiting_approval and suspends the run until resolved.
Only the first matching hook that returns deny, stop, or suspend takes effect; remaining hooks for that event are skipped.
Context injection
inject adds ephemeral content to the chat context on the next turn. Injections are stored as chat messages and respect ttl_turns (0 = permanent until replaced). Use dedup_key to replace a prior injection from the same key.
system is shorthand for a permanent injection keyed by the handler URL.
Blocking tool calls
Use agent.tool_call with a synchronous webhook to implement programmatic guardrails — for example, blocking destructive tools unless your policy service approves:
1// POST body from platform → your webhook2{3 "event": "agent.tool_call",4 "data": { "tool": "delete_file", "arguments": { "path": "/etc/passwd" } }5}67// Your response to block8{ "decision": "deny", "reason": "path not allowed" }The platform marks the invocation failed with your reason and does not call the tool. This is separate from per-tool approval, which pauses for a human in the UI or CLI.
Observing tool results
agent.tool_result fires after an async app tool completes (when the backing task finishes and the platform records the result). The payload includes the tool name, status (completed), and the result string.
Today this event is emitted for async app-tool completions only. Synchronous tools (client tools, call tools, MCP, sub-agents that return inline) do not emit agent.tool_result.
Handler types
Webhook
1{2 "event": "agent.tool_call",3 "type": "webhook",4 "handler": "https://example.com/hooks/tool-call",5 "timeout": 156}Your endpoint must return HTTP 2xx. Return a JSON body to apply decisions or injections (sync only).
Task
1{2 "event": "agent.turn_start",3 "type": "task",4 "handler": "my-team/auditor@latest",5 "async": true6}The platform sends the full hook payload as the task agent's input text. Task hooks are useful for audit logging or routing events to a dedicated reviewer agent. They cannot return decision or inject — use a webhook or gate for gating.
Gate
Gate hooks pause the agent run and create an interrupt with reason: hook_gate. Use them when a human or external system must approve before execution continues — for example, reviewing a destructive tool call in your own UI.
1{2 "event": "agent.tool_call",3 "type": "gate",4 "timeout": 600,5 "default_resolution": "deny"6}When the hook fires:
- The platform creates an interrupt linked to the active run (
sourceishook:<event>). Foragent.tool_call,resource_typeistool_invocationandresource_idis the tool invocation ID; for turn-level gates,resource_typeishook_event. - The full hook payload is stored on the interrupt's
metafield. - The run enters
input_requiredwithinterrupt_reason: hook_gate. - For
agent.tool_call, the related tool invocation moves toawaiting_approval.
Resolve the gate with POST /interrupts/:id/resolve (allow or deny). Optional data is stored as resolved_data. Resolving with allow invokes the tool; deny rejects it. The run resumes only after all pending interrupts for that run are resolved — multiple gates on the same run are resolved independently.
Gate expiry
When a gate interrupt is created, the platform schedules a one-shot scheduled trigger (action: resolve_interrupt, scheduled_at = now + timeout) whose input includes the interrupt ID and default_resolution (default allow). At expiry, this trigger resolves the interrupt the same way as a manual resolve — invoking or rejecting the tool for tool_invocation gates. Each gate on the same run gets its own expiry trigger. A background expiry sweep remains as a safety net for orphaned interrupts.
See Triggers API — resolve_interrupt action.
See Interrupt gates for list/resolve endpoints, SDK helpers, and belt interrupt list / belt interrupt resolve CLI commands.
1# List pending gates for a run2belt interrupt list ar_abc12334# Approve and resume (optional data stored as resolved_data)5belt interrupt resolve int_abc123 allowGate hooks require an active run and chat — they cannot fire outside a running conversation.
Configure gate hooks in Python by setting type on the hook dict (the lifecycle_hook() builder does not yet expose a .gate() helper):
1from inferencesh.types import HookEvent, HookHandlerType, InterruptResolution23{4 "event": HookEvent.TOOL_CALL,5 "type": HookHandlerType.HOOK_HANDLER_GATE,6 "timeout": 600,7 "default_resolution": InterruptResolution.ALLOW,8}Example: policy webhook
1# Flask handler (illustrative)2from flask import request, jsonify34@app.post("/hooks/tool-call")5def on_tool_call():6 payload = request.json7 tool = payload["data"]["tool"]8 args = payload["data"].get("arguments", {})910 if tool == "delete_file" and not str(args.get("path", "")).startswith("/tmp/"):11 return jsonify({"decision": "deny", "reason": "only /tmp deletions allowed"})1213 return jsonify({"decision": "allow"})Register it on the agent:
1lifecycle_hook(HookEvent.TOOL_CALL)2 .webhook("https://api.example.com/hooks/tool-call")3 .build()Related
- Approval settings — per-tool
require_approval()for human review - Interrupt gates — list and resolve
hook_gateinterrupts - App tools — async app tools that complete via task callbacks
- Agents API — create and update agent versions with
hooks - Streaming — real-time
tool_call/tool_resultSSE events in the chat stream