> ## Documentation Index
> Fetch the complete documentation index at: https://docs.aethis.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Debug a failing decide

> Agent recipe: when /decide returns the wrong answer, or an error, or undetermined — what to check and in what order.

<Note>
  Successful decision responses identify the serving build in `engine_version`.
  Read that field from your response rather than relying on a version copied into a tutorial.
  [What the deployed engine serves](/reference/deployed-contract).
</Note>

**You are a coding agent. A `/decide` call returned something unexpected.** Work through this page in order; each step is safe.

***

## Step 1 — Read the envelope, not just the decision

Every `/decide` response carries an audit envelope:

```json theme={null}
{
  "decision": "undetermined",
  "ruleset_id": "uk-fsm-child-eligibility:20260528-244428c1",
  "slug": "aethis/uk-fsm/child-eligibility",
  "ruleset_version": "v3",
  "content_digest": "sha256:EXAMPLE_DIGEST_0000000000000000000000000000000000000000000000",
  "engine_version": "aethis-core@0.48.0",
  "decision_id": "dec_abc123...",
  "inputs_hash": "sha256:5f4dcc3b...",
  "decision_time": "2026-07-26T01:26:33Z",
  "fields_provided": 1,
  "fields_evaluated": 2,
  "field_errors": null,
  "missing_fields": ["child.school_type"]
}
```

<Note>
  Values above are illustrative — `content_digest` is a placeholder, and the
  `ruleset_id` / `ruleset_version` you get back depend on what is published
  when you call. The **field names and their meanings are exact**.
</Note>

Look first at:

* **`field_errors`** — read this before `decision`. If it is non-null, at least one input could not be applied, and the decision is forced to `undetermined` no matter what the rest of the envelope says. A key here is an unknown field name or a value that failed the field's type. Fix those first; nothing below matters until they are clear.
* **`decision`** — `eligible` / `not_eligible` / `undetermined`. The only field to treat as the outcome. Never re-derive it by aggregating group statuses.
* **`missing_fields`** — which fields the engine wanted but didn't get
* **`fields_provided` vs `fields_evaluated`** — if provided > evaluated, the extra fields are either unknown to this ruleset (check `field_errors`, it is probably a typo) or not relevant to the active path
* **`slug` resolved** — if you sent a slug, this confirms what the resolver picked. Different from what you expected? Wrong slug, wrong tenant.
* **`ruleset_id` / `ruleset_version` / `content_digest`** — the resolved immutable identity of what was actually decided. `ruleset_version` is a mutable authoring label; `content_digest` is the immutable content identity. **Pin replay to the digest.** For a published leaf ruleset, `ruleset_version` is never `"unknown"` — if you see that value, you are either on a composed rulebook or on an engine older than this contract.
* **`engine_version`** — same as your local expectation? Mismatched engine versions can change outcomes; pin the version if replay matters

See [Decision envelope](/concepts/decision-envelope) for the full contract.

***

## Step 2 — Re-run with `include_trace: true`

Trace shows per-group status for every criterion group in the ruleset:

<Tabs>
  <Tab title="CLI">
    ```bash theme={null}
    aethis decide \
      -b aethis/uk-fsm/child-eligibility \
      -i '{"child.age": 16, "child.school_type": "state_funded"}' \
      --explain
    ```
  </Tab>

  <Tab title="Python SDK">
    ```python theme={null}
    from aethis_sdk import Aethis

    with Aethis() as client:
        response = client.decide(
            ruleset_id="aethis/uk-fsm/child-eligibility",
            field_values={"child.age": 16, "child.school_type": "state_funded"},
            include_trace=True,
        )
        print(response.trace["group_statuses"])
    ```
  </Tab>

  <Tab title="curl">
    ```bash theme={null}
    curl -X POST https://api.aethis.ai/api/v1/public/decide \
      -H "Content-Type: application/json" \
      -d '{
        "ruleset_id": "aethis/uk-fsm/child-eligibility",
        "field_values": { "child.age": 16, "child.school_type": "state_funded" },
        "include_trace": true
      }'
    ```
  </Tab>

  <Tab title="MCP">
    Ask your agent to re-run with both trace and explanation enabled:

    > *"Re-run the Aethis decision for a 16-year-old at a state-funded school under `aethis/uk-fsm/child-eligibility`. Include the trace and the explanation."*

    Your agent invokes:

    ```
    aethis_decide({
      ruleset_id: "aethis/uk-fsm/child-eligibility",
      field_values: { "child.age": 16, "child.school_type": "state_funded" },
      include_trace: true,
      include_explanation: true
    })
    ```
  </Tab>
</Tabs>

```json theme={null}
{
  "decision": "not_eligible",
  "trace": {
    "status": "not_eligible",
    "group_statuses": {
      "school_type_check": "satisfied",
      "age_check": "satisfied",
      "age_upper_check": "not_satisfied"
    }
  }
}
```

