Tasks
Units of work moving through a workflow — each transition guarded, dispatched and recorded.
Overview
A task is one item of work: it names the workflow it runs on, the state it is
currently in, and a caller-owned payload. Moving it is not an update — you fire
a transition by name and the runtime decides whether that move is legal from
the current state, evaluating the workflow's guard and parking the move if it
requires approval.
Entering a state can dispatch automation (the workflow's on_enter), so a task
is also the thing that drives agents, tools and
orchestrations without a caller in the loop. Every move is
appended to an immutable history, including the ones a machine made.
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 → Tasks.
Data Model
Task
| Field | Type | Description |
|---|---|---|
id | string | Public task ID. |
project_id | string | The owning project. |
workflow_id | string | The workflow it runs on. |
workflow_version | integer | Fixed at creation — the machine this task keeps running. |
title | string | |
state | string | The current state's name. |
status | string | open or closed (reaching a terminal state closes it). |
payload | object, nullable | Caller-owned data; readable by guards as task.payload and by dispatch mappings. |
metadata | object, nullable | Caller-owned key/value attribution — invisible to guards and payload_writes, unlike payload. |
last_result | any, nullable | Server-owned: the current state's last completed dispatch result. |
assignee | string, nullable | |
active_dispatch | object, nullable | { kind, id, status } of the dispatch in flight. |
automation_status | string, nullable | running, completed, failed, unrouted, paused; null until a state with automation is entered. |
automation_chain_depth | integer | How many machine-driven moves have run back-to-back — the loop bound. |
pending_transition | string, nullable | A requires_approval move parked awaiting a human. |
pause_requested_at | string (date-time), nullable | When this task's automation was paused, or null when no pause is in force. |
pause_reason | string, nullable | The reason given with the pause, when one was. |
entered_state_at | string (date-time) | |
created_at / updated_at | string (date-time) |
TaskTransition (history entry)
| Field | Type | Description |
|---|---|---|
id | string | Public entry ID. |
task_id | string | |
from_state / to_state | string | The move. |
transition | string | The transition's name. |
principal_kind | string | user, api_key, automation or approval — who moved it. |
principal_id | string, nullable | The acting principal; for key auth, the key's own id. |
generation_id | string, nullable | Set when an agent dispatch's generation caused the move. |
orchestration_run_id | string, nullable | Set when an orchestration run caused it. |
tool_id | string, nullable | Set when a tool dispatch caused it. |
note | string, nullable | |
created_at | string (date-time) |
Key Concepts
You fire a transition, not a state
POST /v1/projects/{project_id}/tasks/{task_id}/transitions
takes a transition name. An undeclared name is a 400; a name that is not
legal from the current state is a 409. Both come from the runtime, which is
what makes the workflow the single source of truth about legal moves —
PATCH /v1/projects/{project_id}/tasks/{task_id}
edits the title, assignee and payload, and never the state.
Model a process as a workflow
fires revise to send a task back to an earlier state.
A task is pinned to a workflow version
workflow_version is
stamped at creation
and never moves. Rewiring the
workflow leaves
in-flight tasks on the machine they started on.
Automation, and the loop bound
If the entered state declares on_enter, the task dispatches it and reports
progress through active_dispatch and automation_status. A dispatch may itself
transition the task,
which is recorded with principal_kind: automation;
automation_chain_depth counts how many such moves have run back-to-back, which
is what stops a workflow from looping forever without a human or a client.
A pause stops the automation, not the board
POST /v1/projects/{project_id}/tasks/{task_id}/pause
suppresses every dispatch a state would start — an agent generation, a tool call,
an orchestration — and abandons a retry chain's remaining attempts, so the task
stops spending without losing its place. Transitions keep working under it: a move
costs nothing while the dispatch it would start is suppressed, so a board stays
usable. A state entered under a pause records automation_status: paused, and
POST /v1/projects/{project_id}/tasks/{task_id}/resume
is the only thing that lifts one — it starts the dispatch the pause suppressed and
leaves a state whose work had already completed alone.
A negative balance stops what a task would dispatch
Entering a state can generate, so
POST /v1/projects/{project_id}/tasks/{task_id}/transitions
and
POST /v1/projects/{project_id}/tasks/{task_id}/resume
answer 402 insufficient_credit and dispatch nothing while the project owes for
usage already served. It is the project owner's balance. See
A negative balance stops managed generation.
A Free account that has used its plan's monthly runs answers
403 plan_limit_reached with resource: "runs" here as well, on its own
credential too. See
A plan's run allowance can stop generation.
A move into a state that declares no on_enter is refused with the rest: which
states carry one is in the workflow, not in the request. Reading, closing and
pausing are never refused — a pause is how the debt stops growing.
An automation chain already dispatching is paused instead. A chain runs on
nobody's request, so nothing can refuse it; the reconciliation that discovers the
debt pauses every open task whose automation_status is running, with a
pause_reason saying to top up. A task whose automation has come to rest is left
alone, since what would move it on is a transition, which is refused meanwhile.
The history is the audit trail
GET /v1/projects/{project_id}/tasks/{task_id}/history
returns every move in order, each naming who made it and what caused it — the
generation, the run or the tool. It is append-only.
Model a process as a workflow
reads one with a move back to an earlier state in it.
Who may do what
Every route needs any project member.
Examples
Open a task on a workflow
- CLI
- SDK
- curl
naturali create-task \
--project-id proj_V1StGXR8Z5jdHi6B \
--workflow-id wf_V1StGXR8Z5jdHi6B \
--title "Invoice 4471" \
--payload '{ "amount": 4200, "customer": "acme" }'
const { data: task } = await naturali.tasks.createTask({
path: { project_id: 'proj_V1StGXR8Z5jdHi6B' },
body: {
workflow_id: 'wf_V1StGXR8Z5jdHi6B',
title: 'Invoice 4471',
payload: { amount: 4200, customer: 'acme' },
},
});
curl -X POST https://api.naturali.ai/v1/projects/proj_V1StGXR8Z5jdHi6B/tasks \
-H "Authorization: Bearer $NATURALI_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"workflow_id": "wf_V1StGXR8Z5jdHi6B",
"title": "Invoice 4471",
"payload": { "amount": 4200, "customer": "acme" }
}'
Move it through a declared transition
- CLI
- SDK
- curl
naturali transition-task \
--project-id proj_V1StGXR8Z5jdHi6B \
--task-id task_V1StGXR8Z5jdHi6B \
--transition approve \
--note "under the 10k limit"
const { data: moved } = await naturali.tasks.transitionTask({
path: {
project_id: 'proj_V1StGXR8Z5jdHi6B',
task_id: 'task_V1StGXR8Z5jdHi6B',
},
body: { transition: 'approve', note: 'under the 10k limit' },
});
console.log(moved?.state, moved?.pending_transition);
curl -X POST https://api.naturali.ai/v1/projects/proj_V1StGXR8Z5jdHi6B/tasks/task_V1StGXR8Z5jdHi6B/transitions \
-H "Authorization: Bearer $NATURALI_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "transition": "approve", "note": "under the 10k limit" }'
Read who moved it, and why
- CLI
- SDK
- curl
naturali get-task-history \
--project-id proj_V1StGXR8Z5jdHi6B \
--task-id task_V1StGXR8Z5jdHi6B
const { data: history } = await naturali.tasks.getTaskHistory({
path: {
project_id: 'proj_V1StGXR8Z5jdHi6B',
task_id: 'task_V1StGXR8Z5jdHi6B',
},
});
for (const move of history ?? []) {
console.log(move.transition, move.principal_kind, move.generation_id);
}
curl https://api.naturali.ai/v1/projects/proj_V1StGXR8Z5jdHi6B/tasks/task_V1StGXR8Z5jdHi6B/history \
-H "Authorization: Bearer $NATURALI_TOKEN"