Teaching the system without a developer

The checks, the document types and the tolerances that make one firm different from another can now be added as data, tried before anyone relies on them, and kept as a record of what people decided. Five pictures: two kinds of shape, building a check, teaching a new document, the record of decisions, and what runs on its own.

1

Two kinds of shape

Everything the system checks against comes in two layers. The compiled layer is what I ship: the workflows, the document types, their rules and letters. The version layer is what you add on top: a rule from the builder, a drafted type, a tolerance for one client, a pack imported from another install. A version sits in front of the compiled shape until a date you chose, then steps aside. If it turns out to be wrong you do not wait for the date: withdraw it and the shape rolls back a step that moment. Nothing you add can break the engine, because the engine never changes.

Two blocks side by side: the compiled build on the left, marked compiled and always there; versions on the right, each marked version N until a date, sitting in front of the compiled shape and stepping aside either at that date or when you withdraw them.
Under the hood

The Definitions view (sidebar → Definitions) shows every workflow and document type as it resolves now. The badges: offered means members can run it and its rules are in the manifest; not offered means withdrawn, past runs kept, rules hidden; compiled means no version is in front of it; version N until date means a database version wins until that date; withdrawn on a document type means it is out of the pickers and the classifier. Rules carry error or warning, and one the live version added carries from version N, so it is visible what withdrawing would take away. Two actions live on the page because neither edits a definition: Withdraw version N archives the version being served and resolution falls back to the next live one or to the compiled shape, and Override in a rule’s Tolerance column sets how far apart two figures may be before that rule says anything, for the install or for one collection, with a reason and its own expiry. Export pack downloads the lot as a file; Import pack publishes every entry of such a file as the next version of its own, with one expiry, overwriting nothing (earlier versions stay, the compiled definition stays underneath). Versions live in workflow_definition_versions and extract_schema_definition_versions; the highest published, unexpired one wins; the compiled definition is the fallback for everything.

2

Building a check from a sentence

A senior writes what should agree with what. A bounded model turns it into one declared rule in one of five kinds, using only the fields the workflow already exposes; it cannot write code and cannot decide a verdict. The rule is tried on nineteen made-up engagements and on the client’s own last run, with nothing written, and then published at warning. A ratio or a percentage is refused, and the refusal says why. So is a check that already exists — two rules for one comparison is noise — which is why changing one is a button rather than a sentence: press Edit beside it and the same four steps become a replacement, keeping the rule’s id and its severity. Before you write anything, the page lists what this workflow already checks, so you can see what is there.

Four steps left to right: a sentence, one declared rule with its formula, a dry run over synthetic engagements and the client's last run, then publish at warning. Below, three red cards: a percentage is refused, a duplicate is refused naming the existing rule with a note that Edit is how you change it instead, an unknown path is refused with the closest real ones.
Under the hood

Definitions → Build a check. The proposal is forced by schema into reconcile, continuity, unique, rowReconcile or rowUnique; the same validator that gates an imported definition runs on the workflow with the rule appended. The dry run calls validateAssembled twice, with and without the rule, over evals/corpus/environments and over the collection’s latest run re-read through RLS. Publish goes through the shared publishDefinitionVersion with a mandatory expiry. Tolerances are absolute amounts, there is no subtraction (A less B equals C is written C plus B equals A), and the fire rate of a warning rule shows on the Feedback page before a person promotes it. An edit pins the rule’s id in three places rather than hinting at it: the brief says the rule is being revised, the prompt lifts its do-not-duplicate rule for that rule only, and the answer’s id is forced back — so a revision cannot land as a second check firing beside the one it was meant to replace. Asked instead to loosen a check, the model will say that a tolerance change is an override and not a new rule, which is right, and that is set from the Definitions view. evals/rule-builder.eval.ts gates the drafter in CI, with the duplicate refusal and the same rule named as an edit sitting side by side so the exemption cannot quietly cost the refusal.

3

Teaching it a new document