The offending group is now obvious. Reading the [ruleset's `/explain` output](/interfaces/rest-api#inspect-a-ruleset) for that group tells you which clause of the source legislation it compiled from.

`include_explanation: true` adds a structured `explanation` object to the response — the gate-level checklist plus the supporting facts that proved each satisfied criterion. Shape:

```json theme={null}
{
  "explanation": {
    "decision": "eligible",
    "decision_path": "age_under_19",
    "groups": [
      {
        "group": "section_a",
        "status": "satisfied",
        "criteria": [
          {
            "criterion_id": "age_under_19",
            "title": "Child is age 19 or under",
            "status": "satisfied",
            "supporting_facts": [{"field": "child.age", "value": 10}],
            "source_refs": ["EducationAct1996#s512"]
          }
        ]
      }
    ],
    "unused_facts": ["child.eye_colour"]
  }
}
```

* `groups[].status` and `criteria[].status` are `satisfied` / `not_satisfied` / `pending` — the gate-level checklist.
* `supporting_facts` lists the answers that proved each satisfied criterion. For an `Or` branch, only the satisfied disjunct's facts appear (no over-reporting alternatives).
* `unused_facts` lists field names you provided that no criterion in the ruleset references — the typo signal. If you sent `child.school_kind` instead of `child.school_type`, it shows up here.

***

## Step 2b — Visualize with `include_graph_overlay`

`include_trace` and `include_explanation` are both text. If you want a **map** of the ruleset with this decision's status stamped onto every node — to render a UI, or just to see at a glance which branch failed — add `include_graph_overlay: true`:

```bash theme={null}
curl -X POST https://api.aethis.ai/api/v1/public/decide \
  -H "Content-Type: application/json" \
  -d '{
    "ruleset_id": "aethis/uk-fsm/child-eligibility",
    "field_values": { "child.age": 10, "child.school_type": "state_funded" },
    "include_graph_overlay": true
  }'
```

The response gains a `graph_overlay` field: the same `{nodes, edges, sections, stats}` structure `GET /rulesets/{id}/graph` returns, but with each criterion's `overlay` stamped `{"status": "satisfied" | "not_satisfied" | "pending", "supporting_facts": [...]}` for *this* decision instead of `null`. Walk the `not_satisfied` nodes to find the failing branch without parsing `trace.group_statuses` by hand — useful when you're rendering rather than reading raw JSON. Full shape and the structure-only (no decision) form: [Ruleset & rulebook graphs](/reference/graph).

***

## Step 3 — If `decision = not_eligible`, use `/explain-failure`

<Tabs>
  <Tab title="CLI">
    ```bash theme={null}
    aethis decide \
      -b aethis/uk-fsm/child-eligibility \
      -i '{"child.age": 16, "child.school_type": "state_funded"}' \
      --explain
    ```

    `--explain` runs `/decide` first, then calls `/explain-failure` when the outcome is `not_eligible` and renders both in one pass.
  </Tab>

  <Tab title="Python SDK">
    <Note>Requires aethis-sdk >= 0.6.0.</Note>

    `/explain-failure` resolves a slug the same way `/decide`, `/schema`, and `/explain` do — pass either the slug or the concrete `ruleset_id` from the `/decide` envelope.

    ```python theme={null}
    from aethis_sdk import Aethis

    with Aethis() as client:
        response = client.decide(
            ruleset_id="aethis/uk-fsm/child-eligibility",
            field_values={"child.age": 16, "child.school_type": "state_funded"},
            include_trace=True,
        )
        if response.decision == "not_eligible":
            # Either the slug or the concrete ruleset_id from the envelope works.
            explanation = client.explain_failure(
                ruleset_id=response.ruleset_id,
                field_values={"child.age": 16, "child.school_type": "state_funded"},
                expected_outcome="eligible",
            )
            print(explanation)
    ```
  </Tab>

  <Tab title="curl">
    ```bash theme={null}
    # Either the slug (URL-encoded, %2F for /) or the concrete ruleset_id
    # from the /decide envelope resolves here.
    curl -X POST https://api.aethis.ai/api/v1/public/rulesets/aethis%2Fuk-fsm%2Fchild-eligibility/explain-failure \
      -H "Content-Type: application/json" \
      -d '{
        "field_values": { "child.age": 16, "child.school_type": "state_funded" },
        "expected_outcome": "eligible"
      }'
    ```
  </Tab>

  <Tab title="MCP">
    Ask your agent to explain the failure:

    > *"Explain why a 16-year-old at a state-funded school was not eligible under `aethis/uk-fsm/child-eligibility`."*

    Your agent invokes `aethis_explain_failure` (accepts either slug or concrete `ruleset_id`):

    ```
    aethis_explain_failure({
      ruleset_id: "aethis/uk-fsm/child-eligibility",
      field_values: { "child.age": 16, "child.school_type": "state_funded" },
      expected_outcome: "eligible"
    })
    ```
  </Tab>
</Tabs>

Returns the minimal unsatisfied core — just the criteria that caused the failure, with source references and a mechanism hint for fixing the rule.

***

## Step 4 — If you got a 4xx, check the error reason\_code

Every error response has a `reason_code` under `detail`:

```json theme={null}
{
  "detail": {
    "error": "forbidden",
    "reason_code": "reserved_namespace",
    "message": "Slug namespace 'aethis/' is reserved for internal use."
  }
}
```

Full catalogue: [Errors reference](/reference/errors). The eight most common:

| Status | `reason_code` | What to do |
| -: | - | - |
| 401 | (no body) | Missing `x-api-key` on a scoped endpoint. Check [Nomenclature](/concepts/nomenclature) — rulebook lookups need a key. |
| 403 | `reserved_namespace` | You tried to publish under the reserved `aethis/*` namespace. Use a tenant namespace (e.g. `my-team/*`). |
| 403 | `invalid_scope` | Your key doesn't have the scope the endpoint needs. |
| 404 | (no `reason_code`) | Ruleset/rulebook not found — check slug spelling and visibility. |
| 409 | `slug_conflict` | Slug is owned by another tenant. Pick a different namespace. |
| 422 | `invalid_slug_format` | Slug must be `^[a-z][a-z0-9-]*(/[a-z][a-z0-9-]*)*$`. |
| 422 | outcome\_logic validation | You sent `{"expr": "..."}` instead of an Expr AST. See [Nomenclature](/concepts/nomenclature) and the [Author recipe](/recipes/author-a-rule#composing-sections-into-a-rulebook). |
| 429 | `daily_quota_exceeded` | Rate limit hit for an [operation class](/interfaces/rest-api#rate-limits). `Retry-After: 3600` on a per-key class, `86400` on an anonymous IP limit. Check `X-RateLimit-Remaining` or [`/usage`](/interfaces/rest-api#check-your-usage) to stay ahead of it. |

***

## Step 5 — Still confused? Minimal reproduction

Print everything — trace + explanation + timing + cache bypass — and attach the output to whatever support channel you're using. The `decision_id` is the handle; the server logs every decision keyed by that id.

<Tabs>
  <Tab title="CLI">
    ```bash theme={null}
    aethis decide \
      -b <your ruleset_id> \
      -i '{ ... }' \
      --explain
    ```

    The CLI doesn't currently expose `include_timing` or `no_cache` flags — for a cache-bypassed timing repro, use the curl tab.
  </Tab>

  <Tab title="Python SDK">
    The SDK's `decide()` exposes `include_trace` and `include_explanation` (0.7.0+) but not `include_timing` or `no_cache`. For full-diagnostic repros that need those flags, drop to `httpx`:

    ```python theme={null}
    import httpx

    r = httpx.post(
        "https://api.aethis.ai/api/v1/public/decide",
        json={
            "ruleset_id": "<your ruleset_id>",
            "field_values": { ... },
            "include_trace": True,
            "include_explanation": True,
            "include_timing": True,
            "no_cache": True,
        },
    )
    print(r.json())
    ```
  </Tab>

  <Tab title="curl">
    ```bash theme={null}
    curl -X POST https://api.aethis.ai/api/v1/public/decide \
      -H "Content-Type: application/json" \
      -d '{
        "ruleset_id": "<your ruleset_id>",
        "field_values": { ... },
        "include_trace": true,
        "include_explanation": true,
        "include_timing": true,
        "no_cache": true
      }'
    ```
  </Tab>

  <Tab title="MCP">
    Ask your agent for the full diagnostic envelope:

    > *"Run aethis\_decide for ruleset `<your ruleset_id>` with field\_values \<...>, include trace, explanation, and timing, and bypass the cache. Show me the full response."*
  </Tab>
</Tabs>

***

## Common gotchas

* **`ruleset_id: "aethis/uk-fsm"`** — that's a rulebook slug. Move the value to `rulebook_id` and add `x-api-key`. See [Nomenclature](/concepts/nomenclature#ruleset_id-vs-rulebook_id-on-decide).
* **Anonymous `/decide` on a private ruleset returns 404** — the engine doesn't leak existence of private rulesets. If you own the ruleset, add your API key.
* **Slug resolves to an unexpected version** — slug pointers transfer on re-publish. Check `ruleset_id` in the response envelope to see which ruleset you actually hit.
* **`missing_fields` is empty but decision is `undetermined`** — discretionary clause (see [system-wide discretion principle](/concepts/how-it-works)) or a case outside the compiled rules. The ruleset may need regeneration or an explicit rule for your case.

***

## Next steps

* [Errors reference](/reference/errors) — every `reason_code` the API emits
* [Decision envelope](/concepts/decision-envelope) — the audit/replay contract
* [Evaluate a case](/recipes/evaluate-a-case) — the happy-path workflow


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.