fluid apply
Stage 7 of the 11-stage pipeline. Execute a FLUID contract (or a saved plan) end-to-end: provision infrastructure, run transformations, apply governance, and publish to configured destinations.
0.8.0 adds a 6-mode apply matrix (--mode) with explicit destruction gating (--allow-data-loss) and cryptographic plan-binding (bundleDigest / planDigest verification).
Why it matters Deploying a data product is safe and reversible — the plan you reviewed is the plan that runs, and destructive changes are gated.
fluid applyre-verifies thebundleDigest+planDigestbefore any DDL and refuses a tampered plan;--allow-data-lossis required for destructive operations.
Syntax
fluid apply CONTRACT
CONTRACT can be:
- A FLUID contract file (e.g.
contract.fluid.yaml) — plans and applies in one shot. - A saved plan JSON file (e.g.
runtime/plan.json) — applies the already-planned actions, with digest verification.
Apply mode (stage-7 dispatch)
--mode | What it does |
|---|---|
dry-run | Render the planned DDL without calling the warehouse. No state mutation. Safe in every environment. |
create-only | CREATE … IF NOT EXISTS, plus a pre-check that fails if the target already exists. Use when a fresh project needs a clean provision. |
amend (default) | ALTER TABLE ADD COLUMN IF NOT EXISTS; views CREATE OR REPLACE. Data preserved; new columns backfilled NULL. The everyday mode. |
amend-and-build | Same DDL as amend, plus dbt run / dbt test (or the configured build runner). Transforms refreshed on top of the amended schema. |
replace | Auto-snapshot the target, then CREATE OR REPLACE TABLE (Snowflake / BigQuery) or DROP+CREATE in a transaction (Redshift). Destructive: requires --allow-data-loss in non-dev environments or when the target has rows. |
replace-and-build | Same as replace, plus a full dbt run --full-refresh to rebuild everything from sources. Destructive. |
The build-augmented modes (amend-and-build, replace-and-build) run the configured build runner. Pass --build-id <id> to filter execution to a single build job from the contract's builds[]; when unset, every build runs.
Safety gates
| Option | Description |
|---|---|
--allow-data-loss | Required to run replace / replace-and-build when FLUID_ENV != dev or the target already has rows. Two independent risk surfaces (env + population) → two-factor opt-in. Never default. |
--no-verify-plan-binding | Emergency escape hatch. Skip the bundleDigest / planDigest verification that stage 7 normally enforces on a saved plan. Logged at WARNING so audit trails catch it. Use only during documented DR procedures. |
--no-verify-federation | Emergency escape hatch. Skip the federated-consumes[] upstream-digest gate (drift between a pinned upstreamDigest and the live upstream). Logged at WARNING for audit. A distinct trust domain from plan binding — each gate has its own narrowly-scoped waiver. |
--adopt-shared-container | (since 0.13.0) Confirm taking ownership of a container this contract previously referenced as a shared pool (packaging shared → isolated). Emits a structured packaging_adoption_override audit event; the data-loss gate still applies. See Packaging modes. |
Since 0.13.1, the structured override events these gates emit — opentofu_destructive_gate_override (--allow-data-loss) and packaging_adoption_override (--adopt-shared-container) — log at WARNING, so audit pipelines filtering at WARNING-and-above catch them. On 0.13.0 and earlier they logged at INFO; event names and payloads are unchanged.
Packaging modes
Opt-in, new in 0.13.0
Only relevant to contracts that declare fluidVersion: "0.7.6" and carry a packaging block. A contract without one resolves to the LEGACY sentinel, can never transition, and applies exactly as it did before 0.13.0.
A packaging block declares whether this product owns each infrastructure container (isolated) or writes into a pre-existing, platform-owned pool (shared). Full reference: fluid generate iac — Packaging modes.
Changing a container's mode changes who owns it, but OpenTofu only sees a resource that left the configuration and plans a destroy — on a shared pool that reaches every other tenant's data. So apply diffs the resolved ownership model against tofu state list before tofu plan:
| Transition | Behaviour |
|---|---|
isolated → shared (owned → referenced) | Always blocked — there is no flag. apply fails closed and prints copy-pasteable tofu -chdir=<workdir> state rm <address> commands. State surgery touches zero bytes of infrastructure; re-run apply afterwards. |
shared → isolated (referenced → owned) | Requires --adopt-shared-container. Without the gate, brownfield adoption would tofu import the platform's pool into this product's state with force_destroy restored — the exact blast radius the feature exists to close. |
# Blocked — drop the resource from state first, then re-run.
fluid apply runtime/plan.json --provider aws --yes
# → packaging_transition_blocked: aws_s3_bucket.data (owned → referenced)
# → tofu -chdir=.fluid/iac/aws/<product-id> state rm aws_s3_bucket.data
# Taking ownership of a previously-shared container (audited)
fluid apply runtime/plan.json --provider aws --yes --adopt-shared-container
The printed tofu state rm commands include -chdir pointing at the per-contract working directory (.fluid/iac/<provider>/<id>/), so they run against the right state without you having to find it.
Structured packaging_transition_blocked / packaging_adoption_override events are emitted for CI log scrapers. This guard runs earlier than, and is independent of, the data-loss gate — that gate remains the unconditional last line.
Plan binding
When you pass a saved plan (runtime/plan.json) instead of a contract, apply verifies:
bundleDigestinplan.jsonmatches the MANIFEST SHA-256 of the tgz bundle the plan was built from. Mismatch →PlanBindingError(kind="bundle-mismatch")before any DDL executes.planDigestinplan.jsonmatches a re-computed digest of the plan's action list (internal consistency check). Mismatch →PlanBindingError(kind="plan-tamper").
This is the Terraform-style "apply consumes exact plan" guarantee, enforced cryptographically.
Key options
General
| Option | Description |
|---|---|
--env | Apply an environment overlay (dev / staging / prod / …) |
Execution control
| Option | Description |
|---|---|
--yes | Skip confirmation |
--dry-run | Alias for --mode dry-run |
--ensure-opentofu | (since 0.8.8) If the tofu binary is missing, provision a pinned, SHA-256-verified OpenTofu build before a cloud apply — no root, gpg, cosign, curl, or unzip needed (Python stdlib only). Idempotent (a usable tofu at/above the engine floor is left untouched) and a no-op for native / local applies. Pin via FLUID_OPENTOFU_VERSION. fluid generate ci bakes this into the generated apply stage so cloud applies work on locked-down / non-root runners. |
--timeout TIMEOUT | Global timeout in minutes |
--parallel-phases | Execute independent phases in parallel |
--max-workers MAX_WORKERS | Maximum workers for parallel execution |
Safety and rollback
| Option | Description |
|---|---|
--rollback-strategy | none, immediate, phase_complete, or full_rollback |
--require-approval | Require explicit approval for destructive work |
--backup-state | Create a backup before execution |
--validate-dependencies | Validate dependencies before execution |
Reporting
| Option | Description |
|---|---|
--report | Output path for the execution report |
--report-format | Report format |
--metrics-export | Export metrics to monitoring backends |
--notify | Send notifications to destinations such as Slack or email |
Build execution
| Option | Description |
|---|---|
--build-id BUILD_ID | Filter build execution to a specific build job by ID from the contract's builds[]. Combine with --mode amend-and-build or --mode replace-and-build. When unset and the mode requires builds, every build runs. |
--delay DELAY | Seconds between build iterations |
--fail-fast | Stop on first failure |
--no-output | Suppress build script output |
Debugging and advanced
| Option | Description |
|---|---|
--verbose | Detailed progress output |
--keep-temp-files | Keep temporary files |
--workspace-dir | Custom workspace directory |
--state-file STATE_FILE | Custom state file location |
--config-override | Override contract config with JSON |
--provider-config | Path to provider-specific configuration |
Examples
Everyday dev workflow
# Quickstart — default --mode amend, no destructive action
fluid apply contract.fluid.yaml --yes
# Preview-only
fluid apply contract.fluid.yaml --mode dry-run
fluid apply contract.fluid.yaml --dry-run # same thing
Production 11-stage pipeline (plan-bound)
# Stage 6 produces the plan with bundleDigest + planDigest
fluid plan contract.fluid.yaml --out runtime/plan.json
# Stage 7 verifies both digests before executing any DDL
fluid apply runtime/plan.json --mode amend --env prod --yes
Destructive modes (explicit opt-in required)
# Prod replace — REQUIRES --allow-data-loss (two-factor opt-in)
fluid apply runtime/plan.json --mode replace --env prod --yes --allow-data-loss
# Full rebuild (dbt --full-refresh + destructive DDL)
fluid apply runtime/plan.json --mode replace-and-build --env prod --yes --allow-data-loss
After a replace, use fluid rollback to restore from the auto-snapshot if something goes wrong.
Build-augmented apply (dbt run)
fluid apply runtime/plan.json --mode amend-and-build --env dev --yes
DR escape hatch (skip digest verification — audit-logged)
# Only in documented DR procedures. Emits WARNING to the audit log.
fluid apply runtime/plan.json --mode amend --no-verify-plan-binding --yes
# Skip only the federated-consumes upstream-digest gate
fluid apply runtime/plan.json --mode amend --no-verify-federation --yes
Notes
- The recommended sequence is
bundle → validate → plan → apply. - For local-first onboarding,
fluid apply contract.fluid.yaml --yesis the shortest path after a quickstart scaffold — default--mode amendis safe. --mode replace/replace-and-buildalways create an auto-snapshot before destructive DDL. On Snowflake this is a zero-copyCLONE; on BigQuery it'sbq cp --force; on Redshift it'sCREATE TABLE _backup AS SELECT *. Snapshot names are recorded in.fluid/rollback-state.json.- If apply's provider dispatcher logs
unknown_action_opfor your contract's actions, the provider doesn't yet implement the abstract op. This is a known gap for some high-level ops (e.g.provisionDataset,scheduleTask) and is addressed by a translator layer inproviders/<platform>/. - For Iceberg exposes,
applyprovisions the prerequisites dbt refuses to create — the SnowflakeEXTERNAL VOLUMEand AWS GlueCATALOG INTEGRATION(since 0.13.1), and the GCS warehouse bucket on GCP (since 0.14.0). Names are derived by the same deterministic helper thatfluid generateuses for dbt'scatalogs.yml, so the objects apply creates are exactly the ones dbt references. See the Snowflake and GCP provider pages. - (since 0.14.0) Apply hooks run on the OpenTofu path too, so
--envplumbing reaches Snowflake / AWS / GCP cloud applies. Previously hooks ran only on the native apply path.
Extension point: apply hooks
As of 0.8.3, fluid apply runs any apply hook plugins registered via Python entry-points before invoking the providers. Use apply hooks to enforce runtime invariants that can't be checked at validate time — required env vars, image signatures, bundle-digest drift, business-hours gating, anything that depends on the deploy environment rather than the contract content.
A hook that appends an error aborts the apply with exit code 1. Pass --force-pattern-drift to downgrade all hook errors to WARNINGs (audit-logged) and let the apply proceed.
- Author a hook: SDK & Plugins → Apply hook journey
- Reference: Entry points →
fluid_build.apply_hooks - Example:
prod-key-guard