Skip to content

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, and the installs they call, each with the app it runs. 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 Decision Intelligence (DI) Infrastructure. The policy packs demonstrate how hybrid deterministic rules (CEL, schema, regex, velocity, lists) and AI models (decide & generate via ChatGPT, Claude, Gemini, etc.) work together across enterprise domains—risk, fraud, customer operations, claims, spend approvals, and onboarding. The packs hold no jurisdiction-specific mandate and no fixed vendor lock-in. Their thresholds, weights, and criteria are starter defaults designed to be adapted to each team's precise policy.

The pack format ​

PolicyPack in @actanomos/aletheia-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": {} }]
  },
  "installs": { "chatgpt": "chatgpt" }
}

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. Keys within a group must be unique; the pack name is a slug like a definition key.

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 call installs by name, so the pack declares each name they use in installs, with the app it expects that install to run: { "screening": "http-api" }. The declaration holds the app's name only, no version and no configuration; the import checks the actions the pack calls against the version the tenant's install runs. The schema refuses a pack whose definitions call a name installs does not declare, with the issue at the step or rule that calls it; a declared name nothing calls is allowed, and a pack that calls no app leaves installs out or empty. At most 50 installs, as many as a tenant may have.

Import and export ​

Three routes; import and export are audited (definitions.imported and definitions.exported with the item counts and keys; see Audit):

RoutePermissionDoes
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, the declared installs) 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/packsdefinitions:readThe 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, item counts and installs.
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, with installs declaring each install they call; 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 }).

Installs at import and export ​

The import weighs every install the pack declares against the tenant's before it writes anything, and answers each one in installs: { name, app, status, actions, appAvailable, issues }, where actions are the actions the pack calls through it.

  • Present. The tenant has an install of that name, or a former name an install keeps as an alias after a rename, running the app the pack expects. The actions the pack calls are checked against the version it runs: one the version lacks (unknown_action), or an asynchronous one an app rule calls (async_action: a rule cannot wait for a vendor's webhook, a step can), is listed in the install's issues. Drafts are imported all the same; publish=true refuses the pack with these as items of kind install (actions.<action>), before anything is written.
  • Running another app. The import is refused with an item of kind install at app, such as install "screening" runs acme-screening; the pack expects http-api: the pack's definitions would otherwise call the wrong vendor. Call the install by another name in the pack, or declare the other app if it does the same job.
  • Missing. Definitions may name an install before it exists, so a missing install is reported, not refused, publish=true included; a run that reaches a step calling it fails with unknown_install until it exists. appAvailable says whether the tenant's catalog offers the app (a published version no operator block stops), so that an install of that name can be created. For chatgpt, the admin console offers Install chatgpt with a sample key in one click: it installs the platform's latest chatgpt release with an OpenAI connection (Responses API, adapter decide engine, storing off), approves OpenAI's hosts, and stores a placeholder openaiApiKey. Until an administrator replaces it with their own OpenAI key, OpenAI refuses the calls, so the pack's model steps fail their runs (with the vendor's refusal recorded on the invocation record and under the run's app calls) and its model rules send decisions to review. Otherwise, the console offers to create the install from its app's page, or to upload the app first when the catalog lacks it.

The definitions.imported audit event records the declarations as installs: [{ name, app }].

The export declares each install its definitions call, by the name they call it (an alias stays the alias), with the app the tenant's install runs. A definition calling an install the tenant does not have is a 409 unknown_install whose details.installs lists each missing name with the items calling it, since the pack could not say which app it runs: create the install, or leave out what calls it.

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-10

The import validates the pack locally with the core schema before sending it, which catches an install the definitions call without declaring it, and prints the API's per-item problems when the server refuses it. After the definitions it prints each install the pack declares: present (with what its version cannot do for the pack), or missing, with the install to create, or the app to upload first when the workspace's catalogue lacks it. The export writes <dir>/pack.json pretty-printed, with the installs declared. 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 ​

PackIndustryShows
Merchant KYB onboardingPayments & fintechThe development seed as a pack: a collection flow with an optional document, country and ownership rules in a weighted rule set, a model that reads the merchant's business description before a fast-track, a case
Individual KYC with documentsPayments & fintechA two-step flow with a required document upload, country, age and residence rules, a model that reads the applicant's account of their identity, and a case where a person checks the document
Transaction velocityPayments & fintechVelocity rules over ingested events (sum, count, filtered count, distinct count), a monitoring rule set, a model that triages alerts and drafts an analyst's narrative, a high-priority case
Sanctions and PEP escalationPayments & fintechA sanctions hit on the subject record that branches to a critical case before any rule runs, a model that briefs a reviewer from the subject's own words, and a scored review path for softer signals
High-risk geographyPayments & fintechAllow and block list lookups against tenant lists, a vendor-style score that contributes to the risk score, a model that reads trade descriptions before a fast-track, a review with an SLA
Seller and payout screeningMarketplaces & platformsTwo parties checked against one sanctions list: a listed seller is rejected, an unlisted seller screened by an app rule with ChatGPT, a listed payout account holder opens a review
Driver onboardingGig & on-demand workA phone-first flow with camera uploads, city, age and experience rules, an identity result your platform writes on the subject, a model that reads the driver's insurance before activation
Supplier due diligenceSupplier & third-party riskA questionnaire with a branch for software suppliers, a sanctions list check, a country risk list, a contract-value threshold, a model that checks supply-chain disclosures
Tenant screeningProperty & rentalsRent-to-income CEL rules, a branch between employer and accounts, a model that checks income statements, the same criteria applied to every applicant
Clinician credentialingHealthcare credentialingDate arithmetic in CEL for a license expiration, an exclusion list check, a model that reads the clinician's account of their practice, findings routed to a committee with a brief
Loan affordabilityLending & creditAn affordability CEL rule, an amount referral, repeat applications counted from events, an underwriting case with an AI affordability assessment rule
Claims triageInsuranceA claim form with a police-report branch for thefts, policy and early-claim rules, claim frequency from events, a model that reads the claim before a fast-track and routes it, an adjuster's brief
Grant eligibilityPublic sector & grantsAn eligibility formula as CEL, near-limit and missing-evidence referrals, a model that reads changes in circumstances before any automatic outcome, a caseworker's brief
Report triageTrust & safetyNo form: a classifier score on the subject, strike and report counts from events, a model that reads user reports, a one-hour moderation SLA
Phishing triageEmail & messagingNo form: scan and DMARC results on the sender, a reputation threshold, reports counted per sender address, a model that evaluates phishing indicators
Responsible gamingGaming & bettingDeposits summed from events, a loss threshold, accounts counted per device, a model that evaluates signs of harm, a responsible-gaming case
Refunds and returnsE-commerce & retailAn automated screening gate, return and order rules, a model that evaluates return reasons against policy, a customer support review
Support ticket triageCustomer operationsSpam classification, paying customer prioritization, ticket urgency rules, a model that categorizes customer issues and summarizes tickets
Lead qualificationSales & marketingMinimum team size and answers rules, a model that qualifies lead intent and fit, automated routing between sales and nurture
Spend approvalsCorporate spendCategory blocks and budget limits, a model that checks spend justification, policy compliance rules, an approval case with a manager note

Every pack calls one model app install named chatgpt (app chatgpt), declared in its installs: { "chatgpt": "chatgpt" }. The model assists the rules and human reviewers by reading free-text descriptions, assessing risk or circumstance changes, and drafting reviewer briefs. No model makes a final legal or compliance determination, and no model screens sanctions or PEP registries. Until an administrator stores a real OpenAI API key on the chatgpt install's page, the runs whose path calls the model fail (with the vendor's refusal recorded on the invocation record and under the run's app calls), and no case opens for those runs.

Every pack uses keys with its own prefix (kyc_, velocity_, screening_, geo_, payout_, gig_, supplier_, tenant_, cred_, loan_, claim_, grant_, report_, phish_, play_, lead_, refund_, spend_, ticket_), 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) with an AI step overlay: importing it re-creates the seeded definitions as new draft versions.

