research (plan → multi-source web search → synthesis) and video (model generation) run as background jobs: you enqueue them, get a job_id back immediately, then poll until they finish.
Lifecycle
- Enqueue —
POST /api/v1/researchor/api/v1/videoreturns202with{ id, job_id, status: "queued", poll_url }. - Poll —
GET /api/v1/jobs/{job_id}returns the currentstatus:queued→active→completed(orfailed). - Read the result — when
status === "completed",resultholds the typed payload.
SDK usage
waitForJob() does the polling loop for you, with an optional onProgress callback and an AbortSignal.
theo.video() + waitForJob<VideoJobResult>().
Polling cadence & limits
- Default poll interval is 2s;
waitForJobgives up after 5 minutes by default (maxWaitMs) and throws aTheoTimeoutError. RaisemaxWaitMsfor long video renders. - Don’t poll faster than ~1s — it wastes rate-limit budget without finishing the job any sooner.
Cancellation
Pass anAbortSignal. When it fires, waitForJob stops polling and rejects with a TheoCancelledError (it does not cancel the server-side job — it just stops your client from waiting).
Result schemas
result is typed via the generic on job<T>() / waitForJob<T>(). The shapes mirror the REST payload (Get Job Status):
ResearchJobResult—{ report, sources: { title, url }[], queries, sourceCount }VideoJobResult—{ videoUrl, model, engine, durationMs }ImageJobResult—{ imageUrl, model, engine }DocumentJobResult—{ title, format, downloadUrl, sizeBytes }Anymodel/enginefields are Theo-branded — raw upstream identifiers never appear.
Cost & usage
Job results do not yet carry a per-jobcost_cents field. To attribute spend for research/video jobs, read the aggregated Usage report (theo.usage()), which includes a by_mode breakdown covering research and video.
Failures
Whenstatus === "failed", result is null and error holds the reason string. The enqueue call itself can also return 503 queue_unavailable if the job queue is temporarily down — retry shortly.