Skip to content

Admin console: Define ​

Define is the admin console's authoring area: Rules, Rule sets, Workflows, Collection flows and Lists, each with a list page and an editor, plus a history page per definition and the approvals inbox under Operate. The editors are generated from JSON Schema the API serves, so a new step type, rule type or field type shows up in the console without UI work. This page documents those schema and validation endpoints, how the editors use them, and the version and approval routes every definition kind shares; the rule reference covers what each rule type evaluates.

Reading needs definitions:read (lists: lists:read), which analysts hold, so they see Define read-only. Editing needs definitions:write, publishing definitions:publish and approving definitions:approve, which only admins hold; backtests need backtests:run, which both roles hold. The integration role sees none of it.

Packages:

  • @aletheia-dev/core — StepTypeDescription, FieldTypeDescription, FormUiHints, DefinitionIssue, ValidationReport (builder.ts), and the step, field and flow schemas the descriptions are derived from.
  • @aletheia-dev/workflow-engine — validateWorkflowBody / validateWorkflowInput (pure, validate.ts) and stepOutputs, the context keys each step writes.
  • @aletheia-dev/collection-flow — validateFlowBody / validateFlowInput (pure, validate-flow.ts).
  • @aletheia-dev/definitions — the normalized diff behind the diff route.
  • apps/api — the catalogues (src/builder/step-types.ts, src/builder/field-types.ts), the schema and validation routes (src/routes/builder.ts; api.builder.* in the API client) and the version and approval routes (src/routes/governance.ts).
  • apps/admin-console — src/areas/define (rules, rule sets, lists, history, policy packs), src/areas/workflows and src/areas/flows.

Editing and publishing ​

Every definition kind works the same way:

  • Save updates the stored version in place while it is a draft (PATCH <prefix>/:key/versions/:version) and otherwise creates the next draft version (PUT <prefix>/:key); an older version opened with Restore as draft always becomes a new one. A version is fixed once approval is requested or it is published. While the stored version is a draft, edits also autosave in place two seconds after the last change, unless the editor's own checks or the server's validation report an error. Each version has a change note (one line, shown on the versions list) and records who last saved it (updatedBy). The rule, workflow and collection-flow editors also keep unsaved edits in the browser and offer to restore them on the next visit.
  • The publish control reads Publish → or, when the tenant requires approvals (or a publish answered 409 approval_required, or the user lacks definitions:publish), Request approval →, which takes a note for the approver. Unsaved edits are saved first. A publish refused for unpublished references lists every issue.
  • History opens the definition's history page.
  • Each editor has a form and a JSON view of the same body; both round-trip.

Rules ​

Define › Rules lists the latest version of every rule (GET /rule-definitions) with its type, severity, weight, version, status, where it is used (rule sets and workflows, from GET /definitions/usage) and its hits over the last 30 days (Activity), filtered by status, type, severity and tag. New rule → and Import policy pack need definitions:write. A rule's page (/define/rules/<key>) holds:

  • Definition: the key (new rules only: lowercase letters, digits, _ or -, fixed afterwards), name, type, severity (info, warn, block), weight, tags, enabled, the type's config and the description. The config form is generated from the type's configSchema (GET /rule-types) and uiHints: a list field becomes a list picker, an expression field the CEL editor with a cheat sheet (data, subject, subjectId, now, inList, hasPath, getPath, daysSince and the pitfalls). Changing the type reseeds the config from the schema's defaults. The Change note describes the version.
  • Test against a subject: a dry run of the saved version, and of the published one when it differs, in one call against a stored subject or inline JSON (POST /rule-definitions/:key/dry-run with versions; definitions:write or backtests:run, so analysts can run it), each result with a sentence explaining it. Apps are not called in a dry run (app rules come back skipped), and result details are redacted as a run's are for callers without runs:context.
  • Backtest (backtests:run): replays the saved version over a window of 1 to 180 days (30 by default) and up to 5000 subjects, optionally under a published rule set (POST /backtests, polled every 2 seconds), then shows outcome counts, changed outcomes, overlap with other rules and sample hits. Hits per day charts the last 15 days in the tenant's time zone: the hits every evaluation recorded, or, once a backtest of the saved version completed, its sample's recorded hits against the version's, day by day (GET /stats/rules/:key/hits). Previous backtests lists the rule's earlier runs.
  • Validation in the side panel: the editor's own checks, then POST /rule-definitions/validate (see rule and rule set issue codes), whose warnings, such as a list not changed in over 30 days, never block a save; and Used by.

