Skip to main content
Authoring is invite-only private beta. Everything on this page needs an Aethis API key with authoring scopes, plus your own model-provider key for the generation steps. Request access, or start on the open evaluate tier with your first decision — no key required. The two tiers are set out on Capabilities and access.
Authoring is invite-only private beta — request access. Generation uses an LLM to compile your sources into constraint logic; you supply the selected model provider’s API key per request and it is never stored.

Choose a generation model (API and CLI)

Sonnet 5 remains the default. To choose a model for an individual generation or refinement, use the optional API request field model or the CLI flag --model (aethis-cli 0.41.0 or later): Omitting model preserves the server’s configured default, which is Sonnet 5 unless overridden by the server operator. An explicit selection applies to that request; repeat it when refining with the same model. From an existing CLI project, with the selected provider key in your environment:
The CLI sends only the selected provider’s credential for generation. External API callers must supply that provider’s key; a missing key does not fall back to a platform credential or another model. Model selection applies to rule generation and refinement. See REST API and provider credentials.

Structured acceptance contracts

An expected outcome checks one part of a ruleset’s behaviour. A versioned acceptance contract can also require specific pending reviews, review-completion mappings and unanswered fields that must remain available. A candidate is accepted only when every supplied check passes. Invalid scenario inputs fail the check, even when ignoring those inputs would produce the expected outcome. This also applies to outcome-only scenarios in project test runs. A completed generation may retain a failing draft for inspection; use the acceptance verdict, rather than job completion alone, to determine whether it passed. Each test retains name, field_values and expected_outcome. Its optional expectations object supports: The optional expected_review_bindings catalogue maps each completion field to its complete token-to-Boolean-or-null mapping. Each declared field must identify exactly one review point, and no additional mapped point is allowed. false and null are distinct: a false outcome rejects that review criterion; a null outcome leaves it pending. Omitting the catalogue adds no catalogue assertion; {} asserts that there are no mapped review points. A null catalogue is invalid. For a prepared generic JSON contract file, use:
This CLI workflow requires aethis-cli 0.42.0 or later. The file contains contract_version: 1, test_cases, and optionally expected_review_bindings. The CLI replaces the scenarios and catalogue atomically, then verifies their readback before requesting generation. An older engine that drops the contract is rejected. Scenario assertions authored under expect in tests/scenarios.yaml are also preserved without a separate file. A review-binding catalogue belongs in the generic contract file; placing expected_review_bindings or contract_version at the top level of tests/scenarios.yaml is rejected with instructions to use --acceptance-contract. API callers send the same fields to the project’s /tests endpoint with replace: true. Structured assertions require version 1. A legacy append or replacement cannot silently erase an existing contract; replacing or clearing it requires an explicit version 1 replacement. See the REST request envelope for the body shape. Keep independent hold-out cases outside the generation feedback. Passing the supplied contract establishes coverage of those checks, not complete fidelity to every source clause or explanatory passage.

The TDD loop

Rule generation is test-driven. The engine generates rules from your source text. Your test suite validates the output. Failing tests become the basis for targeted guidance — feedback that points to the specific source clause the engine missed. The loop repeats until all tests pass.
Rules are compiled from your source text and guidance — not reverse-engineered from your tests. Tests validate the output. Better tests catch more edge cases; better guidance converges faster.

Step 1 — Create a ruleset

If you completed Phase 2 (field vocabulary), you already have confirmed field names and a project_id — write your test cases using those names and skip to Step 2. If starting fresh on a single-section domain, you need test cases before creating the ruleset. Two approaches:
  1. Inspect an existing ruleset in the same domain with aethis_schema to learn the field naming convention, then write tests using those names.
  2. Make your best guess at field names, create the ruleset with preliminary tests, then run aethis_discover_fields to confirm. If names don’t match, add corrected tests via aethis_create_ruleset with a new project — the cost is low.
Either way, the field names in your test cases must exactly match what the engine discovers. Phase 2 exists to prevent this mismatch.
Returns:
Write test cases after running aethis_discover_fields. Test field names must exactly match the engine’s discovered field names. A mismatched name causes the engine to treat that field as absent — the test may silently pass or fail for the wrong reason.

Step 2 — Generate and test

Takes 60–120 seconds for most sections. Returns (first attempt — partial failure):
Two failures: the boundary condition at age 4 is wrong (strict > instead of ≥), and the school type restriction isn’t compiled. Each failure includes a hint pointing to the likely cause.

Step 3 — Refine with guidance

Add guidance that references the specific source clause:
aethis_refine adds the guidance, then makes the minimal edit to fix the failing tests — seeded from the section’s active ruleset and keeping the passing tests green, rather than re-authoring the whole section. Returns (second attempt — all passing):
All tests pass after one refinement. Move to publish.

Guidance variants

Add guidance without regenerating

Useful when you want to accumulate several pieces of guidance before triggering a generation run:
Then trigger generation separately:

Check accumulated guidance

Returns:

Domain-level guidance

Add once, applies to all projects in the domain — no need to repeat cross-section principles on each ruleset:

