When you call POST /v1/agents/{agent_id}/run, the response returns immediately with a
task_id and status: "running" — the agent is still working in the background. To get
the final result you poll one of the task-read endpoints until status is terminal.
Pick the right endpoint to poll
| Endpoint | Use when |
|---|
GET /v1/tasks/{task_id} | Polling. Returns status + a few timestamps. Cheapest call. |
GET /v1/tasks/{task_id}/summary | The task is completed and you only want the final answer. |
GET /v1/tasks/{task_id}/result | Debugging or audit — returns the full reasoning feed (every tool call and intermediate message). |
A typical production loop looks like: poll /v1/tasks/{task_id} on a backoff schedule;
once status hits a terminal value, call /summary once.
Terminal vs non-terminal statuses
| Status | Terminal? | What to do |
|---|
pending | No | Keep polling — the orchestrator hasn’t picked it up yet. |
queued | No | Keep polling — the agent is at its concurrent-task limit. |
running | No | Keep polling. |
waiting_for_input | No | The agent called ask_user. Reply via POST /v1/tasks/{task_id}/message, then resume polling. See Interactive tasks. |
awaiting_review | No | A human in the dashboard needs to approve the next step. |
completed | Yes | Done. Fetch /summary. |
failed | Yes | Errored. Fetch /result for the reasoning trail. |
stopped | Yes | Cancelled (by you or by the system). |
A real polling loop
import os, time, requests
BASE = "https://agents.nanonets.com"
HEADERS = {"Authorization": f"Bearer {os.environ['NANONETS_API_KEY']}"}
TERMINAL = {"completed", "failed", "stopped"}
def wait_for_task(task_id: str, timeout_s: int = 600) -> dict:
deadline = time.time() + timeout_s
delay = 1.0
while time.time() < deadline:
r = requests.get(f"{BASE}/api/v1/tasks/{task_id}", headers=HEADERS, timeout=10)
r.raise_for_status()
body = r.json()
if body["status"] in TERMINAL:
return body
if body["status"] == "waiting_for_input":
raise RuntimeError(f"Task {task_id} needs a reply — see /interactive-tasks")
time.sleep(delay)
delay = min(delay * 1.5, 10.0) # gentle exponential backoff, cap at 10s
raise TimeoutError(f"Task {task_id} did not finish within {timeout_s}s")
final = wait_for_task("f1c2a3b4-...")
if final["status"] == "completed":
summary = requests.get(
f"{BASE}/api/v1/tasks/{final['task_id']}/summary", headers=HEADERS
).json()
print(summary)
Start your polling interval at 1–2 seconds and back off to ~10 seconds. The status
endpoint is cheap, but tight loops still burn through rate limit budget for no benefit
— most tasks take seconds to minutes.
Cancelling
If you no longer want the result, send POST /v1/tasks/{task_id}/cancel. The task moves
to stopped. The step currently executing on the worker is not interrupted — it
finishes naturally, but no further steps are scheduled. Cancel is idempotent: calling on
an already-terminal task returns the current state with 200.
Tasks that legitimately take a long time
Some agents do work that takes minutes (multi-step research, large document extraction).
A few rules of thumb:
- Keep your client-side timeout generous (≥10 minutes) for those agents.
- Don’t poll faster than every couple of seconds — you won’t get the answer any sooner.
- For very long jobs, persist the
task_id and poll from a background worker, not a
user-facing HTTP request.