# Quickstart

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

Updated 2026-11-13

Install an SDK and send a text state with a named question to get a structured answer.

## Get a key

Create a named key under API keys in an active workspace. The full key appears only when it is created.

Set `SYSTEM_ZERO_API_KEY` in the process that makes the call. Keep the key on your server and call over HTTPS.

[Get an API key](https://www.rightnowai.co/dashboard/settings/api-keys?create=1)

**Your first call, step by step.**

1. **[Get a key](https://www.rightnowai.co/docs/decision-model/quickstart#get-a-key)**: Set `SYSTEM_ZERO_API_KEY` on your server.
2. **[Install an SDK](https://www.rightnowai.co/docs/decision-model/quickstart#install-sdk)**: Run `npm install @rightnowai/system-zero@0.4.7` or `python -m pip install rightnow-system-zero==0.4.7`.
3. **[First call](https://www.rightnowai.co/docs/decision-model/quickstart#first-call)**: Copy `decide.ts`, `decide.py` or `decide.sh` and run it.
4. **[Read the answer](https://www.rightnowai.co/docs/decision-model/quickstart#read-the-answer)**: Read `answers.damaged.decision`. It is `true` or `false`.

## Install an SDK

Node 22 or newer, ES modules, server side only. Python 3.10 or newer, server side only.

[TypeScript package README](https://www.npmjs.com/package/@rightnowai/system-zero) and [Python package README](https://pypi.org/project/rightnow-system-zero/).

**TypeScript (install.sh)**

```shell
npm install @rightnowai/system-zero@0.4.7
```

**Python (install.sh)**

```shell
python -m pip install rightnow-system-zero==0.4.7
```

SDK 0.4.7 adds typed decision provenance, webhook methods and request compression.

Tool selection: the RightNow decision model picks which of your tools to call next, or none, with a probability for each. It does not write arguments or messages, and it does not run tools.

Text, PDF and video allow 9 tools and none, or 10 when a tool is required; image or audio allows 25 tools and none, or 26 when a tool is required.

Use `toolQuestions` / `tool_questions` and `selectTool` / `select_tool`. See [Tool selection](https://github.com/RightNow-AI/system-zero-sdk/blob/sdk-v0.4.7/TOOL-SELECTION.md).

## First call

Send a state and a named question, then read the answer and list the available models.

**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
```

Request bodies of 32 KB or more are sent gzip-compressed when that makes them at least 10 percent smaller; pass `compressRequests: false` / `compress_requests=False` to turn this off.

## Read the answer

Read `answers.damaged` in your response. `answers.damaged.probability` is the likelihood of true. `decision` selects true when true is at least as likely as false; `confidence` is its probability. `alternatives` contains the other outcome and its confidence. `calibrated` is false in this release.

**The shape of a reply. Each type stands in for a value.**

**reply shape**

```text
id: string
model: string
served_by: string
created: integer
context:
  route: string
  tokens: integer
answers:
  damaged:
    type: "boolean"
    probability: number
    confidence: number
    calibration_version: string
    decision: boolean
    alternatives:
      decision: boolean
      confidence: number
    calibrated: boolean
usage:
  input_tokens: integer
  cached_input_tokens: integer
  media_tokens: integer
  long_input_tokens: integer
  media_items: integer
  thought_tokens: integer
```

## SDK retries

**SDK retry rules**

| Rule | Behavior |
| --- | --- |
| Bounds | 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. |
| Safe requests | Only GET requests or requests carrying a non-empty idempotency key can retry. With retries enabled, decide POSTs receive a generated key when none is supplied. An explicit blank key omits the header and disables decision retries. Each retry retains the same key and serialized body. Setting the retry count or budget to zero disables retries. |
| Retry flags | Marked means error.retryable is true or the x-should-retry header is true. An explicit false in either marker vetoes retry. |
| 429 | capacity\_exhausted, rate\_limit\_exceeded, concurrency\_limit\_exceeded or workspace\_limit\_exceeded. |
| 5xx | except 504 and maintenance, including service\_unavailable and system\_zero\_unavailable. |
| 409 | idempotency\_conflict. |
| Transport | A qualifying connection failure or timeout before any response byte can also retry, even if the request was sent. Failures after a response byte never retry. Errors of authentication and permission types never retry. |
| Wait | 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. |
| Cold start | A cold pool returns marked 503 system\_zero\_unavailable with a wait in Retry-After. The SDK retries the same request body with the same idempotency key. An explicit false retry marker stops retries. |

## Handle a refusal

The SDK follows the retry rules above. For Manual HTTP, use the policy printed in that example.

Keep the same `Idempotency-Key` and JSON body for the decision's retries. A new decision needs a fresh key. See [Idempotency](https://www.rightnowai.co/docs/decision-model/idempotency) and [Errors](https://www.rightnowai.co/docs/decision-model/errors).

## Next

[Authentication](https://www.rightnowai.co/docs/decision-model/authentication)
