> ## 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.

**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",
  "slug": "aethis/uk-fsm/child-eligibility",
  "ruleset_version": "unknown",
  "engine_version": "aethis-core@0.36.0",
  "decision_id": "dec_abc123...",
  "inputs_hash": "sha256:5f4dcc3b...",
  "decision_time": "2026-04-19T22:00:00Z",
  "fields_provided": 1,
  "fields_evaluated": 2,
  "missing_fields": ["child.school_type"]
}
```

Look first at:

* **`decision`** — `eligible` / `not_eligible` / `undetermined`
* **`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 (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. (The dated `ruleset_id` is also returned — that's the immutable version-pin if you need byte-exact replay.)
* **`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 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>

    Pass the concrete `ruleset_id` from the `/decide` envelope, not the slug —
    `/explain-failure` path resolution doesn't accept slugs.

    ```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":
            # ruleset_id must be the concrete identifier, not a slug
            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}
    # Use the concrete ruleset_id (from the decide envelope), not the slug —
    # /explain-failure path resolution doesn't currently accept slugs.
    curl -X POST https://api.aethis.ai/api/v1/public/rulesets/$RULESET_ID/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 | (no body)                 | Rate limit hit. `Retry-After: 86400` on daily-tier exhaustion.                                                                                                                    |

***

## 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
