# Jobs and executions

> Poll, list, cancel and delete Nodaro runs from TypeScript. client.jobs tracks single generations, and client.executions tracks whole workflow runs.

Source: https://nodaro.ai/docs/developers/sdk/jobs-and-executions

A **job** is one generation in Nodaro, such as one image, one video render or one voiceover, and an **execution** is one run of a whole workflow. **`client.jobs`** reads, lists, cancels and deletes jobs, and **`client.executions`** reads, lists and cancels workflow runs. **`client.videoPro`** stops or continues a long Generate Video Pro run. The methods call the same endpoints as the [Jobs](https://nodaro.ai/docs/developers/api/jobs) and [Executions](https://nodaro.ai/docs/developers/api/executions) REST APIs.

## Methods

| Method | What it does |
| --- | --- |
| [`executions.get(id)`](#executionsgetid) | Read a workflow run, with the state of every node |
| [`executions.listForWorkflow(workflowId, params?)`](#executionslistforworkflowworkflowid-params) | List the runs of a workflow |
| [`executions.cancel(id, params?)`](#executionscancelid-params) | Stop a workflow run |
| [`jobs.get(id)`](#jobsgetid) | Read a job with its input and output |
| [`jobs.list(params?)`](#jobslistparams) | List your jobs, newest first |
| [`jobs.getStatus(id)`](#jobsgetstatusid) | Read the status of a job, for polling |
| [`jobs.cancel(id)`](#jobscancelid) | Stop a job and refund its reserved credits |
| [`jobs.delete(id)`](#jobsdeleteid) | Delete a job and the media it produced |
| [`videoPro.stop(jobId)`](#videoprostopjobid) | Stop a Generate Video Pro run and keep what is done |
| [`videoPro.continueRun(jobId, opts?)`](#videoprocontinuerunjobid-opts) | Continue a Generate Video Pro run as a new job |

## Statuses

A job moves through these statuses:

| Job status | Meaning |
| --- | --- |
| `pending`, `queued` | Waiting for a worker. |
| `processing` | Running. `progress` goes from 0 to 100 when the model reports it. |
| `pending_review` | Held for human review by a content policy of this deployment. Not an end state. |
| `completed` | Done. `output_data` holds the result. |
| `failed` | Failed. `error_message` explains why, and `error_hint` may classify it. |
| `cancelled` | Stopped by a cancel. |

An execution has its own statuses: `pending`, `running`, `completed`, `failed`, `cancelled`, `stopping`, `timed_out` and `discarded`. Each node inside it is `pending`, `running`, `completed`, `failed` or `skipped`.

## client.executions

An execution is one run of a workflow. It groups one job per AI node and the state of every node.

### executions.get(id)

Reads a workflow run, including a map of every node's state. When the id belongs to a single-node job instead of a workflow run, the server answers with the same shape for that one node.

```ts
get(id: string): Promise<{ data: WorkflowExecution }>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "The execution id that client.workflows.run() returned." },
}}
/>

```ts
const { data } = await client.executions.get(executionId)
console.log(data.status, `${data.completedNodes}/${data.totalNodes}`, data.totalCreditsUsed)
```

A `WorkflowExecution` has `id`, `workflowId`, `status`, `triggerType` (`manual`, `webhook`, `schedule`, `app_run` or `single-node`), `nodeStates`, `totalNodes`, `completedNodes`, `failedNodes`, `totalCreditsUsed`, `errorMessage`, `startedAt`, `completedAt`, `createdAt` and `updatedAt`.

**Reading a node's output.** `nodeStates[nodeId].output` is present when the node `completed`. It can also be present when the node `failed` but the run kept a usable result, as the 3D scene nodes do. Check the field, not the status, and never treat a present `output` as success:

```ts

const node = data.nodeStates["scene-1"]
if (node.status === "failed") {
console.error(node.error) // the failure stands
if (node.output?.plan) {
// the draft the run kept is still here, and it was billed
}
}
nodeStateMayCarryOutput(node.status) // true for "completed" and "failed"
```

`OUTPUT_BEARING_NODE_STATUSES`, the set of those two statuses, is exported too.

### executions.listForWorkflow(workflowId, params?)

Lists the runs of one workflow, newest first, a page at a time. The list includes single-node runs started on that workflow.

```ts
listForWorkflow(workflowId: string, params?: ListExecutionsForWorkflowParams): Promise<{
data: WorkflowExecutionSummary[]
nextCursor?: string
}>
```

<TypeTable
type={{
workflowId: { type: 'string', required: true, description: "The workflow id." },
limit: { type: 'number', description: "The page size." },
cursor: { type: 'string', description: "The nextCursor of the previous page." },
status: { type: 'string', description: "A comma-separated list of statuses, such as pending,running." },
source: { type: '"editor" | "all"', description: "editor leaves out runs started by apps, webhooks and schedules." },
}}
/>

```ts
const { data: runs, nextCursor } = await client.executions.listForWorkflow(workflowId, {
limit: 20,
status: "completed",
})
```

### executions.cancel(id, params?)

Stops a workflow run. There are three ways to stop it:

- **Immediately** (the default): running jobs are cancelled, their reserved credits are refunded, and the status becomes `cancelled`.
- **`mode: "after_current"`**: the status becomes `stopping`. Running nodes finish and land on the canvas and in your library, then the run stops.
- **`mode: "discard"`**: no new nodes start, but running jobs are not cancelled, because an external model call cannot be stopped halfway. They finish and are saved to your library, but their results do not return to the canvas. The status becomes `discarded`, and there is no refund, because the jobs completed.

```ts
cancel(id: string, params?: { mode?: "after_current" | "discard" }): Promise<{ success: true }>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "The execution id." },
mode: { type: '"after_current" | "discard"', description: "How to stop. Omit it to stop immediately." },
}}
/>

```ts
await client.executions.cancel(executionId, { mode: "after_current" })
```

## client.jobs

A job is one generation: one image, one video render, one voiceover. A workflow run creates one job per AI node, and every single-node run creates one job. Job fields use snake_case, as the API sends them.

### jobs.get(id)

Reads a job, including its input and output.

```ts
get(id: string): Promise<{ data: Job }>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "The job id." },
}}
/>

```ts
const { data: job } = await client.jobs.get(jobId)
if (job.status === "completed") console.log(job.output_data)
```

A `Job` has these fields:

| Field | Description |
| --- | --- |
| `id`, `status`, `progress` | The job id, its status and its progress from 0 to 100. |
| `input_data`, `output_data` | The request and the result. Server-only values are removed from both. |
| `error_message` | Why the job failed, or `null`. |
| `error_hint` | A structured reason for some failures. See below. |
| `credits` | The credits reserved for the job, or `null`. |
| `credit_status` | `reserved`, `committed` or `refunded`: where the credits stand. `null` for a job without a charge. |
| `job_type` | The kind of job. |
| `created_at`, `started_at`, `completed_at` | Timestamps. |
| `user_id` | The owner. |
| `source`, `source_detail` | Where the job came from: `sdk`, `cli`, `mcp`, `app`, `web`, `api` and more, with a detail such as `sdk/2.17.0`. |
| `recovering` | `true` while the platform recovers a job whose worker stopped after the model delivered. |

**`error_hint`** has two kinds. Narrow on `kind` before you read the rest:

- **`safety-block`**: the model's own safety filter refused the output. `class` is `copyright`, `likeness` or `safety`. `retried` says whether Nodaro already retried once, and `suggestedProvider`, when present, names a model you can retry the same request on. See [When a model blocks the prompt](https://nodaro.ai/docs/nodes/image/generate-image#when-a-model-blocks-the-prompt).
- **`policy-block`**: a content policy of this deployment rejected the request or the result. `reason` is written for users, so show it as it is.

### jobs.list(params?)

Lists your jobs, newest first, a page at a time (`GET /v1/jobs`).

```ts
list(params?: { type?: string; origin?: string; limit?: number; cursor?: string }): Promise<{
data: Job[]
next: string | null
}>
```

<TypeTable
type={{
type: { type: 'string', description: "Only jobs created by this route, such as llm-structured or video-analysis. Exact match." },
origin: { type: 'string', description: "Only jobs whose request carried this origin value, the name of the app that sent it. Exact match." },
limit: { type: 'number', default: '50', description: "The page size, 1 to 100." },
cursor: { type: 'string', description: "The next value of the previous page." },
}}
/>

```ts
let cursor: string | undefined
do {
const page = await client.jobs.list({ type: "llm-structured", origin: "my-app", cursor })
for (const job of page.data) console.log(job.id, job.status)
cursor = page.next ?? undefined
} while (cursor)
```

A page can hold fewer rows than `limit`, even none, and still have a `next`. Page on `next`, never on the number of rows.

### jobs.getStatus(id)

Reads only the status of a job: `id`, `status`, `progress`, `output_data`, `error_message`, `error_hint` and `credit_status` (`GET /v1/jobs/:id/status`). It skips the request data and the cost fields, so it is much lighter than `get()`. Use it in polling loops.

```ts
getStatus(id: string): Promise<{ data: JobStatusResult }>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "The job id." },
}}
/>

```ts
async function waitForJob(jobId: string) {
for (;;) {
const { data } = await client.jobs.getStatus(jobId)
if (data.status === "completed") return data.output_data
if (data.status === "failed" || data.status === "cancelled") {
throw new Error(data.error_message ?? data.status)
}
await new Promise((resolve) => setTimeout(resolve, 2_000))
}
}
```

[`client.nodes.runAndWait()`](https://nodaro.ai/docs/developers/sdk/nodes#runandwaittype-params-opts) runs this loop for you, with progress, cancellation and typed errors.

### jobs.cancel(id)

Cancels a job and refunds the credits it had reserved. A job held for review can be cancelled too.

```ts
cancel(id: string): Promise<{ success: true; cancelled: number }>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "The job id." },
}}
/>

```ts
const { cancelled } = await client.jobs.cancel(jobId)
```

### jobs.delete(id)

Deletes a job and the private media it produced (`DELETE /v1/jobs/:id`). Only the job's owner may delete it. A running job is deleted as it is, so cancel it first when its worker should stop.

```ts
delete(id: string): Promise<{ success: true }>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "The job id." },
}}
/>

```ts
await client.jobs.cancel(jobId)
await client.jobs.delete(jobId)
```

## client.videoPro

Run control for [Generate Video Pro](https://nodaro.ai/docs/nodes/video/generate-video-pro), the node that renders long videos in segments. It works on Nodaro Cloud. Start the run like any node run, then use these methods on its job.

### videoPro.stop(jobId)

Stops a running Generate Video Pro job gracefully. The segment in progress is abandoned and still billed, and the remaining segments are skipped. The finished segments are joined into the job's final video, and the unused reserve is refunded. A job that has not started yet is cancelled with a full refund.

```ts
stop(jobId: string): Promise<StopVideoProResult>
```

<TypeTable
type={{
jobId: { type: 'string', required: true, description: "The Generate Video Pro job id." },
}}
/>

```ts
await client.videoPro.stop(jobId)
const { data } = await client.jobs.getStatus(jobId) // becomes completed, with the partial video
```

Keep polling the job. It completes with `output_data.pro.stopped` set to `true`.

### videoPro.continueRun(jobId, opts?)

Continues a stopped, failed or completed run as a **new job**. Segments before `fromSegment` are reused, and every segment from it on is generated again. You pay only for the regenerated segments plus the flat Pro fee.

```ts
continueRun(jobId: string, opts?: { fromSegment?: number }): Promise<ContinueVideoProResult>
```

<TypeTable
type={{
jobId: { type: 'string', required: true, description: "The job to continue." },
fromSegment: { type: 'number', description: "The first segment to generate again, counted from 1. By default, the first segment that was not delivered." },
}}
/>

```ts
const { jobId: newJobId, fromSegment } = await client.videoPro.continueRun(jobId, { fromSegment: 4 })
```

The result carries the new `jobId` to poll, and may carry `continuedFromJobId`, `fromSegment`, `segmentCount` and `deduped`.

## Frequently asked questions

### What is the difference between a job and an execution?

A job is one generation, such as one image or one video. An execution is one run of a whole workflow, and it groups one job per AI node plus the state of every node.

### Which method should a polling loop call?

client.jobs.getStatus(jobId). It returns only the status, progress, output and error, so it is much lighter than client.jobs.get.

### Does cancelling a job refund its credits?

Yes. client.jobs.cancel stops the job and refunds the credits it had reserved. Cancelling an execution immediately does the same for its running jobs.

### What does the status pending_review mean?

A content policy of this deployment is holding the result for a person to review. It is not an end state: the job becomes completed, failed or cancelled later. Keep waiting and do not run the request again.
