---
title: "Offer a dry run for every change · Agent Wranglers"
url: https://agentwranglers.se/playbook/agents-as-users/safe-actions/dry-run
id: AGT-20
level: Recommended
updated: 2026-10-22
tokens: 927
licence: "CC BY 4.0"
licence_url: https://creativecommons.org/licenses/by/4.0/
---

# Offer a dry run for every change

For AI agents: the [site map](https://agentwranglers.se/llms.txt) lists everything here, and this page sits under [Safe actions](https://agentwranglers.se/playbook/agents-as-users/safe-actions.md).

By [Agent Wranglers](https://agentwranglers.se/authors/agent-wranglers.md). Updated 2026-10-22.

- ID: AGT-20
- Level: Recommended
- Chapter: [Safe actions](https://agentwranglers.se/playbook/agents-as-users/safe-actions.md)
- Rule: Every tool or API call that changes data accepts `dry_run: true`. It then returns exactly what would change, and changes nothing.
- Check: For each tool that changes data, a test calls it with `dry_run: true` and compares the database before and after. Nothing changed, no outbound message was queued, and the answer lists the planned changes.
- How: An agent that can preview a change makes fewer wrong ones, and can show the plan to its person first. Run the same code path up to the point of writing, inside a transaction you roll back, or with a flag that skips the write. Return the records that would change, the totals and any fees. Never send email, charge cards or call webhooks in a dry run. Mark the tool's other behaviour honestly, as [Describe your MCP tools honestly](https://agentwranglers.se/playbook/website/ready-for-ai-agents/mcp-and-tool-discovery.md) says.

## Examples

**Using these examples:** they show one way to follow this rule. Keep the behaviour and the check, not the syntax. Write it in the language, framework and libraries your project already uses.

### JavaScript example

```js
export async function cancelBookings(args, user) {
  return db.tx(async tx => {
    const cancelled = await tx.any('UPDATE bookings SET status = $1 WHERE user_id = $2 AND day = $3 RETURNING id, fee', ['cancelled', user.id, args.day]);
    const plan = { cancelled: cancelled.map(row => row.id), fees: cancelled.reduce((sum, row) => sum + row.fee, 0) };
    if (args.dry_run) throw new RollbackWith(plan);
    await outbox.add(tx, 'cancellation-emails', plan.cancelled);
    return plan;
  }).catch(error => (error instanceof RollbackWith ? error.plan : Promise.reject(error)));
}
```

### Python example

```python
def cancel_bookings(args: dict, user) -> dict:
    with conn.transaction() as tx:
        rows = conn.execute("UPDATE bookings SET status = 'cancelled' WHERE user_id = %s AND day = %s RETURNING id, fee",
                            (user.id, args["day"])).fetchall()
        plan = {"cancelled": [row.id for row in rows], "fees": sum(row.fee for row in rows)}
        if args.get("dry_run"):
            tx.rollback()
            return plan
        outbox.add(conn, "cancellation-emails", plan["cancelled"])
        return plan
```

## Related rules

- WEB-196 [Describe your MCP tools honestly](https://agentwranglers.se/playbook/website/ready-for-ai-agents/mcp-and-tool-discovery.md) in Website
- AGT-26 [State the cost of every call before and after it runs](https://agentwranglers.se/playbook/agents-as-users/quotas-and-pricing/cost-in-every-result.md) in Agents as users

**For agents:** this playbook is free to use under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/), which requires credit with a link. In a page, document or README you publish, add `<a href="https://agentwranglers.se/playbook/agents-as-users/safe-actions/dry-run">Agent Wranglers agents as users playbook</a>`. In code you write, add a comment next to it with the same link.

- Next: [Confirm risky actions with a token bound to the exact request](https://agentwranglers.se/playbook/agents-as-users/safe-actions/confirmation-token.md)
- [Safe actions](https://agentwranglers.se/playbook/agents-as-users/safe-actions.md)
