Give an AI Agent Write Access One Verb at a Time

# ai# llm# architecture# automation
Give an AI Agent Write Access One Verb at a TimeNabeel Hassan

The first version of every agent I have worked on was read-only, and nobody was scared of it. It...

The first version of every agent I have worked on was read-only, and nobody was scared of it. It looked things up, it summarized, it drafted a reply. Then someone on the team asks the obvious question: if it already knows the answer, why is a human retyping it into the CRM?

That question is where the project quietly changes. An assistant that is wrong produces a bad sentence. An agent that is wrong produces a booked appointment, a sent text, a changed record or a refund. Same model, same prompt, completely different engineering problem.

This post is about the part that has nothing to do with the model: deciding which actions the agent is allowed to take, how you find out it did the wrong one, and how you hand over write access without betting anything on a demo.

Three levels, decided per action

There are three levels an agent can operate at, and the cheapest decision on the whole project is naming which one each action lives at.

  1. Read-only. It looks things up and tells a person. Nothing it produces touches a system until a human acts on it.
  2. Draft and wait. It composes the booking, the reply, the ledger entry, and a person clicks the button. The model does the work, the human keeps the authority.
  3. Act unsupervised. It does the thing, and you find out from a log.

The mistake I see most is treating this as a property of the product. "Is it autonomous?" is the wrong question. Useful systems mix all three, and the mix is decided per action.

The voice agents we built for CallGuard AI and CallSetter AI are a plain example. They book appointments on their own, because a calendar slot is cheap to move if it was wrong. Anything touching a price, a medical question or an upset caller goes to a person. That split is a design decision, not a limit of the technology.

Sort every verb by what it costs to undo

Before writing any tool code, write the list of verbs the agent can perform against named systems. Then sort that list into three buckets.

  • Free to undo. An internal note, a tag, a draft, a log entry, a field only your team reads.
  • Awkward to undo. A text to a customer, a booked slot, a field someone else is looking at right now. Recoverable, but a person has to apologize or reconcile.
  • Not undoable. Money leaving, an email to the whole list, a deleted record, anything filed with a third party. One-way doors.

I like making this explicit in code, so the classification lives next to the tool instead of in a planning doc nobody opens again. Roughly this shape:

type UndoCost = "free" | "awkward" | "one_way";

interface AgentAction {
  verb: string;            // "create_appointment", "send_sms", ...
  system: string;          // "calendar", "crm", "twilio", ...
  undoCost: UndoCost;
  mode: "dry_run" | "needs_approval" | "autonomous";
  maxPerHour: number;
}
Enter fullscreen mode Exit fullscreen mode

The point is not the type. The point is that nobody can add a tool without answering "what does undo look like for this?" in the same pull request.

Approval gates go where recoverability ends

Once the list is sorted, the approval gate goes at the line between awkward and one-way. Not everywhere.

This matters more than it sounds. A system that asks permission for every action gets clicked through without being read inside a week. That is worse than no gate at all, because now there is a human signature on a decision nobody actually made. Approval fatigue turns a safety control into an audit trail that lies.

The second axis is frequency. A rare, irreversible action can afford to wait for a person. A cheap action that happens a thousand times a day cannot, so it needs limits instead of approvals: a cap per hour, an allowlist of destinations, and a hard stop when the rate looks unusual. Those limits live in code, not in the prompt. A sentence politely asking the model not to send a thousand texts is not a rate limit.

Four failures that never throw

This is the section I wish someone had handed me earlier. None of these show up as an exception, which is exactly why they need an answer in the design, not in the incident review.

1. Confidently wrong. The action completes perfectly and should never have happened. Nothing alerts, because nothing failed. The only defense is making every action checkable after the fact: what changed, which field, which conversation triggered it, and the text that led to the decision. If a person cannot answer "why did this record change at 2am on Sunday" in under a minute, the log is not good enough.

2. Done twice. A timeout, a retry, and now there are two bookings or two charges. This one is ordinary integration engineering rather than anything to do with AI, it is solved with idempotency keys and dedupe, and it is the failure most likely to show up in month one. I wrote a whole post on the version of this that bites hardest, so I will not repeat it here beyond one line: your retry logic has to know which side effects are safe to repeat.

3. Stopped halfway. Multi-step sequences fail in the middle. It books the slot and never sends the confirmation. Decide up front, per sequence, whether an incomplete run rolls back or gets flagged to a human. Doing neither is the default, and it is how customers end up holding appointments nobody on the team knows exist.

4. The silent skip. The worst one. The agent decides not to act, and because nothing happened, nothing looks wrong. Your monitoring has to alert on absence, not only on errors. That means logging the actions the agent considered and declined, not just the ones it took. A day where an agent that usually books forty appointments books three should page someone, even though every one of those three succeeded.

The fourth one changed how I log. Every decision point writes a record, whether or not a tool gets called:

{
  conversationId: "...",
  considered: "create_appointment",
  decision: "declined",
  reason: "caller asked about pricing, routed to human",
  at: "2026-09-30T14:02:11Z"
}
Enter fullscreen mode Exit fullscreen mode

Declines are data. Without them you can only ever see what the agent did, never what it should have done.

Ship it as a dry run first

The rollout sequence below costs a couple of extra weeks, and it removes most of the argument about whether the agent is trustworthy, because it replaces opinions with a log.

Step one: wire everything, execute nothing. Connect every integration for real, let the agent decide what it would do, write the intended action to a log, and skip the call. One or two weeks of that log is the most honest accuracy report you will ever get, because it runs against real traffic instead of the examples someone picked for the demo.

Step two: draft and approve for everything. The team reviews a queue instead of doing the work by hand. Approvals and rejections are now labelled data, broken down by action class, and they tell you exactly which verbs are ready.

Step three: graduate one action class at a time. Cheapest to undo first, each with its own cap and its own kill switch. If the create_note action has had a clean month in the approval queue, flip it to autonomous. send_sms waits until it has its own clean month. Anything in the one-way bucket may never leave step two, and that is a fine outcome.

With the mode field on each action, this rollout is a config change per verb rather than a redeploy, which also means rolling one back at 2am is a config change too.

What I check before an agent gets a new verb

A short list, and I run it every time a tool is added, not only at the start:

  • What does undo look like? Who does it, how long it takes, and who finds out.
  • What stops a runaway? A cap, a kill switch, and the name of the person who can pull it.
  • How would I know it skipped this? If there is no alert on absence, the silent skip is live.
  • Who re-tests it when the model changes? Providers ship updates you did not ask for. Every prompt edit and every model bump is a deployment and should go back through the dry-run log.

The bottom line

Write access is the difference between a demo everyone compliments and a system that actually saves someone an afternoon, so avoiding it is not the answer. Being specific about it is. The agents we run on real systems, including the booking behind CallGuard AI and the multilingual voice and SMS intake behind Fortell AI, where the agent writes real intake data for Community Action Agencies, are safe to run for a boring reason. It is not a better prompt. It is a short, explicit list of verbs, and a recoverable answer for each one.

I wrote the longer, client-facing version of this for Null Studio, including the questions to ask a studio before you commission an agent.