Pick three examples the client already sent, name the type, say which fields matter. The model reads them with the names already replaced and answers with a list of fields, never a schema; code builds the schema, so it is right by construction. Each example is extracted twice and only what both runs agree on can be confirmed, after you have reviewed it in the Verify tab. Publishing re-extracts every confirmed example and refuses a type that no longer reproduces them.

Four steps: three redacted examples, a list of fields, try twice then review and confirm, and a green gate that re-extracts the confirmed examples and refuses under ninety percent. Below, what happened the first time: a receipt type refused at eighty-five percent because a guessed unit price came and went between runs, fixed by wording the request, then published at forty-nine of forty-nine.
Under the hood

Definitions → Draft a type. The drafter returns a declaration (names, kinds, instructions, required, up to three repeated groups) and zodForProposal builds the Zod object and JSON Schema, proved to hydrate before a draft version row is written. A try persists the first run’s extract under the draft’s name, grounded, so the Verify tab and its correction route work unchanged; the second run exists only to be intersected with the first (stableValues). Confirm records the stable paths plus every corrected one in extract_schema_examples. Publish needs three confirmed examples and 90% of confirmed values recovered on a fresh run; the same check runs on a live type any time. The extraction model ignores temperature, so a value it has to infer is not stable between runs: when the gate names such a field, the fix is the wording of the request (say what to leave null), not the bar. A drafted type gets extraction, classification, grounding and review from its schema; a code check, a derive rule or a letter for it is still a deploy.

4

The record of decisions

The system used to keep only the latest state of what people decided. Now it keeps the decisions themselves: a finding accepted and later reopened, a run signed off and delivered, a letter sentence rewritten, a chase list sent and answered. The Feedback page reads them back, and that is the material the next tolerance, rule and prompt change is made from, by a person, with the evidence in front of them.

Four cards feeding one record: a finding settled, a run delivered, a letter rewritten, a chase list sent. The record is read on the Feedback page, and the next change is made from it by a person.
Under the hood

Append-only tables, one per decision: workflow_run_finding_reviews (acceptances carry forward by fingerprint, which deliberately excludes the definition version so a tolerance tweak does not orphan them), workflow_run_outcomes (signed off, delivered with the artefact, came back wrong with a reason, withdrawn; delivery never clears), workflow_run_report_edits (the redacted narrative before and after, keyed to the slot; a regeneration writes what it replaced as superseded), and workflow_job_messages (the chase list as sent: the classified gaps it asked for in clear, the recipient and body encrypted under the vault key, the reply as a person marks it, and the documents that arrived since counted at read time). The chase list itself is serialised by code from workflow_gaps: client gaps only, documents never figures, no internal vocabulary, the preparer’s gaps as an internal note. Notes are scrubbed of structured identifiers, not of names.

5

What runs on its own, and what never does

A job can now run itself: every Monday at seven, or on the second of the month, queued exactly as if you had pressed the button, on the budget of the person who set it. The exams run on every change, and the revalidation on every new document. But nothing reaches a client and nothing is published without a person’s click. The schedule runs the job; you do the rest.

Two panels: on the left, what runs on its own — the job on its schedule, the exams on every change, the revalidation on every new document; on the right, what happens only by a person's click — sending to a client, publishing a definition, promoting a rule to error, re-checking a type's golden set.
Under the hood

A Schedule card on the job page stores a cadence (daily, one weekday, or one day of the month, at an hour in the install zone) in workflow_schedules with the owner and the next due instant, computed across daylight saving in lib/workflows/schedules/cadence.ts. One Inngest cron every fifteen minutes queues due jobs with the owner as requester (queueJobs, then workflow/job.run); a busy job, an owner past their spend cap or a withdrawn workflow is skipped with the reason on the card, and the schedule advances to its next due time rather than retrying every quarter hour. The periodic tasks a person keeps: send the chase list after an incomplete run, sign off and mark delivered, read Feedback weekly, promote warning rules after a fortnight of data, re-check each bespoke type’s golden set monthly and after a model change, renew versions before they expire, export the pack before an instance sync.