Module · M4 · The control layer
The audit row you can defend
Lesson 17 of 21 · 8 min
The run is finished. Six months from now somebody asks why this customer was given what they were given. What do you show them?
If the answer is “the chat”, you have not deployed an agent — you have deployed exposure. The last node in the canvas exists so that the answer is a row on the account, readable by somebody who has never opened the builder.
The end of the run
After the human approves, one node resolves the record and one writes the note. Neither of them decides anything.
Scroll for all 12 steps →
| An audit row has to say | In this build |
|---|---|
| what was proposed | the proposal, in the execution log the row points at |
| what the check found | the reviewer’s score, in the note itself |
| who released it | the human’s decision, as the option they clicked |
| what it was about | the note is attached to the deal, so it is on the account, not in a log |
what was proposed
- In this build
- the proposal, in the execution log the row points at
what the check found
- In this build
- the reviewer’s score, in the note itself
who released it
- In this build
- the human’s decision, as the option they clicked
what it was about
- In this build
- the note is attached to the deal, so it is on the account, not in a log
This node was broken for the entire build, and nobody could see it
The first time this node ever executed, it failed. It had been carrying four independent defects for the whole build phase, and not one of them was visible, for a reason worth sitting with: the audit row sits behind the human gate, and no automated run had ever got past the gate.
| The defect | Why it stayed hidden |
|---|---|
| the record id was read from a tool the agent holds — and a tool is never a step, so it is not in scope | it resolved to an empty string, and was not even reported as a template that had been detected |
| the query it was reading from had no id column in it | it would have failed even with the first defect fixed |
| the obvious repair — have the reviewer carry the id — produced a fabricated identifier | it passes every shape check and attaches a decision to nothing |
| the account name was interpolated into SQL as raw text, not as a bound value | zero rows, reported as success |
the record id was read from a tool the agent holds — and a tool is never a step, so it is not in scope
- Why it stayed hidden
- it resolved to an empty string, and was not even reported as a template that had been detected
the query it was reading from had no id column in it
- Why it stayed hidden
- it would have failed even with the first defect fixed
the obvious repair — have the reviewer carry the id — produced a fabricated identifier
- Why it stayed hidden
- it passes every shape check and attaches a decision to nothing
the account name was interpolated into SQL as raw text, not as a bound value
- Why it stayed hidden
- zero rows, reported as success
Precision made it worse, not better. Told “copy the id column”, the model emitted the account name — wrong, and obviously wrong. Told “it is a UUID, 36 characters, NOT the name”, it emitted a perfectly formed UUID that matches no record in the system — wrong, and invisible. Tightening the wording did not improve accuracy; it improved the camouflage. You cannot prompt your way to a fact.
The repair, and why it is structural
The fix is not a better instruction. It is a node: after the gate, a database read takes the search term the reviewer carried, and returns the identifier. The model supplies the argument; the database supplies the fact.
A wrong search term returns zero rows and fails loudly. A wrong identifier fails silently, forever. That asymmetry is the whole reason to route facts through a lookup rather than through a prompt — the same principle as lesson 7, applied one layer further out.
With that step in place the run completed end to end and the note landed on the real account. It is short by design:
RESOLUTION EXECUTED. Reviewer score 5/5. Human decision: Approve.Keep the payload to short scalars. Interpolating the violations array — or any free agent prose — into the note’s JSON payload breaks the payload: the array arrives as raw JSON inside a quoted string, and prose arrives with quotes and newlines in it. Put the score in the note and leave the narrative in the execution log, which is already keeping it for you.
The lesson under the lesson
The node whose entire job is to prove what happened was the only broken node in the canvas — and it was broken precisely because it was the safest part of the system. The property that made it protected made it untested.
You have not tested the path you cannot reach. Before you ship a control layer, force a run all the way through the gate — approve one yourself — and watch the last node execute. Everything before the gate gets exercised a hundred times while you build. Everything after it gets exercised the first time it matters.

