Skip to main content
Not every task runs straight through. If the agent calls ask_user — or if you’ve built a conversational agent that expects a back-and-forth — the task pauses with status: "waiting_for_input" until you reply.

The reply loop

run agent  →  poll  →  status == "waiting_for_input"


                  POST /v1/tasks/{id}/message


                       poll again  →  status == "completed"
The reply is processed asynchronously. After posting it the task usually flips back to running within a second; resume polling GET /v1/tasks/{task_id} exactly like a fresh run. See Async tasks and polling for the polling loop itself.

Detecting that input is needed

Just check status. The same GET /v1/tasks/{task_id} you’re already polling tells you:
{
  "task_id": "f1c2a3b4-...",
  "agent_id": "...",
  "status": "waiting_for_input",
  "title": "Reconcile the March invoices",
  "created_at": "2026-05-12T10:15:00Z",
  "updated_at": "2026-05-12T10:15:42Z"
}
To see what the agent asked, fetch the message log:
curl https://agents.nanonets.com/api/v1/tasks/$TASK_ID/messages \
  -H "Authorization: Bearer $NANONETS_API_KEY"
The response is the chronological conversation log — user replies you’ve sent plus the agent’s messages back, oldest first. The most recent assistant message is the question the agent is waiting on.

Sending the reply

curl -X POST https://agents.nanonets.com/api/v1/tasks/$TASK_ID/message \
  -H "Authorization: Bearer $NANONETS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"message": "Use the second invoice line, not the first."}'
You can also send a message to a task that’s still running — it gets appended to the conversation and the agent picks it up on its next turn. This is how multi-turn conversational agents work.

End-to-end: handle the pause inside your polling loop

Adapting the polling loop from the previous guide to handle waiting_for_input:
Python
import os, time, requests

BASE = "https://agents.nanonets.com"
HEADERS = {"Authorization": f"Bearer {os.environ['NANONETS_API_KEY']}"}
TERMINAL = {"completed", "failed", "stopped"}

def get_last_assistant_message(task_id):
    msgs = requests.get(f"{BASE}/api/v1/tasks/{task_id}/messages", headers=HEADERS).json()
    for m in reversed(msgs["messages"]):
        if m["role"] == "assistant":
            return m["content"]
    return None

def reply(task_id, message):
    requests.post(
        f"{BASE}/api/v1/tasks/{task_id}/message",
        headers=HEADERS, json={"message": message}, timeout=10,
    ).raise_for_status()

def run_until_done(task_id, answer_fn, timeout_s=600):
    deadline = time.time() + timeout_s
    delay = 1.0
    while time.time() < deadline:
        body = requests.get(f"{BASE}/api/v1/tasks/{task_id}", headers=HEADERS).json()
        if body["status"] in TERMINAL:
            return body
        if body["status"] == "waiting_for_input":
            question = get_last_assistant_message(task_id)
            reply(task_id, answer_fn(question))
        time.sleep(delay)
        delay = min(delay * 1.5, 10.0)
    raise TimeoutError(task_id)
answer_fn is your callback — typically a function that prompts a human, looks up an answer from a database, or asks another LLM. The shape of the call is up to you; the API just takes whatever string you send.

Cancelling instead of replying

If you can’t answer (the user walked away, the timeout has elapsed), call POST /v1/tasks/{task_id}/cancel to mark the task stopped. The agent won’t progress further, but the conversation history stays available via the messages endpoint.