Skip to content

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" }
  }'
FieldWhat it is
ruleSetKeyA 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.
subjectIdA subject the tenant has (POST /subjects). Give this or subject.
subjectThe 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.
dataRule 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.
deadlineMsHow 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.
updateSubjectRiskSet 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.
correlationIdYour 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 200 with the decision the first request made, its replayed true and the header Idempotent-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 409 idempotency_key_reused, with the first decision's id in details.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 app rule 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 a deadlineMs of 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 needUse
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 onesPOST /decide with escalate
Documents, a vendor's asynchronous check, an applicant's form, several stepsPOST /workflow-runs with a workflow

Settings ​

VariableDefaultWhat it does
DECIDE_DEFAULT_DEADLINE_MS250The deadline of a request that sets none.
DECIDE_PLAN_CACHE_SECONDS2How 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_MINUTE60 000Requests a minute per caller to this route, in place of API_RATE_LIMIT_PER_MINUTE.
DATABASE_POOL_MAX10The 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.

Released under the Apache-2.0 License.