Submit against a device, read back at the top level
The routing fact most integrations trip over, stated first:- Create a task with
POST /v1/devices/{device_id}/tasks. - Read it back with
GET /v1/tasks/{task_id}.
GET /v1/devices/{device_id}/tasks/{task_id}. Guessing that URL
returns 404, and a 404 here never means the task is gone; it means the route
does not exist. GET /v1/devices/{device_id}/tasks lists a device’s tasks,
and each task in that list is read individually at the top-level route.
409 device_busy. Submission accepts an Idempotency-Key, so a create that
times out and is retried replays the first result instead of starting twice;
see conventions.
A task carries its outcome in result: the agent’s answer on success, the
failure payload on failure, and null while it runs. A caller is never told
only that a task failed with nothing to act on.
Streaming events
GET /v1/tasks/{id}/events streams the task’s steps as server-sent events,
each carrying a monotonic seq. Resume from the last seq you stored.
The server closes a stream after 300 seconds by design. It is poll-based
and holds no database connection between polls. A consumer must reconnect from
its last seq; one that treats the close as the end sees every long task
stall at exactly five minutes.
The TypeScript client does this for you, with reconnect bounds so a broken
deployment cannot turn your consumer into a hot loop:
429 on reconnect honours its Retry-After. If you
hand-roll the SSE loop, you own all of that.
needs_human is not terminal
A task that hits a login, captcha, or two-factor wall parks as needs_human
and waits for a takeover. It is not finished and not failed; treat it as a
signal to put a person in front of the device via
live view. The task resumes when the human releases
control.
Cancel
operator:tasks:write and was exercised end to end in
the current preview build. Cancel requests the stop; the terminal transition
lands when the worker settles the task, so read the task back rather than
assuming the 202 finished the job.