See testing a rule before it decides anything.

Rule sets ​

Define › Rule sets lists every set with its use case, policy and rule count. A set's page edits its key, name, use case and description, the rules in evaluation order (each with enabled, a weight override and shadow; add, remove, move up and down) and the aggregation policy: Max severity, Sum of weights (clamp and contiguous bands, with Normalize bands) or First match (with a default outcome). The Simulation panel works from the draft without evaluating anything: each rule's effective weight, the highest possible score and, under Sum of weights, the band a given score falls in. The side panel validates the set with POST /rule-sets/validate: a rule that does not exist is an error, a rule without a published version a warning. See rule sets and aggregation.

Lists ​

Lists are live data, not versioned definitions: rules read the current entries. Define › Lists (lists:read) shows every list with its kind (string, email, country, ip_cidr, number) and entry count; New list → and Edit details set the key, name, kind, case-insensitive matching and description (PUT /lists/:key; lists:write). A list's page searches its entries on the server, 100 a page, adds entries (one value per line, optionally value,reason), imports a CSV (value,reason,expires_at; POST /lists/:key/import) and removes the selected entries. An expiry can only be set through the CSV import. See lists.

Workflows ​

Define › Workflows lists every workflow with its trigger, version, step count, runs per day and the share decided automatically over the last 30 days (Activity). New workflow → and Import policy pack (a pack file, imported as drafts) need definitions:write. The editor shows the definition as a Graph, as a list of Steps (reorder, delete) or as JSON, with Validate, Save, the publish control and History in the header.

  • Palette: the step types from GET /workflow-step-types; clicking one adds it below the selected step. Problems, below it, lists the validation issues; clicking one selects its step.
  • Properties: with no step selected, the workflow's key, name, description, trigger and entry step; with a step selected, its form (step id, name, next step and the type's own fields) or its JSON (Advanced), Delete step, Make entry step and the keys the step writes.

Step types ​

GET /workflow-step-types returns one entry per member of core's WorkflowStep union:

  • configSchema: the step's own fields as JSON Schema (z.toJSONSchema in input mode, so fields with defaults are optional). id, name, next and type are left out: the editor manages them itself.
  • dataPaths: the context paths the step may read, as hints for the path picker.
  • outputs: the context keys the step writes, with <outputKey> standing for the step's configured output key. Later steps' path pickers offer them.
  • terminal: the step ends the run and takes no next.
  • uiHints: field order, labels, help and pickers.
TypeReadsWritesPickers
evaluate_rulessubjectId, subject.*, submission.*, rules.*, <outputKey>.*<outputKey> (default rules)ruleKeys: rule, ruleSetKey: ruleSet
call_appsame<outputKey>, <outputKey>ExternalId (async calls)app: app, action: appAction, inputMapping: path (values)
wait_for_collectionnothing<outputKey> (default submission), …Id, …UrlflowKey: flow
create_casenothing<outputKey> (default manualDecision), …CaseIdnone
emit_decisionrules.*, <outputKey>.*nothing; terminalnone
branchsubjectId, subject.*, submission.*, rules.*, <outputKey>.*nothingconditions[].when: condition, conditions[].next: step, default: step

Picker keys are dotted paths into configSchema; [] steps into array items. The picker values are the ones FormUiHints documents: list, rule, ruleSet, flow, app, appAction, step, path, condition, cel.

evaluate_rules needs exactly one of ruleKeys and ruleSetKey. JSON Schema cannot express that refinement, so the form allows both; the validate endpoint reports it as a schema issue.

