# Pipelines

> Start a Story to Video pipeline over REST, follow its stages, approve or reject each gate, chat with the director, and branch a finished pipeline from a stage.

Source: https://nodaro.ai/docs/developers/api/pipelines

The **Pipelines API** runs Story → Video from code. You send a one-line story. The pipeline engine writes a script, creates the cast, props and places, and plans the shots. It then renders keyframes, animates them with sound and merges the film. Each stage can stop for your approval, run by itself, or open a chat with the director.

Pipelines run on Nodaro Cloud only; Community and Business editions answer `403 edition_required`. On a self-hosted install, build the same steps as a workflow with nodes such as [Generate Script](https://nodaro.ai/docs/nodes/video/generate-script), [Generate Image](https://nodaro.ai/docs/nodes/image/generate-image) and [Generate Video](https://nodaro.ai/docs/nodes/video/generate-video). The routes take a bearer token; OAuth app tokens need the scopes listed below. See [Authentication](https://nodaro.ai/docs/developers/api/authentication).

## Endpoints

| Method | Path | Scope | What it does |
| --- | --- | --- | --- |
| `POST` | `/v1/pipelines` | `pipelines:execute` | Create and start a pipeline. |
| `GET` | `/v1/pipelines` | `pipelines:read` | List your pipelines, most recent first. |
| `GET` | `/v1/pipelines/:id` | `pipelines:read` | Get the status, the current stage and the credits. |
| `GET` | `/v1/pipelines/:id/events` | `pipelines:read` | Stream the pipeline's events (server-sent events). |
| `GET` | `/v1/pipelines/:id/stages/:stage` | `pipelines:read` | Get one stage's status, output and reviewer feedback. |
| `GET` | `/v1/pipelines/:id/pending-approvals` | `pipelines:read` | List the stages waiting for approval. |
| `GET` | `/v1/pipelines/:id/timeline` | `pipelines:read` | Get the assembled film: scenes, durations and audio. |
| `POST` | `/v1/pipelines/:id/stages/:stage/approve` | `pipelines:approve` | Approve a stage, optionally with edits. |
| `POST` | `/v1/pipelines/:id/stages/:stage/reject` | `pipelines:approve` | Reject a stage with feedback, so it runs again. |
| `POST` | `/v1/pipelines/:id/sub-gates/:gate/approve` | `pipelines:approve` | Approve a checkpoint inside the animate stage. |
| `POST` | `/v1/pipelines/:id/sub-gates/:gate/reject` | `pipelines:approve` | Reject that checkpoint and stop the pipeline. |
| `POST` | `/v1/pipelines/:id/stages/:stage/chat` | `pipelines:approve` | Send a message to the director (guided mode). |
| `GET` | `/v1/pipelines/:id/stages/:stage/chat` | `pipelines:read` | Read the chat of a stage. |
| `POST` | `/v1/pipelines/:id/stages/:stage/chat/turns/:turnId/apply` | `pipelines:approve` | Apply a change the director proposed. |
| `POST` | `/v1/pipelines/:id/branch` | `pipelines:execute` | Re-run a completed pipeline from a stage, as a new pipeline. |
| `POST` | `/v1/pipelines/:id/fork` | `pipelines:execute` | Stop the pipeline and keep its canvas as ordinary nodes. |
| `POST` | `/v1/pipelines/:id/cancel` | `pipelines:execute` | Cancel a running pipeline and refund unspent credits. |

## Stages and modes

A pipeline walks eight stages in order. `:stage` in a path is one of these names.

| Stage | What it produces |
| --- | --- |
| `script` | The story plan: the title, the scenes, the cast, the locations and the props. |
| `characters` | A character for each role in the cast. |
| `objects` | The props the story needs. |
| `locations` | The places the story visits. |
| `shot_list` | The shots of each scene, with their camera and continuity choices. |
| `scene_images` | A keyframe for each shot. |
| `animate_audio_edit` | The animated shots, with dialogue, narration, music and the edit. |
| `post_merge` | The final merged film. |

The `mode` you choose at creation decides who moves the pipeline forward:

| Mode | What happens |
| --- | --- |
| `manual` (default) | Every stage stops at `awaiting_approval`. You approve it, edit it or reject it, and the pipeline goes on. |
| `auto` | The engine runs every stage by itself. Automatic reviewers check the script, the coverage of the cast, locations and props, and the keyframes. After 3 blocking verdicts in a row, the pipeline fails and the unspent credits are refunded. |
| `guided` | Like `manual`, plus a chat with the director on the `script` and `post_merge` stages. |

## Start a pipeline

`POST /v1/pipelines` creates the pipeline, reserves its credits and starts it. It answers `201` with `{ id }`.

**curl**

```bash
curl -X POST https://app.nodaro.ai/v1/pipelines \
  -H "Authorization: Bearer $NODARO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
"root_node_id": "0e6b2f7c-4a1d-4c8e-9b3f-5d7a2c1e8f4b",
"story_prompt": "A lighthouse keeper must restart the light before the storm hits.",
"format": "short_film",
"target_duration_seconds": 60,
"mode": "auto",
"output_resolution": "720p"
}'
```

**TypeScript SDK**

```ts

const client = createClient({
baseUrl: 'https://app.nodaro.ai',
auth: new StaticTokenAuth(process.env.NODARO_API_KEY!),
})

const { id } = await client.pipelines.create({
root_node_id: crypto.randomUUID(),
story_prompt: 'A lighthouse keeper must restart the light before the storm hits.',
format: 'short_film',
target_duration_seconds: 60,
mode: 'auto',
})
```

```json
{ "id": "c2f8a4e6-1b9d-4f3a-8c7e-6d2b9f1a5e3c" }
```

<TypeTable
type={{
story_prompt: { type: 'string', description: 'What the film is about, up to 4,000 characters.', required: true },
format: { type: "'trailer' | 'short_film' | 'music_video' | 'reel' | 'commercial'", description: 'The kind of film. It sets the allowed length.', required: true },
target_duration_seconds: { type: 'integer', description: 'The film length in seconds, inside the range of the format. 600 at most.', required: true },
root_node_id: { type: 'string', description: 'An id for the pipeline root. When no canvas node started the pipeline, send any unique id, such as a new UUID.', required: true },
mode: { type: "'manual' | 'auto' | 'guided'", description: 'Who moves the pipeline forward.', default: 'manual' },
output_resolution: { type: "'480p' | '720p' | '1080p' | '4K'", description: 'The resolution of the final film.', default: '720p' },
language: { type: 'string', description: 'The language of the script and the dialogue.', default: 'en' },
workflow_id: { type: 'string (uuid)', description: 'A workflow of yours to build the film in.' },
config: { type: 'object', description: 'Optional switches: music_enabled, narration_enabled and lipsync_enabled, each true by default.' },
max_cost_credits: { type: 'integer', description: 'A cap on the credits the pipeline may spend.' },
}}
/>

Each format allows a range of lengths:

| Format | Shortest | Longest |
| --- | --- | --- |
| `reel` | 7 s | 90 s |
| `commercial` | 10 s | 90 s |
| `trailer` | 30 s | 180 s |
| `short_film` | 12 s | 600 s |
| `music_video` | 30 s | 600 s |

## Follow a pipeline

`GET /v1/pipelines/:id` returns the pipeline's state. Poll it every few seconds, or open `GET /v1/pipelines/:id/events` to receive the changes as server-sent events.

```json
{
"id": "c2f8a4e6-1b9d-4f3a-8c7e-6d2b9f1a5e3c",
"status": "running",
"current_stage": "scene_images",
"mode": "auto",
"spent_credits": 412,
"reserved_credits": 1180,
"upfront_credit_estimate": 1650,
"failure_reason": null,
"current_progress_message": "Rendering keyframe 5 of 9",
"branched_from_pipeline_id": null,
"branched_from_stage": null
}
```

`status` is `queued`, `running`, `awaiting_approval`, `completed`, `failed`, `cancelled` or `forked`. `failure_reason` explains a `failed` pipeline.

When the pipeline is `completed`, `GET /v1/pipelines/:id/timeline` returns the film as data you can render or hand to an editor:

```json
{
"fps": 24,
"width": 1280,
"height": 720,
"scenes": [
{ "compositeUrl": "https://cdn.nodaro.ai/pipelines/scene-1.mp4", "durationSeconds": 8.5 },
{ "compositeUrl": "https://cdn.nodaro.ai/pipelines/scene-2.mp4", "durationSeconds": 11 }
],
"musicUrl": "https://cdn.nodaro.ai/pipelines/score.mp3",
"narrationUrl": "https://cdn.nodaro.ai/pipelines/narration.mp3"
}
```

While the animate stage runs, the timeline also carries `animateProgress`: `{ totalShots, shotsDone, percent }`. To finish the cut in an external editor, see [Export a timeline](https://nodaro.ai/docs/developers/api/studio-productions#export-a-timeline-to-an-editing-app).

**curl**

```bash
curl https://app.nodaro.ai/v1/pipelines/c2f8a4e6-1b9d-4f3a-8c7e-6d2b9f1a5e3c \
  -H "Authorization: Bearer $NODARO_API_KEY"

curl https://app.nodaro.ai/v1/pipelines/c2f8a4e6-1b9d-4f3a-8c7e-6d2b9f1a5e3c/timeline \
  -H "Authorization: Bearer $NODARO_API_KEY"
```

**TypeScript SDK**

```ts
const pipeline = await client.pipelines.get(id)
console.log(pipeline.status, pipeline.current_stage)

const timeline = await client.pipelines.getTimeline(id)
for (const scene of timeline.scenes) {
console.log(scene.compositeUrl, scene.durationSeconds)
}
```

## Approve or reject a stage

In `manual` and `guided` mode, each stage stops at `awaiting_approval`. `GET /v1/pipelines/:id/pending-approvals` lists the stages waiting, each `{ stage_name, output }`. Read a stage in full with `GET /v1/pipelines/:id/stages/:stage`, which returns `{ status, output, critic_feedback }`.

- **Approve.** `POST /v1/pipelines/:id/stages/:stage/approve` returns `{ ok: true }` and the pipeline moves on. To change the stage's output first, send `{ edits }`: a JSON Patch applied to the output before approval.
- **Reject.** `POST /v1/pipelines/:id/stages/:stage/reject` with `{ feedback }` returns `{ ok: true }`. The engine runs the stage again and takes your note into account.

**curl**

```bash
curl -X POST https://app.nodaro.ai/v1/pipelines/c2f8a4e6-1b9d-4f3a-8c7e-6d2b9f1a5e3c/stages/script/approve \
  -H "Authorization: Bearer $NODARO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "edits": [{ "op": "replace", "path": "/title", "value": "The Last Light" }] }'

curl -X POST https://app.nodaro.ai/v1/pipelines/c2f8a4e6-1b9d-4f3a-8c7e-6d2b9f1a5e3c/stages/script/reject \
  -H "Authorization: Bearer $NODARO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "feedback": "Make the story darker and more suspenseful." }'
```

**TypeScript SDK**

```ts
const approvals = await client.pipelines.pendingApprovals(id)
const { output } = await client.pipelines.getStage(id, 'script')

await client.pipelines.approveStage(id, 'script', [
{ op: 'replace', path: '/title', value: 'The Last Light' },
])
// or
await client.pipelines.rejectStage(id, 'script', 'Make the story darker and more suspenseful.')
```

### Checkpoints inside the animate stage

In `manual` and `guided` mode, the `animate_audio_edit` stage can stop at two checkpoints of its own:

- **`dialogue_recheck`.** The stage compares the real length of the dialogue with the plan and adjusts the scene timing. When a scene cannot stay within 10% of its target, the stage waits for your approval.
- **`silent_cut_preview`.** The stage assembles a preview of the cut without music and waits for your approval before it generates, and pays for, the music.

`POST /v1/pipelines/:id/sub-gates/:gate/approve` resumes the stage and returns `{ ok: true, gate, resumed_at }`. `POST /v1/pipelines/:id/sub-gates/:gate/reject` fails the stage and the pipeline, and refunds the unspent credits. In `auto` mode, the stage continues without stopping.

## Chat with the director

In `guided` mode, a stage waiting for approval has a chat. Send a message of up to 8,000 characters with `POST /v1/pipelines/:id/stages/:stage/chat` and `{ message }`. The director answers in one sentence and may propose a change.

```json
{
"turnId": "f1a3c5e7-9b2d-4f6a-8c1e-3d5b7f9a2c4e",
"role": "assistant",
"content": "I moved the keeper's reason for staying into scene 2 and tightened the ending.",
"proposed_change": {
"change_type": "edit_artifact",
"json_patch": [{ "op": "replace", "path": "/scenes/1/description", "value": "The keeper finds his late wife's log and decides to stay." }]
}
}
```

`proposed_change` is `null`, an `edit_artifact` with a `json_patch`, or a `suggest_branch` with `from_stage` and a `reason`.

| Stage | What the director can propose | Turns |
| --- | --- | --- |
| `script` | An edit to the plan (a JSON Patch on the title, the scenes, the cast, the locations or the props), or a branch when the change is too structural to patch. | 20 per pipeline |
| `post_merge` | A diagnosis of the final film and a branch from an earlier stage. The film itself cannot be patched. | 8 per pipeline |

To accept a proposal, call `POST /v1/pipelines/:id/stages/:stage/chat/turns/:turnId/apply`. It returns `{ applied: true, attemptId, newOutput }` and approves the stage. When the change breaks the plan, for example by removing a character a scene still uses, it returns `{ applied: false, error }` and the director adds a turn that explains what to fix. A proposal that is not valid, or a stage that is no longer waiting, answers `409`. `GET /v1/pipelines/:id/stages/:stage/chat` returns `{ turns }`, the whole conversation.

A guided pipeline reserves 40 extra credits for the chat when it starts. Unused credits are refunded.

**curl**

```bash
curl -X POST https://app.nodaro.ai/v1/pipelines/c2f8a4e6-1b9d-4f3a-8c7e-6d2b9f1a5e3c/stages/script/chat \
  -H "Authorization: Bearer $NODARO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "message": "Make the keeper'\''s motivation clearer in scene 2." }'
```

**TypeScript SDK**

```ts
const reply = await client.pipelines.chatStage(id, 'script', "Make the keeper's motivation clearer in scene 2.")
if (reply.proposed_change) {
const result = await client.pipelines.applyChatProposal(id, 'script', reply.turnId)
if (!result.applied) console.log(result.error.code)
}
```

## Branch a finished pipeline

`POST /v1/pipelines/:id/branch` re-runs a `completed` pipeline from one stage, as a new pipeline. The stages before `fromStage` are copied as approved, `fromStage` starts running, and the stages after it are created fresh. The original pipeline stays `completed`.

The branch copies the characters, objects and locations into the new pipeline but reuses their image files, so no file is duplicated. It starts with an empty chat.

**curl**

```bash
curl -X POST https://app.nodaro.ai/v1/pipelines/c2f8a4e6-1b9d-4f3a-8c7e-6d2b9f1a5e3c/branch \
  -H "Authorization: Bearer $NODARO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "fromStage": "scene_images" }'
```

**TypeScript SDK**

```ts
const branch = await client.pipelines.branch(id, { fromStage: 'scene_images' })
console.log(branch.pipelineId, branch.clonedStages)
```

The route answers `201` with `{ pipelineId, clonedStages, clonedEntities }`: the new pipeline's id, the stages copied as approved, and the number of characters, objects and locations copied. `fromStage` is any stage name from the table above.

## Cancel or fork a pipeline

- **Cancel.** `POST /v1/pipelines/:id/cancel` stops a running pipeline, refunds the credits reserved for work that has not run, and returns `{ ok: true }`. Cancelling a finished pipeline changes nothing.
- **Fork.** `POST /v1/pipelines/:id/fork` takes the canvas away from the pipeline. Every node it created becomes an ordinary node you can edit, the unspent credits are refunded, and the status becomes `forked`. A fork cannot be undone. To continue with the engine, start a new pipeline.

## Credits

The pipeline reserves its estimated cost when it starts; `upfront_credit_estimate` shows it. `spent_credits` and `reserved_credits` show where the run stands. A cancelled or failed pipeline refunds what it did not spend, and `max_cost_credits` caps the total. See [Credits](https://nodaro.ai/docs/developers/api/credits).

## Use it from MCP and the SDK

The SDK wraps every route as `client.pipelines.*`. The CLI has no pipeline commands. AI assistants use these MCP tools:

| Tool | Scope | What it does |
| --- | --- | --- |
| `start_pipeline` | `pipelines:execute` | Start a pipeline. Its default mode is `auto`. |
| `get_pipeline_status` | `pipelines:read` | Read the status, the stage and the credits. |
| `pipeline_pending_approvals` | `pipelines:read` | List the stages waiting for approval. |
| `chat_pipeline_stage`, `apply_chat_proposal` | `pipelines:approve` | Chat with the director and apply a proposal. |
| `get_pipeline_stage_chat` | `pipelines:read` | Read a stage's chat. |
| `branch_pipeline` | `pipelines:execute` | Branch a completed pipeline. |

See [Film Director](https://nodaro.ai/docs/mcp/film-director) for the guided way to direct a film from an assistant.

## Errors

| Status | Code | Meaning |
| --- | --- | --- |
| `400` | `validation_error` | The body is invalid, for example a length outside the format's range. |
| `400` | `pipeline_not_completed` | A branch was requested from a pipeline that is not `completed`. |
| `400` | `invalid_stage` | The stage name is not one of the eight stages. |
| `400` | `invalid_change_type_for_stage` | A patch was proposed for the `post_merge` stage, which accepts only a branch. |
| `401` | `unauthorized` | The token is missing, invalid or revoked. |
| `402` | `insufficient_credits` | The account cannot cover the reservation. |
| `403` | `edition_required` | The instance is not Nodaro Cloud. |
| `403` | `insufficient_scope` | An OAuth app token lacks the route's scope. |
| `404` | `pipeline_not_found` | No pipeline with that id belongs to you. |
| `409` | `patch_invalid` | A chat proposal is not a valid change and could not be applied. |
| `409` | `stage_not_awaiting` | The stage is no longer waiting for approval. |
| `501` | `chat_not_wired_for_stage` | The stage has no chat. |

## Frequently asked questions

### What does a Story to Video pipeline produce?

A finished film and the workflow behind it. The pipeline writes a script, creates the characters, objects and locations, plans the shots, renders keyframes, animates them with sound, and merges the result. GET /v1/pipelines/:id/timeline returns the assembled scenes.

### Can a pipeline run without anyone approving it?

Yes. Create it with mode set to auto and the engine runs every stage by itself, with automatic reviewers checking the output. Poll GET /v1/pipelines/:id until the status is completed or failed.

### How do I re-run a finished film from one stage?

Call POST /v1/pipelines/:id/branch with the stage to re-run from. Nodaro creates a new pipeline that keeps the stages before it and runs that stage and the ones after it again. The original stays unchanged.

### What happens to my credits when I cancel a pipeline?

The credits reserved for work that has not run yet are refunded. Credits for work already done are kept. Cancelling a pipeline that has already finished changes nothing.

### Can I run pipelines on a self-hosted install?

No. Pipelines run on Nodaro Cloud only, and self-hosted editions answer 403 edition_required. On a self-hosted install, build the same steps as a workflow on the canvas.
