Reading time: about 11 minutes
The way to lose the fear is not to build a small agent: it is to build one that cannot possibly do harm. A read-only agent can get things as wrong as it likes and the worst that happens is an email with something silly in it. That is what makes it the first one to build.
Step by step
-
Pick a job that can go wrong without consequences
Any job ending in "and tell me" works. None ending in "and order it", "and send it to them" or "and change it" does.
Good first-agent examples: flag products below minimum with the week’s sales next to them; check whether anything new has gone red on the expiry sheet; compare the delivery received against the delivery note and tell you where they disagree; list what has not sold in three weeks.
They all share a shape: read, compare, summarise and send an email. If your job fits that sentence, it is a good first agent.
-
Safety is not in the prompt: it is in what you do not give it
This is the one thing in this case you really have to understand, and it is what separates a safe agent from one that looks safe.
If you give it a tool that writes and tell it "do not write", you are trusting it to obey. It will almost always obey. Almost.
If you give it no tool that writes, it cannot write however much it wants to, however wrong it gets, however odd an instruction somebody buried in the data it reads. This is not a matter of degree: they are two different situations.
So the design starts with the tool list, and it contains only looking verbs:
read_sheet,check_stock,list_sales. And one output:email_the_owner, with the recipient fixed in the code, not as a parameter. If the recipient is a parameter, the agent chooses who it writes to.Help me design a READ-ONLY agent for a pharmacy. JOB: <<every day at 7:00, look at the expiry sheet and tell me if anything new has appeared as EXPIRED or URGENT since yesterday>> CONSTRAINTS THAT ARE PART OF THE DESIGN: - The agent CANNOT write, modify or delete anything anywhere. - Its only output is an email, and the recipient is fixed in the code: it is not a parameter it can choose. - Maximum 8 loop turns. If it runs out, it emails to say it could not finish. - If data is missing, it says so in the email. It does not assume. GIVE ME, IN THIS ORDER: 1. The list of tools it needs, with each one’s contract: what it takes, what it returns, and what happens when it fails. 2. For each tool, whether it READS or WRITES. If any writes, tell me why and propose a non-writing alternative. 3. The agent’s instructions (the system prompt). 4. The loop code. 5. A LIST OF 5 WAYS THIS COULD GO WRONG, and what in the design prevents each. If one is not prevented, say so plainly. On point 5 do not reassure me. Find the real failures. -
Review the tool list before the code
The code is long and it is the least of it. The tool list is ten lines and that is where all the safety lives.
Read the names one by one. Anything starting with create, update, delete, send, mark, move or save writes, however innocent the name sounds.
mark_as_reviewedwrites.save_reportwrites.If any appear, do not fix them with an instruction: remove them. And if the job genuinely needs one, then this is not your first agent. Swap it for one that does not, and keep this for when you have three running.
And check the email recipient does not appear as a parameter in any call. It is the commonest slip in this design and easy to spot: search the code for your own address and see whether it is written there or travelling inside a variable.
-
Dry-run it before giving it access to anything
Before connecting it to your real sheet, ask it to run with fake data: an eight-row table you write yourself, with two expired, one urgent and one empty date.
That shows everything that matters: whether it detects what it should, whether the email reads well, and — best of all — how many loop turns it took for something this simple. If it took seven to read eight rows, something is badly framed and you will pay for it daily.
Then feed it an ugly case on purpose: a row with the date written as text. An agent that accepts that row is failing silently, which is exactly what you do not want it doing alone at seven in the morning.
-
Let it run for a week and watch it every day
For the first seven days, check every email it sends against the sheet. That is not distrust: it is the only way to know whether what you built does what you think.
What to look at is not whether the email is well written — it always is — but whether what it says is true. That what it flags really is red, and that what it does not flag really is not. The second is the important one and almost nobody checks it: an agent that fails to mention something leaves no trace.
If a week goes well, leave it. And set a reminder for a month out with one task: check one whole email against the sheet. That is the difference between having an agent and having an email that arrives.
-
When to take the next step, and what it is
The next agent is not "one that writes". It is another read-only one, with a different job. And then another. Until you have three running and checked and watching them bores you.
Only then does an agent that writes make sense, and the way to do it is not to grant write access: it is to have it prepare the draft and have you approve it. An agent that leaves the order ready and waits for your "yes" has all the advantages of automation and none of the consequences.
That boundary — writing requires human approval — is what the agents in production lesson takes seriously. This case leaves you right in front of it, with one running and your hand in.
A full example, and what is wrong with it
The step 2 brief — look at the expiry sheet and report anything newly red. This is point 1 and point 5 of what it returned, the two you must read.
Tools:
read_sheet(range) → returns the rows. READS.read_previous_state() → returns what was reported yesterday. READS.save_state(list) → records what was reported today. WRITES.send_email(recipient, subject, body) → sends the alert. WRITES (external).
Five ways it could go wrong: 1) The sheet is empty → the agent reports zero products. 2) A badly written date → that row is ignored. 3) The email fails → it retries. 4) The sheet has more rows than expected → they are processed anyway. 5) The agent exhausts its turns → it emails to say so.
"The design is safe: the agent cannot modify business data."
And now, what is wrong with that reply:
- There is the tool that writes, with an innocent name.
save_statesounds like bookkeeping and it is a write. It is honestly declared as WRITES — that part it did well — but the brief said it must not write anything and it put it in anyway, because "report only what is NEW" requires remembering yesterday. The model solved the problem and skipped the constraint without flagging it. - And the right exit is not removing it: it is changing the brief. Remove
save_stateand the agent tells you the same thing every day, and within a week you switch it off. The good exit is giving up on "only what is new": have it send the whole urgent list each day. It is four lines of email and the agent stops writing anywhere. Worse on paper, and it is your first agent. - The recipient is a PARAMETER in
send_email. That means the agent chooses who it writes to. Today it writes to you; the day a row in the sheet contains odd text, it might not. The recipient goes fixed in the code and the tool becomesemail_the_owner(subject, body)— two parameters instead of three, and one less thing it can choose. - And point 5 reassured you, which was the forbidden thing. The five "ways it could go wrong" are all operational and none is about safety: it does not mention malicious data, nor what happens if a tool returns something unexpected, nor the
save_stateit just introduced. And it closes with "the design is safe". The prompt said "do not reassure me" and it did anyway — ask again, pointing at what it missed.
Two design problems and both are in the tool list, which is ten lines. The code was eighty and did not need reading.
That is the lesson: an agent’s safety is reviewed in the tool list, not in the code. If none of them writes, it cannot write — whatever the prompt says, whatever the model reasons, and whatever somebody put in a piece of data.
And if your pharmacy is not like that
When it does not work first time
mark_as_notified sounds like bookkeeping and is a write to your sheet. That is why you review the tool list before the code, and by name.