Skip to main content
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:
{
  "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:
  • decisioneligible / 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 for the full contract.

Step 2 — Re-run with include_trace: true

Trace shows per-group status for every criterion group in the ruleset:
aethis decide \
  -b aethis/uk-fsm/child-eligibility \
  -i '{"child.age": 16, "child.school_type": "state_funded"}' \
  --explain
{
  "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 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:
{
  "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

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.
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:
{
  "detail": {
    "error": "forbidden",
    "reason_code": "reserved_namespace",
    "message": "Slug namespace 'aethis/' is reserved for internal use."
  }
}
Full catalogue: Errors reference. The eight most common:
Statusreason_codeWhat to do
401(no body)Missing x-api-key on a scoped endpoint. Check Nomenclature — rulebook lookups need a key.
403reserved_namespaceYou tried to publish under the reserved aethis/* namespace. Use a tenant namespace (e.g. my-team/*).
403invalid_scopeYour key doesn’t have the scope the endpoint needs.
404(no reason_code)Ruleset/rulebook not found — check slug spelling and visibility.
409slug_conflictSlug is owned by another tenant. Pick a different namespace.
422invalid_slug_formatSlug must be ^[a-z][a-z0-9-]*(/[a-z][a-z0-9-]*)*$.
422outcome_logic validationYou sent {"expr": "..."} instead of an Expr AST. See Nomenclature and the Author recipe.
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.
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.

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.
  • 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) or a case outside the compiled rules. The ruleset may need regeneration or an explicit rule for your case.

Next steps