# Decision records

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

Updated 2026-11-13

Read a stored decision, or its trace, with the API key that created it.

Published SDK 0.4.7 provides `client.getDecision(id, options?)` and `client.getDecisionTrace(id, options?)` in TypeScript. Python provides `client.get_decision(id)` and `client.get_decision_trace(id)`, plus `get_decision_with_response` and `get_decision_trace_with_response`.

**TypeScript (lookup.ts)**

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

const key = process.env.SYSTEM_ZERO_API_KEY;
const id = process.env.DECISION_ID;
if (!key || !id) {
  throw new Error("Set SYSTEM_ZERO_API_KEY and DECISION_ID first.");
}

// Use the API key that created the decision.
const client = new SystemZeroClient({ apiKey: key, baseURL: 'https://api.rightnowai.co/v1' });

// The stored decision, then its trace.
const stored = await client.getDecision(id);
const trace = await client.getDecisionTrace(id);
console.log(JSON.stringify(stored, null, 2));
console.log(JSON.stringify(trace, null, 2));
```

**Python (lookup.py)**

```python
import json
import os
from system_zero import SystemZero

key = os.environ["SYSTEM_ZERO_API_KEY"]
decision_id = os.environ["DECISION_ID"]

# The stored decision, then its trace, with the key that created it.
with SystemZero(api_key=key, base_url="https://api.rightnowai.co/v1") as client:
    stored = client.get_decision(decision_id)
    trace = client.get_decision_trace(decision_id)
    print(json.dumps(stored, indent=2))
    print(json.dumps(trace, indent=2))
```

**Manual HTTP (lookup.sh)**

```shell
# Read a stored decision with the API key that created it.
# Use the id of a decide reply sent with an Idempotency-Key.
: "${SYSTEM_ZERO_API_KEY:?Set SYSTEM_ZERO_API_KEY first}"
: "${DECISION_ID:?Set DECISION_ID to the decision id}"
url="https://api.rightnowai.co/v1/system-zero/decisions/$DECISION_ID"

# The stored decision, exactly as it was returned.
curl --silent --show-error --fail-with-body "$url" \
  --header "Authorization: Bearer $SYSTEM_ZERO_API_KEY"
printf '\n'

# Its trace: metadata only, without answer content.
curl --silent --show-error --fail-with-body "$url/trace" \
  --header "Authorization: Bearer $SYSTEM_ZERO_API_KEY"
