Nodaro Docs
DocumentationNode ReferenceModelsAI Agents (MCP)DevelopersSelf-hostingResearch
REST API

Workflows

Run saved Nodaro workflows from the API with new input values, wait for the result or poll for it, and list, create, update, export and move workflows.

A workflow is a saved canvas of connected nodes, and the API can run it, give it new input values for one run, and manage it like any other resource. A run starts with one request that returns an executionId, and you poll the run until it finishes. A short run can also wait for its result in the same request.

Endpoints

Run a workflow

MethodPathWhat it does
POST/v1/workflows/:id/runRun the saved workflow, or some of its nodes. Answers 202 with an executionId.
GET/v1/api/workflowsList the workflows your API token can run. Supports ?limit= and ?cursor=.
GET/v1/api/schema?workflowId=…A workflow's input fields and outputs, with estimatedCredits.
POST/v1/api/runRun a workflow with new input values. Add ?wait=true&timeout=… to wait for the result.
GET/v1/api/status/:execIdThe run's status, node counts and credits used.
GET/v1/api/result/:execIdThe run's outputs, once its status is completed or failed.
POST/v1/app/:slug/runRun a published app with its form fields. See MiniApps.

Manage workflows

MethodPathWhat it does
GET/v1/projects/:projectId/workflowsThe workflows of one project, without their nodes and edges.
GET/v1/workflowsYour workflows across all projects.
GET/v1/workflows/:idOne workflow with its nodes, edges and settings.
POST/v1/projects/:projectId/workflowsCreate a workflow in a project.
PATCH/v1/workflows/:idChange any subset of a workflow's fields.
DELETE/v1/workflows/:idDelete a workflow.
GET/v1/workflows/:id/exportExport the workflow as a JSON bundle. Add ?assets=true to include its characters, objects and locations.
POST/v1/workflows/importCreate a workflow from a bundle.
POST/v1/workflows/:id/moveMove a workflow to another project.
GET/v1/workflows/shared-with-meWorkflows other people shared with you.

Sharing, collaborators and per-workflow permissions are covered in Workspaces and organizations.

Three ways to run a workflow

EndpointCredentialInput valuesUse it when
POST /v1/workflows/:id/runAny token. OAuth tokens need workflows:execute.The saved values. nodeIds runs a subset.You run the workflow as saved, for your account or for an OAuth user.
POST /v1/api/runA personal API tokeninputs replaces input-node values for this run.A script needs different values on every run, or wants to wait for the result.
POST /v1/app/:slug/runAny tokenThe app's form fields, plus raw node overridesThe workflow is published as an app with a curated form.

The five /v1/api/ endpoints are the original API-token lane. They predate published apps and remain supported, but new integrations that need inputs usually publish the workflow as an app and use POST /v1/app/:slug/run. That route takes the app's fields as flat inputs and an optional inputOverrides object of raw { nodeId: { field: value } } overrides. The two are merged, and inputOverrides wins on any field both set.

Run a saved workflow

POST /v1/workflows/:id/run starts a run of the workflow as it is saved. Send an empty body to run every node, or nodeIds to run a subset:

curl -s -X POST https://app.nodaro.ai/v1/workflows/8c2d7f1e-3a4b-4c5d-9e6f-7a8b9c0d1e2f/run \
  -H "Authorization: Bearer $NODARO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'
{ "executionId": "3f9e2b1a-7c6d-4e5f-8a9b-0c1d2e3f4a5b", "status": "pending" }
const { executionId } = await client.workflows.run(workflowId)

// Or run only some nodes:
await client.workflows.run(workflowId, { nodeIds: ['text-prompt-1', 'generate-image-1'] })
nodaro workflows run 8c2d7f1e-3a4b-4c5d-9e6f-7a8b9c0d1e2f --watch
nodaro workflows run 8c2d7f1e-3a4b-4c5d-9e6f-7a8b9c0d1e2f --node text-prompt-1 generate-image-1

Prop

Type

The answer is 202 Accepted with { executionId, status }, where status is pending or running. Poll the run with GET /v1/workflow-executions/:id, described in Executions. With --watch, the CLI polls for you and exits with code 2 when the run fails and 130 when it is cancelled.

