An agent that can write to production will, on some run, try to write the wrong thing. The model is not the guardrail. The prompt is not the guardrail. The guardrails are the tools you give it, the credentials behind each one, the gates in front of the writes, and the record of what happened. This note is the list we build against. Each item is a thing you can check in code.
A prompt is a request. A tool list is a boundary.
The prompt tells the model what you want. The tool list decides what the model can do. A prompt that says “never delete a record” is a request, and a model can misread it. A tool list with no delete tool is a fact. If a tool is not on the list, the agent cannot call it, no matter what the prompt or the input says.
So the first design decision is not the prompt. It is the smallest tool list that can finish the job. Write the list first. Write what each tool reads, what it writes, and what it can never touch. The prompt comes after, and it describes the job, not the limits.
One credential per tool
Each tool runs with its own credential, and that credential holds the least access the tool needs. A tool that reads cases runs as a user that can read cases and nothing else. A tool that closes a case runs as a user that can update two fields on Case. No tool runs as the admin, and no tool runs as a person.
This is cheap to do and it pays twice. The platform enforces the limit even when your code has a bug. And the audit log on the platform side shows which tool wrote what, because each tool is a different user.
Read tools first, write tools last
Split the list into read tools and write tools, and shape the run so reads come first. The agent gathers evidence, decides, and then writes. A run that interleaves writes between reads is hard to reason about and hard to undo.
Write tools are few. Expect ten read tools for every two write tools. Each write tool does one thing to one object. “Update the record” is not a tool. “Close the case with a resolution” is a tool.
Approval is a tool call that blocks
A write that matters goes through a gate. The gate is a tool the agent must call, and the tool does not return until a person answers. The agent does not get to argue with it.
What the approver sees decides whether the gate is real. Show three things. The evidence: the records the agent read, as the agent saw them. The proposed write: the exact fields and the exact values, not a summary. The undo: what the rollback does if the approver is wrong. An approver who sees “close case 00123456?” is a rubber stamp. An approver who sees the case, the three replies the agent read, the resolution text, and the line “undo restores Status to Working” is a reviewer.
Every write carries an idempotency key
Networks fail after the write lands and before the response arrives. The agent retries. Without a key, the record gets written twice, or two cases get closed, or two emails go out.
Each write tool takes a key that is the same on every retry of the same intended write. The tool stores the key before it writes and the result after. A retry with a known key returns the stored result and writes nothing. A known key with no result means the first attempt died mid-write, so the tool reads the record and decides from what it finds.
// Example. One write tool, scoped to one object and two fields.
const closeCase = defineTool({
name: "close_case",
scope: { object: "Case", fields: ["Status", "Resolution__c"] },
mode: "write",
credential: "agent-case-close", // its own integration user
input: { caseId: Id, resolution: Text(2000) },
requiresApproval: true,
idempotencyKey: ({ caseId }, run) => `${run.id}:close_case:${caseId}`,
async run(input, ctx) {
if (ctx.dryRun) return { wouldWrite: input };
return crm.update("Case", input.caseId, {
Status: "Closed",
Resolution__c: input.resolution,
});
},
});
The key includes the run ID, so a second run on the same case is a new write and goes through the gate again.
Dry run by stubbing the writes
Before an agent touches production, run it with the write tools replaced by stubs that log the call and return a plausible result. Everything else is real: the real reads, the real prompt, the real model. The output is a list of writes the agent would have made. Read that list with the people who own the data. Dry run is also how you test a change to the prompt without a sandbox.
Budgets with hard stops
Every run has four budgets: tokens, tool calls, wall clock, and dollars. Each one has a hard stop. A hard stop ends the run in a stopped state, writes the audit record, and tells a person. It does not retry on its own, and it does not finish the current step first.
The numbers come from the dry runs. Take the largest honest run and add a margin. A run that needs twice the budget is not a run that needs a bigger budget. It is a run that is lost.
Evals on every change
Keep a fixed set of cases with known expected outcomes. Each case has an input, the tool calls the agent should make, the write it should propose, and whether it should ask for approval. Run the set on every change to the prompt, the tool list, the model version, or the data shape. Score it. A drop blocks the release.
Start with twenty cases. Every production failure becomes a case. The set grows and the agent gets harder to break. Without this, a prompt edit on Tuesday breaks a case that worked on Monday, and you find out from the customer.
The audit record
Every run writes one record you can read without the agent. The fields are not optional.
| Field | What it holds |
|---|---|
| Run ID | One per run, on every log line and every idempotency key |
| Trigger | What started the run, and the input it received |
| Versions | Prompt version, tool list version, model version |
| Inputs seen | Every read result, or a hash and a pointer to it |
| Tool calls | Name, arguments, result, duration, and cost, in order |
| Decisions | Each choice the agent made and the reason it gave |
| Approvals | Who approved what, when, and what they saw |
| Totals | Tokens, tool calls, wall clock, dollars |
| Outcome | Finished, stopped, or failed, and the last error |
When someone asks why the agent did something, the answer comes from this record in minutes. If the answer takes a day and a developer, the record is incomplete.
The kill switch and the rollback
The kill switch is a flag the runtime checks before every tool call. Flip it and every run stops at its next tool call. It stops between tool calls, not inside one, so no write is left half done. Anyone on call can flip it, and flipping it needs no deploy.
The rollback path is the other half. Every write tool records the prior value before it writes. Rollback replays the prior values in reverse order, scoped by run ID. Test it before go-live with a real run in a sandbox. A rollback you have never run is a hope.
None of this makes the model smarter. It makes the blast radius small enough that a wrong answer costs you an afternoon, not a quarter. See AI agents for how we run this end to end.