printf '\n'
```

## Look up a decision

- **`GET /v1/system-zero/decisions/{id}`**: The stored decision, byte for byte.
- **`GET /v1/system-zero/decisions/{id}/trace`**: Metadata about the same decision, without answer content.

Send the active API key that created the decision. Another key in the same workspace cannot read it.

Use the `id` of a decide reply: `szd_` followed by 32 lowercase hexadecimal characters. Any other id returns `404 decision_not_found`.

A found decision returns its stored status and reply, exactly as they were first returned. On this path, `model` and `served_by` name the model by its earlier ID, whichever ID the request sent. A legacy reply keeps its stored answer shape. Do not use legacy `confidence` as decision probability.

`HEAD` returns the same status and headers without a body. Neither read takes a request body or query parameters. There is no list endpoint.

## Read the trace

The SDK's `DecisionTrace` type describes a JSON object whose `object` is `system_zero.decision_trace`. It reads these fields from the stored decision.

- **`id`**: The decision's id.
- **`created`**: When the decision was made, in Unix seconds.
- **`model`**: The model named in the stored reply.
- **`model_version`**: The stored reply's `served_by`.
- **`route`**: The stored reply's `context.route`.
- **`usage_units`**: Seven usage counts, each a whole number or null.
- **`refusal_codes`**: The distinct question refusal codes in the decision, sorted.
- **`refusal_code`**: The one refusal code when there is exactly one, else null.

These fields are null in this release: `request_id`, `render_id`, `attachments`, `timings.queue_ms`, `timings.node_ms`, `timings.settle_ms`, `usage_units.cached_media_tokens`, `idempotency`, `stream_id` and `state_version`.

A missing usage count is null, never zero. The trace holds no question names, answer values, probabilities, messages or request content.

**trace.json**

```json
{
  "object": "system_zero.decision_trace",
  "id": "szd_7c41f0a9b2d84e6c9a1f3b5d7e9c0a2b",
  "request_id": null,
  "created": 1791100800,
  "model": "system-zero-3.0.0",
  "model_version": "system-zero-3.0.0",
  "render_id": null,
  "route": "direct",
  "attachments": null,
  "timings": {
    "queue_ms": null,
    "node_ms": null,
    "settle_ms": null
  },
  "usage_units": {
    "input_tokens": 214,
    "cached_input_tokens": 0,
    "media_tokens": 0,
    "cached_media_tokens": null,
    "long_input_tokens": 0,
    "media_items": 0,
    "thought_tokens": 0
  },
  "refusal_code": null,
  "refusal_codes": [],
  "idempotency": null,
  "stream_id": null,
  "state_version": null
}
```

## Availability and retention

Send decide with an `Idempotency-Key`. A lookup finds only decisions stored under an `Idempotency-Key`; see [Idempotency](https://www.rightnowai.co/docs/decision-model/idempotency).

Unkeyed decisions, older decisions without a lookup index and replies over the storage limits are unavailable.

A stored decision is kept for up to 24 hours, and its answer or index can be removed earlier. Reading it never extends that time.

A lookup runs no new decision and is not charged. It still counts toward your requests a minute. The lookup index keeps no copy of request or answer content.

A decision id is not a permanent archive. Keep the answers you need in your own storage.

## Errors and headers

Both reads can return these refusals.

Unknown, unkeyed, foreign or expired ids raise `DecisionNotFoundError` (`404 decision_not_found`) in the SDK.

- **`400 invalid_trace_header`**: X-Client-Request-Id is blank, not printable ASCII or over 512 characters.
- **`401 invalid_api_key`**: The key is missing, malformed or unknown.
- **`403 auth_error`**: The key is revoked.
- **`403 account_suspended`**: The workspace is suspended or waiting for access.
- **`404 decision_not_found`**: The id is invalid, or no stored decision matches it for this API key.
- **`405 method_not_allowed`**: The method is not GET, HEAD or OPTIONS.
- **`429 rate_limit_exceeded`**: Too many requests from this key, workspace or address. Also returned when the workspace has read back more than 500 MB of retained answers in one UTC day.
- **`503 service_unavailable`**: A store the lookup reads cannot answer right now.
- **`503 limiter_unavailable`**: Rate limiting cannot answer right now.

**decision-not-found.json**

```json
{
  "error": {
    "message": "The requested decision is not available.",
    "type": "not_found_error",
    "param": null,
    "code": "decision_not_found",
    "retryable": false
  },
  "request_id": "request-id-from-response"
}
```

`decision_not_found` has `error.retryable`: `false`. It does not say whether a decision was never stored, expired, was removed or belongs to another key.

Retry only when `error.retryable` is true. Wait for `Retry-After-Ms`, in milliseconds, or `Retry-After`, in seconds; prefer `Retry-After-Ms`. See [Errors](https://www.rightnowai.co/docs/decision-model/errors).

Lookups use the same requests a minute as decide. Their `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset` headers report them; see [Rate limits](https://www.rightnowai.co/docs/decision-model/rate-limits).

`X-Request-Id` identifies the lookup, not the original decision. A valid `X-Client-Request-Id` you send is echoed back.

Lookups never set `X-RightNow-Idempotent-Replay`. Every response sends `Cache-Control: no-store`. A `405` lists `Allow: GET, HEAD, OPTIONS`.

## Next

[Errors](https://www.rightnowai.co/docs/decision-model/errors)