StatusCodeMeaning
402insufficient_creditsYour credits cannot cover the run's worst-case cost.
403forbiddenYou may see the workflow but not run it. In a workspace, running needs edit access and active membership.
404not_foundNo such workflow, or none you can see.
409already_runningThe workflow already has an active run. The response carries that run's executionId.

Run a workflow with new input values

POST /v1/api/run takes an inputs object that replaces the values of input nodes for this one run. It authenticates with a personal API token. The keys of inputs are node ids or, as a convenience, unique node labels. Inside each key, name the node's input field, such as text for a text prompt.

promptTexttext-prompt-1Generate Imagegenerate-image-1
The example workflow: a Text node, whose text the API run replaces, feeds a Generate Image node.

The example below runs a workflow with a Text node, id text-prompt-1, wired into a Generate Image node.

Find the input fields

GET /v1/api/schema lists the workflow's inputs, with the field each one takes, its outputs, and an estimate of the credits a run costs:

curl -s "https://app.nodaro.ai/v1/api/schema?workflowId=$WORKFLOW_ID" \
  -H "Authorization: Bearer $NODARO_API_KEY"
{
  "workflowId": "8c2d7f1e-3a4b-4c5d-9e6f-7a8b9c0d1e2f",
  "name": "Sunset stills",
  "estimatedCredits": 45,
  "inputs": [
    { "nodeId": "text-prompt-1", "key": "text", "label": "Prompt", "type": "text" }
  ],
  "outputs": [
    { "nodeId": "generate-image-1", "label": "Generate Image", "type": "image" }
  ]
}

Start the run

curl -s -X POST https://app.nodaro.ai/v1/api/run \
  -H "Authorization: Bearer $NODARO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
        "workflowId": "8c2d7f1e-3a4b-4c5d-9e6f-7a8b9c0d1e2f",
        "inputs": {
          "text-prompt-1": { "text": "a cat at sunset" }
        }
      }'
{ "executionId": "3f9e2b1a-7c6d-4e5f-8a9b-0c1d2e3f4a5b", "status": "pending" }

Poll until the run ends

GET /v1/api/status/:execId returns the status, the node counts and the credits used so far. Poll every 2 to 5 seconds until the status is completed, failed, cancelled, timed_out or discarded.

Fetch the result

GET /v1/api/result/:execId returns the outputs of a completed or failed run. For a run that ended cancelled, timed_out or discarded, read errorMessage from the status response instead: those runs have no result payload, and the result route keeps answering 202.

{
  "executionId": "3f9e2b1a-7c6d-4e5f-8a9b-0c1d2e3f4a5b",
  "status": "completed",
  "creditsUsed": 45,
  "durationMs": 12450,
  "errorMessage": null,
  "outputs": [
    {
      "nodeId": "generate-image-1",
      "label": "Generate Image",
      "type": "image",
      "url": "https://…/output.png"
    }
  ]
}

The whole flow as one script, and the same calls from TypeScript:

BASE="https://app.nodaro.ai"
WORKFLOW_ID="8c2d7f1e-3a4b-4c5d-9e6f-7a8b9c0d1e2f"

EXEC=$(curl -s -X POST "$BASE/v1/api/run" \
  -H "Authorization: Bearer $NODARO_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"workflowId\": \"$WORKFLOW_ID\", \"inputs\": {\"text-prompt-1\": {\"text\": \"a cat at sunset\"}}}" \
  | jq -r .executionId)

while true; do
  STATUS=$(curl -s -H "Authorization: Bearer $NODARO_API_KEY" \
    "$BASE/v1/api/status/$EXEC" | jq -r .status)
  echo "Status: $STATUS"
  case "$STATUS" in completed|failed|cancelled|timed_out|discarded) break;; esac
  sleep 5
done

curl -s -H "Authorization: Bearer $NODARO_API_KEY" "$BASE/v1/api/result/$EXEC" | jq .
// The /v1/api/ lane has no dedicated SDK method: call it with client.request.
const schema = await client.request('GET', '/v1/api/schema', {
  query: { workflowId },
})

