Liquibase Preconditions: Gating Changesets for Safer Multi-Environment Rollouts
Schema drift breaks deployments when changelogs assume a single path. Liquibase preconditions check actual database state before running a changeset, letting one changelog adapt across environments—if you choose onFail actions carefully.
12 Oct 2026, 10:13 UTC

Your CI pipeline is green, staging looks fine, and production fails because someone manually added a column three weeks ago. The DATABASECHANGELOG table says the changeset never ran, but the database disagrees. Liquibase preconditions solve this by checking actual schema state at runtime instead of trusting the changelog history alone.
Preconditions are not an environment branching system. They are a runtime gate: before a changeset executes, Liquibase evaluates a condition against the target database and decides whether to proceed, skip, or halt. Used deliberately, they let a single changelog stay idempotent across dev, test, and production. Used carelessly, they hide drift.
The problem: when DATABASECHANGELOG lies
Liquibase records which changesets it has executed in the DATABASECHANGELOG table. That table tracks Liquibase's actions, not the actual schema. A DBA may have applied a hotfix directly, a restore may have recovered a partial snapshot, or two teams may have run divergent changelogs against the same database. Contexts and labels help you select which changesets run in which environment, but they do not verify that the schema matches your assumptions.
Preconditions close that gap. They run a query or metadata check against the target database immediately before the changeset. If the condition fails, the configured onFail behavior decides what happens next.
Preconditions as a runtime gate
Common precondition types include tableExists, columnExists, dbms, and sqlCheck. They are evaluated in order, and they reflect the live database, not the changelog history. This makes them useful for heterogeneous fleets where schema state may diverge, but it also means they add a runtime check to every guarded changeset.
Worked example: idempotent column addition
A frequent source of deployment failure is adding a column that already exists in one environment but not another. The following pattern guards the change with both a table check and a column check, and uses onFail="MARK_RAN" so reruns are idempotent.
<changeSet id="add-status-to-orders" author="platform-team">
<preConditions onFail="MARK_RAN">
<tableExists tableName="orders"/>
<not>
<columnExists tableName="orders" columnName="status"/>
</not>
</preConditions>
<addColumn tableName="orders">
<column name="status" type="varchar(20)" defaultValue="pending"/>
</addColumn>
<rollback>
<dropColumn tableName="orders" columnName="status"/>
</rollback>
</changeSet>
If the orders table exists and the status column does not, the column is added. If the column already exists, the changeset is skipped and recorded as ran in DATABASECHANGELOG, so subsequent deployments do not retry it.
Running and checking the result
Run these commands from the directory containing your changelog and liquibase.properties file. You need read access for status and write access for update.
liquibase status --defaults-file=liquibase.properties
liquibase update --defaults-file=liquibase.properties
After the update, confirm the outcome in your database client with read access:
SELECT id, exectype FROM DATABASECHANGELOG
WHERE id = 'add-status-to-orders';
An exectype of EXECUTED means the column was added. MARK_RAN means the precondition failed and Liquibase recorded the changeset as ran without applying it.
Choosing onFail as a deployment policy
The onFail attribute turns a technical check into a team policy. The choice depends on whether a mismatch is a benign idempotency case or a signal that something is wrong.
| onFail value | Behavior | When to use it |
|---|---|---|
HALT | Stops the deployment immediately. | Structural changes where drift indicates a real problem. |
WARN | Logs a warning and continues to the next changeset. | Non-critical checks where you want visibility without blocking. |
CONTINUE | Skips the changeset but does not mark it ran. | When you want Liquibase to retry the changeset on the next run. |
MARK_RAN | Skips the changeset and records it as executed. | Idempotent guards for known drift, such as the column example above. |
The risk with WARN and CONTINUE is that they can mask missing migrations. If a precondition fails because the schema is in an unexpected state, a warning in the logs is easy to miss during a busy release. Reserve MARK_RAN for cases where skipping is the correct behavior, not a silent fallback.
Limitations and how to verify
Preconditions add runtime overhead because each guard issues metadata or SQL queries against the target database. They also do not replace rollback design: a precondition controls whether a changeset runs, but it does not undo a change that already executed. Your <rollback> block remains the only compensating mechanism.
SQL-based preconditions such as sqlCheck are database-specific. A query that works on PostgreSQL may fail or behave differently on MySQL or SQL Server. Test any SQL precondition against each dialect you support. The behavior described here is stable across recent Liquibase 4.x releases; confirm the exact syntax against your installed version.
Before promoting to production, test the changelog against a copy of the production schema in staging. Run liquibase status with the same parameters your CI pipeline uses, then run liquibase update and inspect DATABASECHANGELOG to confirm which changesets were executed, skipped, or marked ran.
Actionable takeaways
- Use preconditions at known drift boundaries, not as a general substitute for changelog discipline.
- Default to
HALTfor structural changes; useMARK_RANonly when idempotency is the explicit goal. - Verify precondition behavior in staging against a production schema copy before promoting.
- Keep
<rollback>blocks complete; preconditions gate execution but never compensate for it.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.