Adding a vendor step ​

A pack decides on the data it is given. To ask a vendor as well, install the platform's http-api app once for that vendor, add a call_app step where the pack's page says, before the step that evaluates the rules, and add a rule that reads the vendor's answer to the pack's rule set. For a screening vendor that takes POST /v2/screenings with an API key:

  1. The install. Connect › Apps › http-api › Add install, named screening: the vendor's endpoint https://api.screening.example/v2, whose host the save approves, an api-key auth whose value is <<apiKey>>, and apiKey declared a secret variable. Store the key's value on the install's page, then enable the install (App catalogue).

  2. The step, the run data the vendor needs mapped into the request's variables, with the previous step's next pointing at it:

    json
    {
      "id": "screen",
      "type": "call_app",
      "app": "screening",
      "action": "request",
      "input": {
        "endpoint": "/screenings",
        "method": "POST",
        "body": {
          "contentType": "application/json",
          "body": "{\"name\": \"<<name>>\", \"country\": \"<<country>>\"}"
        },
        "pick": ["hit", "matches"]
      },
      "inputMapping": {
        "variables.name": "submission.legalName",
        "variables.country": "submission.country"
      },
      "outputKey": "sanctions",
      "next": "rules"
    }
  3. The rule. The step's output is the vendor's status, headers and body, so its verdict is at sanctions.body.hit. A comparison that fails on a hit:

    json
    { "path": "sanctions.body.hit", "op": "eq", "value": true, "passWhen": false }
  4. The declaration. A pack that carries the step declares the install it calls, with its app, so a workspace importing the pack finds out whether its own screening runs http-api, or that it has none yet:

    json
    "installs": { "screening": "http-api" }

    pnpm policy:export writes the declaration from the install the workspace has.

The case a pack opens then carries the vendor's answer in its context. An app rule through the same install asks the vendor while the rules are evaluated instead, without a step (Rules). A vendor that answers later through a webhook takes a start step through the same kind of install once its webhook is set up, and one that takes the applicant's documents gets them in a multipart or binary body once the install's documents are on (App catalog; Documents shows such a step). A vendor that needs the applicant on its own page takes an app of your own (Apps: authoring).

How the packs are kept honest ​

Each pack directory carries an expect.json and one or more expect-<name>.json fixtures: 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, the expected final run status, decision and rule outcomes, and the expected model app calls (expect.calls). pnpm policies:check (scripts/policies-check.sh) starts a local stub server (scripts/ai-stub.mjs), creates the chatgpt model install against it, imports every pack with --publish, drives each fixture through the API, asserts the expectations, then exports the pack again and compares it with the original after normalization (@aletheia-dev/definitions), the installs the original calls included. Cue words in fixture data ([[stub:high]], [[stub:low]]) select high or low model responses on the stub wire. 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",
      "tradeDescription": "Antarctic supply [[stub:high]]",
      "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 },
    "calls": { "chatgpt/decide": 1 }
  }
}

events, submission, case, expect.rules, expect.context and expect.calls are optional; minutesAgo places an event relative to the run. A document is uploaded as the applicant through the presigned upload, finalized 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, and add an expect.json so the check can run it. A pack whose steps or rules call an app declares each install they call in installs, and runs only where an install of that name exists: the import says which are missing. The packs in this repository call one model app install, chatgpt, and a test checks that every pack declares it and conforms to its contract. 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.

Released under the Apache-2.0 License.