Worked example · a second workload, not software

"I want agents to manage my finances."

Follow the same eight questions, in order, that turn that sentence into a declared workload: one mechanism per answer, then the files that declare it, then the commands that run it with the CLI. The point of doing this with money instead of code: none of the 's own logic had to change to support a household ledger. Only new declaration files did. Everything here runs today, scripted, under the project's CI, and the screenshots are the 's read-out of the result.

Vocabulary — for this page specifically Full glossary ↗
workload
A declared set of unit types for one kind of job. This page's job is a household ledger, not code.
capability profile
A bounded grant of tools and writable paths — the bookkeeper can write the ledger, never the bank statement.
work contract
What a unit is authorized to decide, split into fixed, delegated, and unresolved.
obligation
A signal that stays open until a person or S3 actually resolves it — an unknown merchant, say.
consent
An explicit yes to something irreversible. Silence is never read as consent.
closeout gate
The check that re-runs the ledger's own tests before trusting any unit's report.
Before you start
Time~15 minutes to read straight through; a few more if you run step 3's commands yourself.
PrerequisitesNone beyond curiosity to read it. To run it, packages/regulator built and regulator on the path (OPERATING.md). Otherwise, just follow along.
What you'll have at the endA second workload, a household ledger, designed, declared, driven through five units, and then read back in the control room.
The goal

What these agents actually do, month to month

Strip away the control-system vocabulary for a moment. Once a month, this system is supposed to:

  1. Read the bank's statement: a CSV export the bank produces, dropped into a folder by hand.
  2. Sort every transaction into a category: groceries, rent, utilities, each taken from a fixed list the agent may not extend.
  3. Check the math: the categorized ledger has to add up to exactly what the bank statement says, row for row.
  4. Write a plain-English report: a short monthly summary a person can actually read.
  5. Draft a payment, then stop, e.g. "pay the landlord $1,800 on the 5th", and wait for a person to say yes before anything is final. It never sends money on its own.

The rest of this page builds that system for real, in five steps: design it as a control system first, declare it as files, run it end to end, read the results back, and check it against what it honestly doesn't do yet.

Why this domain

A ledger is the mechanism hierarchy in miniature

The course builds regulator against a software repository because that is where agents are most common today. But the control plane is generic over workloads, and the fastest way to see which parts of the design are yours to make is to put a different domain under it.

A source of truth outside the harnessThe bank's statement. The harness reads it and must never write it. When the ledger and the statement disagree, the ledger is wrong.
Exactly one action that can hurtA payment. Consent has a concrete object, and "silence is not consent" has a concrete test.
Judgment on top of arithmeticWhich category a merchant gets is judgment. Whether the month balances is arithmetic, and can be checked without believing anyone.
The rule that makes it workNothing reads its own progress off the ledger. Whether August is closed is execution state; what the ledger contains is domain state; what was verified is regulatory state.
Step 1 of 5 · Design first

Eight questions, and the level each answer sits at

For each function of the Viable System Model, ask the regulatory question it exists to answer, and answer it with a mechanism at the highest level of the hierarchy that fits (type → gate → typed tool → judgment → prompt), with a reason whenever it is not the top. "The agent will be careful" is not an answer.

