Playbooks
Versioned templates of steps that a case fills per subject, recomputing only what a changed value reaches.
Overview
A playbook declares nodes: each has inputs, typed by JSON Schema, that a person
writes or that read another node's field, and an output. A collect node's
output is its inputs, an execute node runs an agent, a
tool, a decider, a workflow or
an orchestration, and a confirm node waits for a person
to approve its inputs. A case is one subject — a client, a contract — filled
against one version of the playbook over as long as it takes.
Every write that makes a node runnable runs it, and a changed output runs the nodes that read it, once each: Fill a case per client corrects one value and reruns only the goal that reads it. Final nodes render their output as files through transformation tools.
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 → Playbooks.
Data Model
Playbook
| Field | Type | Description |
|---|---|---|
id | string | Public playbook ID. |
project_id | string | The owning project. |
name | string | Unique in the project. |
version | integer | Incremented by every write that changes definition. |
definition | object | { upstreams, nodes }; each node has id, title, kind, final, inputs, and an executor or transformations where its kind allows. |
Case
| Field | Type | Description |
|---|---|---|
id | string | Public case ID. |
playbook_id | string | |
playbook_version | integer | The version the case runs on, until it is upgraded. |
subject | string | Who or what the case is about. |
actor_id | string, nullable | The end-user actor the case belongs to; null when none. Fixed at create. |
upstreams | object | The case bound to each of the playbook's declared upstreams, by name. Fixed at create. |
revision | integer | The case's counter; every value written takes the next one. |
automation_status | string | paused, running while a node is queued or runs its executor, waiting while every node in flight waits on a task or orchestration run it started, else idle. |
pause_requested_at | string (date-time), nullable | When the case was paused. |
nodes | array | On a single-case read: every node's state, missing, blocked_by, run, inputs and output. |
A node's state is computed on every read: blocked, ready, running,
waiting, done, stale, failed or rejected. A blocked node names the
nodes it waits on in blocked_by. A waiting node is parked on the task or
orchestration run it started, named in run, read live: a task's state and
automation_status (failed when its dispatch failed), or a run's status.
run is null on every other node.
Key Concepts
A case can read another case
A definition may declare upstreams: { name, playbook_id } pairs naming other
playbooks of the project. Creating a case then needs each name bound, in
upstreams, to a case of that playbook with the same actor_id; anything else
is 400 PLAYBOOK_CASE_UPSTREAM_INVALID. The binding is fixed, and many cases
may share one upstream case. A monthly case binds the client's one company
case, so the company's data is filled once.
An upstream node (upstream: { name, node_id }, no inputs, no executor) pulls
that node's output from the bound case. The first pull is automatic once the
node there is done; after a change upstream the node is stale until
POST /v1/projects/{project_id}/playbook-cases/{case_id}/nodes/{node}/run
pulls again. Later nodes read it with an ordinary { node_id, field } source.
Deleting a case other cases are bound to is
409 PLAYBOOK_CASE_HAS_DEPENDENTS, and deleting a playbook another playbook
declares as an upstream is 409 PLAYBOOK_HAS_DEPENDENTS.
- CLI
- SDK
- curl
naturali create-playbook-case \
--project-id proj_V1StGXR8Z5jdHi6B \
--playbook-id pbk_V1StGXR8Z5jdHi6B \
--subject "ACME Ltda, 2026-09" \
--actor-id actor_V1StGXR8Z5jdHi6B \
--upstreams '{ "company": "pbk_case_V1StGXR8Z5jdHi6B" }'
const { data: monthly } = await naturali.playbooks.createPlaybookCase({
path: {
project_id: 'proj_V1StGXR8Z5jdHi6B',
playbook_id: 'pbk_V1StGXR8Z5jdHi6B',
},
body: {
subject: 'ACME Ltda, 2026-09',
actor_id: 'actor_V1StGXR8Z5jdHi6B',
upstreams: { company: 'pbk_case_V1StGXR8Z5jdHi6B' },
},
});
curl -X POST https://api.naturali.ai/v1/projects/proj_V1StGXR8Z5jdHi6B/playbooks/pbk_V1StGXR8Z5jdHi6B/cases \
-H "Authorization: Bearer $NATURALI_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "subject": "ACME Ltda, 2026-09", "actor_id": "actor_V1StGXR8Z5jdHi6B", "upstreams": { "company": "pbk_case_V1StGXR8Z5jdHi6B" } }'
A case belongs to an actor
actor_id ties a case to an end user.
GET /v1/projects/{project_id}/playbook-cases
filters by actor_id and by an exact subject. Every route that acts on one
case takes actor_id as a pin: a case of another actor, or of none, answers
404. Pin it in a tool
so an agent reaches only the cases of the person it serves.
Writes carry the revision they read
POST /v1/projects/{project_id}/playbook-cases/{case_id}/inputs
writes a batch, all or none. Each value names the revision of the field it read
in expected_revision; a field written since answers 409 and nothing is
written. A confirmation names the revision of each input it decided on the same
way.
A write can wait for the nodes it starts
Writing inputs, confirming a node and upgrading a case answer once the write is
done, while the nodes it makes runnable run in the background. With
wait: true they answer once every node the write started, and every node
those start in turn, has settled or parked on a task or run, and return the case
as it then stands. A paused case answers at once.
Running a node by hand
POST /v1/projects/{project_id}/playbook-cases/{case_id}/nodes/{node}/run
runs a ready, stale, failed or done node now. It is how a failed node is
retried, and how a done node is recomputed after an upgrade changed its
executor; an output equal to the current one changes nothing downstream.
Entries say who wrote them
Every entry, execution, export and upgrade carries
created_by_principal_type and created_by_principal_id, and
on_behalf_of when a naturali tool
wrote it: the tool, the agent and the generation.
Nodes run as the version's author
A node's executor, and each export's tool, runs as the credential that wrote the
case's playbook version, with that credential's permissions as they stand when it
runs. A case also refuses writes from its own work: an executor's result reaches
its case only as the node's output. A write made with that credential to the
case, or to a case reading it as an upstream at any depth, is
403 PLAYBOOK_SELF_WRITE.
Exports render a final node
POST /v1/projects/{project_id}/playbook-cases/{case_id}/nodes/{node}/exports
calls each transformation's tool on one revision of the node's output and files
what it returns in the project's files. An export never feeds a
node, and is stale once the node's output moves past it.
Upgrading a case
A case stays on the version it was created on.
POST /v1/projects/{project_id}/playbook-cases/{case_id}/upgrade
moves it to another version, keeping values by node ID and recording what
moved.
A negative balance stops what a case would run
A case runs nodes on the writes that make one runnable, so creating a case,
writing inputs, running a node, confirming, exporting, resuming and upgrading
answer 402 insufficient_credit and run 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" on the same routes. See
A plan's run allowance can stop generation.
Reading and pausing are never refused.
POST /v1/projects/{project_id}/playbook-cases/{case_id}/pause
stops a case starting anything; what is already running settles.
A cascade already running is paused instead. The reconciliation that
discovers the debt pauses every case whose automation_status is running;
restore it from the account's stops once the balance is positive. A case
waiting on a task or run it started is left alone: that task or run is what
spends, and it is stopped on its own.
An export counts toward storage
An export adds a file, so at the account's storage ceiling it answers
403 plan_limit_reached with resource: "storage". See
Files.
Who may do what
Every route needs any project member.
Examples
Create a playbook
- CLI
- SDK
- curl
naturali create-playbook \
--project-id proj_V1StGXR8Z5jdHi6B \
--name client-pricing \
--definition '{
"nodes": [
{ "id": "revenue", "title": "Annual revenue", "kind": "collect",
"inputs": [{ "name": "amount", "schema": { "type": "number" },
"required": true, "source": "manual" }] },
{ "id": "pricing", "title": "Pricing", "kind": "collect", "final": true,
"inputs": [{ "name": "revenue", "schema": { "type": "number" },
"required": true,
"source": { "node_id": "revenue", "field": "output.amount" } }] }
]
}'
const { data: playbook } = await naturali.playbooks.createPlaybook({
path: { project_id: 'proj_V1StGXR8Z5jdHi6B' },
body: {
name: 'client-pricing',
definition: {
nodes: [
{
id: 'revenue',
title: 'Annual revenue',
kind: 'collect',
inputs: [
{
name: 'amount',
schema: { type: 'number' },
required: true,
source: 'manual',
},
],
},
{
id: 'pricing',
title: 'Pricing',
kind: 'collect',
final: true,
inputs: [
{
name: 'revenue',
schema: { type: 'number' },
required: true,
source: { node_id: 'revenue', field: 'output.amount' },
},
],
},
],
},
},
});
curl -X POST https://api.naturali.ai/v1/projects/proj_V1StGXR8Z5jdHi6B/playbooks \
-H "Authorization: Bearer $NATURALI_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "client-pricing",
"definition": {
"nodes": [
{ "id": "revenue", "title": "Annual revenue", "kind": "collect",
"inputs": [{ "name": "amount", "schema": { "type": "number" },
"required": true, "source": "manual" }] },
{ "id": "pricing", "title": "Pricing", "kind": "collect", "final": true,
"inputs": [{ "name": "revenue", "schema": { "type": "number" },
"required": true,
"source": { "node_id": "revenue", "field": "output.amount" } }] }
]
}
}'
Open a case and fill it
- CLI
- SDK
- curl
naturali create-playbook-case \
--project-id proj_V1StGXR8Z5jdHi6B \
--playbook-id pbk_V1StGXR8Z5jdHi6B \
--subject "ACME Ltda"
naturali write-playbook-case-inputs \
--project-id proj_V1StGXR8Z5jdHi6B \
--case-id pbk_case_V1StGXR8Z5jdHi6B \
--inputs '[{ "node_id": "revenue", "name": "amount", "value": 4000000, "expected_revision": null }]'
const { data: playbookCase } = await naturali.playbooks.createPlaybookCase({
path: {
project_id: 'proj_V1StGXR8Z5jdHi6B',
playbook_id: 'pbk_V1StGXR8Z5jdHi6B',
},
body: { subject: 'ACME Ltda' },
});
await naturali.playbooks.writePlaybookCaseInputs({
path: {
project_id: 'proj_V1StGXR8Z5jdHi6B',
case_id: 'pbk_case_V1StGXR8Z5jdHi6B',
},
body: {
inputs: [
{ node_id: 'revenue', name: 'amount', value: 4000000, expected_revision: null },
],
},
});
curl -X POST https://api.naturali.ai/v1/projects/proj_V1StGXR8Z5jdHi6B/playbooks/pbk_V1StGXR8Z5jdHi6B/cases \
-H "Authorization: Bearer $NATURALI_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "subject": "ACME Ltda" }'
curl -X POST https://api.naturali.ai/v1/projects/proj_V1StGXR8Z5jdHi6B/playbook-cases/pbk_case_V1StGXR8Z5jdHi6B/inputs \
-H "Authorization: Bearer $NATURALI_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "inputs": [{ "node_id": "revenue", "name": "amount", "value": 4000000, "expected_revision": null }] }'
Read where a case stands
- CLI
- SDK
- curl
naturali get-playbook-case \
--project-id proj_V1StGXR8Z5jdHi6B \
--case-id pbk_case_V1StGXR8Z5jdHi6B
const { data: current } = await naturali.playbooks.getPlaybookCase({
path: {
project_id: 'proj_V1StGXR8Z5jdHi6B',
case_id: 'pbk_case_V1StGXR8Z5jdHi6B',
},
});
for (const node of current?.nodes ?? []) {
console.log(node.id, node.state, node.missing);
}
curl https://api.naturali.ai/v1/projects/proj_V1StGXR8Z5jdHi6B/playbook-cases/pbk_case_V1StGXR8Z5jdHi6B \
-H "Authorization: Bearer $NATURALI_TOKEN"