Workflows
The state machines work moves through — states, the transitions between them, their guards, and the automation each one fires.
Overview
A workflow is a declaration, not an execution: it names the states a piece of work can be in, the legal moves between them, and what should happen on entering a state. The work itself is a task, and the runtime — not your client — decides whether a move is legal.
Every write that changes the machine bumps version and archives the previous
one, so a task already in flight keeps running the machine it started on. That is
the point of versioning here: editing a workflow must never rewrite the history
of work already done.
This module is a verbatim mirror of the runtime: every field, method, status code and error shape is the runtime's own, re-rooted under the project in the path.
See the OpenAPI spec for the full endpoint and schema reference, or browse it rendered under API Reference → Workflows.
Data Model
Workflow
| Field | Type | Description |
|---|---|---|
id | string | Public workflow ID. |
project_id | string | The owning project. |
name | string | Human-readable name. |
description | string, nullable | |
version | integer | Bumped on every change to the machine; the previous version is archived. |
states | array of WorkflowState | The states a task can occupy. |
transitions | array of WorkflowTransition | The legal moves. |
payload_schema | object, nullable | JSON Schema a task's payload is validated against. |
created_at / updated_at | string (date-time) |
WorkflowState
| Field | Type | Description |
|---|---|---|
name | string | Required. Unique within the workflow. |
initial | boolean | Where a task starts. |
terminal | boolean | Reaching it closes the task. |
kind | string, nullable | human parks the task: the state dispatches nothing and waits for a person. |
stalled_after | integer, nullable | Seconds before a task sitting here is reported as stalled. |
on_enter | object, nullable | The automation fired on entry — an agent, a tool or an orchestration dispatch. |
WorkflowTransition
| Field | Type | Description |
|---|---|---|
name | string | Required. What you pass when moving a task. |
from | array of string | Required. States the move is legal from. |
to | string | Required. The destination state. |
guard | object, nullable | JSON Logic over the task; a falsy result refuses the move. |
requires_approval | boolean | Parks the move as a pending decision instead of applying it. |
WorkflowVersion
| Field | Type | Description |
|---|---|---|
id | string | Public version ID. |
workflow_id | string | The workflow it belongs to. |
version | integer | The archived version number. |
config | object | states, transitions and payload_schema as they stood. |
label | string, nullable | Human tag, set with version_label on the write that archived it. |
created_by | string, nullable | The user whose write produced it; null for key-authenticated writes. |
created_at | string (date-time) |
Key Concepts
Versioning
PATCH /v1/projects/{project_id}/workflows/{workflow_id}
archives the machine as it was and bumps version. A task records its
workflow_version at creation and keeps it, so rewiring a workflow never
retroactively changes what an in-flight task was allowed to do. Read the archive
with
GET /v1/projects/{project_id}/workflows/{workflow_id}/versions
and put an old machine back with
POST /v1/projects/{project_id}/workflows/{workflow_id}/versions/{version}/restore,
which is itself a new version rather than an edit of history.
Guards and approvals are the runtime's decision
A guard is JSON Logic evaluated over the task; requires_approval parks the
move for a person. Both are enforced upstream, which is why an illegal move comes
back as a 409 from the runtime rather than something this API decides.
Automation hangs off states, not transitions
on_enter is what makes a workflow do work: entering a state dispatches an
agent, a tool or an orchestration, and
its result drives the next move. A human
state deliberately dispatches nothing.
Model a process as a workflow
dispatches a writer agent on entering draft and waits for a person in a
human review state.
Who may do what
Every route needs any project member.
Examples
Declare a two-state workflow
- CLI
- SDK
- curl
naturali create-workflow \
--project-id proj_V1StGXR8Z5jdHi6B \
--name invoice-review \
--states '[{ "name": "open", "initial": true }, { "name": "approved", "terminal": true }]' \
--transitions '[{ "name": "approve", "from": ["open"], "to": "approved" }]'
const { data: workflow } = await naturali.workflows.createWorkflow({
path: { project_id: 'proj_V1StGXR8Z5jdHi6B' },
body: {
name: 'invoice-review',
states: [
{ name: 'open', initial: true },
{ name: 'approved', terminal: true },
],
transitions: [{ name: 'approve', from: ['open'], to: 'approved' }],
},
});
curl -X POST https://api.naturali.ai/v1/projects/proj_V1StGXR8Z5jdHi6B/workflows \
-H "Authorization: Bearer $NATURALI_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "invoice-review",
"states": [
{ "name": "open", "initial": true },
{ "name": "approved", "terminal": true }
],
"transitions": [{ "name": "approve", "from": ["open"], "to": "approved" }]
}'
Add a guarded, approval-gated move
- CLI
- SDK
- curl
naturali update-workflow \
--project-id proj_V1StGXR8Z5jdHi6B \
--workflow-id wf_V1StGXR8Z5jdHi6B \
--version-label pre-rewire \
--transitions '[{ "name": "approve", "from": ["open"], "to": "approved", "guard": { "<": [{ "var": "task.payload.amount" }, 10000] }, "requires_approval": true }]'
await naturali.workflows.updateWorkflow({
path: {
project_id: 'proj_V1StGXR8Z5jdHi6B',
workflow_id: 'wf_V1StGXR8Z5jdHi6B',
},
body: {
version_label: 'pre-rewire',
transitions: [
{
name: 'approve',
from: ['open'],
to: 'approved',
guard: { '<': [{ var: 'task.payload.amount' }, 10000] },
requires_approval: true,
},
],
},
});
curl -X PATCH https://api.naturali.ai/v1/projects/proj_V1StGXR8Z5jdHi6B/workflows/wf_V1StGXR8Z5jdHi6B \
-H "Authorization: Bearer $NATURALI_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"version_label": "pre-rewire",
"transitions": [
{ "name": "approve", "from": ["open"], "to": "approved",
"guard": { "<": [{ "var": "task.payload.amount" }, 10000] },
"requires_approval": true }
]
}'
Read the version history
- CLI
- SDK
- curl
naturali list-workflow-versions \
--project-id proj_V1StGXR8Z5jdHi6B \
--workflow-id wf_V1StGXR8Z5jdHi6B
const { data: versions } = await naturali.workflows.listWorkflowVersions({
path: {
project_id: 'proj_V1StGXR8Z5jdHi6B',
workflow_id: 'wf_V1StGXR8Z5jdHi6B',
},
});
curl https://api.naturali.ai/v1/projects/proj_V1StGXR8Z5jdHi6B/workflows/wf_V1StGXR8Z5jdHi6B/versions \
-H "Authorization: Bearer $NATURALI_TOKEN"