Field notes / Automation boundaries
Scripts do the deterministic steps
Where the model stops and the script starts, drawn twice in one day: a social publisher that refuses drift, and a contact importer that never lets a name reach a transcript.
copy_hash = sha256_copy(sections)
if copy_hash != str(approval.get("copy_sha256")):
set_failed(frontmatter, "copy changed after approval")In an earlier note I wrote that the model can propose facts but is never allowed to establish them. That was about a practice-plan pipeline. It turns out the same rule runs the company’s operations, and on September 7 I drew the line twice in one day.
The test I use is simple. If a step is a command with a checkable result, a script does it and a model must not. If a step needs judgment or touches the words a person will read, a model does it, and a script checks what it produced. The model is never the thing that presses the button.
A reply that only says “approve” never reaches a model.
The social publisher
The ruling that started it was mine, in plain words: materials get produced, I check each one and either approve it or send it back with edits, and some automation schedules and posts the approved items. I wanted exactly one point of contact for myself, and everything else automated.
The obvious design is an agent with the platform logins. The obvious design is wrong for the same reason a one-call practice plan was wrong: nothing between the approval and the post can prove that what went out is what I approved.
So the queue is the source of truth. One file per post, with a status line, the scheduled time, the platforms, and one section of exact copy per platform. Producers write drafts. My reply changes status. A script writes receipts.
- 01DraftModel
What enters the queue, the copy itself, and the per-platform variants. Judgment and voice. The brand checker must exit clean before an item is allowed into a packet.
- 02PacketScript
Renders the pending items into one review email and sends it. Deterministic: same queue, same packet.
- 03ApproveOwner
I reply in plain words. Approve some, send others back with an edit.
- 04Set statusScript
A reader turns the reply into status changes. Approval records who, which message, when, and a SHA-256 of the platform sections exactly as they stood.
- 05RedraftModel
Only an item that came back with an edit reaches a model again. It goes through the brand check and into the next packet.
- 06PublishScript
A scheduled job with no agent in it. Checks status, time, hash, and brand gate, posts through the official API, verifies the live object, writes the receipt.
Step 6 is the one that earns its place. At the scheduled time the publisher will refuse, and mark the item failed with a reason, if the status is not approved, if the approval record is missing its hash, if the hash of the copy sections does not match the hash recorded at approval, or if the brand checker no longer exits clean. Only then does it post, per platform, and it will not post a platform that already has a receipt. After a post it reads the live object back and writes the post id and URL into the item and into a receipts file.
The status machine is closed. A file can only move through the transitions the script knows about, and an approval without a name and a message id is rejected. The dry-run flag prints the planned request and touches nothing, which is how the build was accepted before any key existed.
What it forced
Approval is a hash, not a status. A status can be set by anyone who can edit a file. A hash of the exact copy, recorded at the moment of approval, is a fact the publisher can check against the file in front of it, with no opinion involved.
The split of who runs what fell out of the same test. Building and testing the script, checking tokens, validating the queue, rendering and sending the packet, flipping statuses from a parsed reply, installing the scheduled job, reading receipts back: all commands with exact expected output, all run by a coding agent from dated briefs. Deciding what enters the queue, writing copy, interpreting an edit request, the weekly read of what worked: the hub, with judgment. Posting at the scheduled time: the scheduler and the script, with nobody watching. Platform keys and approval replies: me, the account owner and the one touchpoint.
The contact importer
The same afternoon, a research model produced spreadsheets of band directors and private teachers in one state from public directories, for outreach that follows the existing consent rules. The spreadsheets had to land in the live contacts table.
The tempting shortcut is to let an agent read the sheet and write the rows. Then a few hundred names, schools, and email addresses are in a transcript, and the transcript is the one artifact I cannot scrub.
So the importer is a script with a narrow shape, and the model that launched it only ever saw counts.
- The sheet must have exactly the fifteen expected columns. A missing column or an extra one is a refusal, not a guess. Consent must literally read
noneon every row, because nothing in a public directory is consent. - The default is a dry run that prints counts only: rows, with email, without email, how many would merge and how many would insert. It does not read the environment and does not touch the network.
- When it does run for real, it reads the two keys it needs by grepping a named line out of the environment file and never prints them. No agent opens that file.
- Rows with an email merge on email. Rows without one are inserted only if no existing row has the same name and organization, so a second research pass cannot duplicate the first.
- The report that came back to me, and into the log, is counts: first pass 237 contacts live, second pass 126 directors and 108 teachers. Not one name.
What it forced
Research output is data, not instructions and not context. It goes from the researcher’s file to a validated import through a script, and the coordinating model reads the summary line. If a contact list has to pass through a model, the design is wrong.
The rule, stated once
Both systems are small. Neither is clever. What they share is a boundary that can be checked by a computer: a hash, a column list, a status transition table, an exit code. The model sits on one side of each boundary doing the part that needs a mind. The script sits on the other side doing the part that needs to be the same every time.
- Approval is a fact about content, not a flag. Record what was approved in a form a script can compare.
- The button is never a model. Posting, importing, sending: a script with no judgment in it, run by a scheduler.
- Personal data goes around the model, not through it. Models get counts. Scripts get rows.
- Dry run is the default. The build is accepted on a synthetic queue and a counts-only import before any key exists.
- Refuse with a reason. Every failure writes why into the item it failed on, so the next reader, human or agent, does not have to reconstruct it.
Honest status
If a step is a command with a checkable result, a script does it. If it needs a mind, a model does it and a script checks it.
Counts in this piece are from the build receipts and the worklog entries of September 7, 2026, and from the publisher and importer source.
This field note was drafted with AI assistance from those artifacts. I reviewed the claims against them.