Skip to main content
Task execution was exercised end to end in the current preview build: a real browser was provisioned through this API, a task was submitted against it, and the task completed with its result on the wire. What follows is the contract the server enforces for submission, reads, streaming, and cancellation.

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}.
There is no 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.
One task per device: a second submit while one is live answers 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:
Reconnects that fail to advance the cursor are capped (default 5), total reconnects are capped outright (default 500), every reconnect waits at least 100 ms with jitter, and a 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

Cancellation is scoped 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.