DEV Community

sekera-radim
sekera-radim

Posted on Originally published at impri.dev

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 state
Enter fullscreen mode Exit fullscreen mode

The 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.

Top comments (0)