Guide
June 20, 2026
7 min read

Human-in-the-Loop Database Migrations: Approving What the Agent Wants to Run

Most human-in-the-loop advice stops at approve database changes. Here is how to actually render, gate, route, and record a pending migration before an agent runs it.

#AI agents#Migrations#Human in the loop#Guardrails#Postgres#MongoDB

Where the advice stops

Search for guidance on letting AI agents touch production data and you will find the same sentence everywhere: "database changes should require human approval." It is correct, and it is useless on its own. It tells you the outcome you want without telling you how to build it.

The gap matters because schema changes are exactly the class of operation where a wrong call is expensive and hard to undo. A migration is not a SELECT you can re-run. It rewrites the structure other systems depend on, and on a busy table it can take locks that stall every query behind it. OWASP's 2025 guidance on prompt injection is blunt about the principle: implement "human-in-the-loop controls for privileged operations to prevent unauthorized actions," and handle critical functionality "in code rather than providing them to the model." That is the right instinct. This article is about turning that instinct into a working gate.

The hard part is rendering a decision a human can make

Most teams that bolt approval onto an agent stop at a Slack message that says "Agent wants to run a migration. Approve? [Yes] [No]." Clicking yes there is not approval. It is a coin flip with a UI.

Real approval requires that the reviewer can see what is about to happen, in terms they can judge, before they ratify it. The hard part is not the button. It is assembling a pending migration into something a senior engineer can read in thirty seconds and either trust or reject. That assembly step is where most homegrown setups quietly fail, because the agent that wrote the migration is also the thing summarizing it, and a summary written by the actor is not an independent control.

What an approvable migration view needs

A reviewer needs four things, and a vague natural-language description of intent is none of them.

  • The exact diff. Not "add an index to speed up the orders query." The literal statement that will execute, byte for byte, with the target object named. If the agent rewrote a statement after the human read the first version, the approval is void.
  • Estimated lock behavior. The reviewer should see which locks the statement will take and what they block. In Postgres, an ACCESS EXCLUSIVE lock blocks reads and writes for the duration, and any migration waiting on a lock makes new queries queue behind it, so a "small" change can stall a whole table (Xata). In MySQL, an online DDL with ALGORITHM=INPLACE still needs a brief exclusive metadata lock to finalize, and a long-running transaction holding a metadata lock can block it (MySQL docs). The view should make that concrete.
  • Rows or documents affected. A backfill that touches a thousand rows and one that touches a hundred million are the same SQL shape and a completely different risk. The reviewer needs the scale.
  • A rollback plan. What undoes this if it goes wrong, and is that undo itself safe to run under load. "We will restore from backup" is not a rollback plan for a migration that ran at 2pm on a Tuesday.

If the approval surface cannot show all four, the human is rubber-stamping, and you have built ceremony, not control.

Where the gate has to live

Here is the part that decides whether any of this holds up. The gate cannot live inside the agent.

If the rule "pause and wait for a human before running DDL" is a line in the system prompt or a tool the agent chooses to call, then the agent's reasoning is what enforces it. That is fine until the agent reasons its way around it, or until it ingests content that tells it to. Indirect prompt injection does not need to be human-readable to work; it only needs to be parsed by the model (OWASP). A migration tool the agent is trusted to gate itself is a guardrail made of suggestions. We have written before about why read-only access is not enough and why instruction-level controls fail under adversarial input.

The gate has to sit below the agent, in the data path, where every statement bound for the database passes through it whether the agent wants it to or not. The agent emits a migration. The control plane intercepts it, classifies it as a schema change, and holds it. The agent cannot talk its way past a component that is not listening to instructions. This is the same architecture we describe in our guide to safe AI database access: enforcement out of band, in the connection path, not in the prompt.

An SDK interrupt is a suggestion. An out-of-band gate is a wall.

Agent frameworks now ship human-in-the-loop primitives. LangGraph's interrupt() pauses a graph before a tool node and resumes once a human responds. These are genuinely useful for building good agent UX, and you should use them. But understand what they are: a cooperative pause inside the agent's own runtime. They work because the agent reached that node and chose to honor the interrupt.

An out-of-band gate is different in kind. It does not depend on the agent reaching a node or honoring anything. It lives on the database connection, sees the actual statement, and refuses to forward it until the decision is ratified. If a prompt injection convinces the agent to skip its interrupt, the framework gate is gone and the data-path gate is still there. One is a suggestion the agent can decline. The other is a wall the agent cannot see around. You want both, but only one of them is a control you can put in a compliance document. The same reasoning applies to agents accessing a production database generally.

