# A support agent that issues refunds, with a person on the big ones (/blog/support-refunds-with-an-approval-step)

![Illustration: A small friendly robot with a round camera-eye head stands at a low garden gate holding a paper receipt.](/blog/support-refunds-with-an-approval-step/hero.webp)

A support agent can read a customer's message, look up the charge, and issue the refund on its own. The risk is the large refund nobody looked at. With Smart Approval you write one sentence, "Ask me before any refund over $100", and refunds above that line pause for a person while the small ones run.

Every name, amount and result in this post is a fictional example. The tool names are real.

**Small refunds run, big ones pause**

Policy: Ask me before any refund over $100.

* gmail\_\_search\_emails, read the thread: never gated
* Stripe charge lookup, a read: never gated
* stripe\_\_stripe\_api\_write, refund $40: Allow, runs at once, Activity shows ok
* stripe\_\_stripe\_api\_write, refund $240: Ask, held, a card goes to the member who made the call
* The agent calls manage\_\_approval\_wait and keeps waiting; once approved, the frozen $240 refund runs exactly once

## What does the flow look like end to end? \[#what-does-the-flow-look-like-end-to-end]

The agent runs in the client you already use, for example Claude Code with CoreSpeed added as its MCP server. Your organization has connected Gmail and Stripe as shared connections. An admin has turned Smart Approval on at Dashboard → Approvals and left the default behavior on Balanced.

A customer writes in about a damaged order. The agent handles it in five calls.

| Step                       | Tool                       | Kind  | Verdict                   |
| -------------------------- | -------------------------- | ----- | ------------------------- |
| Read the customer's thread | `gmail__search_emails`     | read  | never gated               |
| Look up the charge         | Stripe connector, a read   | read  | never gated               |
| Refund $40                 | `stripe__stripe_api_write` | write | Allow: runs and is logged |
| Refund $240                | `stripe__stripe_api_write` | write | Ask: held for a person    |
| Save the resolution        | `memory__remember`         | write | Allow                     |

Reads are never gated. The agent reads the thread and the charge without interruption. Only the refund call is judged, and the verdict depends on the amount.

## What does the policy say? \[#what-does-the-policy-say]

The policy is plain English on the policy page. The example from the [Approvals docs](/docs/approvals) fits a support agent as written:

```text
Sam handles support. Replies to customers who have written to us first are
fine. Ask me before anything goes to a public channel or an external company,
and before any refund over $100. Never post on our behalf about pricing,
funding, or anything that reads like a product promise.
```

On Balanced, a write the policy never mentions runs, unless it is destructive, in which case it asks. A written sentence beats the default. The refund sentence decides every refund, whatever the amount. Two guards sit above every verdict: a low-confidence answer becomes Ask, and an Allow that looks like prompt injection becomes Ask.

The judge sees your policy, the tool name and description, its annotations, who is calling and from which client, how often this caller used the same tool recently, and the argument values. The argument values are what make "over $100" work: the amount is in the call. The judge never sees the agent's conversation.

Test policy on the same page runs a hypothetical $240 refund through the same evaluation without executing anything. Do that before you rely on the sentence. Every save is a new policy version with history.

## What happens when the refund is over $100? \[#what-happens-when-the-refund-is-over-100]

The `stripe__stripe_api_write` call returns at once with an error-shaped receipt: approval required, not run, call `manage__approval_wait` with this id. The agent calls `manage__approval_wait` and waits.

A card reaches three places. The dashboard queue is the only place the full call is shown. The email carries the tool, who asked, and approve and deny links, with no arguments in the mail. Slack gets a card in a DM or in one org channel, posted by the CoreSpeed bot in the workspace an admin installed once. The agent's own Slack tools cannot post a card or write into that channel.

Who can decide: the member who made the call, or the owner of the agent or API key that made it. An agent cannot approve its own action. Admins can see every card, but seeing is not deciding.

Whichever channel decides first wins. The call was frozen when it was held, so the $240 that was approved is the $240 that runs, exactly once. Two approvals at the same moment hand it over once. `manage__approval_wait` then returns the real Stripe result, or a denial that carries the reason the person typed.

Two rules for the agent. Do not re-send the call: a fresh `tools/call` is a new request and can raise a second card. Keep waiting: the card expires about thirty seconds after the agent stops waiting, and a card nobody decides expires after an hour. Claude Code sends a progress token, so it receives a progress notification every ten seconds while it waits.

## What does the agent save afterward? \[#what-does-the-agent-save-afterward]

After the refund lands, the agent writes one line with `memory__remember` and `scope: "shared"`, so the next agent on the queue can find it: "Order 4821 refunded in full on 2026-10-07 after a damaged shipment. Customer declined a replacement." A later session that searches [memory](/docs/memory) for that customer gets the line back.

Memory is context, not policy. It does not change what the gate does. Save the decision and the constraint. Do not save card numbers or the whole thread. Memory operations cost 0 credits per call.

## What does the trail show? \[#what-does-the-trail-show]

Dashboard → [Activity](/docs/activity) holds one record per call: action, actor, org, outcome, surface and time. The $40 refund shows `ok`. The $240 refund shows `held` while the card is open, then `ok` or `denied`, with its approval id. Each metered call references a charge in the ledger, so cost and action sit side by side.

The [Smart Approval launch post](/blog/introducing-smart-approval) walks through the same refund scene with stills from the launch film.

## FAQ \[#faq]

**Does every refund wait for a person?** No. Only refunds over the amount in your sentence pause. Refunds under it run and are logged. Reads are never gated.

**What if the judge times out?** A failed evaluation goes to a separate Ask or Deny setting, Ask by default. No failure path lets a write run.

**Can the agent reach Stripe another way and skip the card?** The gate sees only calls made through CoreSpeed. Keep the Stripe credential in CoreSpeed's custody and out of the agent's own config, and every write is judged by the same rulebook.

**What does the agent see when the refund is denied?** `manage__approval_wait` returns an error with the reason the person gave. The agent can then tell the customer what happens next.