---
title: Human in the Loop
description: Add approvals and external decisions to agent runs.
type: guide
summary: Use tool approvals, manual hooks, cancellation, denial, and serverless resume flows.
---

# Human in the Loop



Hooks let an agent suspend while your application waits for a decision. Tool
approvals are the built-in hook workflow.

## Require tool approval

Pass `require_approval=True` to `@ai.tool` when a tool needs approval:

```python
@ai.tool(require_approval=True)
async def notify_mothership(message: str) -> str:
    """Notify the mothership."""
    return f"Sent: {message}"
```

When the model calls the tool, the agent emits a hook event. Resolve it with
`ai.resolve_hook`:

```python
ai.resolve_hook(
    "approve_tool_call_id_here",
    ai.tools.ToolApproval(granted=True, reason="approved"),
)
```

## Resolve approvals in a live app

Listen for pending hook events and resolve the matching hook from another task,
request handler, or UI callback:

```python
async with agent.run(model, messages) as stream:
    async for event in stream:
        if (
            isinstance(event, ai.events.HookEvent)
            and event.hook.status == "pending"
        ):
            print(event.hook.hook_id, event.hook.metadata)

# Later, from the approval path:
ai.resolve_hook(
    "approve_tool_call_id_here",
    ai.tools.ToolApproval(granted=True, reason="approved"),
)
```

## Build manual hooks

Hooks let an agent suspend while your application waits for external input,
such as a human approval:

```python
approval = await ai.hook(
    "approve_contact_mothership",
    payload=ai.tools.ToolApproval,
    metadata={"tool": "contact_mothership"},
)
```

Resolve the hook from another part of your application:

```python
ai.resolve_hook(
    "approve_contact_mothership",
    {"granted": True, "reason": "approved"},
)
```

Use hooks when a tool or workflow needs a decision that cannot happen inside the
model call.

## Deny or cancel work

Return a denial by resolving the hook with `granted=False`:

```python
ai.resolve_hook(
    "approve_tool_call_id_here",
    ai.tools.ToolApproval(granted=False, reason="not allowed"),
)
```

Cancel a live hook when the waiting workflow should stop:

```python
await ai.cancel_hook("approve_tool_call_id_here", reason="client disconnected")
```

## Resume in serverless flows

For serverless or resumable flows, keep the pending hook part from the emitted
event, call `ai.abort_pending_hook(hook_part)` to end the current run, persist
`stream.messages`, and call `ai.resolve_hook` before replaying the agent.

## Persist approval state

Persist `stream.messages` when a run stops on a pending hook. When the user
responds, restore those messages, register the resolution, and run the same
agent again:

```python
messages, approvals = ai.agents.ui.ai_sdk.to_messages(ui_messages)
ai.agents.ui.ai_sdk.apply_approvals(approvals)

async with agent.run(model, messages) as stream:
    async for event in stream:
        ...
```

The agent marks interrupted assistant turns for replay, so the resumed run can
dispatch the original tool call without asking the model to emit it again.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)