const { executionId } = await client.request<{ executionId: string }>('POST', '/v1/api/run', {
  body: { workflowId, inputs: { 'text-prompt-1': { text: 'a cat at sunset' } } },
})

const final = ['completed', 'failed', 'cancelled', 'timed_out', 'discarded']
let status = 'pending'
while (!final.includes(status)) {
  await new Promise((r) => setTimeout(r, 3_000))
  ;({ status } = await client.request<{ status: string }>('GET', `/v1/api/status/${executionId}`))
}

const result = await client.request('GET', `/v1/api/result/${executionId}`)

The CLI runs workflows with their saved values only. To pass values from the terminal, publish the workflow as an app and run nodaro apps run <slug> --input prompt="…".

Prop

Type

A token with a workflow scope can run only the workflows in its scope. Any other workflow answers 403 forbidden. POST /v1/api/run and GET /v1/api/workflows count against the token's per-minute limit; the status, result and schema reads do not. See Rate limits.

Sync or async

POST /v1/api/run is asynchronous by default: it answers 202 Accepted with { executionId, status: "pending" } at once, and you poll.

For a short workflow, hold the connection until it finishes:

POST /v1/api/run?wait=true&timeout=120
  • The server checks the run every 5 seconds for up to timeout seconds. The default is 120 and the maximum is 600.
  • If the run finishes in time, the response is the same payload as GET /v1/api/result/:execId. Its status is completed, failed, cancelled, timed_out or discarded.
  • If it does not, the response is 202 with { executionId, status: "pending" }, and you poll from there.

Use the synchronous form for runs you expect to finish in under a minute, such as text generation and light image work. Use the asynchronous form for workflows that render or upscale video. From the SDK, raise timeoutMs in createClient above your timeout, because the client gives up after 60 seconds by default.

POST /v1/workflows/:id/run and the generation routes are always asynchronous. For a single node, the SDK's nodes.runAndWait polls the job for you: see Run a single node.

What a run cannot change

A run request cannot re-point a workflow's outbound nodes. Where a workflow sends to or fetches from is decided by the workflow itself, on every run endpoint, including POST /v1/workflows/:id/run, POST /v1/api/run and POST /v1/app/:slug/run.

The outbound nodes are Webhook Output, the social publishers such as Publish to Social, and the fetchers: Web Scrape, RSS Feed, Telegram Channel Feed and Video URL. On those nodes, an override may not touch:

  • any field whose name ends in Url;
  • target, targets, query, channel, chatId, connectionId, credentialId, platform, webhook, endpoint, host or privacy;
  • the actor and mode selectors that decide which destination field a fetcher reads.

The rule covers values nested in an object or a fieldMappings entry, and an empty value as well as a new address: blanking a destination would make the node read its upstream text instead. Such a request answers 400 locked_field before anything runs. The error names up to ten of the offending fields and counts the rest, and an override nested more than 32 levels deep on such a node is refused outright.

Ordinary fields on those nodes, such as a caption or a limit, and the media url fields of input nodes, such as uploads and reference audio, stay overridable.

Parameter corrections

The image and video generation routes accept one vocabulary for aspectRatio, resolution and quality, whatever the model. No model supports all of it. Instead of rejecting a value the chosen model does not accept, the server corrects it to one the model does accept and tells you what it changed. A rejection in the middle of a workflow would fail every node beside it that had already generated and been billed.

The routes that correct values are POST /v1/generate-image, /v1/image-to-image, /v1/edit-image, /v1/text-to-video and /v1/generate-video:

