NodeTool exposes workflow REST endpoints under /api/workflows from a single server (nodetool serve): /api/workflows for CRUD and query operations, and POST /api/workflows/{id}/run to run a workflow.

This page collects the basics from the project README. See API Reference for the canonical endpoint list and auth requirements. Send Authorization: Bearer <token> on every request except the public routes noted below. A server in Local mode trusts loopback callers without a token, and a server in Supabase mode requires one for everything else. See Authentication.

Loading Workflows

const response = await fetch("http://localhost:7777/api/workflows/");
const { workflows, next } = await response.json();

GET /api/workflows lists the caller’s workflows, full graphs included. Query parameters:

Parameter Effect
limit Page size. Defaults to 100 and caps at 500
cursor The next value from the previous page
run_mode Only workflows saved with this run mode
project_id Only workflows in this project

GET /api/workflows/{id} returns one workflow. The caller must own it, hold a collaborator grant, or find it marked access: "public". Anything else is a 404 with { "detail": "Workflow not found" }.

Creating, Updating, and Deleting

POST /api/workflows creates a workflow and returns it. name and a graph with nodes and edges arrays are required, and a missing one is a 400.

curl -X POST "http://localhost:7777/api/workflows" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Greeting",
    "description": "Says hi",
    "graph": { "nodes": [], "edges": [] },
    "access": "private"
  }'

Optional body fields are description, tags, tool_name, package_name, path, thumbnail, thumbnail_url, access ("public" or anything else for private), settings, run_mode (default "workflow"), workspace_id, project_id (default "default"), html_app, and app_doc. A project_id the caller does not own is refused.

To start from a shipped example, add ?from_example_name=<name> and optionally &from_example_package=<package> (default nodetool-base). When the body carries no graph nodes, the example’s graph and app UI are copied in.

PUT /api/workflows/{id} replaces the workflow’s name, description, tags, tool_name, package_name, and graph. It takes the same body, and name and graph are required again. Other fields change only when the body includes them. The call creates the workflow under that id when none exists. An owner or an editor collaborator may update, and only the owner changes access and project_id. Saves are optimistic. Send expected_updated_at with the updated_at you last read, and a stale value is a 409. An expected_updated_at for a workflow that does not exist is a 404.

DELETE /api/workflows/{id} removes a workflow the caller owns, along with its collaborator and share rows, and answers 204 with no body. It is a 404 for a workflow the caller does not own.

Workflow responses carry the stored fields plus an etag.

Running a Workflow

HTTP API

curl -X POST "http://localhost:7777/api/workflows/<workflow_id>/run" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
    "params": {
        "param_name": "param_value"
    }
}'
const response = await fetch(
  "http://localhost:7777/api/workflows/<workflow_id>/run",
  {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "Authorization": "Bearer YOUR_TOKEN",
    },
    body: JSON.stringify({
      params: params,
    }),
  }
);

const body = await response.json();
// body has the shape:
// {
//   "job_id": "<uuid>",
//   "workflow_id": "<uuid>",
//   "status": "completed" | "cancelled" | "failed",
//   "outputs": { /* one property per output node, keyed by node name */ },
//   "error": null,
//   "message_count": 42,
//   "background": false
// }
// outputs values can be a string, image, audio, etc.

POST /api/workflows/{id}/run runs the workflow to completion and returns a single JSON response — it does not stream. For real-time progress (job and node updates, incremental output), run the workflow over the WebSocket endpoint instead.

The body is optional. Its fields:

Field Effect
params Input values keyed by input node name
background Return a receipt at once and keep running. The response has status: "running", background: true, job_id, id, and a poll hint. Read the settled job and its outputs through the jobs tRPC procedures
interactive Park on a failed node and hand the decision to the caller. See Interactive Runs
max_decisions, max_retries_per_node, decision_timeout_ms Bounds for an interactive run
project_id Must match the workflow’s project, or the call is a 400
node_ids Run only these nodes and what they need upstream. See Running Part of a Workflow
reuse_results With node_ids: false runs every upstream node again instead of reusing saved generations. Default true

POST /api/workflows/{id}/debug takes the same body. It returns a debug report instead of the run summary: job_id, workflow_id, status, outputs, error, a per-node summary (status, errors, logs, edges, LLM calls), and a verdict with a headline. Use it when a caller needs to know what happened inside the run.

Failures before the run starts:

Status Meaning
404 The caller has no workflow with that id
400 The workflow’s run_mode is not "workflow", a provider the graph needs has no key (the detail names the secret), project_id names another project, node_ids is not a non-empty list of strings or names a node the graph does not have, or reuse_results is not a boolean
429 An interactive run was refused because the caller already holds the maximum of 8 live debug sessions

Running Part of a Workflow

node_ids is the API form of the editor’s Run Node and Run selected. The run executes the named nodes and the upstream nodes they need. Unrelated branches and downstream nodes do not run and are not billed.

