# Run nodes

> Run any Nodaro node from TypeScript without a workflow. Discover node types, start runs, wait for results, and pass references and camera direction.

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

**`client.nodes`** lists the node types a Nodaro server supports and runs any one of them directly, without building a workflow. A run posts your parameters to the node's endpoint, `POST /v1/<type>`, and returns a job you can wait for. It is the same path the [CLI](https://nodaro.ai/docs/developers/cli) uses for `nodaro nodes run` and the one Nodaro's MCP tools use. See [Run a single node](https://nodaro.ai/docs/developers/api/nodes) for the REST view.

## Methods

| Method | What it does |
| --- | --- |
| [`nodes.list()`](#list) | List every node type, with its models and credit cost |
| [`nodes.get(type)`](#gettype) | Read one node type |
| [`nodes.run(type, params?, options?)`](#runtype-params-options) | Start a node and return its job id at once |
| [`nodes.runAndWait(type, params?, opts?)`](#runandwaittype-params-opts) | Start a node, poll its job, and return the output |
| [`nodes.runMany(type, paramsList, opts?)`](#runmanytype-paramslist-opts) | Start several runs of one node at once and wait for all of them |

## client.nodes

### list()

Lists every node type the server supports. The answer can be cached by the server for 5 minutes. The call costs nothing and needs no scopes.

```ts
list(): Promise<{ data: NodeDescriptor[] }>
```

```ts
const { data: nodes } = await client.nodes.list()

const imageGenerators = nodes.filter((n) => n.category === "ai-image")
const takesReferences = nodes.filter((n) => n.capabilities?.includes("supports-reference-image"))
```

Each `NodeDescriptor` has these fields:

| Field | Type | Description |
| --- | --- | --- |
| `type` | `string` | The API type, such as `generate-image`. Pass it to `run()`. |
| `label` | `string` | The node's name in the editor. |
| `category` | `string` | The category, such as `ai-image`, `ai-video`, `ai-audio`, `ai-text`, `processing` or `parameter`. |
| `description` | `string` | A one-line description. |
| `outputType` | `string` | `text`, `image`, `video`, `audio`, `data` or `none`. |
| `creditCost` | `number \| string` | The credit cost: a number when fixed, or a range such as `"2-620"` when it depends on the model. Nodaro Cloud only. |
| `providers` | `string[]` | The model ids the node can run, for the `provider` parameter. |
| `capabilities` | `string[]` | Flags such as `supports-reference-image` or `supports-end-frame`. |
| `inputSchema` | `{ fields }` | The input fields you can set, each with `key`, `type`, `required` and `options`. |
| `maxDurationSec` | `number` | The longest duration the node accepts, where it has one. |
| `providerResolutions` | `Record<string, string[]>` | The resolutions each model accepts, where they differ. |

Self-hosted Community and Business installs have no credit system, so their descriptors omit `creditCost`.

### get(type)

Reads the descriptor of one node type.

```ts
get(type: string): Promise<{ data: NodeDescriptor }>
```

<TypeTable
type={{
type: { type: 'string', required: true, description: "The node's API type, such as generate-image or generate-video." },
}}
/>

```ts
const { data: node } = await client.nodes.get("generate-video")
console.log(node.providers)  // every video model id
console.log(node.creditCost) // Nodaro Cloud only
```

To show prices next to the models, pass `node.providers` to [`client.credits.modelCosts()`](https://nodaro.ai/docs/developers/sdk/models-and-credits).

### run(type, params?, options?)

Starts one node and returns at once. The body is sent to `POST /v1/<type>`, the route every generation node uses. Field names match the node's input fields.

```ts
run(type: string, params?: Record<string, unknown>, options?: { idempotencyKey?: string }): Promise<RunNodeResult>
```

<TypeTable
type={{
type: { type: 'string', required: true, description: "The node's API type, such as generate-image, image-to-video or text-to-speech." },
params: { type: 'Record<string, unknown>', default: '{}', description: "The request body. generate-image, generate-video, text-to-video and assemble-narrated-video have typed parameters, listed below." },
idempotencyKey: { type: 'string', description: "Sent as the Idempotency-Key header. Reuse the same value when you retry a request that timed out, so the run is not started or charged twice." },
}}
/>

```ts
const result = await client.nodes.run("generate-image", {
prompt: "A snow leopard in the mountains",
provider: "nano-banana-2",
})

if ("jobId" in result) {
const { data: job } = await client.jobs.getStatus(result.jobId)
console.log(job.status)
}
```

**What comes back.** Most node types are asynchronous: the result carries a `jobId`, and a worker does the generation. Poll [`client.jobs.getStatus(jobId)`](https://nodaro.ai/docs/developers/sdk/jobs-and-executions) until it ends, or use `runAndWait()`. A few inline node types, such as `combine-text`, return their full result at once, without a `jobId`. Branch on the presence of `jobId`.

**Parameter corrections.** On the image node types (`generate-image`, `image-to-image` and `edit-image`), the server may correct a value the chosen model does not accept. The run goes ahead with the corrected value, and the credits reserved match it. The result then carries `adjustments`, one entry per corrected field:

```ts
const result = await client.nodes.run("generate-image", {
prompt: "A snow leopard",
provider: "gpt-image-2",
aspectRatio: "3:2",
})
if ("adjustments" in result && result.adjustments?.length) {
for (const a of result.adjustments) {
console.warn(`${a.field}: ${a.from} -> ${a.to ?? "(dropped)"} (${a.reason})`)
}
}
```

Each adjustment has `field` (`aspectRatio`, `resolution`, `quality` or `duration`), `from`, `to` and `reason`. `adjustments` is absent when nothing changed.

Throws `InsufficientCreditsError` when the account cannot pay, `StorageExceededError` when storage is full, and `JobBlockedError` when a content policy of the deployment refuses the request. See [Errors](https://nodaro.ai/docs/developers/sdk/errors).

### runAndWait(type, params?, opts?)

Runs one asynchronous node to completion. It calls `run()`, takes the `jobId`, and polls [`client.jobs.getStatus()`](https://nodaro.ai/docs/developers/sdk/jobs-and-executions) until the job ends. It resolves with the job's output when the status is `completed`.

```ts
runAndWait(type: string, params?: Record<string, unknown>, opts?: RunAndWaitOptions): Promise<NodeJobOutput>
```

<TypeTable
type={{
type: { type: 'string', required: true, description: "The node's API type." },
params: { type: 'Record<string, unknown>', default: '{}', description: "The request body, as for run()." },
signal: { type: 'AbortSignal', description: "Stops the waiting and rejects with JobAbortedError. The job itself keeps running." },
onProgress: { type: '(status: JobStatusResult) => void', description: "Called with the job status on every poll. status.progress runs from 0 to 100 when the model reports it." },
pollMs: { type: 'number', default: '2000', description: "How often to poll, in milliseconds." },
maxMs: { type: 'number', default: '900000', description: "How long to wait in total, in milliseconds, before JobTimeoutError." },
idempotencyKey: { type: 'string', description: "Sent with the run request. Reuse it when you retry the same request." },
}}
/>

```ts
const output = await client.nodes.runAndWait(
"generate-video",
{ prompt: "Rain falls on a neon street at night", provider: "seedance-2-fast", duration: 4 },
{ onProgress: (s) => console.log(`${s.progress ?? 0}%`) },
)
console.log(output.videoUrl, output.thumbnailUrl)
```

The output is a `NodeJobOutput`: `imageUrl` for image nodes, `videoUrl` and `thumbnailUrl` for video nodes, `audioUrl` for audio nodes, plus any other fields the node writes. [Audio Separation](https://nodaro.ai/docs/nodes/audio/audio-separation), for example, adds one URL per stem, such as `vocalUrl` and `instrumentalUrl`.

It throws these typed errors:

| Error | When |
| --- | --- |
| `InsufficientCreditsError`, `StorageExceededError`, `JobBlockedError` | The run request was refused, before any polling. |
| `JobFailedError` | The job ended as `failed` or `cancelled`. It carries `jobId` and the job's error message. |
| `JobTimeoutError` | `maxMs` passed. The job is not cancelled. |
| `JobAbortedError` | Your `signal` fired. The job is not cancelled. |
| `JobHeldError` | The job is held for human review, on deployments with a content policy. |

**Slow recoveries.** Sometimes a model delivers its result after the worker gave up on it. The job then stays `processing` with `recovering: true` while the platform recovers it, which can take tens of minutes for slow models. If `JobTimeoutError` ends your wait, fetch the job later with `client.jobs.get(jobId)`, or raise `maxMs`.

### runMany(type, paramsList, opts?)

Starts several runs of one node type at the same time and waits for all of them, for example to generate a grid of candidates. Each entry runs through `runAndWait()`.

```ts
runMany(type: string, paramsList: Record<string, unknown>[], opts?: RunAndWaitOptions): Promise<RunManyResult[]>
```

<TypeTable
type={{
type: { type: 'string', required: true, description: "The node's API type, used for every run." },
paramsList: { type: 'Record<string, unknown>[]', required: true, description: "One request body per run." },
opts: { type: 'RunAndWaitOptions', description: "The signal, onProgress, pollMs and maxMs shared by every run. A shared signal stops the whole batch." },
}}
/>

```ts
const results = await client.nodes.runMany("generate-image", [
{ prompt: "A snow leopard at sunrise" },
{ prompt: "A snow leopard at golden hour" },
{ prompt: "A snow leopard at blue hour" },
])
for (const { jobId, output } of results) console.log(jobId, output.imageUrl)
```

It resolves when every run has finished, to one `{ jobId, output }` per entry, in input order. It rejects as soon as any run fails, with the same errors as `runAndWait()`. To choose the best result afterwards, pass the URLs to [`client.reduce.run()`](https://nodaro.ai/docs/developers/sdk/llm-and-reduce).

## Typed parameters

Four node types have typed parameters, so your editor completes and checks their fields. Every other node type takes a plain object; its fields are the node's input fields, listed in `inputSchema` and on its page in the [Node Reference](https://nodaro.ai/docs/nodes). The 3D scene nodes also have typed parameters: see [3D scenes](https://nodaro.ai/docs/developers/sdk/scenes-3d).

| Node type | Parameter type | Node page |
| --- | --- | --- |
| `generate-image` | `GenerateImageParams` | [Generate Image](https://nodaro.ai/docs/nodes/image/generate-image) |
| `generate-video` | `GenerateVideoParams` | [Generate Video](https://nodaro.ai/docs/nodes/video/generate-video) |
| `text-to-video` | `TextToVideoParams` | [Generate Video](https://nodaro.ai/docs/nodes/video/generate-video) |
| `assemble-narrated-video` | `AssembleNarratedVideoParams` | [Assemble Narrated Video](https://nodaro.ai/docs/nodes/video/assemble-narrated-video) |

Every typed parameter object also accepts other fields of the route. The server validates the full body.

### GenerateImageParams

<TypeTable
type={{
prompt: { type: 'string', description: "What the image shows." },
provider: { type: 'string', description: "The model id, such as nano-banana-2 or gpt-image-2. Omit it for the node's default model." },
negativePrompt: { type: 'string', description: "What the model should avoid." },
referenceImageUrls: { type: 'string[]', description: "Reference image URLs, added after connectedReferences." },
connectedReferences: { type: 'ConnectedReference[]', description: "Labeled references, as the editor wires them. See References below." },
describedReferences: { type: 'DescribedReference[]', description: "Up to 10 subjects you can name and describe but have no picture for." },
referenceOrder: { type: 'string[]', description: "Reference ids in the order the model should see them." },
direction: { type: 'object', description: "Cinematic direction as picker ids. See Direction below." },
}}
/>

### GenerateVideoParams

The image-to-video lane: a start frame, an optional end frame, and references.

<TypeTable
type={{
prompt: { type: 'string', description: "What happens in the clip. Optional when a start frame is given." },
provider: { type: 'string', description: "The model id, such as seedance-2-fast or kling-3.0." },
imageUrl: { type: 'string', description: "The start frame." },
endFrameUrl: { type: 'string', description: "The closing frame, on models that support one." },
frameFit: { type: '"original" | "ratio" | "resolution"', default: '"resolution"', description: "How a start or end frame is reshaped. resolution resizes it to the size the model renders, ratio fixes only the aspect, original sends it untouched." },
frameDelivery: { type: '"auto" | "frame" | "reference"', default: '"auto"', description: "Whether the frame is sent as a real start frame or as a reference image named as the opening frame. auto chooses per model." },
referenceImageUrls: { type: 'string[]', description: "Reference images." },
referenceVideoUrls: { type: 'string[]', description: "Reference videos, on models that accept them." },
referenceAudioUrls: { type: 'string[]', description: "Reference audio, on models that accept it." },
referenceVideoCaptions: { type: 'string[]', description: "One caption per reference video, in the same order." },
referenceAudioCaptions: { type: 'string[]', description: "One caption per reference audio clip, in the same order." },
connectedReferences: { type: 'ConnectedReference[]', description: "Labeled references." },
describedReferences: { type: 'DescribedReference[]', description: "Named subjects without a picture." },
referenceOrder: { type: 'string[]', description: "The reference order." },
direction: { type: 'object', description: "Cinematic and motion direction as picker ids." },
}}
/>

### TextToVideoParams

The prompt-only video lane, `POST /v1/text-to-video`. The prompt is required, and start and end frames belong to `generate-video` instead. A model without a text-to-video mode answers `400 image_required`.

<TypeTable
type={{
prompt: { type: 'string', required: true, description: "What happens in the clip." },
provider: { type: 'string', description: "The model id." },
duration: { type: 'number', description: "The length in seconds. The allowed values depend on the model." },
sound: { type: 'boolean', description: "Native audio, on models that render sound." },
negativePrompt: { type: 'string', description: "What to avoid." },
aspectRatio: { type: 'string', description: "The frame shape, such as 16:9." },
resolution: { type: 'string', description: "The resolution, such as 720p." },
seed: { type: 'number', description: "A fixed seed, on models that support one." },
referenceImageUrls: { type: 'string[]', description: "Reference images." },
referenceVideoUrls: { type: 'string[]', description: "Reference videos." },
referenceAudioUrls: { type: 'string[]', description: "Reference audio." },
direction: { type: 'object', description: "Cinematic and motion direction as picker ids." },
subject: { type: 'object', description: "Subject picker ids, such as a person or an animal, placed before the direction wording." },
}}
/>

### AssembleNarratedVideoParams

Joins video blocks with narration into one video. The run costs `3 + ceil(blocks / 6)` credits. See [Assemble Narrated Video](https://nodaro.ai/docs/nodes/video/assemble-narrated-video) for how each block is fitted to its narration.

<TypeTable
type={{
blocks: { type: '{ videoUrl: string; audioUrl?: string }[]', required: true, description: "1 to 60 blocks, in play order." },
voiceVolume: { type: 'number', default: '100', description: "Narration volume, 0 to 200." },
clipAudioVolume: { type: 'number', default: '40', description: "The clips' own audio volume, 0 to 200." },
maxSlowdown: { type: 'number', default: '1.5', description: "How much a clip may be slowed to fit its narration, 1 to 2." },
trimStartFrames: { type: 'number', default: '0', description: "Frames to cut from the start of each clip, 0 to 120." },
trimEndFrames: { type: 'number', default: '0', description: "Frames to cut from the end of each clip, 0 to 120." },
}}
/>

## References

`generate-image`, `generate-video` and `text-to-video` accept references the way the editor wires them. The server turns them into numbered directives such as `@image_1` in the prompt, so you do not write "Image 1 is..." yourself.

- **`connectedReferences`** is a list of `ConnectedReference` entries, the editor's wired-reference shape, exported from the SDK. The server removes duplicates and keeps as many as the model accepts. `referenceOrder` sets their order by id.
- **Identity lock.** An entry may carry `identityLock: { enabled: true, text? }`. The server then adds a short line that tells the model to keep that reference's identity. `text` replaces the built-in wording, and `{ref}` in it stands for the reference's name. It is off by default.
- **`describedReferences`** takes up to 10 `{ name, description }` entries for subjects you can name but have no picture of, such as a role in a script. Each becomes a line `Name — description.` in the prompt. Keep the name in your prompt text, so the model knows who it is.
- **`descriptionOverride`** on a reference entry replaces its stored description for this run only.
- **Captions for video and audio references.** `referenceVideoCaptions` and `referenceAudioCaptions` follow the order of `referenceVideoUrls` and `referenceAudioUrls`. Each caption appears in the prompt as `@video_1: caption.` or `@audio_1: caption.`.
- **Mention an image reference in the prompt.** On `generate-image`, name a reference and mention it as `@<name>:<index>` or `@<name>:<index>:<role>`, for example `@town:1:background`. The mention renders that reference, or its role, at that point of the prompt. `~lock` and `~nolock` work as on character mentions.

See [Reference roles](https://nodaro.ai/docs/guides/reference-roles) for the roles you can use and [Consistent characters](https://nodaro.ai/docs/guides/consistent-characters) for identity work.

## Direction

`generate-image`, `generate-video` and `text-to-video` accept a `direction` object of picker **ids** instead of words. Nodaro writes tested wording for each id into the prompt, so your code sends ids and the wording stays up to date.

```ts
await client.nodes.runAndWait("generate-image", {
prompt: "A detective waits under a street lamp",
direction: { shotSize: "wide-shot", timeOfDay: "golden-hour", mood: "suspicious" },
})
```

- **Keys** are picker dimensions, such as `shotSize`, `lightingStyle`, `style`, `mood`, `photographer` or `era`. The video routes add motion keys, such as `cameraMotion`, `actionFx`, `transition`, `loopSubject` and the `temporal` keys.
- **Values** are one id or an array of ids. A dimension that takes several values keeps at most its own limit and drops the rest. The request is refused only above 8 values per key or 100 characters per id.
- **Unknown keys and ids are skipped**, not refused. An empty `direction` leaves your prompt unchanged.
- **One map serves images and video.** A still-only key sent to a video run is accepted and adds nothing.
- `extend-video` takes no `direction`, because its prompt continues an existing clip.

Read the valid ids from [`client.pickerCatalogs`](https://nodaro.ai/docs/developers/sdk/pickers-and-prompts). See [Picker catalogs](https://nodaro.ai/docs/developers/picker-catalogs) for every dimension.

## Language-model nodes

`run(type, params)` posts to `/v1/<type>`. For language-model nodes that route exists only for `generate-script`, `image-critic`, `qa-check` and `describe-to-picker`. The other language-model nodes use a longer path, so call them with [`client.request()`](https://nodaro.ai/docs/developers/sdk/client#requestmethod-path-options):

| Node type | Endpoint |
| --- | --- |
| `llm-chat` | `/v1/llm-chat/generate` |
| `after-effects` | `/v1/after-effects/generate` |
| `motion-graphics` | `/v1/motion-graphics/generate` |
| `lottie-overlay` | `/v1/lottie-overlay/generate` |
| `3d-title` | `/v1/3d-title/generate` |
| `image-to-text` | `/v1/image-to-text/describe` |
| `video-composer` | `/v1/scene-graph/generate` |

```ts
await client.nodes.run("generate-script", {
prompt: "A 3-scene product launch script for a smart water bottle",
reasoningEffort: "high",
})
```

- **`reasoningEffort`** is `"none"`, `"low"`, `"medium"`, `"high"`, `"xhigh"` or `"max"`, depending on the model. Omit it, or send a level the model does not support, for the model's default. `xhigh` and `max` bill one credit tier higher. See [Prompt](https://nodaro.ai/docs/nodes/automate/prompt) for the models and their tiers.
- **`advancedMode: true`** runs a Gemini model directly on its maker's API. Only there do `temperature`, `maxTokens` and the full effort range take full effect. It bills one credit tier higher, on top of any effort increase. A model without that option answers `400 advanced_mode_unsupported`.
- **Streaming is not wrapped.** The SDK does not read the streaming answer of the Prompt node. Use `fetch` with a readable stream for it.

## Model-specific rules

Some video models accept values the others do not. Each model page lists the full options and prices.

- **[Seedance 2](https://nodaro.ai/docs/models/video/seedance-2)** accepts `resolution: "4k"` and `aspectRatio: "adaptive"` or `"21:9"`. [Seedance 2 Fast](https://nodaro.ai/docs/models/video/seedance-2-fast) and [Seedance 2 Mini](https://nodaro.ai/docs/models/video/seedance-2-mini) render at 480p or 720p only.
- **[Seedance 2.5](https://nodaro.ai/docs/models/video/seedance-2-5)** renders at 480p, 720p or 1080p, runs up to 30 seconds in one call, and takes 30 image, 10 video and 10 audio references. With a start frame it uses that frame's aspect and refuses an explicit `aspectRatio`.
- **[MiniMax Hailuo 3](https://nodaro.ai/docs/models/video/minimax-h3)** (`minimax-h3`) takes 9 image, 3 video and 3 audio references at `resolution: "2K"` (the default) or `"768P"`. Any other value renders and bills as 2K.
- **[Wan 3.0](https://nodaro.ai/docs/models/video/wan-3-0)** (`wan-3` and the faster `wan-3-prime`) takes 10 image, 5 video and 5 audio references. The reference lists cannot be combined with `imageUrl` or `endFrameUrl`. `duration` is a whole number from 2 to 30, and `resolution` is `480p`, `720p` or `1080p`.
- **[Gemini Omni Flash](https://nodaro.ai/docs/models/video/gemini-omni-flash)** takes the same request as [Gemini Omni](https://nodaro.ai/docs/models/video/gemini-omni): durations of 4, 6, 8 or 10 seconds, 720p to 4K, and 16:9 or 9:16 only.

**Text to Speech.** When you omit `provider`, [Text to Speech](https://nodaro.ai/docs/nodes/audio/text-to-speech) uses [ElevenLabs v3](https://nodaro.ai/docs/models/audio/elevenlabs-v3) for text up to 3,000 characters. Longer text falls back to [ElevenLabs Turbo v2.5](https://nodaro.ai/docs/models/audio/elevenlabs-turbo-v2-5), whose limit is 40,000 characters, so it is not cut short. A `provider` you name is always used.

## Scrapers and other input nodes

Input nodes run the same way. A scraper, such as [Web Scrape](https://nodaro.ai/docs/nodes/automate/web-scrape), answers at once: the result carries a `jobId` for your history and the data itself, so you can use it without polling. The request fields are the node's input fields, listed in `inputSchema`.

## Frequently asked questions

### How do I generate an image with the Nodaro SDK?

Call client.nodes.runAndWait with the node type generate-image and a prompt. It resolves with the job output, whose imageUrl is the generated picture.

### What is the difference between run and runAndWait?

run starts the node and returns a jobId at once. runAndWait starts it, polls the job every 2 seconds, and resolves with the output when the job completes.

### How long does runAndWait wait?

Up to 15 minutes by default. Change it with the maxMs option. A timeout does not cancel the job, which usually still completes on the server.

### Which node types can I run?

Call client.nodes.list() for every type the server supports, with its category, output type, models and credit cost. Every node in the Node Reference has an API type.

### How do I avoid paying twice when I retry a request?

Pass an idempotencyKey in the options of run or runAndWait, and reuse the same key when you retry. The platform returns the first run instead of starting a second one.
