> ## Documentation Index
> Fetch the complete documentation index at: https://docs.hitheo.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Async Jobs (Research & Video)

> Run long jobs the right way: enqueue with research()/video(), then poll the job with waitForJob().

Some Theo capabilities are **too slow to return inline**. `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.

<Warning>
  Do **not** send `mode: "research"` or `mode: "video"` to `/completions` or `theo.stream()`. Those run the work **inline**, which blows past the request timeout. The SDK throws a `TheoUsageError` immediately if you try. Use the dedicated methods below.
</Warning>

## Lifecycle

1. **Enqueue** — `POST /api/v1/research` or `/api/v1/video` returns `202` with `{ id, job_id, status: "queued", poll_url }`.
2. **Poll** — `GET /api/v1/jobs/{job_id}` returns the current `status`: `queued` → `active` → `completed` (or `failed`).
3. **Read the result** — when `status === "completed"`, `result` holds the typed payload.

## SDK usage

`waitForJob()` does the polling loop for you, with an optional `onProgress` callback and an `AbortSignal`.

```typescript theme={null}
import { Theo, type ResearchJobResult } from "@hitheo/sdk";

const theo = new Theo({ apiKey: process.env.THEO_API_KEY! });

const job = await theo.research({
  prompt: "Compare React, Vue, and Svelte for enterprise apps in 2026",
  depth: "advanced",
  max_sources: 10,
});

const final = await theo.waitForJob<ResearchJobResult>(job.job_id, {
  intervalMs: 3000,                       // poll cadence (default 2s)
  maxWaitMs: 180_000,                      // give up after 3 min (default 5 min)
  onProgress: (s) => console.log(`${s.status} ${s.progress}%`),
  signal: AbortSignal.timeout(180_000),   // optional hard cancel
});

if (final.status === "completed") {
  console.log(final.result?.report);
  console.log(`${final.result?.sourceCount} sources`);
}
```

Video works the same way with `theo.video()` + `waitForJob<VideoJobResult>()`.

### Polling cadence & limits

* Default poll interval is **2s**; `waitForJob` gives up after **5 minutes** by default (`maxWaitMs`) and throws a `TheoTimeoutError`. Raise `maxWaitMs` for long video renders.
* Don't poll faster than \~1s — it wastes rate-limit budget without finishing the job any sooner.

### Cancellation

Pass an `AbortSignal`. 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).

```typescript theme={null}
const controller = new AbortController();
// e.g. user navigates away
document.addEventListener("stop", () => controller.abort());

await theo.waitForJob(job.job_id, { signal: controller.signal });
```

## Result schemas

`result` is typed via the generic on `job<T>()` / `waitForJob<T>()`. The shapes mirror the REST payload ([Get Job Status](/api-reference/jobs/get)):

* **`ResearchJobResult`** — `{ report, sources: { title, url }[], queries, sourceCount }`
* **`VideoJobResult`** — `{ videoUrl, model, engine, durationMs }`
* **`ImageJobResult`** — `{ imageUrl, model, engine }`
* **`DocumentJobResult`** — `{ title, format, downloadUrl, sizeBytes }`
  Any `model` / `engine` fields are Theo-branded — raw upstream identifiers never appear.

## Cost & usage

Job results do not yet carry a per-job `cost_cents` field. To attribute spend for research/video jobs, read the aggregated [Usage report](/api-reference/usage/get) (`theo.usage()`), which includes a `by_mode` breakdown covering `research` and `video`.

## Failures

When `status === "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.
