Of all the things we let agents do at LaraCopilot, database migrations scared me the most.
Everything else an agent writes is recoverable. A bad controller gets reverted. A wrong test fails loudly. But a migration that drops the wrong column runs once against production data and the column is gone. You don't revert your way out of that; you restore from backup and explain yourself. So when we started letting agents generate migrations, I treated schema changes as a different risk class from the rest of the code, and built the guardrails accordingly.
Here's the system that lets me sleep.
The core rule: additive by default, destructive by exception
Most migrations don't need to be dangerous. Adding a column, an index, a table: fully reversible, low blast radius. Renames and drops are where the damage lives.
So the agent operates under a standing rule baked into its context: prefer additive changes, and never destroy data as a side effect. Renaming a column becomes add-new, backfill, switch reads, drop-old, staged across separate migrations rather than a single renameColumn that rewrites a live table in place. Removing a column becomes a two-step deprecation: stop writing to it now, drop it in a later migration once nothing reads it.
This is exactly how a careful human does zero-downtime schema changes. We didn't invent a new discipline for the agent. We wrote down the one good engineers already follow and made it non-optional.
Destructive operations need a human hand on the switch
Some changes are genuinely destructive: dropping a column, dropping a table, changing a type in a way that can truncate. The agent is allowed to propose these. It is not allowed to run them unattended.
Any migration containing a drop, a destructive type change, or raw SQL gets flagged and routed to a human gate before it can touch anything beyond a disposable branch. The flag isn't a vibe; it's a check that parses the generated migration and looks for the specific operations we've classified as irreversible. Additive migrations flow through automatically. Destructive ones stop and wait for a click. The agent stays fast where fast is safe and stops where stopping matters.
Every migration is proven reversible before it counts
Laravel hands you a gift here: down(). A migration that can't cleanly roll back is a migration you can't trust.
So the pipeline runs the round trip on a throwaway database seeded to look like production's shape: migrate, then migrate:rollback, then migrate again. If the schema doesn't return to where it started, or the down method throws, the migration fails the check and never leaves the branch. This catches the classic agent mistake, a confident up() with a down() that's empty or wrong, before it can become a problem you discover during an incident.
Dry run against a production-shaped clone
Tests on an empty schema prove syntax. They don't prove the migration survives contact with real data volumes and real constraints.
Before anything reaches production, the migration runs against a clone with production's schema and representative data: enough rows to expose a missing index that would lock a large table, real foreign keys to catch a drop that violates a constraint, the actual column types. We watch two things: does it complete, and how long does it take. A migration that would hold a lock on a ten-million-row table for thirty seconds is not a bug the syntax checker can see, but it's exactly the kind of 2 a.m. incident I'm trying to design out.
The deploy gate is boring on purpose
By the time a migration reaches the point of running against production, it has: passed the additive-first review, cleared the destructive-operation gate if it needed to, proven its rollback on a throwaway database, and run clean against a production-shaped clone with timing in bounds. A person clicks approve on the deploy.
None of these steps is clever. That's the point. Clever is what fails at 2 a.m. The reason I sleep isn't that the agent is smart about migrations; it's that a migration has to survive four dumb, mechanical checks before it earns the right to run, and each one catches a different way schema changes go wrong.
What I'd tell you before you try this
If you're about to let an agent near your schema:
- Separate schema changes from everything else in your pipeline. They deserve their own risk class and their own gates. Treating a migration like an ordinary code change is the mistake.
- Make
down()a hard requirement, and verify it. An unverified rollback is a rollback that doesn't exist. - Test against data shaped like production, not against empty tables. The dangerous failures are all about volume, locks, and constraints, and none of them show up on a fresh schema.
- Keep a human on destructive operations for now. The gate costs seconds. The alternative costs a restore and a very bad afternoon.
Agents made a lot of our workflow faster. Migrations didn't get faster; they got safer, because we finally automated the caution that a tired human forgets to apply. That turns out to be the more valuable thing.
I'm building LaraCopilot, an AI development platform for Laravel, and writing about agentic systems and MCP as I go. If you're automating risky parts of your own pipeline, email me. I usually reply within a day.