# Decisions

[RightNow AI](https://www.rightnowai.co/) / [Decision model](https://www.rightnowai.co/docs/decision-model) / [Decisions](https://www.rightnowai.co/docs/decision-model/decisions)

Updated 2026-11-13

A decision pairs your state with named questions and returns each result under its name.

**Illustration of question types.**

```text
[ What answer do you need? ]
  |
  |-- yes or no --> boolean
  |
  |-- named option --> choice
  |
  '-- ordered scale --> score
```

Choose boolean, choice or score to match the answer you need.

**TypeScript (decide.ts)**

```typescript
import { SystemZeroClient } from '@rightnowai/system-zero';

const client = new SystemZeroClient({
  apiKey: process.env.SYSTEM_ZERO_API_KEY,
  baseURL: "https://api.rightnowai.co/v1",
});
const reply = await client.decide({
  "model": "rightnow-decision",
  "state": "The blender I ordered arrived with a cracked jar.",
  "questions": {
    "damaged": {
      "type": "boolean",
      "instructions": "Did the item arrive damaged?"
    }
  }
});
console.log(JSON.stringify(reply, null, 2));
const models = await client.models.list();
console.log(models);

// Set SYSTEM_ZERO_API_KEY to your API key before running this example.
// Run this example on your server.
// SDK defaults: The default is 5 retries, also the maximum, with 150 seconds
// per attempt and a 60-second total retry budget. A single wait longer than 60
// seconds raises the last error.
// Use Retry-After-Ms, or Retry-After when milliseconds are absent. The wait is
// never shorter than a random backoff floor: a random fraction of 0.5 seconds
// doubled for each attempt, capped at 8 seconds.
```

**Python (decide.py)**

```python
from system_zero import SystemZero

import os

with SystemZero(
    api_key=os.environ["SYSTEM_ZERO_API_KEY"],
    base_url="https://api.rightnowai.co/v1",
) as client:
    reply = client.decide({
        "model": "rightnow-decision",
        "state": "The blender I ordered arrived with a cracked jar.",
        "questions": {
            "damaged": {
                "type": "boolean",
                "instructions": "Did the item arrive damaged?"
            }
        }
    })
    print(reply)
    models = client.models.list()
    print(models)

# Set SYSTEM_ZERO_API_KEY to your API key before running this example.
# Run this example on your server.
# SDK defaults: The default is 5 retries, also the maximum, with 150 seconds per
# attempt and a 60-second total retry budget. A single wait longer than 60
# seconds raises the last error.
# Use Retry-After-Ms, or Retry-After when milliseconds are absent. The wait is
# never shorter than a random backoff floor: a random fraction of 0.5 seconds
# doubled for each attempt, capped at 8 seconds.
```

**Manual HTTP (decide.sh)**

```shell
# Set SYSTEM_ZERO_API_KEY to your API key before running this example.
# Generate once per logical decision; reuse this key when retrying.
idempotency_key="$(uuidgen 2>/dev/null \
  || cat /proc/sys/kernel/random/uuid 2>/dev/null \
  || openssl rand -hex 16 2>/dev/null \
  || python3 -c 'from uuid import uuid4; print(uuid4())' 2>/dev/null)"
cat > request.json <<'EOF'
{
  "model": "rightnow-decision",
  "state": "The blender I ordered arrived with a cracked jar.",
  "questions": {
    "damaged": {
      "type": "boolean",
      "instructions": "Did the item arrive damaged?"
    }
  }
}
EOF
# Headerless error responses require Python 3 to read error.code.
# Manual example policy: 130 seconds per attempt, a 600-second resend window
# with no retry-count cap. If x-should-retry is present, resend only when
# x-should-retry is true. If the header is absent, resend on 429, or on 409 and
# 5xx only with a retryable error.code. No answer also retries. Wait Retry-After
# plus up to 10 %, otherwise a random wait doubling from 1 s to 30 s. Stop if
# Retry-After is over 60 s. Also stop when a 429 has no Retry-After, or a 5xx
# has x-should-retry but no Retry-After.
# SDK defaults: The default is 5 retries, also the maximum, with 150 seconds per
# attempt and a 60-second total retry budget. A single wait longer than 60
# seconds raises the last error.
# Use Retry-After-Ms, or Retry-After when milliseconds are absent. The wait is
# never shorter than a random backoff floor: a random fraction of 0.5 seconds
# doubled for each attempt, capped at 8 seconds.
random() { od -An -N4 -tu4 /dev/urandom | tr -d ' '; }
# Stop resending after 10 minutes.
start=$(date +%s) attempt=0
while :; do
  : "${SYSTEM_ZERO_API_KEY:?set it first}"
  : "${idempotency_key:?could not make one}"
  rm -f reply.json
  out=$(curl https://api.rightnowai.co/v1/system-zero/decide \
    --max-time 130 -sS -o reply.json -D - -w '%{http_code}' \
    -H "Authorization: Bearer $SYSTEM_ZERO_API_KEY" \
    -H "Content-Type: application/json" \
    -H "Idempotency-Key: $idempotency_key" \
    -d @request.json)
  code=$(printf '%s' "$out" | tail -c 3)
  retry_header=$(printf '%s\n' "$out" \
    | grep -i '^x-should-retry:' | tail -n 1)
  retry=$(printf '%s\n' "$retry_header" \
    | sed 's/^[^:]*:[[:space:]]*//;s/[[:space:]]*$//')
  after=$(printf '%s\n' "$out" | grep -i '^retry-after:' \
    | tail -n 1 | sed 's/^[^:]*:[[:space:]]*//;s/[[:space:]]*$//')
  case $after in *[!0-9]*|'') after= ;; esac
  if [ -z "$retry_header" ]; then
    case $code in
      000|429) ;;
      409|5??)
        error_code=$(python3 -c 'import json, sys
try:
    print(json.load(sys.stdin).get("error", {}).get("code", ""))
except (ValueError, AttributeError):
    pass' < reply.json 2>/dev/null) || break
        case $code:$error_code in
          409:idempotency_conflict) ;;
          5??:service_unavailable) ;;
          5??:system_zero_unavailable) ;;
          5??:billing_settlement_error) ;;
          5??:idempotency_unavailable) ;;
          5??:limiter_unavailable) ;;
          5??:internal_error) ;;
          5??:maintenance) ;;
          *) break ;;
        esac ;;
      *) break ;;
    esac
  else
    [ "$retry" = true ] || break
    case $code in 4??|5??) ;; *) break ;; esac
  fi
  if [ -n "$after" ]; then
    after=$(printf '%s' "$after" | sed 's/^0*//')
    after=${after:-0}
    [ "$after" -le 60 ] || break
    ms=$(( after * 1000 + $(random) % (after * 100 + 1) ))
  else
    [ "$code" != 429 ] || break
    if [ -n "$retry_header" ]; then
      case $code in 5??) break ;; esac
    fi
    cap=$(( attempt < 5 ? 1000 << attempt : 30000 ))
    ms=$(( $(random) % (cap + 1) ))
  fi
  [ $(( $(date +%s) - start + ms / 1000 )) -lt 600 ] || break
  sleep "$(( ms / 1000 )).$(printf '%03d' $(( ms % 1000 )))"
  [ $(( $(date +%s) - start )) -lt 600 ] || break
  attempt=$(( attempt + 1 ))
done
echo "HTTP $code"
cat reply.json 2>/dev/null
```

## State

State accepts a JSON object, array or string. Nested numbers, booleans and null remain JSON. Supply your own units, coordinate frames and freshness data.

Use compact keys. Send large integers and exact decimals as numeric strings when precision matters. Keep application data in state and the task in each question's instructions.

Plain HTTP decide has no state\_version or sequence field. Keep your own sequence or timestamp inside state and associate each response with the request you sent.

Standard text state supports up to 38,000 tokens. The complete JSON request, including questions and attachments, must fit 3,665,920 bytes. The 64k and 128k bands are served through Danger zone with SZ-Danger-Zone: long\_context\_64k or long\_context\_128k. Read [Danger zone](https://www.rightnowai.co/docs/decision-model/danger-zone) for access steps and limits. A token limit does not guarantee the serialized body will fit. Questions and request formatting also count toward the rendered input limit.

For changing state, use [Streaming](https://www.rightnowai.co/docs/decision-model/streaming). Each accepted mutation advances `state_version`. Check the version on each decision before using its answer; newer state may already have arrived.

## Question types

**Choose a question type**

| Type | Use it for | Result |
| --- | --- | --- |
| `boolean` | A yes-or-no decision. | A boolean decision and the probability of true. |
| `choice` | Selecting a named option. | The selected option and a probability for each option. |
| `score` | Assigning an ordered rating. | An expected level, its legend and probabilities for the levels. |

Each question has a name, a `type` and `instructions`. Instructions must be a nonblank string or a nonempty JSON object or array.

Use a criteria object for choice and an ordered criteria array for score. Keep their counts within `limits.choice_options` and `limits.score_levels` from the [model list](https://www.rightnowai.co/docs/decision-model/models#fields); other counts return `422 invalid_criteria`.

**criteria.json**

```json
{
  "model": "rightnow-decision",
  "state": "The blender arrived with a cracked jar and is needed before Saturday.",
  "questions": {
    "team": {
      "type": "choice",
      "instructions": "Which team should handle this ticket?",
      "criteria": {
        "replacements": "Ship a new item or part",
        "refunds": "Return money to the customer",
        "support": null
      }
    },
    "urgency": {
      "type": "score",
      "instructions": "How urgent is this ticket?",
      "criteria": [
        "Routine",
        "Priority",
        "Immediate"
      ]
    }
  }
}
```

## Results

Read each result under its question name in `answers`.

Read `answers.damaged` in your response. For a boolean, `probability` is the likelihood of true. `decision` is true when true is at least as likely as false; `confidence` is the selected outcome's probability.

For a choice, `decision` equals `choice` and `confidence` is `probabilities[choice]`, the maximum option probability. A tied maximum preserves `choice`.

For a score, `decision` is the most likely level key string; ties choose the lowest level. `confidence` is that level's probability; `score` remains the expected level index.

`alternatives` lists every other outcome as `{ decision, confidence }` objects. Entries use descending confidence; tied choices use UTF-8 key byte order and tied score levels use level order.

`calibration_version`: An opaque public identifier, or `unpublished` when none is assigned. A nonempty identifier does not prove calibration.

`calibrated` is false in this release; a `calibration_version` value does not make it true.

Legacy idempotent replays retain their stored answer shape. Do not use legacy `confidence` as decision probability.

With a [request policy](https://www.rightnowai.co/docs/decision-model/request-policy), an answer below your threshold has `abstained: true` and no `decision`.

## Question refusals

A flagged question can return an error while the other questions are answered. Check each entry in `answers`, even when the HTTP status is 200.

**refused-answer.json**

```json
{
  "error": {
    "code": "injection_suspected",
    "retryable": false
  }
}
```

If every question is flagged, the whole request is refused.

## Next

[Sessions](https://www.rightnowai.co/docs/decision-model/session-affinity)
