Browse the docs

Ask a person mid-run

Pause a run on a question, have AMC route it to the right person, and resume when their answer arrives.

Before you startAn endpoint that serves `/status`. See [Run status and steps](/developers/byo-run-status/).View as Markdown

Your agent asks a person a question by parking a step and saying so in its status. AMC opens an intervention, routes it, tracks its SLA, and posts the answer back to your endpoint.

The flow

  1. Your agent parks a step: step status becomes waiting, the question goes in that step's delivered, and the run status becomes human_intervention.
  2. On its next poll AMC finds the waiting step and opens an intervention for it, with your question as the request. It appears in the Review queue, the bell, and email.
  3. A person replies in AMC. AMC calls POST /runs/{id}/messages with the answer.
  4. Your agent resumes from the parked step, sets the run back to running, and returns the new status.
GET /runs/{id}/status while waiting
{
  "orchestratorRunId": "run_5f2c0e8a41b94c55",
  "status": "human_intervention",
  "currentStep": 3,
  "totalSteps": 3,
  "steps": [
    { "stepId": "fetch", "name": "Fetch invoice", "status": "completed", "tokenCount": 0 },
    { "stepId": "summarize", "name": "Summarize", "status": "completed", "tokenCount": 4810 },
    {
      "stepId": "approve",
      "name": "Approve summary",
      "status": "waiting",
      "startedAt": "2026-10-17T09:31:00.000000+00:00",
      "tokenCount": 0,
      "delivered": "The total is above the EUR 5,000 limit. Approve the summary for INV-1042?"
    }
  ],
  "assigneeRef": "sam@contoso.com"
}
  • Put the question in delivered on the waiting step. Without it the intervention opens with only the step name.
  • Park one step at a time. If several report waiting, AMC picks the one that started first.
  • Waiting holds the run, not your service: keep serving other runs.
  • Add a step.paused event to the live stream if you have one, so a watching browser shows the pause at once.

The answer

POST /runs/{id}/messages
{
  "orchestratorRunId": "run_5f2c0e8a41b94c55",
  "stepId": "approve",
  "message": "Approved. Use the March exchange rate.\n\n[Attached file: rates.pdf (file, 82 KB)]",
  "attachments": [
    { "kind": "file", "filename": "rates.pdf", "mediaType": "application/pdf",
      "sizeBytes": 84213, "foundryFileId": null }
  ]
}
  • message is the person's reply. When they attach files, AMC appends one [Attached file: name (kind, size)] line per file and lists them in attachments. Without files, attachments is left out.
  • Your agent never receives a file or a link to one. Attachments stay inside AMC, scanned for malware; people download them there.
  • Return the run status after the answer, as JSON (200). The run should read running, or terminal if the answer finished it.
  • Return 409 when the run isn't waiting, or stepId isn't the waiting step. Return 404 for an unknown run.

AMC calls your endpoint before it records the reply. If you return an error, nothing is recorded, the intervention stays open, and the person can send again.

Park and resume (FastAPI, in-memory run store)
import asyncio

from fastapi.responses import JSONResponse
from pydantic import BaseModel


class SendMessage(BaseModel):
    orchestratorRunId: str | None = None
    stepId: str | None = None
    message: str = ""


async def ask_human(run: dict, step_id: str, question: str) -> str:
    run["answer"] = asyncio.get_running_loop().create_future()
    run["waitingStep"] = step_id
    set_step(run, step_id, "waiting", startedAt=now(), delivered=question)
    run["status"] = "human_intervention"
    emit(run, "step.paused", step_id)
    try:
        return await run["answer"]
    finally:
        run["answer"] = None
        run["waitingStep"] = None
        run["status"] = "running"
        set_step(run, step_id, "running")


@app.post("/runs/{run_id}/messages")
async def send_message(run_id: str, body: SendMessage):
    run = get_run(run_id)
    answer = run.get("answer")
    if answer is None or answer.done():
        return JSONResponse(status_code=409, content={"error": "run_not_waiting"})
    if body.stepId and body.stepId != run["waitingStep"]:
        return JSONResponse(status_code=409, content={"error": "step_not_waiting"})
    answer.set_result(body.message)
    await asyncio.sleep(0)  # let the run resume before reporting its status
    return await run_status(run_id)

The helpers (set_step, emit, get_run, run_status, now) are the ones in the minimal endpoint. In the run, call answer = await ask_human(run, "approve", "Approve the summary?"), then complete the step.

Who gets the question

An intervention goes to the run's assignee. The run gets one from:

  • the Assignee picked in Run now, or assigneeId in a webhook body;
  • an input whose parameter is marked "role": "assignee" (see Declare run parameters);
  • your agent, by reporting assigneeRef (an email) on /status. Use this when a workflow runs once per person and asks each about their own data.

With no run assignee, the question goes to the workflow's Default assignee, and failing that to any manager of its department. AMC resolves assigneeRef to a user in the tenant, and drops it (with an audit entry) when that person isn't in the workflow's department or can't see a sensitive workflow. An omitted assigneeRef never unassigns a run.

When nobody answers

AMC re-notifies and escalates on the intervention's SLA (see Set intervention SLAs and upload limits). Your run keeps waiting; give it your own deadline if it must not wait forever. If the run ends while waiting, for example because someone cancels it, AMC withdraws the intervention. The person's side is in Respond to a human intervention.

Last updated 2026-10-21