{
  "jobId": "0f1a9c2e-5b7d-4e8a-9c31-6d2f8b4a7e10",
  "adjustments": [
    {
      "field": "aspectRatio",
      "from": "3:2",
      "to": "auto",
      "reason": "GPT Image 2 does not support aspectRatio \"3:2\" — using \"auto\" instead. Supported: auto, 1:1, 16:9, 9:16, 4:3, 3:4."
    },
    {
      "field": "resolution",
      "from": "4K",
      "to": "1K",
      "reason": "GPT Image 2 only renders 1K at the \"auto\" aspect ratio."
    }
  ]
}
  • adjustments is absent when nothing changed. A valid request's response is exactly { "jobId": "…" }.
  • to is absent when the model has no such setting and the value was dropped, for example an aspectRatio sent to an upscaler.
  • Credits follow the corrected value. A GPT Image 2 request for auto at 2K renders and bills at 1K, because auto renders only 1K.
  • Saved workflows are corrected too. A workflow saved through the API or MCP gets the same corrections when it is written.
  • Every model's accepted values are in GET /v1/models. See Discover models.

Corrections on the video routes

/v1/text-to-video and /v1/generate-video return adjustments in the same shape. /v1/generate-video also repeats each correction's reason in its warnings array, beside warnings that are not about parameters, such as voice_unsupported_for_provider. Read adjustments when you need to know which setting moved. The two never disagree.

  • An unsupported value snaps to the nearest option, never to the cheapest or the first. A 4k request on a model that stops at 1080p renders 1080p, not 480p, and a portrait 9:21 becomes 9:16, not 16:9.
  • An omitted resolution is sent at the band it is priced at. When the platform declares a model's default band, you are billed for that band and the model is asked for it. The value appears in the job's input_data, so you can always see what was sent.
  • Spelling is normalized before pricing. 4K is read as 4k, so it prices and renders the 4K band.
  • Some models render a fixed band for any other value. MiniMax Hailuo 3 renders 2K for anything that is not 768P, and the Wan 3.0 family renders 720p. For those, the correction targets the band the model will produce, so the price matches the render. The model's entry in GET /v1/models declares this in unlistedResolutionRendersAs.
  • duration is sent as you give it, except on the LTX 2.3 models. They are priced on a fixed ladder of durations per resolution, so a duration between two steps moves to the nearest one and is reported in adjustments.
  • duration: -1 means Auto on the models that support it, the Seedance 2 family (autoDuration: true in GET /v1/models). The model picks the clip length, which is the source clip's length when it edits a reference video. An Auto run reserves credits for the model's longest clip and refunds down to the length delivered. Other models ignore -1 and render their default duration.

The character and location image routes (/v1/generate-character, /v1/generate-character-asset, /v1/generate-location and /v1/generate-location-asset) correct quality and resolution the same way, but do not return adjustments. The corrected value is visible only in the job's input_data, from GET /v1/jobs/:id.

Manage workflows

List and read

GET /v1/projects/:projectId/workflows returns a project's workflows without their nodes, edges and settings. GET /v1/workflows/:id returns one workflow in full. In an organization, a list follows the workspace you act in: see Workspaces.

curl -s https://app.nodaro.ai/v1/projects/$PROJECT_ID/workflows \
  -H "Authorization: Bearer $NODARO_API_KEY" | jq '.data[] | {id, name}'
const { data: workflows } = await client.workflows.list({ projectId })
const { data: workflow } = await client.workflows.get(workflows[0].id)
console.log(workflow.nodes.length)
nodaro workflows list --project $PROJECT_ID --json
nodaro workflows get 8c2d7f1e-3a4b-4c5d-9e6f-7a8b9c0d1e2f

Create and update

Create a workflow in a project with POST /v1/projects/:projectId/workflows. Everything except the project is optional and falls back to the server's defaults:

const { data: wf } = await client.workflows.create({
  projectId,
  name: 'My workflow',
  nodes: [],
  edges: [],
})

PATCH /v1/workflows/:id changes any subset of fields. To make an update safe against a concurrent edit, send expectedVersion, the integer version from your last read. When the workflow changed since then, the update is refused with 409 workflow_conflict, and the error carries currentVersion, currentUpdatedAt and currentRecord, the whole current workflow. Merge your change onto currentRecord and save again, with no extra read. expectedUpdatedAt, a timestamp, works the same way.

import { WorkflowConflictError } from '@nodaro/sdk'

