Skip to main content
You are a coding agent. Given source legislation + test cases, you’re producing a deterministic rule ruleset. This is the authoring workflow. Three phases — each gated on the previous. Only phase 3 has a TDD loop today; phases 1 and 2 are informal. Requires an Aethis API key with projects:write and rulesets:write. Set AETHIS_API_KEY in your environment. If you’re using MCP, the key goes in the MCP client config (see MCP install).

Phase 1 — Section discovery

Legislation often covers multiple independent criteria. Split each into its own section so they can be tested and regenerated separately. The two interfaces approach this differently. MCP has an explicit aethis_discover_sections tool the agent calls before authoring. The CLI infers section structure at generate time from the source documents plus any guidance you provide — if you want explicit sections, drop one source file per section into sources/ or add section hints to guidance/hints.yaml.
aethis init my-rule           # scaffolds aethis.yaml, sources/, tests/, guidance/
cd my-rule
# Drop legislation into sources/ — one file per section is the
# simplest way to make the section split explicit.
cp ~/Downloads/legislation.pdf sources/
# Optional: add hints in guidance/hints.yaml to steer the split, e.g.
#   - "Section A covers age eligibility; Section B covers household means-test."
Authoring is multi-step and tightly stateful (project → sources → fields → generate → publish). The CLI and MCP tools orchestrate this for you. The Python SDK does not yet expose authoring endpoints; the raw HTTP endpoints are listed in the REST API reference for completeness but are not the recommended path.

Phase 2 — Field vocabulary

Define the input fields each section will consume. Shared vocabulary across sections lets rulebooks compose cleanly. The two interfaces approach this differently too. MCP exposes explicit discovery and pinning tools the agent invokes before generation. The CLI infers fields at generate time from your test inputs in tests/scenarios.yaml — the field IDs you write into your tests become the field vocabulary. Use prefixed names (applicant.age, household.income) to keep namespaces tidy across sections.
# tests/scenarios.yaml — field IDs you write here become the vocabulary
tests:
  - name: "eligible — meets age and visa"
    inputs:
      applicant.age: 35
      applicant.visa_status: settled
    expect:
      outcome: eligible
  - name: "not_eligible — student visa"
    inputs:
      applicant.age: 35
      applicant.visa_status: student
    expect:
      outcome: not_eligible
Field descriptions and enum constraints come from the source text + any hints in guidance/hints.yaml — e.g. applicant.visa_status is an enum with values student, work, settled.”
Phase 2 must be settled before generation starts — fields can’t be renamed freely once rules reference them.

Phase 3 — Rule generation (TDD loop)

The only phase with an automated test gate. Test-first is non-negotiable: write the tests, THEN generate, THEN iterate until green.

Step 3.1 — Write tests

Test cases drive everything — the generator iterates until every test passes, the publish endpoint refuses to ship a ruleset with a failing test, and (for the CLI) the field vocabulary is inferred from your test inputs. The default location after aethis init is tests/scenarios.yaml for the CLI; for MCP-driven authoring, test cases are supplied inline to aethis_create_ruleset or aethis_generate_and_test.
# tests/scenarios.yaml
tests:
  - name: "eligible — meets age and visa"
    inputs:
      applicant.age: 35
      applicant.visa_status: settled
    expect:
      outcome: eligible

  - name: "not_eligible — student visa"
    inputs:
      applicant.age: 35
      applicant.visa_status: student
    expect:
      outcome: not_eligible

Step 3.2 — Generate

aethis generate --poll     # creates a draft ruleset, waits for compilation

Step 3.3 — Review + refine

aethis test                # runs all test cases against the draft ruleset
Refinement in the CLI is iteration on inputs — there is no standalone refine command. If a test fails:
  1. Re-read the failing case — was the relevant clause in sources/ actually clear?
  2. Add a guidance hint in guidance/hints.yaml, e.g. “The age cap is 65, not 60 — see Section 7.2.”
  3. Re-run aethis generate --poll && aethis test.
Rules compile from the source, not from the tests — so you refine the source text or the hints, never the generated rules.

Step 3.4 — Publish

Only publish when aethis test shows 100% pass. The publish endpoint runs the tests again server-side as a gate.
aethis publish --slug my-team/my-rule
# → ✓ Published ruleset <ruleset_id> — slug: my-team/my-rule
The aethis/* namespace is reserved for first-party rulesets maintained by Aethis. Use a tenant namespace (e.g. my-team/*, my-company/*) for your own rulesets. See Nomenclature.

Composing sections into a rulebook

Once each section has an active ruleset with a stable slug, create the rulebook. See the worked UK Free School Meals example for a full multi-section composition including the outcome_logic AST.
curl -X POST https://api.aethis.ai/api/v1/public/rulebooks/ \
  -H "x-api-key: $AETHIS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "My Rulebook",
    "domain": "my_domain",
    "slug": "my-team/my-rulebook",
    "ruleset_refs": [
      { "section_id": "section_a", "pin_mode": "latest_active" },
      { "section_id": "section_b", "pin_mode": "latest_active" }
    ],
    "outcome_logic": {
      "type": "op", "operator": "and",
      "args": [
        { "type": "field_ref", "key": "section_a_group" },
        { "type": "field_ref", "key": "section_b_group" }
      ]
    }
  }'
The outcome_logic must be a proper Expr AST. The server rejects the text shorthand { "expr": "A AND B" } with HTTP 422 at ingress since aethis-core 0.7.2. To find the group names your sections emit, inspect each ruleset’s compiled criteria — or use a known pattern like the one in the FSM example.

Verification checklist

Before declaring a rule complete:
  • aethis test passes 100% on the ruleset that’s about to be published
  • /decide with the published ruleset_id returns the expected decision for each test input
  • If the rule is public-facing, visibility is public (auto-set for aethis/* slugs; use PATCH /rulesets/<id>/visibility otherwise)
  • The ruleset shows up in GET /api/v1/public/rulesets (public) or via the authored key (private)

Next steps