Node Flowdocs

What node-flow is

A workflow orchestrator where the DAG is data, the workers poll, and Postgres is the only dependency.

node-flow runs processes you describe as data. A workflow is a JSON DAG, not a program. Your workers poll a queue for the steps they own, in whatever language they happen to be written in, and node-flow makes sure every step runs, retries, times out and compensates the way the definition said it should — across restarts, deploys and failures.

Orkes Conductor needs Redis, Cassandra and Elasticsearch to stand up. node-flow needs Postgres 18.

The model in one picture

Clients node-flow server Your workers, any language lease / heartbeat / report lease / heartbeat / report HTTP, SQL, gRPC, LLM PostgreSQL 18state, queue, timers, outbox, search External systems Your service / CLI / UI Schedules, webhooks, events api roleREST + long-poll decider rolepure decide function poller roletimers, outbox, system tasks charge worker ship worker
How a run moves through the system

There is one image. NODE_FLOW_ROLES decides which of the three roles a container runs; the development default runs all three in one process.

A workflow is a document

{
  "name": "fulfil_order",
  "version": 1,
  "inputParameters": ["orderId", "amount"],
  "tasks": [
    {
      "name": "charge",
      "taskReferenceName": "charge",
      "type": "SIMPLE",
      "inputParameters": { "amount": "${workflow.input.amount}" }
    },
    {
      "name": "ship",
      "taskReferenceName": "ship",
      "type": "SIMPLE",
      "inputParameters": { "txn": "${charge.output.txnId}" }
    }
  ],
  "outputParameters": { "receipt": "${charge.output.txnId}" }
}

POST that to /v1/ns/default/metadata/workflows and it is registered. Start a run and the engine schedules charge; a worker polling the charge queue leases it, does the work and reports { "txnId": "..." }; the engine resolves ${charge.output.txnId} and schedules ship.

Three families of task

The distinction drives where work happens, and it is the first thing to internalise.

FamilyWho runs itExamples
Worker taskA process you wrote, polling a queueSIMPLE
OperatorThe decider, inside the evaluation transaction — no I/O, never queuedSWITCH, FORK_JOIN, JOIN, DO_WHILE, SUB_WORKFLOW, TERMINATE, SET_VARIABLE, NOOP, EVENT
System taskThe server itself, through the task registryHTTP, INLINE, JSON_JQ_TRANSFORM, JDBC, GRPC, WEBHOOK, EMAIL, BUSINESS_RULE, the AI tasks

A fourth, smaller group is waiting tasksWAIT, WAIT_FOR_WEBHOOK, HUMAN, PULL_WORKFLOW_MESSAGES. Nothing executes them. They are not queued and hold no lease, so a seven-day wait or a three-week approval costs one row and nothing else.

When to use this, and when not to

Temporal, Inngest, Restate and friends say "write an async function, we make it durable". That is a good model and a crowded one. node-flow is deliberately the other model, and the trade is explicit.

Durable execution: Temporal, Restate, Inngest Orchestration: node-flow, Conductor Your code IS the workflow Determinism rules, replay, versioning hazards SDK per language, workflow runtime in-process A JSON document IS the workflow Engine owns control flow; your code owns one step Workers are plain HTTP clients, no runtime
Two models, two boundaries

Choose node-flow when:

  • the process crosses teams, services or languages, and no single codebase owns it;
  • an operator who does not read your code has to see a run, retry a step, or terminate it;
  • the diagram is the artefact people argue about — a payment flow, an onboarding, a claims process;
  • you want long waits (days, weeks) to cost nothing, and human approvals to be first-class;
  • you want one dependency to run, back up and understand.

Choose a durable-execution library when:

  • the workflow lives entirely inside one team's service in one language;
  • the logic is genuinely code — loops with complex state, rich types, business rules that would be miserable as JSON;
  • you never need a non-engineer to look at it.

These are not mutually exclusive. It is completely reasonable to have node-flow own the cross-team process and call a Temporal workflow as one SIMPLE step inside it.

What you get out of the box

OperatorsSWITCH, FORK_JOIN/JOIN, EXCLUSIVE_JOIN, FORK_JOIN_DYNAMIC, DO_WHILE, DYNAMIC, SUB_WORKFLOW, START_WORKFLOW, TERMINATE, SET_VARIABLE, GET_WORKFLOW, YIELD, NOOP, saga compensateWith
System tasksHTTP, HTTP_POLL, INLINE (QuickJS-on-WASM sandbox), jq, SQL, gRPC, events, Kafka publish, signed outbound webhooks, wait-for-webhook, human tasks, business rules, signed JWTs, email
AILLM text and chat, embeddings, chunking, vector indexes in Postgres, MCP client tasks, a durable AGENT loop, image/audio/video generation, guardrails
TriggersLeased cron with timezones, inbound webhooks with signature verification, event handlers on Kafka, NATS, AMQP, SQS and Redis Streams
Control planeNamespaces, RBAC with groups and tag-based access, resource grants, API keys, service accounts, mTLS, OIDC workload identity, OIDC and SAML SSO, sealed secrets, audit log, quotas
OperationsLive execution viewer (SSE), visual DAG editor, retry / rerun-from-task / terminate, bulk actions, human-task inbox, queue and worker dashboards, saved searches
Developer experience@node-flow-dev/testkit unit tests with no server, deterministic replay, the nf CLI, generated Python/Go/Java/TypeScript clients, BPMN 2.0 import
CompatibilityConductor compatibility at /conductor/api — connect existing clients with node-flow credentials and a supported workflow/worker protocol

Design decisions that leak into how you use it

These are not trivia. Each one changes what you can and cannot write.

The engine is a pure function. decide(blueprint, state) → commands does no I/O. That is why nf test can run a whole workflow in milliseconds with no server, and why replay can prove a definition still reproduces a recorded run. It is also why SWITCH will not execute JavaScript and DO_WHILE conditions are a restricted comparison grammar — see Workflows.

Never load the whole workflow. A definition compiles once, at registration, into a blueprint that records exactly which other tasks each task's expressions reference. An evaluation loads the pending frontier plus those refs, so a 30,000-task workflow evaluates as cheaply as a five-task one.

No side effect escapes a transaction. Every outbound action goes through a transactional outbox, so a crash mid-evaluation loses nothing and duplicates nothing that was not already at-least-once.

Secrets are never resolved during evaluation. ${secrets.NAME} survives the decider untouched and is substituted once, at dispatch, into the copy handed to the executor or worker. A resolved secret would otherwise be written into the stored task input, where it would sit in the execution history in clear.

Controls are enforced server-side. Concurrency caps, rate limits and semaphores are applied at dequeue, not in an SDK — because anything enforced in an SDK is advisory and the first worker written in another language bypasses it.

Where to go next

  • Quickstart — from nothing to a completed run, in about a minute.
  • Concepts — the vocabulary: namespaces, queues, references, expressions.
  • Workflows — the JSON DSL, every operator, worked examples.
  • System tasks — everything the server runs for you.
  • Workers — writing one, in TypeScript or over plain REST.
  • Execution controls — retries, timeouts, concurrency, rate limits.
  • Self-hosting — managed Postgres, published images, scaling roles.
  • Configuration — every environment variable.
  • CLI and API.

On this page