How the editors use the schemas ​

  • Schema-driven forms. Rule configs (/rule-types), step properties (/workflow-step-types) and app configurations (an install's config schema) render with @rjsf/core from the served JSON Schema; uiHints become the rjsf uiSchema (order, labels, help, widgets).
  • Pickers. Fields with a picker hint render a widget backed by the API instead of a text input: lists, rules, rule sets, flows, the tenant's installed apps (enabled ones first, the others marked "secrets missing" or "disabled") and the actions of the installed version, steps of the current workflow, and context paths. The path picker offers what the selected step can read (POST /workflow-definitions/context-paths on the draft, refreshed after edits), each with its type and the step that writes it, marked when only some paths to the step write it; the step type's dataPaths follow as hints.
  • Condition builder. condition fields edit a { path, op, value } with a path picker, an operator (equals, does not equal, greater than, at least, less than, at most, is one of, exists) and a value input typed by the operator.
  • CEL editor. cel fields use a CodeMirror editor.
  • Validation. The editors post the draft to the validate endpoint 400 ms after an edit and mark the steps and fields the issues name; errors block publishing, warnings are shown inline.

The visual editor ​

The graph is a projection of the body, not a second model: toGraph(body) in the console's graphModel.ts draws it, and every canvas edit (connect, delete, drag, add, auto layout) is a pure function from body to body, so the saved definition is always the plain WorkflowBody.

  • Nodes and edges. One node per step, with its type, an entry mark, a new or edited mark against the published version, its problem count and a type-specific summary (rule keys or rule set, app action, flow key, case type, decision, branch conditions). Edges come from next, from each branch conditions[].next (labelled with the condition) and from the branch default. Connecting two nodes sets next or a branch target; deleting a node leaves the edges that pointed at it dangling, which validation reports as dangling_next.
  • Positions. Node positions are stored in the definition under ui: { positions: { <stepId>: { x, y } } }. ui is optional and editor-only: the interpreter never reads it, PUT stores it as part of the definition, GET returns it and publishing keeps it. Seeded workflows carry no ui.
  • Auto layout. When a step has no stored position (every step of a seeded workflow), the whole graph is laid out with dagre, top to bottom; Auto layout does the same on demand. A save stores whatever the canvas shows.
  • Validation overlay. The console validates the draft without ui, since positions never affect validity; issues render as badges on the nodes their stepId names and in the Problems list, and publishing is blocked while errors remain. The API still checks a ui it receives: a position for a step id that no longer exists is a layout_unknown_step warning (path definition.ui.positions.<id>), never an error.
  • Versions. Layout is content: a save that only moves nodes saves the draft like any other change (a new draft version when the stored one is published) and has to be published to become the published layout. A tenant's layout-only draft of a seeded workflow is still a difference, so pnpm db:seed re-publishes the seed content over it exactly as it would over any tenant edit of a seeded key.
  • Simulation (dry-running a whole workflow against sample data) is deferred; rules keep their per-rule dry run.

Collection flows ​

Define › Collection flows lists every flow with its shape (steps and fields), version, the workflows whose wait_for_collection step uses it and, over the last 30 days, its completion rate and median time to submit (Activity), with Embed snippet and New flow →. The editor has a Form view, a JSON view (entryStepId, linkTtlSeconds, steps, presentation, outcome and translations) and a Drop-off view:

  • Steps: the steps in order with + Add; move up, move down and remove under each step's Rename; Branches from this step (conditions to other steps; the first match wins, then the default next step or the end of the flow); the Problems list.
  • Flow settings: key, name, description, the version's change note, Link expires after (hours, stored as linkTtlSeconds: one minute to 720 hours; empty takes the tenant's default from Settings › Tenant, seven days unless set), Applicant screens, Translations and the entry step.
  • Applicant screens (Edit screens): the welcome screen's heading, text and minutes (presentation.intro; empty, the flow's name, description and an estimate from its fields), the thank-you screen (presentation.outro), what applicants see after the submit (outcome.visibility: a thank-you only, where the application stands, or that and then the decision; unset, the deployment's COLLECTION_TERMINAL_SHOW_DECISION picks the last or the middle one) and the under-review, approved and not-approved screens (outcome.review, approved, rejected). Empty boxes keep the terminal's own words; text may hold [label](https://…) links.
  • Translations (Edit translations): the flow in other languages (translations, by language tag), one language at a time: its name and description, each step's title and description, each field's label, help, placeholder, unit, option labels and messages, and the screens' copy. Every box shows the flow's own words as its placeholder, and an empty box keeps them. The languages offered are the tenant's (Settings › Tenant › Languages; the first is the one the flow is written in); Add a language starts one the tenant does not list yet, which applicants see once it is added there. Renaming a step keeps its translations; a translation of a step, field or option the flow no longer has is a warning under Problems.
  • Fields of the selected step, added from GET /collection-field-types, and the field form: key, label, required, type, Visible when, help text, placeholder, options, length or value limits, pattern, and for files the accepted types and size limit (fixed), Takes several files with At most (multiple, maxFiles), how applicants add them (capture: the camera, their files, or either) and the Document they are (documentKind), which tells applicants what to take.
  • Live preview: the selected step on a mobile or desktop frame, rendered with StepForm from @aletheia-dev/collection-flow-ui (the collection terminal draws the same fields with its own components). Answers typed there are sample data shared by every step, so visibleWhen conditions can be tried out, and Continue checks them (files excepted). Branches are not followed and nothing is sent anywhere.
  • Preview as applicant (definitions:read) saves unsaved edits first, then opens the stored version in the collection terminal in a new tab (POST /collection-flows/:key/versions/:version/preview returns { url, expiresAt }; the link works for 15 minutes and its token sits in the URL fragment). The terminal walks the real steps, branches and checks with a preview banner: no subject, run or submission exists, nothing is saved or sent, file uploads are off and file fields are not required. Each link is audited as collection_flow.previewed.
  • Drop-off (definitions:read, a saved flow): where applicants got to over the last 7, 30 or 90 days, from the steps the collection terminal showed them (GET /stats/flows/:key/funnel, Activity): how many opened the flow and sent it, and per step how many reached it and how many stopped there without sending, with the uploads that failed per field and reason.

The field form does not edit unit and messages yet: set them through the API or a policy pack, and note that saving the flow from the console drops them.

Field types ​

GET /collection-field-types returns one entry per FieldType, with optionsSchema (what the field editor shows beyond key, label, required, placeholder, help and visibleWhen) and valueSchema (the shape the submission stores):

TypeOptionsValue
textvalidation: min/max length, pattern; autocomplete, inputModestring
emailautocompletestring (format: email)
numbervalidation: min/max; unit shown after the input, such as % or GBP; autocomplete, inputModenumber
selectoptions (at least one { value, label })string
dateautocomplete: bday or offstring (format: date)
booleannone; a required box must be checkedboolean
fileread-only limits: maxBytes (20 MB), contentTypes (JPEG, PNG, WebP, PDF); multiple, maxFiles, capture, documentKind{ fileId, name? }, or an array of them with multiple

Every type also takes messages, copy that replaces the collection terminal's generated errors without changing what is accepted: required when a required field is left unanswered, and invalid when the answer fails its format, length, range or pattern check (up to 200 characters each; boolean and file take only required). An empty string is no answer, whatever the type: an optional field takes it as unanswered and a required one asks for an answer.

autocomplete names what the applicant's browser may fill in (an HTML autocomplete token such as given-name, postal-code or tel, or off; no password or payment-card tokens), and inputMode the on-screen keyboard (text, numeric, decimal, tel, email, url, search). Unset, the terminal picks: the email keyboard and autofill for an email, the decimal keyboard for a number, and a birth date's autofill for a date whose key or label says so. The field form sets both under Autofill and Keyboard.

The embed snippet ​

Embed snippet gives a merchant's developer the two ways to embed a flow, with the deployment's collection terminal address filled in: a plain iframe, or the loader script:

html
<div id="aletheia-flow"></div>
<script src="https://collect.example/embed/aletheia-collect.js"></script>
<script>
  // `link` is the applicant link returned by POST /collection-submissions.
  AletheiaCollect.mount({
    container: '#aletheia-flow',
    link: link,
    onComplete: function (detail) {
      // detail.submissionId; detail.outcome once the decision is known
    },
  });
</script>

For admins the dialog also lists the tenant's allowed embed origins (Settings › Tenant). Embedding the collection flow documents the loader's options, the events and the origin checks.

Schema routes ​

RoutePermissionResponse
GET /rule-typesdefinitions:read{ items: RuleTypeDescription[] }
GET /workflow-step-typesdefinitions:read{ items: StepTypeDescription[] }
GET /collection-field-typesdefinitions:read{ items: FieldTypeDescription[] }
POST /workflow-definitions/validatedefinitions:readValidationReport; the body is a draft
POST /collection-flows/validatedefinitions:readValidationReport; the body is a draft
POST /rule-definitions/validatedefinitions:readValidationReport; the body is a draft
POST /rule-sets/validatedefinitions:readValidationReport; the body is a draft
POST /workflow-definitions/context-pathsdefinitions:read{ items: ContextPath[] } for { definition, stepId? }
GET /definitions/usage?kind=&key=definitions:read{ items: DefinitionUsage[] }

Validation ​

The validate endpoints take the same body as the matching PUT (an UpsertWorkflowDefinitionInput, UpsertCollectionFlowInput, UpsertRuleDefinitionInput or UpsertRuleSetInput), save nothing, and always answer 200 with a ValidationReport:

json
{
  "ok": false,
  "issues": [
    {
      "severity": "error",
      "path": "definition.steps.0.next",
      "stepId": "rules",
      "code": "dangling_next",
      "message": "step \"rules\" points to unknown step \"nowhere\""
    }
  ]
}

ok is false when any issue is an error; warnings never block. A body that fails the schema is not a 400: each Zod issue becomes a schema issue with its path, next to every graph and reference issue, so the editor can mark all of them at once. Paths are relative to the request body (definition.steps.<i>.…); stepId and fieldKey name the step and field when there is one. A PUT still enforces its own checks: validation is advice, the save path is the gate.

Workflow issue codes ​

CodeSeverityMeaning
schemaerrorThe body or a step does not match its schema (including the ruleKeys/ruleSetKey choice).
entry_missingerrorentryStepId names no step.
duplicate_step_iderrorTwo steps share an id.
dangling_nexterrorA next, branch conditions[].next or default names no step.
terminal_has_nexterrorAn emit_decision step has a next.
unknown_ruleerrorA ruleKeys entry names no rule (any version counts, as on PUT).
unknown_rule_seterrorruleSetKey names no rule set.
unknown_flowerrorflowKey names no collection flow.
unknown_apperrorapp names no app the tenant installed.
unknown_actionerrorThe app version the tenant installed has no such action.
app_disabledwarningThe app is installed but disabled: the step's calls fail until it is enabled.
unreachable_stepwarningNo path from the entry step reaches the step.
unknown_output_referencewarningAn inputMapping value or branch condition path starts with a key no earlier step writes on any path.
cyclewarningThe step closes a loop; legal, but a branch must eventually leave it.
layout_unknown_stepwarningA ui.positions key names no step (a stale editor position); ignored by the engine.

For unknown_output_reference, a path's first segment is accepted when it is subject, subjectId, submission or rules, or a key some step before it (on any path from the entry) writes: its outputKey or the derived <outputKey>Id, <outputKey>Url, <outputKey>CaseId, <outputKey>ExternalId.

Publishing still requires every referenced rule and rule set to be published; validation accepts drafts, like PUT.

Rule and rule set issue codes ​

CodeSeverityMeaning
schemaerrorThe body does not match its schema.
configerrorThe rule type rejects the config (path config.<field>).
unknown_listerror/warningA list lookup names a list that does not exist (an error, as on save); an expression's inList does (a warning).
list_stalewarningA list the rule reads has not changed in over 30 days: neither its details nor a new entry.
unknown_apperrorAn app rule names an app the tenant has not installed.
unknown_actionerrorThe app version the tenant installed has no such action.
async_actionerrorAn app rule names an action that answers through a webhook: a rule cannot wait for one.
app_disabledwarningThe app rule's app is installed but disabled.
rule_disabledwarningThe rule is disabled, so it is skipped wherever it runs.
unknown_ruleerrorA rule set entry names a rule with no version.
unpublished_rulewarningA rule set entry names a rule with no published version; publishing the set needs one.
no_live_ruleswarningEvery rule of the set is disabled or shadow, so the set always gives its policy's default.

Context paths ​

POST /workflow-definitions/context-paths takes a workflow definition (saved or not) and the reading stepId, and lists what that step can read: subjectId and subject, which every run starts with, then each path the steps that can run before it write, with its type, the step that writes it (writtenBy) and always (every path from the entry step to the reader runs the writer). Fields are typed where the API knows them: a rule aggregate (<outputKey>.outcome, riskScore, reasons, …), a manual decision, the fields of the latest version of a collection flow and an app action's declared output, from the version the tenant installed. Without stepId, every step's writes.

Usage ​

GET /definitions/usage lists, for every rule, rule set and collection flow another definition names, the rule sets and workflows that name it (usedBy: kind, key, version and status). The published version is linked when it names the definition, else the latest one; a workflow uses a rule directly or through a rule set it names. kind and key narrow the answer; with both, the item comes back even when nothing names it. The console's Used by columns and panels read it.

Activity ​

GET /stats/definitions?kind=&days= (definitions:read; kind is rule, workflow or collection_flow, days 1 to 90, default 30) counts what each key did over the window; keys without activity are absent.

  • rule: the results every evaluation recorded for the key, whatever version or set ran it: evaluated (passed, failed or errored), hits (failed, shadow results included: the rule fired) and errors.
  • workflow: the runs started: runs, volumePerDay, failed, decided (runs with a decision) and autoDecidedPct (of those, decided without a reviewer).
  • collection_flow: the submissions started: started, submitted, completionPct and medianDurationMs from start to submission.

GET /stats/flows/:key/funnel?days= (definitions:read) reads the events the collection terminal reports (POST /collection-submissions/:id/events: a step shown, an upload that failed; ids and codes only) for one flow, any version: opened (submissions shown a step), submitted (of those, sent), and per step reached and stoppedHere (whose last step it was and that are in progress or expired, so not sent), in the order of the latest version, then the steps only older versions had; and uploadFailures per field and reason (a refusal or rejection code, or the API's error code), most first.

GET /stats/rules/:key/hits?days=&compareVersion= (definitions:read) gives one rule's recorded results per day (every day of the window, zero when idle; days of the tenant's console.timeZone, else UTC, named in timeZone) and, with compareVersion, that version's latest completed backtest as backtest: its sample per day of the same zone, recordedHits (the result recorded at the time) against versionHits (the version's on the same evaluations); null when the version has none. A rule backtest records the per-day series in its summary.daily. Days are UTC: the tenant has no time zone yet.

Collection-flow issue codes ​

CodeSeverityMeaning
schemaerrorThe body, a step or a field does not match its schema; linkTtlSeconds outside 60 s to 30 days.
entry_missingerrorentryStepId names no step.
duplicate_step_iderrorTwo steps share an id.
duplicate_field_keyerrorA field key is used twice, in the same step or across steps (submission data is flat).
unknown_branch_targeterrorA next or branches[].next names no step.
select_without_optionserrorA select field has no options.
unreachable_stepwarningNo path from the entry step reaches the step.
visible_when_unknown_pathwarningA visibleWhen.path names no field answered earlier on some path or elsewhere in the same step.
file_field_in_first_step_requiredwarningThe entry step requires an upload; applicants tend to abandon flows that open with one.

Policy packs ​

A policy pack is one JSON file of definitions (Example policies). Import policy pack on the Rules page opens a dialog with three tabs: Starter packs lists the packs the deployment ships (GET /definitions/packs, the repository's examples/policies) and imports one with a click (POST /definitions/import?pack=<id>); Import file checks a pack file or pasted JSON in the browser and imports it (POST /definitions/import); with Publish after import, which needs definitions:publish, either publishes too. Export writes the published version of every published rule as one pack (GET /definitions/export). The same button on the Workflows page imports a pack file at once, as drafts. Export JSON on a history page exports that definition's published version.

Versions and approvals ​

Every definition kind (rules, rule sets, workflows, collection flows) has the same version history, diff and approval routes, under its prefix (/rule-definitions, /rule-sets, /workflow-definitions, /collection-flows; DEFINITION_PATHS in @aletheia-dev/core). The routes live in apps/api/src/routes/governance.ts, the /approvals routes in approvals.ts; the diff is @aletheia-dev/definitions.

RoutePermissionResult
GET <prefix>/:key/versionsdefinitions:read{ items: DefinitionVersionSummary[] }, newest first
GET <prefix>/:key/versions/:versiondefinitions:readthat version in full (any status)
PATCH <prefix>/:key/versions/:versiondefinitions:writethe draft saved in place; 409 once it is not a draft
GET <prefix>/:key/diff?from=&to=definitions:readDefinitionDiff
POST <prefix>/:key/versions/:version/request-approvaldefinitions:writeDefinitionApproval (201)
POST <prefix>/:key/versions/:version/approvedefinitions:approve{ approval, definition }
POST <prefix>/:key/versions/:version/rejectdefinitions:approve{ approval, definition }
POST <prefix>/:key/versions/:version/withdrawdefinitions:write{ approval, definition }
GET /approvals?status=&kind=&mine=&stale=definitions:read{ items: DefinitionApproval[], total } (the inbox)
GET /approvals/:iddefinitions:readApprovalDetail
GET /approvals/:id/commentsdefinitions:read{ items: ApprovalComment[] }, oldest first
POST /approvals/:id/commentsdefinitions:write or definitions:approveApprovalComment (201)

Request, approve, reject and withdraw take an optional { "comment": "..." } body.

History and diffs ​

GET .../versions lists every version with its status (draft, pending_approval, published, archived), name, change note, author (createdBy: the actor that PUT the version; updatedBy: the last to save the draft in place) and timestamps, plus the latest approval request for that version, if any. PATCH .../versions/:version takes the body PUT takes, with an optional note (an absent note keeps the stored one, a blank one clears it), and answers 409 once the version is no longer a draft.

The history page (/define/<kind>/<key>/history) lists the versions, each headed by its change note (else its name) with who last saved it, with Compare, Restore as draft, Request approval (drafts), Withdraw (your own pending request, or any as an admin) and, for approvers, Approve and Reject (hidden on your own request while four-eyes is on). Above the list, the diff between two versions (by default the published one and the latest) shows as a table of changes with their summaries, a tree or the raw delta (and for workflows the Steps view), with the approval of the newer version. Restore as draft reads the old version with GET .../versions/:version and opens the editor with it as unsaved edits; saving them PUTs the body back, which creates a new draft like any other edit.

GET .../diff compares two versions; to defaults to the latest version and from to the published one (or to - 1 when to is the published version or nothing is published). An unknown version is a 404. The result carries changes (one readable line per change) and the raw jsondiffpatch delta for tree renderers. A workflow's diff adds steps: every step of the newer version in its order, with the steps it removed where they stood, each with its op (added, removed, changed, moved, unchanged), its position on each side, one line per side (evaluate kyb-onboarding · {"outputKey":"rules"} → route) and its change summaries. The Steps view lists them with a +/− gutter.

Two versions are compared on a normalized document: name, description and the kind's own fields (rule: type, severity, weight, enabled, tags, config; rule set: useCase, definition; workflow: trigger, definition; collection flow: definition). Ids, keys, versions, statuses, authors and timestamps are ignored, and so is a workflow's editor layout (definition.ui): a version that only moves nodes diffs as "no changes".

Array items are matched by identity, not position: steps by id, fields by key, rule-set rules by ruleKey. A reordered step is reported as moved, never as removed and re-added. Items without an identity (score bands, branch conditions) are matched by position. Each change has an op (added, removed, changed, moved), a dotted path that uses identities instead of indexes (definition.steps.screen.next, definition.rules.pep_hit), a summary, and the before/after values where they apply. Summaries read like:

  • step "idv" added, step "idv" moved (position 4 -> 2)
  • step "screen" next: has_document -> rules, step "route" condition 1 next: review -> decide
  • field "identityDocument" added to step "review", step "ubo" field "uboOwnershipPct" validation.min: 0 -> 25
  • rule "sanctions_hit" severity warn -> block
  • rule set: rule "pep_hit" added, band 2 max 90 -> 85
  • name: KYB onboarding -> KYB onboarding v2
  • <path> changed / added / removed when a whole object or array changes and no better phrasing applies (e.g. definition.policy.bands removed when the policy mode changes).

Approval settings ​

Approvals are per tenant, in PUT /tenants/me/settings under approvals: { required, fourEyes, requiredFor }; the console edits them under Settings › Tenant:

  • required (default false): when on, POST <prefix>/:key/publish is refused with 409 approval_required (the message names the request-approval route) and the console shows Request approval → instead of Publish →. When off, publishing needs no approval, and a version can still be sent for one.
  • fourEyes (default true): the requester of an approval cannot approve it (403 four_eyes). Rejecting your own request is allowed but still needs definitions:approve; the requester withdraws it instead.
  • requiredFor (unset: every kind): the kinds approvals apply to, among workflow, rule, rule_set, collection_flow and app. With app, publishing an uploaded app version opens a request that another administrator decides with apps:publish (Apps: reference); Operate › Approvals shows those requests beside the definitions'.

Seeds and the smoke keep required: false; the smoke turns it on only for its own step.

The state machine ​

  • Request (draft -> pending_approval): only a draft can be requested (409 otherwise); one open request per version. Audited as definition.approval.requested.
  • Approve (pending_approval -> published): needs an open request (409 otherwise) and runs the same checks as publishing (a workflow or rule set may only reference published rules and sets); it closes the request as approved and publishes that version, archiving the previously published one. Audited as definition.approval.approved and <kind>.published (e.g. rule_definition.published, the same action a direct publish records).
  • Reject (pending_approval -> draft): needs an open request; closes it as rejected. The draft can be edited (which creates a new version) or requested again. Audited as definition.approval.rejected.
  • Withdraw (pending_approval -> draft): needs an open request, and the caller must be its requester or an admin (tenants:settings), otherwise 403 not_requester; closes it as withdrawn. Audited as definition.approval.withdrawn.
  • Discuss: requesters and approvers add messages to a request, open or decided, with POST /approvals/:id/comments ({ "body": "..." }, up to 4,000 characters). Messages are never edited or removed. Audited as definition.approval.commented.

definitions:approve belongs to the admin role; analysts can read histories, the integration role sees none of it.

The approvals inbox ​

GET /approvals lists open requests across all kinds, oldest first, or with status=decided the approved, rejected and withdrawn ones, newest decision first. kind narrows to one kind, mine=true to the caller's own requests and stale=true to requests made over a day ago; limit and offset page, and total counts every match. Operate › Approvals shows the views open, requested by me, stale and decided, filtered by kind, each read from the API; the rail counts the open requests, the caller's own and the stale ones. Each open request is a card with the requester and comment, the diff of the requested version against the published one, what it affects, the latest backtest and the eligible approvers, with Approve & publish and Reject (rejecting needs a comment, which tells the requester what to change) and Withdraw on your own requests. With four-eyes on, the console replaces the decision buttons on your own request with "You requested this". The tab and its count need definitions:approve or definitions:write.

GET /approvals/:id returns one request (ApprovalDetail) with what deciding it needs: the requested version in full, the published version's number, the diff from it (from the previous version when the requested one is published or nothing is), the latest completed backtest of a rule or workflow version, what the version names (dependencies: a rule set's rules; a workflow's rules, rule sets and collection flows), the published definitions that name its key (usage: the rule sets and workflows a new rule version reaches, the workflows of a rule set or flow), the directory's users who may approve it, each with eligible and why not (the requester under four-eyes), and the discussion. The console's request page (/approvals/:id) shows all of it, with the decision box, Withdraw… for the requester or an admin, Run a backtest for rules and workflows (backtests:run; a workflow version replays its rule steps, see Rules) and a comment box under the discussion.

Released under the Apache-2.0 License.