Skip to content
AgentField
Execution

Human-in-the-Loop

Pause agent execution for human approval with crash-safe state and webhook callbacks

Execution pauses, waits for approval, resumes

Pause an agent mid-execution, send a review to a human, and resume automatically when they respond.

Some decisions should not be fully automated. app.pause() blocks the agent until a human approves, rejects, or requests changes -- with crash-safe state persisted in PostgreSQL. The control plane resumes execution automatically when the webhook callback arrives.

@app.reasoner()
async def content_pipeline(brief: str) -> dict:
    # Step 1: AI generates a draft
    draft = await app.ai(system="You are a senior copywriter.", user=brief)

    # Pause #1 — human reviews the draft (state is crash-safe in PostgreSQL)
    review = await app.pause(
        approval_request_id="draft-review",
        approval_request_url="https://cms.example.com/review/draft",
        expires_in_hours=24,
    )
    if review.decision == "rejected":
        return {"status": "rejected", "feedback": review.feedback}

    # Step 2: AI refines using human feedback
    final = await app.ai(
        system="Revise based on editorial feedback.",
        user=f"Draft: {draft}\nFeedback: {review.feedback}",
    )

    # Pause #2 — human approves final version before publish
    sign_off = await app.pause(
        approval_request_id="final-approval",
        approval_request_url="https://cms.example.com/review/final",
        expires_in_hours=4,  # auto-reject if no response
    )

    if sign_off.decision == "approved":
        await app.call("publisher.publish", content=final)
        return {"status": "published"}
    return {"status": sign_off.decision, "feedback": sign_off.feedback}
    # If the process crashes between pauses, it resumes exactly where it left off

What just happened

  • The agent ran normally until it reached a human gate
  • Execution state was persisted before waiting, so restarts do not lose progress
  • The human response became structured workflow input, not an external side channel
  • The control plane resumed the same execution instead of starting a new one

Example paused/resumed lifecycle:

{ "execution_id": "exec_a1b2c3", "status": "waiting", "approval_request_id": "draft-review" }
{ "execution_id": "exec_a1b2c3", "status": "running", "decision": "approved" }
{ "execution_id": "exec_a1b2c3", "status": "succeeded", "result": { "status": "published" } }