How to Pause an AI Agent for Human Input
Learn how to pause an AI agent mid-run and wait for a human decision before it continues — without polling hacks or losing state.
The problem: agents don't stop on their own
A tool-calling agent that runs in a loop — plan, call a tool, observe, repeat — has no natural place to stop and ask "should I actually do this?" Once a tool call is dispatched, the side effect happens. Bolting a confirm=True flag onto the tool doesn't help either: nothing forces the agent to check it before firing, and there's no record of who said yes.
What you actually need is a step in the graph that cannot proceed until an external decision arrives. That means the pause has to live outside the agent's own reasoning — in a place the agent can't talk itself past.
Where to insert the pause
If you're building on a graph-based framework (LangGraph, or a hand-rolled state machine), the natural spot is a dedicated node between "agent decided on an action" and "tool executes." That node does three things: push the proposed action somewhere durable, block on the decision, and only then hand control to the real tool.
import time
import requests
IMPRI_BASE = "https://api.impri.dev"
HEADERS = {"Authorization": f"Bearer {IMPRI_API_KEY}"}
def human_gate_node(state):
"""LangGraph node: pause the run until a human approves the pending action."""
proposed = state["pending_action"] # built by the previous agent node
resp = requests.post(f"{IMPRI_BASE}/v1/actions", headers=HEADERS, json={
"kind": "db.schema_change",
"title": f"Apply migration: {proposed['name']}",
"preview": {"format": "markdown", "body": proposed["sql"]},
"expires_in": 1800, # 30 minutes — this decision goes stale fast
"editable": ["preview.body"],
})
action_id = resp.json()["id"]
# Block the graph here. This is the pause.
while True:
result = requests.get(f"{IMPRI_BASE}/v1/actions/{action_id}", headers=HEADERS).json()
if result["status"] != "pending":
break
time.sleep(5)
if result["status"] != "approved":
state["migration_applied"] = False
return state
# Human may have edited the SQL before approving — always use final_preview
state["approved_sql"] = result["decision"]["final_preview"]["body"]
return stateThe graph node blocks on time.sleep(5) inside the loop, so the agent's own control flow physically cannot reach the next node — running the migration — without that status: "approved" coming back from the API.
What the human sees while the agent waits
The moment POST /v1/actions returns, a card appears in the reviewer's inbox (and a notification fires via email, ntfy, or a Slack/Discord/Telegram channel, if configured). The reviewer sees the title, the rendered preview, and — because editable includes preview.body — a text box to tweak the SQL before approving. Nothing about this requires the agent to be reachable; the agent is asleep in its polling loop, and the decision gets written to the action record whenever the human gets to it.
Handling a stale pause
expires_in matters more here than in a fire-and-forget action. A schema migration proposed 25 minutes ago against a database that's since changed shouldn't auto-apply just because a human finally taps approve. Set expires_in tight for anything time-sensitive (this example uses 1800 seconds), and treat expired the same as rejected in your node — fall through without executing, and let the agent decide whether to re-propose.
| Outcome | What the graph should do |
|---|---|
approved |
Proceed to the execution node with final_preview |
rejected |
Skip execution, log the reason if the reviewer left one |
expired |
Skip execution, treat as stale — re-plan if still relevant |
Pausing without a graph framework
If your agent is a plain loop rather than a graph, the same pattern applies — the "pause" is just the function call that doesn't return until the action is decided. For agents running inside Claude Code or another MCP client, `mcp.md` wraps this exact poll loop into a single blocking tool call (impri_await_decision), so you don't hand-write the while loop at all.
Next step
Start with quickstart to get an API key, then see the full REST/MCP walkthrough for the three-call pattern this pause node is built on. If you're integrating with Python specifically, sdk-python covers the client library.