Human-in-the-Loop Continuations

Continue a paused durable run through the queue with background=true. Same job, same run_id, one more attempt.

uv pip install "agno[os]" openai psycopg
durable_continue.py
from agno.agent import Agent
from agno.db.postgres import PostgresDb
from agno.models.openai import OpenAIResponses
from agno.os import AgentOS, QueueConfig
from agno.tools import tool


@tool(requires_confirmation=True)
def delete_temp_files(directory: str) -> str:
    """Delete temporary files in a directory. Requires human confirmation."""
    return f"Deleted temp files in {directory}"


db = PostgresDb(db_url="postgresql+psycopg://ai:ai@localhost:5532/ai")

agent = Agent(
    name="HITL Agent",
    id="hitl-agent",
    model=OpenAIResponses(id="gpt-5.5"),
    db=db,
    tools=[delete_temp_files],
    instructions="Use delete_temp_files when asked to delete or clean up files.",
)

agent_os = AgentOS(
    agents=[agent],
    db=db,
    queue=QueueConfig(durable=True),
)
app = agent_os.get_app()

if __name__ == "__main__":
    agent_os.serve(app="durable_continue:app", port=7777)

A durable run that pauses for confirmation parks its queue job as paused. Continuing it with background=true flips the same job back to queued, merges your confirmations into its payload, and lets whichever replica claims it finish the run. Kill the server after the 202 and restart it: the continuation still runs.

Flow

Submit

curl -X POST localhost:7777/agents/hitl-agent/runs \
  -F "message=Delete the temp files in /tmp/scratch" \
  -F "background=true" -F "stream=false"

Poll until PAUSED

curl "localhost:7777/agents/hitl-agent/runs/{run_id}?session_id={session_id}"

The response carries status: "PAUSED" and a tools array with the pending delete_temp_files call. The job is paused too: GET /queue/jobs/{run_id} shows status: "paused".

Continue through the queue

Replace the placeholder tool_call_id with the pending call ID from the poll response, set confirmed: true, and send the tool entry back with background=true:

curl -X POST localhost:7777/agents/hitl-agent/runs/{run_id}/continue \
  -F "session_id={session_id}" \
  -F "background=true" -F "stream=false" \
  -F 'tools=[{"tool_call_id": "...", "tool_name": "delete_temp_files", "confirmed": true}]'
{"run_id": "{run_id}", "session_id": "{session_id}", "status": "PENDING"}

Poll to completion

The same poll URL returns COMPLETED. GET /queue/jobs/{run_id} shows status: "completed", attempt: 2, max_attempts: 2.

Teams use /teams/{team_id}/runs/{run_id}/continue with requirements; workflows use /workflows/{workflow_id}/runs/{run_id}/continue with step_requirements. The queue semantics are identical.

Job lifecycle

EventJob statusrun_id
Submission acceptedqueuedMinted
Worker claimsrunningSame
Run pauses for inputpausedSame
Continue with background=truequeued (one more attempt granted)Same
Continuation claimedrunningSame
Run finishes, or pauses againcompleted / pausedSame

There is one row per run. Poll, resume, cancel, and idempotency all key on the original run_id across any number of pause and continue cycles. A continuation gets exactly one execution regardless of max_attempts. A continuation that crashes is marked failed and re-driven with requeue, which replays the same confirmations.

Responses

SituationResponse
Continue accepted202 with the same run_id.
Second identical continue while the first is still queued202. The second click attaches to the first. Its inputs are discarded.
Continue while the run is transitioning between the pause and the continuation409 with Retry-After: 1. The window is the gap between two adjacent writes. Retry.
Continue of a cancelled or finished run409.
Continue with stream=true on a run submitted with stream=false409. The continuation would publish no events. Poll it.
Continue without background=true on a queue-owned run409: "Run ... was submitted through the durable queue; continue it with background=true".

Why inline continues are refused

A job in paused, queued, or running owns its run's continuation. Every other continue path (inline sync, inline SSE, MCP, AG-UI, Slack) refuses with 409. Without that rule, an inline continue could validate against the run row while a durable continue validated against the job, and both could pass before either persisted. An approved tool would then execute twice. If the job cannot be looked up at all, the request fails with 503 instead of proceeding unverified.

Runs that never rode the queue, and fork or regenerate requests (which mint a new run), are unaffected.

Cancel and retention

Paused jobs are exempt from retention. A paused run is waiting for a person, and there is no bound on how long that takes, so the job is never removed on age. An abandoned paused run persists until it is cancelled:

curl -X POST "localhost:7777/agents/hitl-agent/runs/{run_id}/cancel?session_id={session_id}"

Cancel moves the job to cancelled and the run row to CANCELLED. A later continue gets 409 instead of resurrecting the run.

A paused run does not serialize other submissions to its session. If later input depends on its outcome, coordinate submissions in your application until the run is continued or cancelled. See Session serialization.

Streaming continuations

If the original submission used stream=true, continue with stream=true and background=true to receive an SSE tail of the post-approval events. Earlier events belong to /resume. Event indices keep increasing across the pause, so a client that resumes with its last index does not replay pre-approval history.

Next Steps

TaskGuide
Re-drive a crashed continuationRequeue
Confirmation and approval flows in generalHuman-in-the-loop
Approvals from the Control PlaneApprovals