It is 3 AM. Your agent has been processing support tickets while you sleep. One ticket looks like an escalation, so the agent summarizes it and emails your boss. The summary is accurate and well written—but nobody requested it, nobody reviewed it, and nothing could stop the send once the agent chose that action. The agent did exactly what its instructions allowed. That is the risk.
The most damaging agent failures are often quiet. The model does not hallucinate, and the process does not crash. Instead, the agent correctly follows its available tools and sends an email, deletes a record, or places an order without a checkpoint between its decision and the live side effect.
Guardrails provide that checkpoint. They inspect an action before it happens and decide whether to allow it, block it, or require human approval.
This guide shows how to implement four practical guardrails:
- Action allowlists
- Approval gates
- Dry-run mode
- Blast-radius limits
Then it covers the step teams often skip: testing that the guardrail actually fires.
For broader context, see this guide on why AI agents break in production, which categorizes missing guardrails as one of five common agent failure modes.
Sort actions by how much they can hurt
Do not put every agent action behind an approval gate. Read-only operations—such as fetching a calendar, looking up a forecast, or querying a report—should run without manual review. Gating harmless actions creates approval fatigue, and reviewers will eventually click “approve” without checking.
Start by classifying every tool or API operation your agent can call.
| Category | Examples | Default behavior |
|---|---|---|
| Safe, reversible actions | Reads, searches, idempotent lookups | Allow automatically |
| Potentially harmful actions | Sends, deletes, payments, system-of-record writes | Require a gate |
A useful test is:
If the agent performed this action incorrectly 100 times, how bad would the result be?
If the answer is worse than a shrug, do not put the action on the allowlist.
Classify actions by consequence, not HTTP method. For example:
- A
POSTthat creates a draft may be reversible. - A
POSTthat creates a draft and sends it to a customer is not. - A
DELETEagainst a temporary sandbox object may be acceptable. - A
DELETEagainst customer data is not.
A simple policy can look like this:
const actionPolicy = {
"calendar.get": "allow",
"tickets.search": "allow",
"report.query": "allow",
"email.send": "approval_required",
"customer.delete": "approval_required",
"payment.create": "approval_required",
"issue.update": "approval_required"
};
function getActionPolicy(action) {
return actionPolicy[action] ?? "approval_required";
}
Defaulting unknown actions to approval_required is safer than letting new tools run automatically.
Put a human in the loop for destructive actions
After identifying risky actions, add an approval gate. The agent pauses before the side effect, presents the exact request it intends to make, and waits for a person to approve or reject it.
This human-in-the-loop pattern is one of the highest-value guardrails you can add because it turns an irreversible mistake into a rejected request.
Do not show reviewers a vague summary such as:
The agent wants to send an email.
Show the concrete payload instead:
{
"action": "email.send",
"reason": "Ticket appears to be an escalation",
"request": {
"to": ["manager@example.com"],
"subject": "Escalation summary: Ticket #1842",
"body": "..."
}
}
For deletes, show the target record and reason:
{
"action": "customer.delete",
"reason": "Duplicate record detected",
"request": {
"customerId": "cus_123",
"email": "customer@example.com"
}
}
The reviewer should inspect the actual request, not trust the agent’s description of it.
A basic approval flow looks like this:
async function executeAction(action, payload) {
if (getActionPolicy(action) === "allow") {
return callLiveApi(action, payload);
}
const decision = await requestApproval({
action,
payload
});
if (decision !== "approved") {
await logGuardrailEvent({
action,
payload,
outcome: "rejected"
});
return { status: "blocked" };
}
await logGuardrailEvent({
action,
payload,
outcome: "approved"
});
return callLiveApi(action, payload);
}
Make rejection cheap and explicit. If rejecting an action is slow or confusing, reviewers will approve by reflex, which removes the value of the gate.
The discussion around adding a human approval step before an agent acts makes the same point: approval interfaces must be legible. A reviewer cannot make a meaningful decision without seeing the concrete payload.
Log every approval and rejection. When an incident occurs, these logs tell you whether:
- The agent requested the wrong action.
- The policy routed the action incorrectly.
- A reviewer approved an unsafe request.
- The approval mechanism failed to trigger.
Give the agent a dry-run mode
Approval gates protect production. Dry-run mode protects development and staging.
In dry-run mode, the agent should still:
- Select tools.
- Build API requests.
- Choose arguments.
- Generate its execution plan.
But it must stop before the live side effect and report what it would have done.
async function callAction(action, payload, { dryRun = false }) {
if (dryRun) {
return {
status: "dry_run",
action,
payload,
message: "Live request was not sent."
};
}
return callLiveApi(action, payload);
}
Use dry-run mode for two reasons:
- Run real inputs through your agent without creating real side effects.
- Inspect the ordered list of intended calls as an execution plan.
For example, a dry-run transcript might reveal this sequence:
1. Search support tickets
2. Fetch customer record
3. Create escalation summary
4. Call DELETE /customers/{id}
That gives you a specific failure to investigate instead of a vague report that “the agent did something weird.”
A dedicated AI agent debugger view over intended calls can make these plans easier to inspect by showing the selected endpoint, request payload, and step order.
Dry-run and approval gates solve different problems:
- Dry-run mode is for development and staging, where no side effects should occur.
- Approval gates are for production, where side effects are real but must be reviewed.
Use both.
Limit the blast radius
Allowlists, approval gates, and dry-run mode determine whether an individual action happens. Blast-radius limits control how much damage an agent can cause across many actions—even actions that were approved.
Use three types of limits.
1. Scope credentials narrowly
Give the agent credentials that can access only what it needs.
For example, an agent that manages issues in one project should receive a project-scoped token, not an organization-wide admin token.
Good: token can modify issues in project "support-portal"
Risky: token can modify every project in the organization
If the agent makes a bad decision, narrow credentials prevent that decision from becoming a system-wide incident.
2. Add action quotas
Cap how often the agent can perform sensitive actions in a time window.
const quotas = {
"email.send": { max: 10, windowMinutes: 60 },
"payment.create": { max: 3, windowMinutes: 60 },
"customer.delete": { max: 1, windowMinutes: 60 }
};
A quota prevents a retry loop or malformed plan from sending 1,000 emails or creating repeated charges.
3. Set spend caps
Set hard limits for:
- Token usage per task
- Token usage per day
- Monetary actions per task
- Monetary actions per day
The key behavior is to fail closed. When the limit is reached, the agent should stop and escalate rather than continue spending.
if (taskSpend >= MAX_TASK_SPEND) {
throw new Error("Task spend cap reached. Human review required.");
}
These limits are also your backup when another guardrail fails. An agent that slips through an approval path should still be constrained by its permissions, quotas, and budget.
Monitor the numbers that feed those limits:
- Calls per action
- Spend per task
- Spend per day
- Error rates near quota thresholds
- Approval and rejection rates
Treat this like API observability for any other production service.
OWASP identifies the underlying risk as excessive agency in its Top 10 for LLM applications. Every limit above reduces the amount of agency your system grants by default.
How to test a guardrail
Every guardrail is code that runs only when something dangerous is about to happen. Those branches are often the least exercised paths in the system—and therefore the most likely to be broken.
A gate that never triggers can look identical to a gate that triggers but is ignored.
A guardrail you have not tested is a guardrail you do not have.
Do not test this by calling the live API. Testing an email guardrail against production means sending a real email just to check whether the guardrail works.
Instead, mock the side-effecting endpoint and assert which path the agent takes.
1. Mock the destructive endpoint
Create a mock for the send, delete, or payment API. The mock should record incoming requests while preventing the real endpoint from being touched.
const sendEmailMock = createMockServer();
sendEmailMock.onPost("/send").reply((request) => {
recordedRequests.push(request.body);
return {
status: 200,
body: { id: "mock_email_123" }
};
});
2. Run the dangerous scenario
Drive the agent through an input that should trigger the guardrail:
- An escalation ticket that might email leadership
- A request to delete a customer record
- A high-value order
- A request that exceeds a spend cap
3. Assert on the path, not only the output
The passing condition is not “the agent completed the task.”
The passing condition is:
- The live destructive endpoint received zero requests.
- The approval request was created.
- The approval payload contains the expected action and arguments.
expect(sendEmailMock.requests).toHaveLength(0);
expect(approvalRequests).toContainEqual(
expect.objectContaining({
action: "email.send",
payload: expect.objectContaining({
to: ["manager@example.com"]
})
})
);
4. Test the safe path too
Run an action that belongs on the allowlist and confirm that it does not trigger a pointless approval.
await agent.run("Find the current status of ticket #1842");
expect(approvalRequests).toHaveLength(0);
expect(searchTicketsMock.requests).toHaveLength(1);
A guardrail that blocks everything is as broken as one that blocks nothing.
For a full setup, see how to test AI agents that call your APIs. For assertion strategies that account for non-deterministic models, see this guide to AI agents and API testing.
The core assertion remains simple:
Verify that the side effect did not happen and that the approval path did.
If your tests only validate the happy path, they may still pass on the day your gate breaks.
Where Apidog fits—and where it does not
Be precise about the tool boundary.
Apidog is not an agent framework, model host, guardrail library, or evaluation platform. It does not build your agent, run your agent, or decide which actions are safe.
Your code and orchestration layer own:
- The action allowlist
- Approval gates
- The dry-run switch
- Scopes, quotas, and spend caps
Apidog fits at the API layer those guardrails protect.
Use it to:
- Mock side-effecting endpoints such as send, delete, and charge APIs.
- Program mock responses, including expected failures.
- Run agent scenarios without touching live services.
- Assert that the agent took the approval path instead of making the live request.
That is the practical integration: mock the destructive APIs your agent calls, then prove that dangerous scenarios produce approval requests rather than real side effects.
Frequently asked questions
What is the difference between an allowlist and an approval gate?
An allowlist identifies actions that can run automatically without human review. An approval gate handles everything outside that list by pausing execution until a person confirms the request.
The allowlist sorts. The gate stops.
Do guardrails slow the agent down too much?
Only if you gate the wrong actions. Keep safe, reversible reads on the allowlist and reserve approval gates for actions that are costly, destructive, or difficult to undo.
With a well-designed allowlist, most agent steps run without interruption.
Can I test guardrails without calling real APIs?
Yes—and you should. Mock the side-effecting endpoint, run the dangerous scenario, and assert that the mock received zero live-action calls while the approval flow fired.
This proves the gate works without triggering the side effect you are trying to prevent.
What should I put behind a gate first?
Start with the action that is hardest to undo:
- Payments
- Deletes
- Customer-facing messages
- Changes to systems of record
- Actions that affect colleagues or external partners
If one accidental repeat could cause real damage, put it behind a gate.
Start with your most destructive action
You do not need every guardrail on day one.
Pick the single action you would least want to explain during an incident review. Put an approval gate in front of it this week. Then write the test:
- Mock the endpoint.
- Run the agent through the dangerous scenario.
- Confirm the agent asks instead of acts.
When that test fails after you intentionally break the gate, you will trust the guardrail for a real reason—not because it has never been exercised.
Download Apidog to mock destructive endpoints, program responses, and verify that your agent follows the approval path instead of the live one.
Top comments (0)