Small page, high-value. If you’re writing a /decide call and unsure whether to pass ruleset_id or rulebook_id, you’re in the right place.
The two objects
The platform has two user-facing nouns: a Rulebook and a Ruleset. Everything else (sections of legislation, authoring projects, the state machine that moves a ruleset towards live) describes how these two objects are managed, not extra primitives you create.
| Term | What it is |
|---|
| Rulebook | The whole form — the unit /decide evaluates against. Owns a locked field vocabulary, a composition expression (outcome_logic), rulebook-level tests, and an integer version history. Identified by rulebook_id (rb_<id>) and optionally a slug (e.g. aethis/uk-fsm). |
| Ruleset | A named, versioned part of a Rulebook (e.g. child_eligibility). Holds the compiled DSL and per-ruleset tests. Each Ruleset has a state (draft → testing → live → archived); exactly one version per name can be live at a time. Identified by ruleset_id (an opaque per-version ID) and optionally by a per-version slug (e.g. aethis/uk-fsm/child-eligibility). |
| Slug | Stable human-readable alias, format <segment>[/<segment>...] (lowercase ASCII + hyphens). Applied to rulebooks and rulesets — survives regeneration of the underlying artefact. |
We talk about “sections of legislation” in prose (“this ruleset encodes the household-qualifying-criteria section of the FSM regulations”) but a Section is not a separate object. The named Ruleset within a Rulebook is the encoded section.
A concrete example — UK Free School Meals:
Rulebook aethis/uk-fsm ← the whole form; what /decide evaluates
├── locked Fields {applicant.age, household.income, child.year_group, …}
├── outcome_logic child_eligibility AND (household_criteria OR universal_infant)
├── rulebook-level tests full-form personas (gate version cuts)
├── version history v1, v2, v3, …
└── Rulesets
├── child_eligibility — versions v1..v3 (v3 = live)
├── household_criteria — versions v1..v2 (v2 = live)
└── universal_infant — version v1 (live)
The ruleset state machine
A ruleset version flows through four states:
| State | Meaning |
|---|
draft | Freshly generated; not yet test-passed; mutable. Iteration in draft doesn’t move the parent Rulebook. |
testing | Ruleset-level tests pass; queryable via single-ruleset /decide for QA; not pinned by any live Rulebook version. |
live | At most one per (rulebook_id, ruleset_name). Pinned by the current Rulebook version. The decision authority. |
archived | Terminal. Never resurrected. New work starts a fresh draft. |
The only way to move a ruleset to live is aethis rulesets promote-to-live. The operation is atomic:
- Demote prior live ruleset of the same name →
archived.
- Promote the candidate →
live.
- Update the Rulebook’s
live_ruleset_pins.
- Cut a new Rulebook version (
v(n+1)) recording the change.
So iteration is cheap (do as much as you want in draft / testing); promotion is explicit and visible (every live change cuts a new Rulebook version).
ruleset_id vs rulebook_id on /decide
The public POST /decide endpoint has two separate fields on the request body, and they go through different resolution paths:
{
"ruleset_id": "aethis/uk-fsm/child-eligibility",
"field_values": { ... }
}
{
"rulebook_id": "aethis/uk-fsm",
"field_values": { ... }
}
ruleset_id — evaluates one ruleset version in isolation. Useful for testing a candidate before promoting. Anon OK on public rulesets.
rulebook_id — evaluates the composed rulebook: every live ruleset combined via the rulebook’s outcome_logic. This is the “real decision” path. Always requires x-api-key — rulebook evaluation is unconditionally scope-gated, regardless of rulebook visibility. Anonymous callers get HTTP 401. (For an unauthenticated taste, hit a public section ruleset by its slug — ruleset_id: "aethis/uk-fsm/child-eligibility" — and the path is anonymous.)
Sending ruleset_id: "aethis/uk-fsm" — a rulebook slug — returns Ruleset 'aethis/uk-fsm' not found. The resolver only looks in the ruleset collection for that field; it doesn’t fall through to rulebooks. If you want the composed decision, move the value into the rulebook_id field.
See interfaces/rest-api.mdx for the full endpoint contract.
Visibility — public vs private
visibility | Anon /decide with ruleset_id works? | Shown in GET /rulesets? |
|---|
public | yes | yes |
private | no — needs an API key with decide | no (unless same tenant) |
Every new ruleset defaults to private. The exception: publishing with a slug under the reserved aethis/* namespace auto-flips visibility to public. For everything else, call PATCH /rulesets/<id>/visibility after publish.
The aethis/* namespace
Reserved for first-party rulesets maintained by Aethis. External tenants attempting to publish slugs starting with aethis/ get HTTP 403 with reason_code: "reserved_namespace".
This is intentional: aethis/* is where the canonical public demo rulebooks and rulesets live (see /getting-started/try-it and /getting-started/examples). The namespace doubles as an allow-list for “safe to reference from docs without fear of the tenant renaming it”.
Authoring lifecycle (one screen)
The four CLI commands that move a Ruleset between states:
# Create a draft (empty shell)
aethis rulesets create aethis/uk-fsm child_eligibility
# OR: populate via the project pipeline + publish into the rulebook
aethis generate --poll # produces a draft ruleset
aethis test # → state=testing if green
aethis publish --rulebook aethis/uk-fsm \
--ruleset-name child_eligibility # stamps FKs, state stays testing
# Atomic promotion → live + new Rulebook version
aethis rulesets promote-to-live aethis/uk-fsm child_eligibility <rs_id>
# Archive a stale ruleset version (rare; happens automatically on promote)
aethis rulesets archive aethis/uk-fsm child_eligibility
aethis rulebooks decide aethis/uk-fsm -i '{...}' runs the composed evaluation against whatever set of rulesets is currently live in the rulebook.
See also
- Decision envelope — what
/decide returns and how to replay it
- REST API — endpoint contracts + rate limits + auth
- MCP tools — the agent-callable wrappers around these endpoints