Appearance
Example policies
A policy pack is one JSON file holding a set of definitions the engine already understands: rules, rule sets, workflows, collection flows and lists with their rows. Packs are how a policy travels: from this repository into a pilot tenant in one command, from a tuned tenant back into a file a colleague can review, or between environments. The packs under examples/policies/, at least one for each industry on the landing page's solution pages, show what the engine is for; each one is a pattern for expressing a policy, not compliance guidance, and says so. The admin console offers them as starter packs (Define › Rules, Import), so a new workspace can import one in a click.
Aletheia is infrastructure. The packs hold no jurisdiction-specific content and no opinion about which checks a business must run. Their thresholds, country codes and weights are placeholders chosen to make the example run; a team adapts them to its own policy before anything real depends on them.
The pack format
PolicyPack in @aletheia-dev/core (policy-pack.ts):
json
{
"name": "high-risk-geography",
"version": "1.0.0",
"description": "What the pack does, in one paragraph.",
"definitions": {
"lists": [
{
"key": "geo_review_countries",
"name": "...",
"kind": "country",
"rows": [{ "value": "AQ" }]
}
],
"rules": [
{ "key": "geo_country_under_review", "name": "...", "type": "list_lookup", "config": {} }
],
"ruleSets": [{ "key": "geo_risk", "name": "...", "definition": {} }],
"collectionFlows": [{ "key": "kyc-individual", "name": "...", "definition": {} }],
"workflows": [{ "key": "geo-risk-review", "name": "...", "definition": {} }]
}
}Every item is the body the matching PUT route accepts (see Rules and Admin console: Define) plus the key it is stored under, so a pack is validated by exactly the schemas the API applies and defaults may be left out. Lists carry their rows (value, optional reason and expiresAt), at most 10 000 per list, the same cap as one batch of the CSV import. App installs are never part of a pack: an install carries a tenant's configuration and secrets, which belong to one tenant. A pack's call_app steps and app rules name the apps it expects the tenant to have installed. Keys within a group must be unique; the pack name is a slug like a definition key.
Import and export
Three routes; import and export are audited (definitions.imported and definitions.exported with the item counts and keys; see Audit):
| Route | Permission | Does |
|---|---|---|
POST /definitions/import[?publish=true] | definitions:write (definitions:publish too with publish) | Validates every item first (rule configs, list references, rule and rule-set references, flow keys) and rejects the whole pack with one 400 listing details.items[].issues; then upserts the lists and creates one draft version per definition. The pack is the body, or with pack=<id> a starter pack. |
GET /definitions/packs | definitions:read | The starter packs the deployment ships: the packs under examples/policies/, bundled into the API build, each with id (its name), name (its README's title), version, description and item counts. |
GET /definitions/export?keys=... | definitions:read (lists:read when a list is named) | The published version of each <kind>:<key> (rule, rule_set, workflow, collection_flow, list) as a pack; name, version and description come from the query or default to export and today's date. |
An import never publishes by itself. With publish=true each created version goes through the same path as POST <prefix>/:key/publish: the publish-time reference checks run (a workflow may only point at published rules and sets), and the tenant's approval settings apply. When the tenant requires approvals the import does not fail with 409 approval_required; every version is moved to pending_approval with an open request, the response says approvalRequired: true, and approvers find the requests in the inbox (GET /approvals). Writes happen in sequence rather than in one transaction: because validation runs first a failure part-way is rare, and anything written before one is a draft, which never affects runs.
Keys are stable: importing the same pack twice creates version 2 of each definition (identical content included) and adds only the list rows that are not there yet. The response names each created version (kind, key, version, status, approvalId) and each list with its row counts. The API client exposes both routes as client.policyPacks.import(pack, { publish }) and client.policyPacks.export(keys, { name, version }).
The CLI
Two thin wrappers over the routes, for a pilot loading a pack in one command and for a contributor turning a tenant's tuned definitions back into a pack:
bash
pnpm policy:import examples/policies/high-risk-geography # drafts only
pnpm policy:import examples/policies/high-risk-geography --publish # publish (or request approval)
pnpm policy:export ./my-pack --keys workflow:geo-risk-review,rule_set:geo_risk,list:geo_review_countries \
--name my-pack --version 2026-10The import validates the pack locally with the core schema before sending it and prints the API's per-item problems when the server refuses it; the export writes <dir>/pack.json pretty-printed. Both authenticate with a personal access token: ALETHEIA_TOKEN, else --token-file, else the admin PAT pnpm auth:seed wrote to docker/zitadel/bootstrap/smoke.pat (the one the smoke uses). The API is ALETHEIA_API_URL or --api-url, default http://localhost:4000.
The packs
| Pack | Industry | Shows |
|---|---|---|
| Merchant KYB onboarding | Payments & fintech | The development seed as a pack: a collection flow, mock screening, optional document verification, a weighted rule set and a case |
| Individual KYC with documents | Payments & fintech | A two-step flow with a required document upload, the mock verifier through the asynchronous app path, rules on the vendor outcome |
| Transaction velocity | Payments & fintech | Velocity rules over ingested events (sum, count, filtered count, distinct count), a monitoring rule set, a high-priority case |
| Sanctions and PEP escalation | Payments & fintech | A screening step that branches on a hit to a critical case before any rule runs, and a scored review path for softer signals |
| High-risk geography | Payments & fintech | Allow and block list lookups against tenant lists, a vendor-style score that contributes to the risk score, a review with an SLA |
| Seller and payout screening | Marketplaces & platforms | Two parties screened through an app: a call_app step whose hit rejects, an app rule on the payout account holder that opens a review |
| Driver onboarding | Gig & on-demand work | A phone-first flow with camera uploads, city, age and experience rules, an identity result your platform writes on the subject |
| Supplier due diligence | Supplier & third-party risk | A questionnaire with a branch for software suppliers, a screening step, a country risk list and a contract-value threshold |
| Tenant screening | Property & rentals | Rent-to-income CEL rules, a branch between employer and accounts, the same criteria applied to every applicant |
| Clinician credentialing | Healthcare credentialing | Date arithmetic in CEL for a licence expiry, an exclusion screen, findings routed to a committee |
| Loan affordability | Lending & credit | An affordability CEL rule, an amount referral, repeat applications counted from events, an underwriting case |
| Claims triage | Insurance | A claim form with a police-report branch for thefts, policy and early-claim rules, claim frequency from events |
| Grant eligibility | Public sector & grants | An eligibility formula as CEL, a near-limit referral and a missing-evidence check routed to a caseworker |
| Report triage | Trust & safety | No form: a classifier score on the subject, strike and report counts from events, a one-hour moderation SLA |
| Phishing triage | Email & messaging | No form: scan and DMARC results on the sender, a reputation threshold, reports counted per sender address |
| Safer gambling | Gaming & betting | Deposits summed from events, a loss threshold, accounts counted per device, a safer-gambling case |
No pack calls a vendor. Six call the deterministic mock apps (mock-sanctions and doc-verify-mock, which the seed installs and any workspace can install from Connect › Apps without configuration or secrets): merchant KYB onboarding, individual KYC with documents, sanctions and PEP escalation, seller and payout screening, supplier due diligence and clinician credentialing; they import anywhere, and run once those apps are installed. The other ten call no app, so they run in an empty workspace as they are; where a real policy would ask an app (an identity check, a classifier, a link scanner), the rule reads a field your systems write on the subject, and the pack's page names the app to build. Every pack but one uses keys with its own prefix (kyc_, velocity_, screening_, geo_, payout_, gig_, supplier_, tenant_, cred_, loan_, claim_, grant_, report_, phish_, play_), so any of them load into one tenant together and none collides with the seed. merchant-kyb-onboarding is generated from the seed (pnpm policy:pack:seed, checked by a test) and intentionally shares its keys: importing it re-creates the seeded definitions as new draft versions.
How the packs are kept honest
Each pack directory carries an expect.json: a fixture subject, optional events to ingest, optional collection-flow data with an optional document fixture, the case the run is expected to open and how to decide it, and the expected final run status, decision and rule outcomes. pnpm policies:check (scripts/policies-check.sh) imports every pack with --publish, drives the fixture through the API, asserts the expectations, then exports the pack again and compares it with the original after normalisation (@aletheia-dev/definitions). The mock apps cannot call themselves back, so while a run waits on one of them the check plays its vendor and posts the signed webhook (App catalogue). CI runs it in the end-to-end job right after the smoke, so a change to the engine that breaks an example fails the build.
json
{
"workflowKey": "geo-risk-review",
"subject": { "kind": "merchant", "data": { "country": "AQ", "geoRiskScore": 20 } },
"events": [{ "type": "payment", "amount": 1500, "currency": "USD", "minutesAgo": 10 }],
"submission": {
"currentStepId": "document",
"data": { "fullName": "Jane Doe" },
"document": {
"field": "identityDocument",
"file": "examples/policies/individual-kyc-with-documents/passport-valid.png"
}
},
"case": { "caseType": "geo_review", "decide": { "outcome": "approve" } },
"expect": {
"runStatus": "completed",
"decision": { "outcome": "approve", "source": "manual" },
"rules": { "geo_country_under_review": "fail" },
"context": { "rules.riskScore": 80 }
}
}events, submission, case, expect.rules and expect.context are optional; minutesAgo places an event relative to the run. A document is uploaded as the applicant through the presigned upload, finalised and scanned before the submission is sent.
Writing your own pack
Start from the pack closest to your policy, export the definitions you tuned in the admin console (pnpm policy:export), or write the JSON by hand against the schemas. Keep keys distinct from those of other packs you may load into the same tenant, keep the mock apps while the pack is an example, and add an expect.json so the check can run it. A pack is the right unit for a review: a pull request that changes a threshold shows the diff in one file, and the check proves the example still behaves as its page says.