Guidance examples

Real guidance from the UK Free School Meals household criteria section (11 tests, 8 qualifying routes). Each hint addresses a specific compilation gap.
Problem: The engine treats all qualifying criteria as AND conditions — a household must meet all routes to qualify, when it should be any one.Guidance:
Problem: The Universal Credit route passes when receives_universal_credit is true, ignoring the income cap.Guidance:
Problem: Looked-after children are being checked against benefit criteria when they should bypass them entirely.Guidance:
Problem: The Child Tax Credit route qualifies anyone receiving CTC, missing the requirement that they must not also receive Working Tax Credit.Guidance:
Pattern: Every effective hint references a specific regulatory clause, names the fields involved, and states the logical relationship (AND, OR, NOT, unconditional). Vague feedback (“fix the UC check”) doesn’t converge.

Diagnosing a specific failure

If a test is failing but the hint isn’t clear enough, use aethis_explain_failure to get a deeper diagnosis:
Returns:

Test coverage strategy

Good coverage catches failures before production:
  • Boundary values — test just below, at, and above every threshold (age 3, 4, 15, 16)
  • Every Enum value — one test per enum value for each Enum field
  • Every code path — combinations of fields that exercise a distinct branch
  • Five tests minimum per section — more is better; sparse test suites let edge-case bugs through

Date arithmetic: years_between

Source text frequently expresses age or duration thresholds in terms of a date of birth or a start date (“must be 18 or over”, “at least 5 years’ continuous residence”). Never ask for or store an age field directly — dates drift out of sync with a stored age the moment time passes, and a re-run of the same ruleset on a later day silently changes the answer. Compile the threshold from the date field instead:
years_between(start, end) returns the number of completed whole years between two Date fields — true “has the Nth anniversary been reached on or before end” logic, leap-year correct (29 February birthdays included). It is not days_between(start, end) / 365, which drifts by roughly a day every four years and silently produces off-by-one-year answers near a birthday. Both helpers compile to the same compiled rule evaluation as every other operator in this reference (see What you can express for the full operator table) — there’s no separate date-math code path to reason about at decide time.

Step 4 — Publish

Once all tests pass:
Returns:
The name is auto-derived from the section ID (e.g. child_eligibility → "Child Eligibility"). To override it, pass name in the publish call:
The ruleset_id is now ready for aethis_decide. Published rulesets are locked and versioned. If you published the ruleset into a Rulebook, it remains in testing until you promote that exact version to live. Promotion re-runs the go-live gates, archives the prior live version of the same named ruleset, and cuts a new Rulebook version atomically. Use aethis rulesets promote-to-live with the rulebook, ruleset name, and exact candidate ruleset_id in that order. The underlying request includes force_unsafe: false by default. If the presence-polarity gate finds an unanswered-field default that can be more favourable than an answered value, promotion fails with non_conservative_presence_op; re-author the default so silence is the strict reading, or make the field explicit. On this gate, force_unsafe: true is restricted to internal Aethis keys and writes an audit event — external authors cannot use it. That restriction is specific to the promotion presence-polarity gate. The force_unsafe flag on publish is a different gate with different rules: it bypasses the server-side TDD gate, it is available to any key with authoring scopes, and it records a publish_force_bypass audit event rather than refusing. Do not carry the internal-only restriction across from one to the other. After a successful promotion, inspect presence_polarity_advisories. It is normally null; a non-null list means the candidate went live through an audited internal override or the advisory path for an existing first-party showcase member. Each entry names the affected field, criterion, and semantic-risk message. The full REST request and response are in REST API.
To update rules (e.g. for a legislative change), generate a new ruleset in the same project and publish again. The previous ruleset remains available by its specific version ID.

Sources: budget, duplicates, and lifecycle

Generation builds its context from every active source on the project, so source hygiene directly affects rule quality. Token budget. Every upload response includes estimated_tokens per file, a project_estimated_tokens running total, and the generation_token_budget it must fit (the model’s context window minus headroom reserved for the generation loop). Before any generation starts, the engine counts the exact prompt — if it exceeds the budget, the request is rejected with 422 token_budget_exceeded (including the count, the budget, and the largest sources) before any model cost is incurred. Duplicates. Re-uploading identical content (or a same-named file) is flagged in the response under possible_duplicates — surfaced, never silently merged. Conflicting versions of the same guidance are a correctness hazard: resolve them by superseding the stale copy. Lifecycle. GET /projects/{project_id}/sources lists every source with its status; PATCH /projects/{project_id}/sources/{source_id} sets it:
  • active — included in generation (the default)
  • superseded — replaced by a newer upload (optionally point at it with superseded_by); kept for provenance, excluded from generation
  • reference_only — kept on the project, excluded from generation
DELETE removes a source outright — but a source cited by any persisted ruleset’s provenance returns 409 rather than orphaning those citations. Prefer superseded when generated rulesets already cite it. The retained bytes behind a source stay downloadable at GET /projects/{project_id}/sources/{source_id}/raw (scope projects:read); see Provenance and citations.

