# Pipelines

> Start a story-to-video pipeline from TypeScript, follow it stage by stage, approve or reject stages, chat with the director, and read the finished timeline.

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

**`client.pipelines`** runs story-to-video pipelines: multi-stage productions that turn a story prompt into a film. A pipeline moves through eight stages, `script`, `characters`, `objects`, `locations`, `shot_list`, `scene_images`, `animate_audio_edit` and `post_merge`, and it can stop at each one for your approval. It is the programmatic version of **Create film** in the studio. The methods call the [Pipelines REST API](https://nodaro.ai/docs/developers/api/pipelines).

## Modes

| Mode | What happens at each stage |
| --- | --- |
| `manual` (the default) | The pipeline waits in `awaiting_approval`. Approve or reject each stage. |
| `auto` | The engine approves every stage itself and runs to the end. |
| `guided` | Like manual, and you can also chat with the director about a stage and apply its proposed changes. |

A pipeline's status is `queued`, `running`, `awaiting_approval`, `completed`, `failed`, `cancelled` or `forked`.

## Methods

| Method | Scope | What it does |
| --- | --- | --- |
| [`create(input)`](#createinput) | `pipelines:execute` | Start a pipeline |
| [`get(id)`](#getid) | `pipelines:read` | Read a pipeline's status and credits |
| [`list()`](#list) | `pipelines:read` | List your pipelines |
| [`cancel(id)`](#cancelid) | `pipelines:execute` | Stop a pipeline |
| [`pendingApprovals(id)`](#pendingapprovalsid) | `pipelines:read` | List the stages waiting for approval |
| [`approveStage(id, stage, edits?)`](#approvestageid-stage-edits) | `pipelines:approve` | Approve a stage, with optional edits |
| [`rejectStage(id, stage, feedback)`](#rejectstageid-stage-feedback) | `pipelines:approve` | Reject a stage and run it again |
| [`approveSubGate(id, gate)`](#approvesubgateid-gate) | `pipelines:approve` | Approve a check inside the animation stage |
| [`getStage(id, stage)`](#getstageid-stage) | `pipelines:read` | Read one stage's output |
| [`getTimeline(id)`](#gettimelineid) | `pipelines:read` | Read the assembled film |
| [`branch(id, input)`](#branchid-input) | `pipelines:execute` | Run a finished pipeline again from a stage |
| [`chatStage(pipelineId, stage, message)`](#chatstagepipelineid-stage-message) | `pipelines:approve` | Ask the director to change a stage |
| [`applyChatProposal(pipelineId, stage, turnId)`](#applychatproposalpipelineid-stage-turnid) | `pipelines:approve` | Accept a change the director proposed |
| [`getStageChat(pipelineId, stage)`](#getstagechatpipelineid-stage) | `pipelines:read` | Read a stage's chat |

The scopes apply to OAuth tokens. API tokens and sessions are not limited by scopes.

## client.pipelines

### create(input)

Starts a pipeline. In `auto` mode it runs to the end on its own; poll `get()` for its status and `getTimeline()` for the result. In `manual` or `guided` mode, drive it with `pendingApprovals()`, `approveStage()` and `approveSubGate()`.

```ts
create(input: PipelineInput): Promise<{ id: string }>
```

<TypeTable
type={{
story_prompt: { type: 'string', required: true, description: "The story, 1 to 4,000 characters." },
target_duration_seconds: { type: 'number', required: true, description: "The target length of the film, 5 to 3,600 seconds." },
format: { type: '"trailer" | "short_film" | "music_video" | "reel" | "commercial"', required: true, description: "The kind of film." },
root_node_id: { type: 'string', required: true, description: "The id of the pipeline's root node in its workflow." },
workflow_id: { type: 'string', description: "The workflow the pipeline belongs to." },
pipeline_type: { type: '"story_to_video" | "song_to_music_video"', default: '"story_to_video"', description: "The kind of pipeline." },
mode: { type: '"manual" | "auto" | "guided"', default: '"manual"', description: "How stages are approved." },
output_resolution: { type: 'string', default: '"720p"', description: "The resolution of the finished film." },
language: { type: 'string', default: '"en"', description: "The language of the script and the voices." },
max_cost_credits: { type: 'number', description: "A spending limit for the whole run, in credits." },
style_directives: { type: 'object', description: "Style instructions for the whole film." },
config: { type: 'object', description: "Overrides of the pipeline's settings, such as the models used." },
}}
/>

```ts
const { id } = await client.pipelines.create({
story_prompt: "A lighthouse keeper finds a message in a bottle that predicts tomorrow's storm.",
target_duration_seconds: 60,
format: "short_film",
root_node_id: "pipeline-root",
mode: "auto",
})
```

### get(id)

Reads a pipeline's current state. Poll it to follow an `auto` run to the end.

```ts
get(id: string): Promise<PipelineRecord>
```

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

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

A `PipelineRecord` has `id`, `status`, `current_stage`, `mode`, `spent_credits`, `reserved_credits`, `upfront_credit_estimate`, `failure_reason` (set when the status is `failed`), `current_progress_message`, and, for a branch, `branched_from_pipeline_id` and `branched_from_stage`.

### list()

Lists your pipelines, newest first.

```ts
list(): Promise<PipelineRecord[]>
```

```ts
const pipelines = await client.pipelines.list()
```

### cancel(id)

Stops a running pipeline. Unspent reserved credits are refunded. Cancelling a pipeline that has already ended changes nothing.

```ts
cancel(id: string): Promise<{ ok: true }>
```

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

```ts
await client.pipelines.cancel(id)
```

### pendingApprovals(id)

Lists the stages waiting for approval, each with its output. The list is empty during a clean `auto` run, because the engine approves stages itself.

```ts
pendingApprovals(id: string): Promise<Array<{ stage_name: PipelineStageName; output: unknown }>>
```

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

```ts
const approvals = await client.pipelines.pendingApprovals(id)
for (const { stage_name, output } of approvals) console.log(stage_name, output)
```

### approveStage(id, stage, edits?)

Approves a stage so the pipeline moves on. Pass `edits`, a JSON Patch, to change the stage's output before it is approved.

```ts
approveStage(id: string, stage: PipelineStageName, edits?: unknown): Promise<{ ok: true }>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "The pipeline id." },
stage: { type: 'PipelineStageName', required: true, description: "The stage to approve, such as script." },
edits: { type: 'JSON Patch operations', description: "Changes to apply to the stage output first, as a list of JSON Patch operations." },
}}
/>

```ts
await client.pipelines.approveStage(id, "script")

await client.pipelines.approveStage(id, "script", [
{ op: "replace", path: "/title", value: "The Keeper's Warning" },
])
```

### rejectStage(id, stage, feedback)

Rejects a stage with a note. The engine runs the stage again and takes the note into account.

```ts
rejectStage(id: string, stage: PipelineStageName, feedback: string): Promise<{ ok: true }>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "The pipeline id." },
stage: { type: 'PipelineStageName', required: true, description: "The stage to run again." },
feedback: { type: 'string', required: true, description: "What to change." },
}}
/>

```ts
await client.pipelines.rejectStage(id, "script", "Make the story darker and more suspenseful")
```

### approveSubGate(id, gate)

Approves a check inside the `animate_audio_edit` stage, such as `dialogue_recheck`, so the pipeline continues with the next step.

```ts
approveSubGate(id: string, gate: SubGateName): Promise<{ ok: true; gate: SubGateName; resumed_at: string }>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "The pipeline id." },
gate: { type: 'SubGateName', required: true, description: "The check to approve, such as dialogue_recheck." },
}}
/>

```ts
await client.pipelines.approveSubGate(id, "dialogue_recheck")
```

### getStage(id, stage)

Reads one stage's `status`, `output` and `critic_feedback`. Use it to inspect the script or the plan before you approve it.

```ts
getStage(id: string, stage: PipelineStageName): Promise<{ status: string; output: unknown; critic_feedback: unknown }>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "The pipeline id." },
stage: { type: 'PipelineStageName', required: true, description: "The stage to read." },
}}
/>

```ts
const { status, output } = await client.pipelines.getStage(id, "script")
```

### getTimeline(id)

Reads the assembled film: the scenes in order with their durations, the audio URLs and the animation progress. Render it yourself, or hand it to an editor.

```ts
getTimeline(id: string): Promise<PipelineTimeline>
```

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

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

A `PipelineTimeline` has `fps`, `width`, `height`, `scenes` (each `{ compositeUrl, durationSeconds }`), `musicUrl`, `narrationUrl`, and `animateProgress` with `totalShots`, `shotsDone` and `percent`.

### branch(id, input)

Runs a finished pipeline again from one stage, as a new pipeline. The stages before it are copied as approved. The original pipeline stays `completed`.

```ts
branch(id: string, input: { fromStage: PipelineStageName }): Promise<{ pipelineId: string; clonedStages: string[]; clonedEntities: number }>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "The finished pipeline." },
fromStage: { type: 'PipelineStageName', required: true, description: "The first stage to run again." },
}}
/>

```ts
const { pipelineId } = await client.pipelines.branch(id, { fromStage: "scene_images" })
```

### chatStage(pipelineId, stage, message)

Sends a message to the director in `guided` mode. The pipeline's mode must be `guided`, and the stage must be `awaiting_approval`. The reply may carry a `proposed_change` you can accept with `applyChatProposal()`. Chat works on the stages that support it, such as `script`.

```ts
chatStage(pipelineId: string, stage: ChatEnabledStage, message: string): Promise<{
turnId: string
role: "assistant"
content: string
proposed_change: ProposedChange | null
}>
```

<TypeTable
type={{
pipelineId: { type: 'string', required: true, description: "The pipeline id." },
stage: { type: 'ChatEnabledStage', required: true, description: "The stage to discuss." },
message: { type: 'string', required: true, description: "Your request." },
}}
/>

```ts
const { content, proposed_change, turnId } = await client.pipelines.chatStage(
id,
"script",
"Can you make the protagonist's motivation clearer in scene 2?",
)
```

### applyChatProposal(pipelineId, stage, turnId)

Accepts the change the director proposed in an earlier turn. The change is checked, saved as a new version of the stage, and the stage is approved.

```ts
applyChatProposal(pipelineId: string, stage: ChatEnabledStage, turnId: string): Promise<
| { applied: true; attemptId: string; newOutput: unknown }
| { applied: false; error: { code: string; detail?: unknown } }
>
```

<TypeTable
type={{
pipelineId: { type: 'string', required: true, description: "The pipeline id." },
stage: { type: 'ChatEnabledStage', required: true, description: "The stage." },
turnId: { type: 'string', required: true, description: "The assistant turn whose proposal to apply." },
}}
/>

```ts
const result = await client.pipelines.applyChatProposal(id, "script", turnId)
if (result.applied) console.log("Approved:", result.newOutput)
else console.log("Not applied:", result.error.code)
```

When the change cannot be applied but you can recover, the result is `applied: false` and the director has already added a reply with a hint. A hard failure throws a 409 error.

### getStageChat(pipelineId, stage)

Reads a stage's chat history. It is empty before the first message.

```ts
getStageChat(pipelineId: string, stage: ChatEnabledStage): Promise<{ turns: ChatTurn[] }>
```

<TypeTable
type={{
pipelineId: { type: 'string', required: true, description: "The pipeline id." },
stage: { type: 'ChatEnabledStage', required: true, description: "The stage." },
}}
/>

```ts
const { turns } = await client.pipelines.getStageChat(id, "script")
```

Each `ChatTurn` has `id`, `turn_n`, `role`, `content`, `proposed_change`, `applied_to_attempt_id` and `created_at`.

## Frequently asked questions

### What is a Nodaro pipeline?

A pipeline makes a film from a story prompt in stages: script, characters, objects, locations, shot list, scene images, animation with audio and edit, and the final merge. It is the headless version of Create film in the studio.

### How do I run a pipeline without approving each stage?

Create it with mode auto. The engine approves each stage itself and runs to the end. Poll client.pipelines.get(id) for the status and read the result with getTimeline(id).

### How do I change a stage's output before approving it?

Pass a JSON Patch as the third argument of approveStage. It is applied to the stage output before the stage is approved.

### Which OAuth scopes do pipelines need?

pipelines:read to read pipelines and stages, pipelines:execute to create, cancel and branch them, and pipelines:approve to approve, reject and chat about stages.
