Appearance
Decisions in the request
POST /decide evaluates a published rule set for one subject and answers with the decision, in the request: no workflow run, so a decision takes milliseconds. It is for decisions a caller waits on, such as a card payment, a payout, a login or a signup, where a workflow run (POST /workflow-runs) is for journeys that wait for documents, vendors or people. The decision is recorded as any other: on the subject's timeline, in GET /decisions, in the audit trail, and sent to webhooks as decision.created. When a person should look at it, the same request can start a workflow (escalate).
The request
bash
curl -X POST "$API/decide" \
-H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: txn_8812" \
-H "Content-Type: application/json" \
-d '{
"ruleSetKey": "card-transaction",
"subject": { "externalId": "cust_42", "kind": "user", "data": { "name": "Ann Lee" } },
"data": {
"transaction": { "amount": 6200, "currency": "EUR", "cardCountry": "DE", "cardFingerprint": "fp_9a1" }
},
"escalate": { "workflowKey": "card-review" }
}'| Field | What it is |
|---|---|
ruleSetKey | A rule set with a published version. Its published rules are evaluated, shadow entries included (recorded, left out of the outcome), and aggregated by its policy. |
subjectId | A subject the tenant has (POST /subjects). Give this or subject. |
subject | The caller's own id for the subject (externalId) and its kind: created when the tenant has none with that id, otherwise data is merged into its data as a JSON merge patch. tags apply to a subject the call creates. Needs subjects:write. |
data | Rule data namespaces besides the subject, as a workflow's step outputs: rules read transaction.amount here. subject and subjectId are set from the subject and cannot be sent. |
deadlineMs | How long the rules have, 10 to 2 000 ms (default 250, DECIDE_DEFAULT_DEADLINE_MS). |
escalate | { workflowKey, on }: start that published workflow for the subject when the outcome is one of on (default ["manual_review"]). Needs runs:start. |
updateSubjectRisk | Set the subject's risk score to the decision's. Off by default, so scoring every payment of a customer does not rewrite its row each time. |
correlationId | Your reference, kept on an escalated run. |
The route needs decisions:write, which the integration role holds (and admins). Analysts decide cases, not requests.
The answer
json
{
"decisionId": "b1f8…",
"subjectId": "4c02…",
"subjectCreated": true,
"outcome": "manual_review",
"riskScore": 30,
"band": null,
"reasons": [{ "code": "rule.failed", "message": "…", "ruleKey": "card_amount_high" }],
"ruleSet": { "key": "card-transaction", "version": 1 },
"results": [{ "ruleKey": "card_amount_high", "outcome": "fail", "severity": "warn", "…": "…" }],
"escalation": { "workflowKey": "card-review", "workflowRunId": "6f2c…", "status": "started" },
"deadlineExceeded": false,
"durationMs": 3.8,
"replayed": false,
"createdAt": "2026-10-13T09:00:00.000Z"
}201 answers a new decision; the Server-Timing header carries the request's time (decide;dur=4.6). results holds every rule's result; their details are redacted for callers without runs:context, as a run's are. escalation.status is started once the workflow is under way and queued when it is still starting: the escalation is queued with the decision, and the worker's trigger dispatcher starts it about 30 seconds later if the request could not (GET /workflow-triggers, Automatic workflow triggers). The run's id is fixed by the decision, so it starts once.
Errors: 400 for a request that does not parse (both or neither of subjectId and subject, reserved data keys, a malformed Idempotency-Key), 403 without the permissions above, 404 when the rule set, the subject or the escalation's workflow has no published version, 409subject_kind_mismatch when subject.externalId names a subject of another kind, and 409idempotency_key_reused (below).
Retries: Idempotency-Key
Send an Idempotency-Key header (1 to 255 printable ASCII characters, no spaces), one per business event: the transaction's or payout's id. The decision's id derives from it, so:
- the same key and the same body answer
200with the decision the first request made, itsreplayedtrue and the headerIdempotent-Replayed: true; nothing is evaluated or written again, and an escalation is not started twice. Requests racing under one key make one decision. - the same key with another body answers
409idempotency_key_reused, with the first decision's id indetails.decisionId: a key names one request, not a subject.
Without the header every request is a new decision. The API client (client.decide(body, { idempotencyKey })) sends a key of its own when you give none, so its own retries are safe.
Deadlines
Every rule answers within deadlineMs or is error with the reason deadline exceeded; the rule set's policy aggregates an error to manual_review (max_severity) or treats it as its policy says, and deadlineExceeded is true. Rules run concurrently, so the deadline bounds the slowest.
- Comparison, expression, list and score rules answer in microseconds to a millisecond. Each CEL expression also runs under its step budget (Rules).
- Velocity rules read the event store with one indexed query each, as of the request.
- An
apprule calls its install through the app runner with what is left of the deadline, under the request's timing (APP_SYNC_TIMING): one attempt, no retries, and its fallback only when a whole attempt still fits. The runner takes no call shorter than 100 ms, so give a rule set with app rules adeadlineMsof 300 or more. Its call is keyed by the decision, so a retried request does not call the vendor twice.
How it relates to workflow runs
A decision from POST /decide and a run's decision differ in where they were made, channelsync or workflow, which GET /decisions?channel= filters on and the console shows as "Automated · in the request". The rule evaluation is recorded the same way, so rule backtests replay sync decisions too (workflow backtests replay their workflow's runs only).
| You need | Use |
|---|---|
| An answer within the request (authorise a payment, gate a login) | POST /decide |
| An answer within the request, and a person to look at the risky ones | POST /decide with escalate |
| Documents, a vendor's asynchronous check, an applicant's form, several steps | POST /workflow-runs with a workflow |
Settings
| Variable | Default | What it does |
|---|---|---|
DECIDE_DEFAULT_DEADLINE_MS | 250 | The deadline of a request that sets none. |
DECIDE_PLAN_CACHE_SECONDS | 2 | How long a published rule set and its rules are kept per API instance; a publish clears its own at once. |
API_DECIDE_RATE_LIMIT_PER_MINUTE | 60 000 | Requests a minute per caller to this route, in place of API_RATE_LIMIT_PER_MINUTE. |
DATABASE_POOL_MAX | 10 | The API's connections; a decision holds one while in flight. |
The latency targets of the route, and the load test that measures them, are in Performance.