Deciders
Versioned question sets answered against a state, producing append-only decisions confined to each question's answer space.
Overview
A decider is a named set of questions. Asked about a state — a ticket, a message, a record — it produces a decision: one answer per question, each one of the options, levels or truth values the question declares. The questions live on the decider, never in the request, so no call site can widen what is judged; and every decision names the question-set version it was answered under.
A decider is answered by exactly one backend: a tool-less
agent, which is shown the questions and the state and answers
within a schema compiled from them, or an http or pipeline
tool of yours, which receives the questions and the state and
answers in the contract below.
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. It serves two collections — /deciders and /decisions — under that
project prefix.
Deciders are not gated by plan. A decision is a run: it counts against the plan's monthly runs, and one answered by an agent on a managed provider spends the balance like any other generation.
See the OpenAPI spec for the full endpoint and schema reference, or browse it rendered under API Reference → Deciders.
Data Model
Decider
| Field | Type | Description |
|---|---|---|
id | string | Public decider ID (dcd_ prefix). |
project_id | string | The owning project. |
name | string | Unique within the project. |
description | string, nullable | |
agent_id | string, nullable | The tool-less agent that answers; null when a tool does. |
tool_id | string, nullable | The http or pipeline tool that answers; null when an agent does. |
version | integer | The question set's version; only a change to questions moves it. |
questions | object | Question id → { type, instructions, criteria }, 1 to 20 of them. |
created_at | string (date-time) | |
updated_at | string (date-time) |
Decision
| Field | Type | Description |
|---|---|---|
id | string | Public decision ID (dec_ prefix) — the polling handle. |
project_id | string | The owning project. |
decider_id | string | The decider asked; kept after the decider is deleted. |
decider_version | integer | The question-set version it was answered under. |
status | string | queued, running, completed, failed. |
answers | object, nullable | Question id → answer. Null until the decision completes, then never rewritten. |
error | object, nullable | { code, message } on a failed decision. |
generation_id | string, nullable | The generation an agent answered in; null for a tool. |
metadata | object, nullable | Your own identifiers, written once with the decision. |
created_at / updated_at | string (date-time) |
The evaluated state is not stored. Put the id of what was judged in metadata
to find the decision again.
Key Concepts
Questions and answers
type | criteria | Answer |
|---|---|---|
choice | option → description, 2 to 20 options | { "type": "choice", "choice": "<option>" } |
score | ordered level descriptions, 2 to 20 levels | { "type": "score", "score": <index>, "legend": "<level>" } |
boolean | optional { "false": "…", "true": "…" } | { "type": "boolean", "value": <bool> } |
A score is the level's zero-based index; legend is that level's text, taken
from the decider. An answer from a tool may also carry probabilities, a
distribution over the answer space. It is carried as the tool supplied it.
Versions
Changing questions archives the previous set and moves version; a rename, a
new description or a new backend does not. Read the criteria a decision was
answered under with
GET /v1/projects/{project_id}/deciders/{decider_id}/versions/{version},
and pass expected_version on
PATCH /v1/projects/{project_id}/deciders/{decider_id}
to refuse a stale write with 409.
A tool backend
The tool is called with { "state": …, "questions": … } and answers
{ "answers": { "<question id>": { "choice" | "score" | "value": …, "probabilities"?: {…} } } }
for every question. Any other answer field, such as confidence, fails the
decision with DECISION_ANSWER_INVALID; so does a value outside the answer
space. An engine that speaks a close dialect is bridged with a pipeline tool:
a step's input maps the request, the pipeline's output maps the answer.
A client, mcp tool, or a tool whose preset_parameters set state or
questions, cannot back a decider: 400 DECIDER_TOOL_NOT_CALLABLE. A tool a
decider names cannot be deleted (409 TOOL_HAS_DEPENDENTS), nor can its agent.
Requesting a decision
POST /v1/projects/{project_id}/deciders/{decider_id}/decisions
takes state (any JSON value), optional metadata and wait. With wait
omitted the answer is 201 with the decision queued; poll
GET /v1/projects/{project_id}/decisions/{decision_id}
or subscribe to decision.completed and decision.failed on a
webhook. With wait: true it is 201 with the decision
settled. A failed decision is terminal: request a new one.
Before anything reaches the runtime, a decision clears the same gates a
generation does. The decider is read for its backend: a decision an agent
answers is refused while that agent's managed model has no price
(503 model_not_priced) or the balance is negative (402 insufficient_credit);
one a tool answers is a run that spends no credit. Either is refused when a Free
account is out of runs (403 plan_limit_reached).
Examples
Create a decider over an agent
- CLI
- SDK
- curl
naturali create-decider \
--project-id proj_V1StGXR8Z5jdHi6B \
--name support-triage \
--agent-id agent_V1StGXR8Z5jdHi6B \
--questions '{
"route": {
"type": "choice",
"instructions": "Which team should own this ticket?",
"criteria": { "billing": "Charges, refunds, invoices", "technical": "Errors, outages" }
},
"needs_human": {
"type": "boolean",
"instructions": "Must a person read this before any automated reply?"
}
}'
const { data: decider } = await naturali.deciders.createDecider({
path: { project_id: 'proj_V1StGXR8Z5jdHi6B' },
body: {
name: 'support-triage',
agent_id: 'agent_V1StGXR8Z5jdHi6B',
questions: {
route: {
type: 'choice',
instructions: 'Which team should own this ticket?',
criteria: { billing: 'Charges, refunds, invoices', technical: 'Errors, outages' },
},
needs_human: {
type: 'boolean',
instructions: 'Must a person read this before any automated reply?',
},
},
},
});
console.log(decider?.id, decider?.version);
curl -X POST https://api.naturali.ai/v1/projects/proj_V1StGXR8Z5jdHi6B/deciders \
-H "Authorization: Bearer $NATURALI_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "support-triage",
"agent_id": "agent_V1StGXR8Z5jdHi6B",
"questions": {
"route": {
"type": "choice",
"instructions": "Which team should own this ticket?",
"criteria": { "billing": "Charges, refunds, invoices", "technical": "Errors, outages" }
},
"needs_human": {
"type": "boolean",
"instructions": "Must a person read this before any automated reply?"
}
}
}'
Request a decision and wait for it
- CLI
- SDK
- curl
naturali create-decision \
--project-id proj_V1StGXR8Z5jdHi6B \
--decider-id dcd_V1StGXR8Z5jdHi6B \
--state 'I was charged twice for the same order.' \
--metadata '{ "ticket_id": "ZD-48213" }' \
--wait true
const { data: decision } = await naturali.deciders.createDecision({
path: { project_id: 'proj_V1StGXR8Z5jdHi6B', decider_id: 'dcd_V1StGXR8Z5jdHi6B' },
body: {
state: 'I was charged twice for the same order.',
metadata: { ticket_id: 'ZD-48213' },
wait: true,
},
});
console.log(decision?.status, decision?.answers);
curl -X POST https://api.naturali.ai/v1/projects/proj_V1StGXR8Z5jdHi6B/deciders/dcd_V1StGXR8Z5jdHi6B/decisions \
-H "Authorization: Bearer $NATURALI_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"state": "I was charged twice for the same order.",
"metadata": { "ticket_id": "ZD-48213" },
"wait": true
}'