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

# Job management

> Wait for, inspect, list, stream, and cancel jobs

Slow NDI operations return a `Job`. You can wait for completion, read the
latest state, stream progress, cancel active work, or list past jobs through
`client.jobs`.

## Submit and wait

```ts theme={"dark"}
import { NdiClient, isParseResult } from "ndi-sdk";

const client = new NdiClient();
const upload = await client.documents.createUpload("report.pdf");
const queued = await client.documents.parse(upload);
const job = await client.jobs.wait(queued.job_id, { timeout: 600 });

if (isParseResult(job.result)) {
  console.log(job.status, job.result.markdown);
}
```

`jobs.wait` polls until the job is terminal. Its default timeout is 300
seconds. A timeout stops waiting but does not cancel the server-side job.

## Choose inline wait or polling

Job-creating methods accept `wait_seconds`. This asks the server to hold the
submission response open for up to 300 seconds:

```ts theme={"dark"}
import { NdiClient } from "ndi-sdk";

const client = new NdiClient();
const upload = await client.documents.createUpload("report.pdf");
let job = await client.documents.parse(upload, { wait_seconds: 30 });

if (!job.is_terminal) {
  job = await client.jobs.wait(job.job_id, { timeout: 600 });
}
```

The document method returns a `Job` whether the work finishes inline or keeps
running. `wait_seconds` is a server-side hold-open window. The `timeout`
passed to `jobs.wait` is the SDK's total polling budget. Neither timeout
cancels the job.

## Retrieve job status

Use `jobs.get` when you control polling or need the latest state once.

```ts theme={"dark"}
import { JobStatus, NdiClient } from "ndi-sdk";

const client = new NdiClient();
const jobId = "550e8400-e29b-41d4-a716-446655440003";
const job = await client.jobs.get(jobId);

if (job.status === JobStatus.SUCCEEDED) {
  console.log(job.result);
} else if (job.status === JobStatus.FAILED) {
  console.log(job.error);
} else {
  console.log(`Still ${job.status}`);
}
```

| Status      | Meaning                           |
| ----------- | --------------------------------- |
| `queued`    | Accepted and waiting to start     |
| `running`   | Processing                        |
| `succeeded` | Result is ready                   |
| `failed`    | Processing ended with `job.error` |
| `cancelled` | The job was cancelled             |

Treat status values as open-ended. Use `job.is_terminal` or the exported
`JobStatus` members instead of assuming this table can never grow.

## Handle failures and timeouts

By default, `jobs.wait` throws `JobFailedError` for failed or cancelled jobs
and `JobTimeoutError` when its local wait budget expires. Both errors carry the
last job state.

```ts theme={"dark"}
import { JobFailedError, JobTimeoutError } from "ndi-sdk";

const jobId = "550e8400-e29b-41d4-a716-446655440003";

try {
  const job = await client.jobs.wait(jobId, { timeout: 600 });
  console.log(job.status);
} catch (err) {
  if (err instanceof JobFailedError) {
    console.error(err.job.status, err.job.error);
  } else if (err instanceof JobTimeoutError) {
    console.log(`${err.job.job_id} is still ${err.job.status}`);
  } else {
    throw err;
  }
}
```

Set `raise_on_failure: false` when you prefer to inspect a failed or cancelled
job as a normal return value.

```ts theme={"dark"}
import { JobStatus } from "ndi-sdk";

const jobId = "550e8400-e29b-41d4-a716-446655440003";
const job = await client.jobs.wait(jobId, { raise_on_failure: false });
if (job.status !== JobStatus.SUCCEEDED) {
  console.log(job.error);
}
```

## Cancel a job

```ts theme={"dark"}
const jobId = "550e8400-e29b-41d4-a716-446655440003";
const cancelled = await client.jobs.cancel(jobId, {
  reason: "The source document was replaced",
});
console.log(cancelled.status);
```

Only queued or running jobs can be cancelled. Cancelling a terminal job throws
`ConflictError` with code `job_not_cancellable`.

## List and paginate jobs

`jobs.list` returns one cursor page, newest first. Filter by workspace, kind,
status, or creation time.

```ts theme={"dark"}
import { JobKind, JobStatus, NdiClient } from "ndi-sdk";

const client = new NdiClient();
const page = await client.jobs.list({
  kind: [JobKind.PARSE],
  status: [JobStatus.SUCCEEDED],
  created_after: new Date("2026-09-01T00:00:00Z"),
  limit: 50,
});

for (const job of page.items) {
  console.log(job.job_id, job.created_at);
}
```

Use `iterAll` to traverse every page without managing cursors.

```ts theme={"dark"}
const workspaceId = "550e8400-e29b-41d4-a716-446655440001";

for await (const job of client.jobs.iterAll({ workspace_id: workspaceId })) {
  console.log(job.job_id, job.status);
}
```

## Stream progress

`jobs.events` yields Server-Sent Events until the job becomes terminal or the
server closes the stream.

```ts theme={"dark"}
const jobId = "550e8400-e29b-41d4-a716-446655440003";

for await (const event of client.jobs.events(jobId)) {
  console.log(event.status, event.message);
}
```

Events are a latency convenience, not a completion guarantee. Always confirm
the final state with `jobs.get` or `jobs.wait`. Pass `last_event_id` when
reconnecting to continue the event sequence.

```ts theme={"dark"}
const jobId = "550e8400-e29b-41d4-a716-446655440003";
const lastEventId = "12";

for await (const event of client.jobs.events(jobId, {
  last_event_id: lastEventId,
})) {
  console.log(event);
}
```

## Result retention

The job row can outlive its result payload. Check `job.result_state` before
using an older result. When retention has removed the payload, artifact reads
throw `ResultExpiredError`.

<Note>
  The TypeScript SDK does not currently wrap `GET /v1/jobs/{id}/request`,
  `DELETE /v1/jobs/{id}`, or `PATCH /v1/jobs/{id}`. Call those REST endpoints
  directly when you need request inspection, soft deletion, or updates.
</Note>

## Methods

```text theme={"dark"}
get(job_id: string): Promise<Job>
list(opts?: { workspace_id?, kind?, status?, created_after?, cursor?, limit? }): Promise<Page<Job>>
iterAll(opts?: { workspace_id?, kind?, status?, created_after?, limit? }): AsyncGenerator<Job>
wait(job_id: string, opts?: { timeout?: number; raise_on_failure?: boolean }): Promise<Job>
cancel(job_id: string, opts?: { reason?: string }): Promise<Job>
events(job_id: string, opts?: { last_event_id?: string }): AsyncGenerator<JobEvent>
groundCrop(job_id: string, artifact_ref: string, opts?: { byte_range?: string }): Promise<Uint8Array>
usage(opts: { period_start: Date | string; period_end: Date | string; workspace_id?: string; group_by?: "day" | "kind" | "workspace" }): Promise<UsageResponse>
```

## Next steps

* [Async and concurrency](/sdks/typescript/async)
* [Error handling](/sdks/typescript/error-handling)
* [Jobs and artifacts guide](/guides/jobs)