Domain
What is the output, and where is the truth?Output: ledger/<period>.csv, reports/<period>.md, payments/pending/*.json. Truth: statements/, protected. The person's record: payments/executed/, protected. The domain's own checks: test/ledger.test.js, which check that every category is in the taxonomy, every period balances to its statement row for row and in total, a report's totals are the ledger's, and a pending payment is well formed.
What are the units of work, and what may each touch?Six unit types, not one "finance agent" (ingest, categorize, reconcile, report, prepare-payment, close), under two that are grants over declared tool effects. The bookkeeper writes under ledger/, reports/, payments/pending/ and gets no shell, so the grant is complete at the tool boundary. The auditor is read-only by declared effect. lib/ and test/, the checks that will judge a unit, are under nobody's write grant.
What stops units tripping over each other?Unchanged from the lab: a lease per worktree, one period per unit, by the orchestrator, and the thrash detector, which reads a category that flips between attempts as a coordination signal.
What is a unit told; what is fixed, delegated, unresolved?A per unit. For categorize: fixed: the statement is not edited, the taxonomy is not extended; delegated: each row's category within the taxonomy, July as precedent, every new merchant reported; unresolved: UNKNOWN MERCHANT 8831, a 120.00 debit nobody can explain: defer. The unit does not guess, and the close carries it as an open item. Budgets and routes per unit type sit in a policy file.
How do we know the close is right without believing the unit?The host runs the ledger's own suite against the committed revision and binds the result to unit, attempt, contract and revision. The report's claims satisfy nothing. identity-untouched diffs the protected prefixes against the base at closeout, so an edit committed around a gate is a failing check. glossary-lint reads the branch for the identity's refused words. inherited-tests runs the suite the unit inherited against the unit's tree, so a test cannot be deleted or weakened to make a close look right. After every merge the base is checked again, and a failure there holds the whole instance.
What in the environment matters?A statement arriving, a bill due, the tax calendar. None of it is automated here: the person drops the file in and dispatches the close. When it is, it is a research unit or the watcher raising an : intelligence starts a loop, and is never itself a decision.
What must the system never be able to do?Write the statement (outside the grant in the session; --protected statements/ at closeout). Write a payment file without a person's yes (ask_human with kind=consent opens a blocking obligation; the pause gate refuses every tool with an effect until it is answered; silence is never , by protocol constant). Write payments/executed/ (as the statement). In your own definition these are INV-005…007 in the identity, seeded into every instance and cited by contracts. The lab's identity is the software harness's, so the contracts here cite the person who fixed them, and the mechanisms are the same.
When must a person be interrupted?Once: before a payment file exists. The unknown merchant is not an interrupt. It is an open item, owed to a person as an obligation, delivered to the outbox, reminded, and it stops nothing. A question that waits and a question that does not are different kinds, and the kind is a typed field.
RuleLevelWhy not higher
a category is from the taxonomy2 · checka type cannot see the file; the ledger's test can
the ledger balances to the statement2 · check—
the statement is never edited2 · gate + check—
the report's totals are the ledger's2 · check—
consent before a payment3 · typed tool + 2 · gatewhether an action needs consent is still the model's to notice (DEBT row 29); an effect-declared pay tool would let a gate derive it
which category a merchant gets4 · judgmentit is judgment; the contract bounds it and the report records the choice
don't guess the unknown merchant4 · judgmentbounded by an unresolved decision; that the report carries every holding row is level 2
"run the tests before you report"5 · adviceadvice is advice

In the docs: The worked example, in full

Step 2 of 5 · The declaration

A control plane is declared, not assembled

This is the whole implementation. Nothing in the orchestrator changed for it except one line: the CLI now loads the workload a contract names. Each file is in packages/regulator and checked by regulator check.

First, the domain. This already exists as a fixture, so there's nothing to invent: a taxonomy, a protected statement, a ledger, and the checks that will judge every unit's work, sitting outside every write grant.

household-ledger/                the domain: packages/regulator/fixture-finance
  categories.json               the taxonomy (the person's)
  statements/                   the bank's exports — protected
    2026-08-checking.csv/.json
  ledger/2026-07.csv            July, closed and categorized
  reports/2026-07.md            July's close
  payments/pending/             a file here is a commitment
  payments/executed/            what was paid — protected
  lib/ledger.js  test/          the ledger's own checks —
                                outside every write grant
  package.json                  scripts.test = node --test

Next, the capability profile: a grant over declared tool effects, not a persona. The bookkeeper gets no shell, so the grant is complete at the tool boundary. The advice is guidance for judgment calls. Enforcement is the two decisions above it.

profiles/bookkeeper.json
{
  "tools": ["read", "write", "edit", "grep", "find", "ls",
            "read_conventions", "run_tests", "run_checks",
            "remember", "ask_human"],        ← no bash
  "writablePaths": ["ledger/", "reports/", "payments/pending/"],
  "advice": [
    "…the statement is the bank's and is never edited…",
    "…a merchant you cannot explain stays in the holding
     category and is surfaced; do not guess.",
    "Writing under payments/pending/ is a commitment: ask_human
     with kind=consent … write nothing without a yes."
  ]
}

Then, the workload: six unit types, naming the profile each runs under and the checks the host will run at closeout. This is the one thing the orchestrator didn't already know how to do: load a workload other than the software one.

workload/personal-finance.json
{
  "name": "personal-finance", "version": 1,
  "unitTypes": [
    { "name": "ingest",     "profile": "bookkeeper",
      "checks": ["run_checks", "run_tests", "inherited-tests",
                 "identity-untouched", "glossary-lint"] },
    { "name": "categorize", "profile": "bookkeeper", "checks": [same] },
    { "name": "reconcile",  "profile": "auditor",
      "checks": ["run_tests", "inherited-tests", "identity-untouched"] },
    { "name": "report",     "profile": "bookkeeper", "checks": [same] },
    { "name": "prepare-payment", "profile": "bookkeeper", "checks": [same] },
    { "name": "close",      "profile": "auditor", "checks": [] }
  ]
}

Finally, a contract: one per unit, and here's the one that touches money. The fixed decision below is the whole of INV-006 in miniature, cited to a person instead of an invariant file because this fixture has no identity fork of its own yet.

contracts/finance/f5-prepare-payment.json
{
  "unitType": "prepare-payment",
  "objective": "Prepare September's rent … after a person consents.",
  "fixed": [
    { "id": "f-consent",
      "decision": "No file is written under payments/pending/
        until a person answers yes to a consent question that
        names the payee, the amount and the due date.
        Silence is not consent.",
      "authorityRef": "human:alice" }      ← INV-006 in your fork
  ],
  "expectedEvidence": [
    { "class": "test",    "required": true,
      "description": "run_tests: the ledger's suite passes" },
    { "class": "command", "required": true,
      "description": "run_checks: protected prefixes untouched" }
  ]
}

Three more files exist in the real fixture, not shown above:

A shortcut this example takes This fixture borrows the software project's identity, so its fixed decisions cite a person (human:alice) instead of a written rule. If you built a standalone finance harness of your own, you'd instead write three permanent invariants into identity/INVARIANTS.md: INV-005 (the bank statement is never edited), INV-006 (nothing is committed to without consent), INV-007 (what was actually paid is the person's record, not a unit's). Every contract would then cite the invariant instead of naming Alice. The mechanism enforcing the rule is identical either way. Only the paperwork changes.
Step 3 of 5 · Running it

Init, doctor, five units, one question

With the declaration from step 2 in place, this is every command from a bare fixture to a closed month, in copy-paste order. Run it against the real fixture, or just read it: each line is annotated with what it does and why.

regulator fixture ~/ledger --finance
cd ~/ledger
pnpm add -D @metacoding.io/regulator @metacoding.io/regulator-pi   # the control plane and the Pi host, installed in the instance
regulator init --writable ledger/,reports/,payments/pending/ \
               --protected statements/,payments/executed/ --by alice
regulator doctor                                        # exit 1 on any problem

R=node_modules/@metacoding.io/regulator; L=$R/contracts/finance; P=$R/policies/finance.json   # the installed package's definition
regulator unit drive $L/f1-ingest.json --policy $P
regulator unit drive $L/f2-categorize.json --policy $P
regulator unit drive $L/f3-reconcile.json --policy $P
regulator unit drive $L/f4-report.json --policy $P
regulator unit drive $L/f5-prepare-payment.json --policy $P   # asks; nobody answers; pauses
regulator obligations                                   # consent: September rent — owed to a person, veto
regulator answer <obligation> --by alice --answer yes
regulator unit drive $L/f5-prepare-payment.json --policy $P   # the next attempt carries the answer
regulator-control-room --definition $R --instance ~/ledger   # @metacoding.io/regulator-control-room
  1. f1–f4The first four units succeed — but not on their own word. For each one, the host itself re-runs the ledger's real test suite, plus the suite as it stood before the unit started, so a test the unit deleted or weakened counts against it. It re-checks that no protected file (like the bank statement) changed, and scans for words the project has banned. All of this runs against the exact code the unit committed, not a summary of it. Before each unit's work is merged in, the host tries the merge in a scratch copy and runs the tests on the combined result. Only a result that passes lands, so the shared ledger is never left broken by a merge.
  2. Open itemOne transaction is left alone, on purpose. A $120 debit from a merchant nobody recognizes doesn't get a guessed category. The categorize unit marks it uncategorized and flags it in its report as unresolved. The ledger still balances to the bank statement byte for byte, and that one row is visibly unresolved instead of silently wrong.
  3. f5The fifth unit — the one that would pay rent — stops and asks first. Before it can write anything, it has to get a yes or no from a person ("pay the landlord $1,800 by the 5th?"). Nobody's answered yet, so it doesn't fail. It pauses, and the question sits in an outbox where a person will eventually see it. Nothing gets written until someone says yes. Once alice answers, re-running the same unit finishes the job and writes the payment file.
  4. CarelessNow try it with a version that cuts corners. This "careless" stand-in edits the bank statement directly and invents a category instead of using the real list. Its first attempt at every unit gets rejected: the tests fail, and a separate check independently notices the statement changed at all. The system automatically retries with a corrected attempt, which succeeds. The bad edit never makes it into the real, merged result. It gets caught and discarded.
  5. MemorySomething worth remembering gets remembered — narrowly. Along the way, the categorize unit learns "GREENGROCER 114 is the weekly grocery run" and saves it. But that fact is only ever shown back to the two unit types that need it (categorize and reconcile). A unit doing something else, like preparing a payment, never sees it.
Checkpoint If you ran the commands above, you now have: a household ledger instance with five closed units and a sixth question answered; a merged ledger the bank's statement agrees with, byte for byte; one open item nobody guessed at; and a payments/pending/ file that only exists because a named person said yes. Nothing above required touching the orchestrator's code.

Without a model, packages/regulator/src/finance.test.ts drives exactly this with the scripted bookkeeper and careless units under pnpm check. The screenshots below are the control room over that instance.

In the docs: Getting started · Operating

Step 4 of 5 · The control room

The same records, read back

The control room is a read-only dashboard: a web page that shows you everything the system recorded about the run in step 3, without being able to change any of it. Below are three real screenshots of it, each with a plain-language breakdown of what's on screen.

The control room's instances view for the household ledger: the manifest line, five units with status, attempts, routing and audit revision, the live lease, the open consent obligation marked veto, the interaction row and the scoped memory entry.
The overview: everything about this run, at a glance
  • Top: which ledger this is, which code revision ran it, and who set it up.
  • A row per unit: four say closed (done and independently verified), one says paused, waiting on a person.
  • One open question, front and center: "can we pay the rent?", flagged as blocking until someone answers it.
  • One remembered fact (the grocery-store categorization from earlier), tagged with a date to double-check it's still true.

The next two screenshots both drill into a single unit's full history. It is the same view applied to two different outcomes, so you can see both paths through it: one unit that's waiting on a person, and one that made a mistake and got corrected automatically.

The unit inspector for f5-prepare-payment: the paused attempt, the consent obligation, the replay from contract to delivery, an empty audit, the contract with its fixed decisions, and the result report with its residual uncertainty.
Drill-down #1: the unit that's waiting on a person
  • This is the payment unit from step 3, the one that stopped to ask "pay the landlord?"
  • Its one and only attempt is marked paused, not failed. It is waiting on an answer.
  • Below that: the exact question it asked, and who it's waiting on to answer.
  • Further down: a timestamped log of everything that's happened to this unit so far, in order.
  • There's no verification section here yet, because the unit hasn't done anything that needs checking.
The unit inspector for f1-ingest run careless: attempt 1 refused with the failing checks named, the repair decision and its hint, the replay with each host check and its observation, and attempt 2 closed, with the pre-merge trial's checks on the merged tree bound to the commit that landed.
Drill-down #2: the unit that made a mistake, then fixed it
  • This is the "careless" run from step 3, the one that edited the bank statement by mistake.
  • Its first attempt is marked failed, with the specific tests and checks that caught it named outright.
  • Next: the system's decision to try again, plus a hint telling the retry exactly what went wrong.
  • Its second attempt is marked closed. This time everything passed, including one final check run after merging.

Two more views exist in the same dashboard, shown smaller here since they're less central to this walkthrough:

The definition's workloads: personal-finance and software-development, each unit type with its profile and checks.
Every kind of job the system knows how to run. This household ledger and the software-development one from the rest of the course, side by side: proof the same engine runs both.
The definition's five profiles with their tools, writable prefixes and thinking level.
Every permission grant the system knows about. What each profile (bookkeeper, auditor, and the software ones) is actually allowed to touch.

More of this same dashboard (the full topology, the assurance and lifecycle views, a live instance under drift) is shown on the product page.

In the docs: The control room

Step 5 of 5 · What it does not do

The honest limits of this example

Four ways this walkthrough is simpler than a real deployment would be, each stated outright.

It's a script pretending to be an assistant, not a live AIEvery unit above was completed by a small stand-in program (a "bookkeeper" and a "careless" one), not a real language model making judgment calls. That's on purpose: this exact scenario replays identically every time in the project's automated tests. The safety mechanisms that matter for a real AI session (limiting what it can touch, gating writes, pausing on a question) exist and are tested elsewhere. A live run with an actual model is the next drill.
Nothing automatically knows a payment is riskyThe system has no built-in sense that "write a payment file" is dangerous. The profile's written guidance tells the assistant to treat it that way, and the assistant follows it. A stricter version would tag the "pay" action itself as irreversible, so a gate could enforce the pause automatically instead of relying on the assistant noticing. This example skips that on purpose: it has no tool that can actually move money, so there's nothing real at stake if it got this wrong.
The safety rules are tied to a person's name, not a permanent policyA standalone system would write its rules into a permanent policy file and have every rule cite that file. This example borrows the software project's setup instead, so its rules just say "because Alice said so." The mechanism enforcing the rule is identical either way (see the callout in step 2); only the paperwork differs.
Nobody's watching for new statements, and open questions stay openA person still has to manually drop the bank statement in and kick off the process. Nothing watches for a new file to arrive on its own. And that unrecognized $120 charge from step 3? It stays flagged forever in this example, because nothing here ever resolves it. A fuller build could add a step that automatically asks a person to identify it.
Next · Your domain

The recipe

You've now followed a household ledger end to end: designed, declared, run, and read back. Here are the same eight moves, generalized, for whatever you're actually building.

  1. Name the domain output and the source of truth. Decide what is protected.
  2. List the unit types. For each, decide what it may write, which tools by effect, and which host-run checks judge it. Put the domain's own checks outside every write grant. Where a unit must be allowed to write tests, declare inherited-tests so the suite that judges it is the one it inherited.
  3. Write the checks. If the domain has no deterministic check of its own, you are about to build a system whose only verification is self-report. Stop and write one.
  4. Write the identity: the invariants, each with the mechanism that checks it, and the words the instance does not use.
  5. Write one contract per unit of the first piece of work: fixed, delegated, unresolved, expected evidence. Cite an invariant or a person for every fixed decision.
  6. Set budgets and routes per unit type. Decide who may answer what.
  7. regulator init with the declared prefixes, then doctor, then drive the first unit and read the evidence, not the summary.
  8. Script the close (a bookkeeper and a careless one) so it runs under CI, then run it live and compare. That is the eval harness pointed at your domain.

Nothing on this page was hypothetical, and neither is your domain's version of it. The fixture and every file in step 2 are real, checked-in code. Start by reading them, not from a blank file.

In the docs: The worked example, in full · Definition files