# Stage 4 of enterprise AI adoption: write and send behind approval

> Source: <https://dev.to/williamchiu/stage-4-of-enterprise-ai-adoption-write-and-send-behind-approval-1a61>
> Published: 2026-09-20 11:25:12+00:00

This is part 4 of a series that started with the full reference diagram in [part 0](https://williamlab.dev/articles/enterprise-ai-adoption-reference-architecture), covered identity and the gateway in [part 1](https://williamlab.dev/articles/enterprise-ai-adoption-identity-gateway), data and retrieval in [part 2](https://williamlab.dev/articles/enterprise-ai-adoption-data-retrieval), and sandboxed agents with read-only connectors in [part 3](https://williamlab.dev/articles/enterprise-ai-adoption-agents-read-only). Stage 4 covers the boxes marked 4 on that diagram: Write connectors and Send connectors inside AGENTS & TOOLS, and the dashed Human approval box, labeled "send & write need a person." On the diagram the approval arrows run to the write and send connectors only. Read connectors are not behind them, because reading was the work of stage 3. What changes at this stage is the nature of a mistake. Up to here a bad run left nothing outside the company; from here a record gets changed, or a message goes out and cannot be pulled back.

The company is the same one as in parts 1 through 3, Qingchuan, about 40 people, 8 engineers and 2 in marketing. Qingchuan is made up and only serves to walk the stage. Coming out of stage 3, everyone logs in through the identity provider, every call carries a role through the gateway, questions over internal material return scoped answers, and an agent can run steps on its own inside a sandbox, reading the repo and the ticket system through read-only connectors, with a log of what it read that the agent cannot rewrite. Nothing Qingchuan's agents do yet is visible from outside the company.

The first box connects the agent runtime to internal systems it may change. At Qingchuan that means an agent that can edit files, commit, and push a branch in the internal tools repo, and one that can write a draft reply into a ticket.

The property that separates writing from sending is that the change stays inside systems the company controls, and there is a previous state to go back to. A bad commit has a diff, and recovering from it costs internal time rather than anything a customer sees. That is also why writing and reading are drawn as separate connectors rather than one connector with a flag: a connector scoped for reading the whole repo is a reasonable thing to have, and the same scope for writing is not. Done looks like this: each write connector names the systems it may change and what inside them, every write leaves a diff or a prior version a person can restore without asking the agent, and the write is attributable to an account issued for that workflow rather than to whoever first lent their own.

The second box is anything that leaves the company. At Qingchuan that is a reply going to the customer who filed the ticket, a post going out on a channel the company owns, and an email to a supplier.

There is no diff here. The question of whether the content was any good is a separate question from whether it should have gone out at all, and the second one is not answerable after the fact. My own rule on my own publishing, which is the closest thing I have to this box in production, is that each outward publish needs a person to say yes at that moment, not a batch authorization granted in advance. One consequence is easy to miss: an approval covers the action it was given for and does not extend to a neighboring one. In a recent publishing run two posts had been approved, and the composer for one of them came up behind a dialog asking to accept updated terms of service. Accepting terms is a different action from posting, so the run stopped there and waited for a person rather than clicking through. Done looks like this: the send connectors enumerate their channels and who receives them, every send carries the approval that released it, and an approval names one send rather than a class of sends.

The third box is drawn dashed, in a different color from the connectors, and it sits between the agent runtime and the two connectors above. The label says a person is required. The common way to build this box is to write the rule into the prompt: do not publish, do not merge, do not touch the customer records.

A prompt is a request. The model can misread it, drop it later in a long run, or talk itself past it while trying to be helpful. A denied action is not a request, and it has no second reading. In my own setup the refusal lives in the tool layer: pushing a branch goes through, while merging a pull request and deleting a remote branch are refused by the tool, not by a sentence telling the agent not to do them. I tested that rather than assuming it, and the practical effect is that the session prepares the pull request and I press merge myself.

Two things follow for a company. Every "the agent cannot do this" has to come with the place it is blocked: the operating system, the tool layer, or the account permissions on the platform itself. A rule that lives only in the prompt is a reminder, and the person who sets the real block is whoever administers accounts and tools, not whoever writes the prompt. The second thing is the size of the boundary. In my own runtime the enforced boundary is the file system, not everything the agent can reach; a sandboxed agent still talks to its model provider over the network, and that traffic is outside the box. Done looks like this: for each write and each send, a named enforcement point, and deleting every instruction about permissions from the prompt changes nothing about what the agent can do.

An approval nobody can retrieve afterwards only ever existed for the person who gave it. The minimum this stage leaves behind is four fields: which agent or which person acted, on what, when, and who approved it.

Mine is not there yet, and the gap is the useful part. My publishing log carries time, brand, article, language, title, platform, status, note, and URL, and the approver lives inside the free-text note, so answering who released a given post means reading sentences rather than querying a column. It should be a field. The reason to keep the record outside the acting tool at all is the failure I did have: a publishing key was bound to an old account, the script ran without an error, the drafts landed in that old account, and the account I was looking at showed zero articles. Nobody pasted anything wrong, and the tool reported success. The state of the world was only visible from a record kept somewhere other than the tool's own account. Done looks like this: the approval record is written by the system that performs the action, the approver is a field rather than prose, and the record survives independently of whatever the agent reports having done.

Run this on real traffic. Take one write and one send from the past week and produce, from records alone and without asking the agent, who approved each and when. If the answer comes back as a sentence someone has to read and interpret, the record is not a record yet.

The second half is a refusal test. In a test account, take a send that is supposed to be behind approval, remove the approval step, and try it. The action has to be refused by something, and you have to be able to name what refused it. If it goes through, the approval box was documentation, and every approval record it produced was describing a control that was not there. Until both halves pass with the system doing the recording and the refusing, stage 5 has nothing solid to govern: the audit ledger and the policy checks that arrive there are built on top of these records, and versioned policy and run-level halt are stage 5's work, not this stage's.

The AGENTS & TOOLS column is drawn the same on the self-hosted variant of the diagram, and hosting changes nothing at this stage: writing targets internal systems and sending targets external channels either way. Part 5 covers governance hardening, where declared policy gets compared against what actually happened, an audit ledger records which policy allowed an action at which version, and a run that goes out of bounds can be stopped while it is still running.