An upstream node that saves its generations, such as an image, video, audio or LLM generator, does not run again when the workflow already has a generation of it. Its saved output is fed to the node that consumes it. The node’s pinned generation (selected_generation) is used when the editor set one, otherwise the newest. The named nodes themselves always run. Runs from the editor and from this endpoint save these generations as assets.

The response carries partial_run:

{
  "node_ids": ["caption"],
  "ran": ["caption"],
  "reused": [
    { "node_id": "image", "job_id": "<job that made it>", "asset_ids": ["<asset>"] }
  ]
}

Agents pass the same node_ids and reuse_results to the run_workflow, debug_workflow and start_background_job tools.

A node that fails during the run does not change the HTTP status. The response is 200 with status: "failed" and an error string.

Listing Names and Tools

Two lightweight GET routes answer “what workflows exist?” without paying for full graphs. Both read the caller’s own library, so both stay behind auth.

GET /api/workflows/names returns an id → name object for up to 1000 of the caller’s workflows. Use it to label a workflow id you already hold — a job record, a saved reference — without fetching the workflow.

curl "http://localhost:7777/api/workflows/names" \
  -H "Authorization: Bearer YOUR_TOKEN"
{ "21fddc0c2c46458493287b151b790cc4": "Greeting" }

GET /api/workflows/tools returns only the workflows saved with run_mode: "tool" — the ones an agent may call as a tool — reduced to what a tool picker needs. limit defaults to 100 and is capped at 500.

curl "http://localhost:7777/api/workflows/tools?limit=50" \
  -H "Authorization: Bearer YOUR_TOKEN"
{
  "workflows": [
    {
      "name": "Summarize",
      "tool_name": "summarize",
      "description": "Summarize a block of text"
    }
  ],
  "next": null
}

A workflow with a tool_name but no run_mode: "tool" does not appear here.

Exporting a Workflow as DSL

GET /api/workflows/{id}/dsl-export returns the workflow’s graph as TypeScript DSL source with content-type: text/plain; charset=utf-8 — the same source nodetool workflows export-dsl writes. Use it to put a workflow under version control, or to hand an agent an editable form of the graph.

curl "http://localhost:7777/api/workflows/<workflow_id>/dsl-export" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -o workflow.ts
import { constant, workflow } from "@nodetool-ai/dsl";

// 1 — nodetool.constant.String
const string = constant.string({
  value: "hi"
});

export const greetingWorkflow = workflow(string);

The route answers 404 unless the workflow is yours or marked access: "public", and 400 when the workflow has no graph or the graph cannot be expressed as DSL.

Public Workflows

A workflow saved with access: "public" is readable without a token. These two routes and the example routes below are the only /api/workflows paths exempt from auth — every other one serves the caller’s private library, graph included, so it stays behind a token.

# Every public workflow (limit defaults to 100, caps at 500)
curl "http://localhost:7777/api/workflows/public"

# One public workflow, full graph included
curl "http://localhost:7777/api/workflows/public/<workflow_id>"

Both return the normal workflow shape (id, name, description, graph, access, …). Asking for a workflow that exists but is not public gets the same 404 as one that does not exist:

{ "detail": "Workflow not found" }

Example Templates

The example workflows NodeTool ships are served from disk rather than the database, so they need no token and exist on a fresh install.

curl "http://localhost:7777/api/workflows/examples"
curl "http://localhost:7777/api/workflows/examples/search?query=chat"

search filters the same list on query against each example’s name, description, and tags, case-insensitively; omitting query returns everything. Both responses carry metadata only — graph comes back empty:

{
  "workflows": [
    {
      "id": "A Boolean Constant.json",
      "name": "A Boolean Constant",
      "description": "The smallest possible graph, and a real one: …",
      "tags": ["example"],
      "package_name": "nodetool-base",
      "thumbnail": "A Boolean Constant.jpg",
      "thumbnail_url": "/api/workflows/examples/thumbnails/A%20Boolean%20Constant.jpg?v=a6dce6b4",
      "graph": { "nodes": [], "edges": [] }
    }
  ],
  "next": null
}

Fetch the image at the thumbnail_url the list hands back:

curl "http://localhost:7777/api/workflows/examples/thumbnails/A%20Boolean%20Constant.jpg" \
  -o thumb.jpg

Only .jpg and .png are served — any other extension is a 400 — and the filename is reduced to its basename, so it cannot escape the examples assets directory.

WebSocket API

For real-time streaming and job control over WebSocket, see the dedicated WebSocket API page. The reference client in examples/workflow_runner/js/workflow-runner.js shows how to consume job_update, node_update, and node_progress messages.

API Demo

  • Grab the example runner (examples/workflow_runner).
  • Open index.html in a browser locally.
  • Select the endpoint (local or api.nodetool.ai for alpha users).
  • Enter an API token from the NodeTool settings dialog.
  • Select a workflow and run it.