Grounding report

Every generated rule cites the source passages it is grounded in. After generation, the job (and the generate-and-test response) carries a provenance_report:
  • totals — how many citations resolved against the uploaded sources (verified), cited passages that don’t exist (flagged), and rules or fields with no citation (uncited)
  • coverage — per source: how many passages are cited by at least one rule, plus a sample of passages nothing cites
The coverage list is the review signal golden tests can’t give you: a statutory exception that no rule cites — and no test exercises — stays green in testing but shows up here. The report never blocks generation; treat it as the reviewer’s punch list. The grounding report is not the same thing as a published citation. It scores how well generated rules trace back to your uploaded sources, and it never blocks. Binding a criterion to the verbatim text it rests on — verified against retained bytes, immutable, fail-closed — happens at publish time with source_targets. See Provenance and citations.

Handling generation timeouts

Rule generation can outlive a client timeout. A timeout means the caller lost its wait for the result; it does not mean the server-side job stopped. Do not start another generation until you have inspected the job — a project admits only one active generation, and retrying blindly can duplicate work.
  1. Check the project status with aethis_generation_status (or aethis status in the CLI). A current recovery envelope advertises generation_contract_version: 1 and returns telemetry_availability, the server-authoritative worker_lifecycle, and retry_readiness alongside the job details. If the marker is absent, do not use the cancellation workflow.
  2. If retry_readiness is blocked, keep polling; cleanup_pending means the server still has ownership cleanup to complete. Retry only when it becomes ready. Turn count, pass-rate progress, last tool, and heartbeat age are monitoring evidence, but an old heartbeat alone does not prove worker death.
  3. If the job is success, run the tests and continue to publish only when the test result is acceptable. If it is failed, use its structured failure detail and source-cited guidance to correct the project before starting a new run.

Cancel only on explicit caller intent

If the caller decides to abandon an in-flight run, call aethis_cancel_generation({ project_id, job_id, confirm_job_id: job_id }) only after showing the exact status job_id and receiving a fresh explicit reply from the caller. Repeating the id in confirm_job_id binds the target and prevents an accidental mismatch; it does not prove that a human approved the action. The MCP host’s destructive-action approval and the caller’s fresh reply are separate requirements. A missing or mismatched id makes no MCP cancellation request. For non-interactive CLI automation, aethis cancel --yes still observes and displays the target first. Cancellation marks that job failed and releases its project ownership so another run can be admitted. It is never performed automatically after a timeout, stalled progress indicator, or disconnect. The response outcome is cancelled for the first transition or already_cancelled when the same exact cancellation is repeated. Both are a successful, idempotent resolution of that job-bound request. Other terminal jobs and a changed active job are conflicts, not successful cancellation. The worker-stop result is intentionally separate from releasing ownership. A worker may cooperate with the cancellation only later; on older workers it may continue until it finishes or the instance is replaced. Read the returned detail before assuming model work or provider spend stopped. Your model-provider key is sent with a generation request only and is never persisted or replayed by status/cancel requests.

Act on structured reasons, not guesses

For a failed job, inspect job.error_detail.reason_code and its retryable flag where present. The server returns sanitised provider detail — never the provider key or unbounded provider error text.

After all sections are published

If your domain has multiple sections, compose them into a rulebook — a container that references published rulesets and defines outcome logic.

Create a rulebook

Via the REST API:

Ruleset references

Each ruleset_ref has:

Outcome logic

Defines how section outcomes compose. Each section is assigned a letter (A, B, C…) in the order listed in ruleset_refs. Supported operators: AND, OR, NOT, parentheses for grouping.

robot_hints — guidance for a conversational agent

A rulebook optionally carries robot_hints: a flat map of beat name → natural-language guidance string, for a conversational agent (like Lisa) driving an applicant through the composed rulebook. Set it on create or update:
Keys are validated against a fixed, known set of beats — an unrecognised key is rejected with a 422 listing the allowed set, so a typo’d beat fails loudly at authoring time instead of silently being ignored by the consumer: Values are free-text strings — there’s no further schema on the guidance itself, only on the key set. robot_hints is additive and back-compat: a rulebook authored before this field existed simply returns robot_hints: null on /schema (below), so an older consumer doesn’t need to change anything.

Activate and evaluate

The /decide endpoint accepts either a ruleset_id (single section) or a rulebook_id (composed multi-section). When evaluating against a rulebook, the response includes section_results showing each section’s individual decision. The rulebook /schema response merges {rulebook_id, sections, fields, robot_hints, engine_version} across every composed section:
  • robot_hints — the rulebook’s guidance map (above), or null for a rulebook authored before the field existed.
  • engine_version — the same aethis-core@X.Y.Z string echoed on every /decide response and at /openapi.json → info.version, so a consumer can record which engine build resolved this schema without a separate call. Useful for pinning replay: if you cache a schema response, engine_version tells you whether a later /decide ran against the same compiled build.
See the UK Free School Meals worked example for a complete multi-section rulebook with live rulesets.