try {
  await client.workflows.update(id, { name: 'Renamed', expectedVersion: 7 })
} catch (err) {
  if (err instanceof WorkflowConflictError && err.currentRecord) {
    await client.workflows.update(id, { name: 'Renamed', expectedVersion: err.currentVersion })
  } else throw err
}
  • Run-state values on a node's data, such as its execution status, current job and progress, are removed on save and never stored.
  • thumbnailUrl sets the workflow's preview image from an image that is already hosted. null clears it.
  • A save that sends only edges and wires a Video Overlay layer rewrites that node, so it answers 409 workflow_conflict when the workflow changed since it was read, even without expectedVersion.
  • visibility is private or workspace. Only the creator or a workspace admin may change it, and a workflow outside a workspace answers 400 not_workspace_scoped.

Delete

DELETE /v1/workflows/:id deletes a workflow. The creator and a workspace admin may delete it, never a collaborator. Deleting a workflow that does not exist, or that you cannot see, answers 404, so a delete is never a silent success. A collaborator who can see the workflow but may not delete it gets 403.

Export and import

GET /v1/workflows/:id/export returns the workflow as a portable JSON bundle. With ?assets=true, the bundle also carries the characters, objects and locations the workflow uses, if they are yours. When nodes point at media another install cannot fetch, such as files on localhost or a private network, the bundle lists them in portability.unreachableMedia.

POST /v1/workflows/import creates a workflow from a bundle:

POST /v1/workflows/import
{ "projectId": "<project uuid>", "workflow_json": { "version": 1, "name": "…", "nodes": [], "edges": [] } }

The import re-creates bundled characters, objects, creatures and locations under your account and re-points the nodes at them. It copies reachable media onto this install's storage: up to 25 files for the workflow's media and 25 more for the bundled entities, with images up to 20 MB and video or audio up to 50 MB. The response carries the new workflow and an importReport:

FieldMeaning
rehostedHow many files were copied onto this install.
unreachableMedia on private hosts, left pointing where it was. Those nodes do not run until the file is uploaded again.
skippedMedia that could not be copied, with the reason, such as HTTP 404.
assetIdMapEach bundled entity id, mapped to the row created for it.
assetsSkippedEntities your storage quota had no room for. The workflow still lands.

From the CLI: nodaro workflows export <id> --with-assets --output bundle.json, then nodaro workflows import bundle.json --project <projectId>. Read Import and export for what crosses over.

Move to another project

POST /v1/workflows/:id/move
{ "projectId": "…" }

A move is a workflow write, so an OAuth token needs workflows:write. PATCH /v1/workflows/:id with a projectId does the same thing and follows the same rules. You may move work you created. Inside an organization, a workspace admin may also move work between two workspaces they administer. A personal project must be yours on both sides.

StatusCodeMeaning
400validation_errorThe workflow is already in that project.
403not_permittedThe workflow is not yours to move, or not yours to move there.
404not_foundNo such workflow, or no such project for you.
409move_blockedThe work was created for an assignment.
409workspace_archivedThe target workspace is archived.

A move that changes workspace removes the workflow's collaborator grants and reports them, so you can tell the people who lost access:

{
  "data": { "id": "8c2d7f1e-3a4b-4c5d-9e6f-7a8b9c0d1e2f", "projectId": "5e4d3c2b-1a09-4f8e-8d7c-6b5a49382716" },
  "droppedCollaborators": [{ "userId": "2b3c4d5e-6f70-4a81-92b3-c4d5e6f70812", "name": "Sam" }]
}

The PATCH form includes droppedCollaborators only when a grant was dropped.

OAuth scopes for workflows

ScopeRoutes
workflows:readGET /v1/projects/:projectId/workflows, GET /v1/workflows, GET /v1/workflows/:id, GET /v1/workflows/:id/export
workflows:writeCreate, update, delete, import and move, and POST /v1/workflows/:parentId/sub-workflows
workflows:executePOST /v1/workflows/:id/run

Personal API tokens need no scopes. See OAuth apps.

Studio production nodes

Some canvas nodes belong to a Studio production and depend on linked frames. They must be generated through the Studio production API: running them through POST /v1/workflows/:id/run, or generating one directly, answers 400 sequence_execution_required. See Studio productions.

Frequently asked questions

Last updated on

On this page