Async AI jobs
Asynchronous AI job tracking
AI endpoints — discovery and deep dive — take seconds to minutes, so they run as jobs. Each creates a job and returns immediately; you then follow that one job to completion.
ai:run
POSTan AI endpoint → 202Accepted with aJob(status: queued).- Poll
GET /v1/jobs/{job_id}— the same endpoint serves both progress and result. - Or stream
GET /v1/jobs/{job_id}/streamfor live progress. - List your jobs with
GET /v1/jobs.
The Job object
1{2 "job_id": "job_8f3a2b...",3 "type": "discover",4 "status": "running",5 "progress": {6 "message": "Reading datasheet — Electrical Characteristics",7 "fraction": 0.4,8 "updated_at": "2026-06-01T17:05:12Z"9 },10 "created_at": "2026-06-01T17:04:00Z",11 "completed_at": null,12 "stream_url": "https://api.zenode.ai/v1/jobs/job_8f3a2b.../stream",13 "usage": {14 "input_tokens": 18234,15 "output_tokens": 2451,16 "total_tokens": 2068517 },18 "result": null,19 "error": null,20 "request_id": "req_2c19..."21}See Job for the full field reference. The result shape depends on
type — a DiscoverResult for discover jobs or a
DeepDiveResult for deep_dive jobs. While running,
status and the coarse progress object track the work; usage reports tokens consumed.
Poll a job
https://api.zenode.ai/v1/jobs/{job_id}Returns the full Job — status and progress while running, the typed result inline once
completed. Poll until status is terminal. 404Not Found once the job ages out (see retention below).
Stream a job
https://api.zenode.ai/v1/jobs/{job_id}/streamServer-Sent Events for live progress. Each frame is a named event: with a JSON data: line, in
standard SSE wire format. A small, stable set of event types is emitted (internal agent chatter is
summarized, not passed through):
| Parameter | Type | Required | Description |
|---|---|---|---|
status | { status } | Optional | Lifecycle transition — queued, running, then a terminal status |
progress | { message, fraction } | Optional | Coarse progress — message is a step label, fraction an optional 0–1 estimate |
pick | DiscoverPick | Optional | Discovery jobs only — one ranked pick, emitted as it's decided |
result | DiscoverResult | DeepDiveResult | Optional | Terminal success — the typed Job.result for this job's type |
error | { type, message } | Optional | Terminal failure |
A discovery stream looks like:
1event: status2data: {"status":"running"}34event: progress5data: {"message":"Reading datasheet — Electrical Characteristics","fraction":0.4}67event: pick8data: {"rank":1,"part":{ /* PartDetail */ },"fit_rationale":"Lowest sleep current with a hardware step counter."}910event: result11data: {"summary":"All three are active, sub-3.6 V 6-axis IMUs…","results":[ /* DiscoverPick[] */ ]}The result event carries exactly the typed Job.result for this job's type — a
DiscoverResult for discover jobs or a
DeepDiveResult for deep_dive jobs. A deep_dive stream
omits pick events.
Reconnecting
The stream survives disconnects and client refreshes. Each event has an id; on reconnect, send
the last id you saw in a Last-Event-ID header and the server resumes from the next event without
gaps:
1curl -N https://api.zenode.ai/v1/jobs/job_8f3a2b.../stream \2-H "Authorization: Bearer $ZENODE_API_KEY" \3-H "Last-Event-ID: 42"If the job already reached a terminal state, reconnecting replays through the final result (or
error) event so you never miss the outcome, up to the retention window.
Webhooks are coming soon. Polling and streaming are the supported ways to follow a job today; a job-completion webhook is on the roadmap.
List your jobs
https://api.zenode.ai/v1/jobsLists your own jobs, newest first.
| Parameter | Type | Required | Description |
|---|---|---|---|
status | string | Optional | Filter by status; accepts a single value or active (= queued + running) |
type | string | Optional | Filter by discover | deep_dive |
limit | int | Optional | Page size, default 20, max 100 |
starting_after | string | Optional | Pagination cursor — pass a prior response's next_cursor |
Returns a cursor-paginated page of JobSummary rows — a Job without
the heavy result:
1{2"jobs": [ /* JobSummary[] */ ],3"next_cursor": "job_6a1f...",4"request_id": "req_77de..."5}next_cursor is null on the last page; otherwise pass it back as starting_after. Fetch
GET /v1/jobs/{job_id} for a row's full payload. This is the only cursor-paginated collection in the API.
Cancel a job
https://api.zenode.ai/v1/jobs/{job_id}/cancelBest-effort cancel of an in-flight job. Idempotent:
1{2 "job_id": "job_8f3a2b...",3 "cancelled": true4}Retention
Jobs and their results are retained for 72 hours after creation, fetchable on
GET /v1/jobs/{job_id} and replayable on the stream. After that they age out to 404Not Found.