Skip to main content
api.aethis.ai serves engine 0.49.1, which is the contract these pages describe; every response carries engine_version so you can confirm which build answered you. What the deployed engine serves.
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:
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.
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.
  • decisioneligible / 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 for the full contract.

Step 2 — Re-run with include_trace: true

Trace shows per-group status for every criterion group in the ruleset:
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:
  • 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:
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.

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

--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:
Full catalogue: Errors reference. The eight most common:

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