Why AI Agents Need Approval Gates

An agent that can act can act on a bad premise — approval gates are what stops a wrong draft from becoming a wrong action taken in the real world.


The gap between "can generate" and "should execute"

Language models are good at producing a plausible next action: a reply, a database write, an API call, a payment. They are not good at knowing when the input they reasoned from was wrong — a hallucinated fact, a poisoned instruction buried in a scraped web page, a misread of ambiguous intent. Generation and judgment are different capabilities, and current agents have plenty of the first and unreliable amounts of the second.

As long as the output is text a person reads before anything happens, that gap is cheap. The moment an agent is wired to a tool that has a side effect — send_email, charge_card, delete_row, post_to_slack — the gap becomes the thing that decides whether a bad generation stays theoretical or becomes an incident.

Three failure modes an approval gate actually addresses

None of these require the agent to be malicious or even particularly buggy — a well-behaved agent following a subtly bad instruction produces the same failure.

What a gate has to guarantee to actually be one

"Add a confirmation step to the prompt" is not a gate — it's a suggestion the model can talk itself past, and it leaves no record of who confirmed what. A real gate is a data dependency: the code path that performs the side effect literally cannot run until an external system returns a decision.

typescript
async function proposeAndExecute(kind: string, title: string, body: string) {
  const create = await fetch("https://api.impri.dev/v1/actions", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.IMPRI_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      kind,
      title,
      preview: { format: "markdown", body },
      expires_in: 3600,
      editable: ["preview.body"],
    }),
  });
  const { id } = await create.json();

  let decision;
  while (true) {
    const poll = await fetch(`https://api.impri.dev/v1/actions/${id}`, {
      headers: { Authorization: `Bearer ${process.env.IMPRI_API_KEY}` },
    });
    decision = await poll.json();
    if (decision.status !== "pending") break;
    await new Promise((r) => setTimeout(r, 10_000));
  }

  if (decision.status !== "approved") {
    return { executed: false, status: decision.status }; // rejected/expired — nothing happens
  }

  // The only line in this function that touches the real system:
  await runTheActualSideEffect(decision.decision.final_preview.body);

  await fetch(`https://api.impri.dev/v1/actions/${id}/result`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.IMPRI_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ status: "executed" }),
  });
  return { executed: true };
}

runTheActualSideEffect is unreachable from any branch except the one guarded by decision.status === "approved". That's what makes this a gate rather than a formality — there's no code path where the model's own confidence substitutes for the human's decision.

Without a gate vs. with one

No gate Gate in place
Bad generation reaches the real world Immediately Never, unless a human approves it
Audit trail of who allowed what None Every decision recorded with an actor and timestamp
Cost of a wrong action Whatever the action does One rejected draft
Agent can still run unattended Yes, but unsupervised Yes, up to the point of the side effect

What this doesn't solve

A gate only holds if the guarded path is the agent's only route to the side effect — if it still holds a standing API key it can call directly, the gate is bypassable. It also doesn't validate that the content is correct; a human still has to actually read the card, not rubber-stamp it. And it isn't a substitute for scoping what the agent can attempt in the first place — least-privilege tool access and a gate are complementary, not either/or. See the propose-approve-execute pattern for the full shape of this, and human-in-the-loop vs. full autonomy for when a gate is worth the added latency versus when it isn't.

Next step

The quickstart gets you an API key in a couple of minutes. If you're deciding between wiring this over plain REST or through MCP tool calls, REST API vs. MCP for human approval walks through the trade-off.