Routing to the right approver and blocking until ratified

A held migration is only useful if it reaches a human who can actually judge it, and a junior on call is not always that human. The gate should route by what the change is, not just that it is a change. Adding a nullable column with no default can route to the on-call engineer. A statement that drops a column, rewrites a table, or backfills millions of rows should route to a database owner or a named approver group, and high-blast-radius changes can require more than one sign-off.

While the request sits in the queue, execution is blocked. Not deprioritized, blocked. The statement does not reach the database until someone with authority ratifies the exact diff that was presented. If the agent regenerates the migration, the clock resets and the new version goes back through the gate. Approval binds to a specific statement, never to "the agent's migrations in general."

Recording the decision so it survives the next audit

Approval that leaves no record is approval you cannot defend later. Every decision, who reviewed it, what exact statement they saw, when they ratified it, and what actually ran, belongs in an append-only log that neither the agent nor the engineer can quietly edit after the fact. When someone asks six months from now why a column was dropped, the answer should be a record, not a Slack scrollback that may have been deleted. This is the immutable ledger we cover in audit trails for AI coding agents: the decision and the executed statement, captured together, tamper-evident by construction.

The same gate across Postgres, MySQL, and MongoDB

The locking specifics differ by engine, but the control plane is the same shape across all of them, and that is the point. A schema change is a privileged, hard-to-reverse operation no matter what speaks it.

  • Postgres. ALTER TABLE variants and index builds can take an ACCESS EXCLUSIVE lock; the gate flags the lock class and the queue risk so the reviewer sees the blast radius before approving.
  • MySQL. Online DDL reduces locking but still needs a brief exclusive metadata lock to commit, and an open transaction can stall it (MySQL docs). The gate surfaces the algorithm and the blocking conditions.
  • MongoDB. On older MongoDB versions a foreground index build could hold a collection-level write lock for its duration; modern versions build indexes under a less restrictive locking scheme, briefly taking stronger locks at the start and end (MongoDB docs). The gate treats the index build as the schema change it is.

The reviewer does not need to memorize each engine's lock matrix. The gate renders the risk in engine-specific terms and applies the same policy: classify the change, show the diff and impact, route to the right human, block until ratified, record the outcome. If you are weighing approaches, our comparison page lays out how an out-of-band control plane differs from agent-side and proxy-side alternatives.

Where Datapace fits

Datapace is the guardrail and audit layer that sits between your AI agents and your production databases, across Postgres, MySQL, MongoDB, and more. It intercepts every statement in the data path, classifies schema changes, renders an approvable view with the diff and impact, routes it to the right human, blocks execution until the exact statement is ratified, and writes the whole decision to an immutable log. Because the enforcement lives outside the agent's context, it holds even when the agent is wrong or compromised. If you want to see what a real approval gate for agent-run migrations looks like, book a call and we will walk through it on your stack.

Sources

Frequently asked questions

Why is a Slack approve button not enough to gate an agent-run migration?
A yes/no prompt with no detail makes the human ratify intent they cannot inspect. Real approval requires the exact statement that will execute, the locks it takes and what they block, the number of rows or documents affected, and a rollback plan. Without those, the reviewer is rubber-stamping rather than judging the change.
Why must the approval gate live outside the agent instead of in its prompt or tools?
If the rule to pause for approval is a system-prompt instruction or a tool the agent chooses to call, the agent's own reasoning enforces it, and that reasoning can be steered by indirect prompt injection, which OWASP notes does not even need to be human-readable. An out-of-band gate on the database connection sees the actual statement and refuses to forward it regardless of what the agent decides, so it survives a compromised or mistaken agent.
How is this different from LangGraph interrupt() or other framework human-in-the-loop features?
Framework interrupts are a cooperative pause inside the agent's runtime; they work because the agent reaches that node and honors it. They improve agent UX and are worth using. An out-of-band gate does not depend on the agent honoring anything, because it enforces in the data path. Use both, but only the data-path gate is a control you can put in a compliance document.
Does this approach work across Postgres, MySQL, and MongoDB?
Yes. The locking specifics differ per engine, Postgres ACCESS EXCLUSIVE locks, MySQL metadata locks during online DDL, MongoDB collection locks during index builds, but the control plane is the same shape. It classifies the schema change, renders the engine-specific risk, routes to the right approver, blocks until the exact statement is ratified, and records the decision in an immutable log.

Keep reading

Ready to let agents touch production, safely?

Bring a use case. We will show you what agents can do on your live data, inside your guardrails.