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

# Ruleset & rulebook graphs

> The visual map of a compiled ruleset — fields, criteria, groups, and the outcome, as structured data and as Mermaid.

`GET /rulesets/{ruleset_id}/graph` (and the composed `GET /rulebooks/{rulebook_id}/graph`) return the ruleset as a **graph**: every field, every compiled criterion, the groups they roll up into, and how those groups combine into the outcome. It's the same compiled logic `/schema` and `/explain` describe, laid out as nodes and edges instead of a flat field list — useful for rendering a visual map, for an agent that wants to reason about rule structure programmatically, or for spot-checking that a generated ruleset compiled the shape you expected.

Public rulesets are accessible **without an API key**, the same as `/decide` and `/schema` — see [Do you even need a key?](/reference/authentication#do-you-even-need-a-key).

## Request

```bash theme={null}
curl https://api.aethis.ai/api/v1/public/rulesets/aethis%2Fuk-fsm%2Fchild-eligibility/graph
```

URL-encode the slug's `/` as `%2F` (or pass the concrete dated `ruleset_id` from a `/decide` envelope — either resolves, same as `/schema` and `/explain`).

## Response shape

```json theme={null}
{
  "ruleset_id": "aethis/uk-fsm/child-eligibility",
  "slug": "aethis/uk-fsm/child-eligibility",
  "name": "UK FSM Child Eligibility",
  "graph": {
    "nodes": [ /* see below */ ],
    "edges": [ /* field -> criterion -> group -> outcome */ ],
    "sections": ["aethis/uk-fsm/child-eligibility"],
    "stats": {
      "total_fields": 2,
      "shared_fields": 0,
      "total_criteria": 3,
      "total_groups": 3,
      "sections": 1
    }
  },
  "mermaid": "graph TD\n\n  subgraph aethis/uk-fsm/child-eligibility[...]\n  ..."
}
```

* **`graph.nodes`** — one entry per field, criterion, group, and a single terminal `outcome` node. `node.type` is one of `field`, `criterion`, `group`, `outcome`.
* **`graph.edges`** — directed `{source, target, type}` triples: `field_to_criterion`, `criterion_to_group`, `group_to_outcome`. Follow them to render your own layout instead of Mermaid.
* **`graph.stats`** — a quick summary (field/criterion/group counts, section count). `shared_fields` is nonzero only on a composed rulebook graph where two sections reference the same field.
* **`mermaid`** — a ready-to-render [Mermaid](https://mermaid.js.org/) flowchart string covering the same graph. Paste it into any Mermaid renderer, or embed it directly if your docs/UI already renders Mermaid.

### Criterion nodes: the three altitudes

Each `criterion` node carries a `display` object with the same underlying compiled expression shown three ways, so a caller can pick the altitude it needs:

```json theme={null}
{
  "id": "criterion:age_at_least_4",
  "type": "criterion",
  "label": "Child is aged 4 or over at start of academic year",
  "title": "Child is aged 4 or over at start of academic year",
  "section_id": "aethis/uk-fsm/child-eligibility",
  "group": "age_check",
  "fields": ["child.age"],
  "display": {
    "sentence": "child.age is at least 4",
    "routes": {
      "id": "r",
      "kind": "leaf",
      "label": "child.age is at least 4",
      "expr": { "type": "op", "operator": ">=", "args": [
        { "type": "field_ref", "key": "child.age" },
        { "type": "const", "sort": "Int", "value": 4, "field_context": null }
      ] },
      "overlay": null
    },
    "expr": { "type": "op", "operator": ">=", "args": [ "..." ] }
  },
  "overlay": null
}
```

| Field              | Altitude                 | What it's for                                                                                                                                                                   |
| ------------------ | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `display.sentence` | Human-readable           | A plain-English rendering of the criterion — what you'd show a caseworker or applicant.                                                                                         |
| `display.routes`   | Structured decision tree | A `RouteNode` tree (`kind`: `leaf` / `all_of` / `any_of` / …) — the shape to walk if you're building your own visual editor or want to know exactly which sub-conditions exist. |
| `display.expr`     | Raw compiled AST         | The exact expression the engine evaluates — `field_ref` / `const` / `op` nodes. Use this if you need byte-for-byte fidelity with what ran, not a paraphrase.                    |

### The empty/legacy case: `display.routes` can be `null`

`display.routes` is a **best-effort structural rendering** built from the same expression as `sentence` and `expr`. If a criterion's compiled expression is shaped in a way the route-tree builder doesn't yet cover, the server catches that internally and returns `routes: null` for that node — `sentence` and `expr` are always populated (they come from the same code path that already backs `/explain`), so a caller can always fall back to `sentence` for display and `expr` for exact logic, even when `routes` is absent. Never assume `routes` is present; branch on it being `null` the same way you'd branch on any optional field.

## `overlay` — decision-time status (`include_graph_overlay`)

By default every node's `overlay` is `null` — the graph above is pure **structure**, independent of any particular decision. Pass `include_graph_overlay: true` on `/decide` to get the same graph back with each node's `overlay` stamped with that decision's outcome, so you can render a map that's colored by what actually happened:

```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's `graph_overlay` field is the same `{nodes, edges, sections, stats}` shape as above, but each criterion's `overlay` (and `display.routes.overlay`) is now `{"status": "satisfied", "supporting_facts": [...]}` (or `not_satisfied` / `pending`) instead of `null`:

```json theme={null}
{
  "id": "criterion:age_at_least_4",
  "display": {
    "routes": { "...": "...", "overlay": { "status": "satisfied" } }
  },
  "overlay": {
    "status": "satisfied",
    "supporting_facts": [{ "field": "child.age", "value": 10 }]
  }
}
```

See [Debug a failing decide](/recipes/debug-a-decide) for a worked example using the overlay to spot which criterion failed.

## Rulebook graphs (composed)

`GET /rulebooks/{rulebook_id}/graph` (path forms: `/rulebooks/{rulebook_id}/graph` and `/rulebooks/{namespace}/{name}/graph`) returns the same `{nodes, edges, sections, stats}` IR, merged across every ruleset the rulebook composes — `graph.sections` lists each member section, and `stats.shared_fields` counts fields two or more sections reference in common. Unlike ruleset-level `/graph`, rulebook endpoints are **not anonymous** — they need an API key (the same rule as `rulebook_id` on `/decide`; see [Do you even need a key?](/reference/authentication#do-you-even-need-a-key) and [`missing_api_key`](/reference/errors#missing_api_key--401)).

## Rendering the Mermaid string

The `mermaid` field is plain text — feed it straight into any Mermaid-compatible renderer:

```mermaid theme={null}
graph TD

  subgraph fsm_child[aethis/uk-fsm/child-eligibility]
    subgraph fsm_child_fields[Fields]
      field_child_age["child.age<br/>(Int)"]
      field_child_school_type["child.school_type<br/>(Enum)"]
    end
    subgraph fsm_child_criteria[Criteria]
      criterion_school_type_state_funded["Child attends a relevant state-funded school"]
      criterion_age_at_least_4["Child is aged 4 or over at start of academic year"]
      criterion_age_under_16["Child is aged under 16 at start of academic year"]
    end
    subgraph fsm_child_groups[Groups]
      group_school_type_check{{"school_type_check"}}
      group_age_check{{"age_check"}}
      group_age_upper_check{{"age_upper_check"}}
    end
  end

  outcome(("Eligibility Outcome (AND all)"))

  field_child_school_type -.-> criterion_school_type_state_funded
  criterion_school_type_state_funded --> group_school_type_check
  field_child_age -.-> criterion_age_at_least_4
  criterion_age_at_least_4 --> group_age_check
  field_child_age -.-> criterion_age_under_16
  criterion_age_under_16 --> group_age_upper_check
  group_school_type_check ==> outcome
  group_age_check ==> outcome
  group_age_upper_check ==> outcome
```

<Note>
  The node IDs above are shortened for readability — the live response prefixes them with the full section slug (e.g. `group_aethis/uk-fsm/child-eligibility_school_type_check`) so a composed rulebook graph never collides node IDs across sections.
</Note>

Solid arrows (`-->`, `==>`) trace the compiled decision path; dotted arrows (`-.->`) trace which fields feed which criteria. A criterion shared by two or more sections in a composed rulebook graph is styled with the `shared` class (gold fill) so it's visually obvious which parts of the map are doing double duty.

## See also

* [Debug a failing decide](/recipes/debug-a-decide) — using `include_graph_overlay` and `include_trace` together to pinpoint a failing criterion.
* [Decision envelope](/concepts/decision-envelope) — the full `/decide` response contract.
* [Errors reference](/reference/errors) — `missing_api_key` and other failure shapes you'll hit calling the rulebook-level endpoints anonymously.
