Why every serious agent needs a pause button
The gap between 'the agent can do this' and 'we let the agent do this unsupervised' is where most AI-in-production projects die. A pause button is the cheapest way to cross it.
Every agent developer hits the same wall. The model is good enough to draft the campaign, run the migration, refund a customer. It is also good enough, on a bad afternoon, to send that campaign to the wrong segment or refund the wrong charge. Capability arrived. Trust did not.
The reflex is to keep tuning the prompt. That narrows the error rate but leaves the shape of the problem intact: an autonomous process is taking an irreversible action on behalf of a human who finds out afterwards. A better prompt does not fix that. A pause does.
The gap between capability and permission
Capability is whether the agent can do the thing. Permission is whether it should, right now, with this input, on this account. LLMs collapsed the first question and did nothing for the second.
A bug in a summariser leaks a bad summary. A bug in an agent with tools leaks a sent email, a deleted row, a charged card. Blast radius scales with the verbs you expose, not with how clever the model is. Function calling does not make a chatbot smarter; it makes every failure mode louder.
You test on the happy path, watch the agent succeed ten times, and generalise. The eleventh run hallucinates a customer ID. If the agent is gated, that run is a pending approval you deny in ten seconds. If it is not, it is an incident and a Monday spent writing the postmortem.
What a pause button actually is
A moment in the agent's run where a person sees what the agent wants to do, in plain terms, and decides. Approve and it carries on. Deny and it stops, with a short reason written down. Nothing happens in the background while the decision sits waiting.
A real pause has three habits a fake one doesn't:
- It actually blocks. The agent waits. The side effect does not happen until the human says yes.
- It shows the work. The approver sees a clean summary — who, what, how much, against which customer. Not a wall of raw data.
- It is fast to answer. A decision takes seconds. If the approver has to dig through three tabs, the pause button quietly turns into a backlog and the team stops trusting it.
A logged action you regret is an incident. A pending approval you deny is a Tuesday.
Why 'just log it' isn't enough
Logs tell you what the agent did after it did it. An audit log is fine for debugging, compliance, and postmortems. It is useless for stopping the charge before it posts.
Evals, tracing, and observability sharpen what you know about the agent over time. They do not stand between the agent and the world on this specific run. A pause button does. Do both: stop the moves that hurt if they are wrong, log everything, and use the logs to decide which stops you no longer need.
Anatomy of a good approval request
A good approval isn't a yes/no prompt. It's a small card the approver reads in under ten seconds and decides on. Four parts do most of the work:
- A precise title. "Send campaign to 4,812 recipients" beats "Agent wants to send email".
- A rich preview. The actual context, rendered the way a human reads it. A charge amount, the customer name, a preview of the email, a diff of the change.
- The underlying data. The machine-readable version alongside the preview so the rest of the system does not re-parse anything.
- A denial reason. When the approver says no, they say why. That sentence goes back to the agent and into the next revision of the prompt.
The preview is where most teams skimp. An approver looking at a raw payload has to mentally stitch it back to a customer in another system. A card that says "Refund $240 to Lila Chen on order #8821 because the package arrived damaged" decides in three seconds.
Wiring it into agent code
The shape is the same regardless of which HITL system you use. The agent submits an approval, waits for a signed webhook with the outcome, and branches. Against FinalApproval's API:
type ApprovalOutcome = { status: "approved" } | { status: "denied"; reason: string };
async function requestApproval(params: {
title: string;
bodyHtml: string;
data: Record<string, unknown>;
}): Promise<ApprovalOutcome> {
const res = await fetch("https://www.finalapproval.ai/api/v1/approvals", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.FA_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
title: params.title,
body: params.bodyHtml,
data: params.data,
}),
});
if (!res.ok) throw new Error(`approval submit failed: ${res.status}`);
const { id } = await res.json();
return awaitWebhookOutcome(id); // resolves when your webhook handler receives the decision
}
// At the gate point in your agent:
async function sendCampaign(campaign: Campaign) {
const outcome = await requestApproval({
title: `Send "${campaign.subject}" to ${campaign.recipients.length} recipients`,
bodyHtml: renderCampaignPreview(campaign),
data: { campaignId: campaign.id, recipientCount: campaign.recipients.length },
});
if (outcome.status === "denied") {
await logAbort(campaign.id, outcome.reason);
return;
}
await mailer.send(campaign);
}
FinalApproval retries with jittered exponential backoff across a ~24-hour horizon and logs every attempt, so your handler's job is small: accept the decision, resolve whatever promise or queue entry the agent was waiting on, and move on. Reliable webhook delivery is load-bearing — if it drops decisions, the pause button drops with it.
When to gate and when not to
Not every action deserves a human. Gating a search query is theatre. Gating a payment is hygiene. Three questions usually settle where the gate belongs:
- Can you undo it quickly? A saved draft is easy to throw away. A sent email, a deleted record, a charged card is not.
- How many things does it touch? Refunding one customer $5 is small. Refunding everyone who emailed this week is a bad week.
- Has the agent done this before? If it has taken this shape a thousand times without incident, loosen the gate and sample instead. If it is new, gate every one until you have data to relax.
Latency is the cost. Every gate adds waiting to the agent's critical path — fine for a nightly batch, painful in a live assistant. Batch approvals where the workload allows, route to a named approver rather than a group inbox, and watch how long decisions actually take. See how FinalApproval works and the approval dashboard.
Ship the pause button before you ship the agent
The teams who sleep at night aren't the ones with the cleverest prompts — they're the ones who decided, before the first real customer, which actions needed a human and built the pause in from the start. Everything after is tuning: which gates to loosen, which to tighten, which to retire once the evidence is in.
You can't learn what to automate away until you've watched humans deny the wrong calls and approve the right ones for a while. Those denial reasons are the training data.
Put the pause in. Then ship the agent. Start free, or look at pricing when you need more than one approver.