Core Pipeline Commands
These commands cover a Rocky pipeline’s whole life. You create the project, check the config, and see what the source holds. Then you plan the SQL, apply the plan, and read back what ran.
The two commands to know first are rocky plan and rocky apply. rocky plan prints the SQL it generates and writes a plan file with an id. For a run plan that file holds the settings the run needs, not the SQL. rocky apply reads the file and recompiles the project to run it. rocky run fuses both into one step for local work.
Global Flags
Section titled “Global Flags”The global flags (--config, --output, --state-path, --state-namespace, --principal, --principal-id, --cache-ttl) apply to every command. Put --config, --state-path and --state-namespace before the subcommand: rocky --config prod.toml run, not rocky run --config prod.toml. See Global flags for the defaults.
--state-namespace
Section titled “--state-namespace”| Flag | Type | Default | Description |
|---|---|---|---|
--state-namespace <KEY> |
string |
(none) | Route this invocation to its own <models>/.rocky-state/<KEY>.redb so concurrent fan-out runs (one per pipeline / client / tenant) don’t serialize on redb’s single-writer lock. Opt-in, default-off: with neither this flag nor [state] namespacing set, behavior is byte-identical to the single global state file. <KEY> must be a SQL identifier (^[a-zA-Z0-9_]+$). An explicit --state-path disables namespacing for that invocation; otherwise this flag wins over the [state] namespacing config. See State namespacing. |
rocky init
Section titled “rocky init”Create a new Rocky project from a template. The command refuses when the directory already holds a rocky.toml.
rocky init [path] [--template <NAME>]Arguments
Section titled “Arguments”| Argument | Type | Default | Description |
|---|---|---|---|
path |
string |
. |
Project directory. Rocky creates it when it does not exist. |
| Flag | Type | Default | Description |
|---|---|---|---|
--template <NAME> |
string |
duckdb |
One of the templates below. Connection values are ${VAR} placeholders, never inline secrets. |
| Template | What it writes |
|---|---|
duckdb |
rocky.toml, models/_defaults.toml, a sample models/stg_orders model, and seeds/seed.sql sample data. |
databricks-fivetran |
rocky.toml for a Fivetran-to-Databricks pipeline, and an empty models/. |
snowflake |
rocky.toml for Snowflake, and an empty models/. |
bigquery |
rocky.toml, models/_defaults.toml, and a models/welcome model. |
trino |
rocky.toml, models/_defaults.toml, and a models/welcome model. |
Examples
Section titled “Examples”rocky initInitialized Rocky project in .
rocky.toml — pipeline config (DuckDB) models/_defaults.toml — shared model defaults models/stg_orders.* — sample transformation model seeds/seed.sql — sample data
Quick start: duckdb playground.duckdb < seeds/seed.sql rocky validate rocky compile --models models/Create a Trino project in a new directory:
rocky init acme-trino --template trinoThe Trino rocky.toml reads ${TRINO_HOST}, ${TRINO_USER} and ${TRINO_PASSWORD} (HTTP Basic), or ${TRINO_JWT} (a JWT bearer token). Comments in the file describe both modes.
Related Commands
Section titled “Related Commands”rocky validate– check the generated configrocky playground– a sample DuckDB project with no credentials
rocky validate
Section titled “rocky validate”Check the pipeline configuration. rocky validate connects to no external API and exits non-zero if any check fails.
rocky validate [flags]No command-specific flags. Uses global flags only.
Checks Performed
Section titled “Checks Performed”The main checks are below. Each finding carries a V code.
| Check | Description |
|---|---|
| TOML syntax | The config file parses without errors as v2 (named adapters + named pipelines). |
| Adapters | Each [adapter.NAME] has a known type with its required fields set. The known types are databricks, duckdb, snowflake, bigquery, trino, postgres, redshift, clickhouse, sqlserver, spark, fivetran, airbyte, iceberg and manual. An unknown type is V017. For Databricks, set token or client_id/client_secret. |
| Pipelines | Each [pipeline.NAME] references existing adapters for source, target, and (optional) discovery, and its schema_pattern parses. |
| DAG validation | If models/ exists, loads all models and checks for dependency cycles. |
Examples
Section titled “Examples”Validate the default config:
rocky validateok Config syntax valid (v2 format)ok adapter.fivetran: fivetranok adapter.prod: databricks (auth configured)ok pipeline.bronze: schema pattern parseableok pipeline.bronze: replication / incremental -> warehouse / stage__{source}
Validation complete.Validate a specific config file:
rocky -c pipelines/prod.toml validateok Config syntax valid (v2 format)ok adapter.fivetran: fivetran!! adapter.prod: no auth configured (token or client_id/secret)ok pipeline.bronze: schema pattern parseableok pipeline.bronze: replication / incremental -> warehouse / stage__{source}
Validation complete.Related Commands
Section titled “Related Commands”rocky plan– preview SQL without executionrocky run– execute the full pipeline
rocky discover
Section titled “rocky discover”List the connectors and tables the configured source exposes. Discover reads metadata only. It reports which schemas and tables exist. It moves no data.
rocky discover [flags]| Flag | Type | Default | Description |
|---|---|---|---|
--pipeline <NAME> |
string |
Pipeline name (required if multiple pipelines are defined). | |
--with-schemas |
bool |
false |
Warm the schema cache for every discovered source. Rocky issues one batch_describe_schema call per (catalog, schema) pair and stores the columns in the state store. Later rocky compile and rocky lsp runs read those entries instead of typing leaf models as Unknown. A source that errors is logged and skipped, so one bad source does not abort the warm-up. |
--no-cache |
bool |
false |
Takes effect only together with --emit-fivetran-state-to. On its own the flag changes nothing about discovery. It makes Rocky fetch the Fivetran state envelope straight from the API and skip the read caches configured under [adapter.<name>.cache]. A successful fetch still writes back to that cache. Use it when you suspect that cache is stale, for example after rotating a Fivetran credential. It does not touch the schema cache or the state store, and an open circuit breaker still short-circuits the fetch. |
--emit-fivetran-state-to <PATH> |
PathBuf |
Write a canonical Fivetran state envelope for every Fivetran adapter in the config. See Emitting the Fivetran state envelope. |
Examples
Section titled “Examples”Discover all sources with JSON output:
rocky discover{ "version": "1.6.0", "command": "discover", "sources": [ { "id": "connector_abc123", "components": { "tenant": "acme", "regions": ["us_west"], "source": "shopify" }, "source_type": "fivetran", "last_sync_at": "2026-03-30T10:00:00Z", "tables": [ { "name": "orders", "row_count": null }, { "name": "customers", "row_count": null } ] }, { "id": "connector_def456", "components": { "tenant": "acme", "regions": ["eu_central"], "source": "stripe" }, "source_type": "fivetran", "last_sync_at": "2026-03-29T22:15:00Z", "tables": [ { "name": "charges", "row_count": null }, { "name": "refunds", "row_count": null } ] } ]}Discover with table output:
rocky -o table discoverconnector_id | components | tables------------------+-------------------------------------+-------connector_abc123 | acme / us_west / shopify | 12connector_def456 | acme / eu_central / stripe | 8Discover a specific pipeline when multiple are defined:
rocky discover --pipeline shopify_usEmitting the Fivetran state envelope
Section titled “Emitting the Fivetran state envelope”--emit-fivetran-state-to <PATH> writes one state envelope per Fivetran adapter declared in rocky.toml. The file layout depends on how many Fivetran adapters the config declares.
one Fivetran adapter two or more Fivetran adapters────────────────────── ─────────────────────────────────────────────<PATH> <STEM>.<account_hash>.<destination_id>.json<PATH>.blake3 <STEM>.<account_hash>.<destination_id>.json.blake3 │ │ │ │ content hash │ │ Fivetran │ of the envelope │ │ destination id └─ rewritten only when └─ short token derived from the the hash changes account, so two adapters that share a destination id name do not race on the same file<STEM> is <PATH> with a trailing .json removed. The per-destination segments land before the extension. So --emit-fivetran-state-to state.json writes state.<account_hash>.<destination_id>.json, never state.json.<account_hash>.<destination_id>.json.
The write is idempotent. The sibling .blake3 file records the envelope’s content hash. If the freshly computed hash matches the value on disk, Rocky leaves the JSON file alone. A stat(2) watcher therefore only fires when the upstream Fivetran state actually changed.
A connector whose connectors/{id}/schemas endpoint returns 404 is left out of the envelope’s schemas map and logged at WARN. It still appears under connectors with its status fields. Discover exits non-zero only when every connector returns 404, so the envelope’s connector count does not always match the Fivetran UI total.
New sources and cross-source collisions
Section titled “New sources and cross-source collisions”Two opt-in discover-time signals help catch onboarding problems before any catalog is created. Both are configured under [pipeline.NAME.source.discovery] and appear as extra fields on the JSON output (omitted entirely when not enabled).
new_sources— setreport_new_sources = trueto diff the discovered inventory against the prior persisted snapshot. First-seen source schemas are listed here; the first discover of a pipeline records the baseline and reports nothing.collision_candidates— seton_collision = "warn"(or"error") to flag the same external object onboarded under more than one schema. Each entry pairs the sharedexternal_object_idwith thesources(schemas) it resolves to. With"error", discover also exits non-zero. Only adapters that resolve external object ids (e.g. Fivetran) populate this.
{ "command": "discover", "sources": [ /* … */ ], "new_sources": ["src__acme__ca_central__shopify"], "collision_candidates": [ { "external_object_id": "act_1234567890", "sources": ["src__acme__us_west__shopify", "src__acme__eu_central__shopify"] } ]}collision_candidates is the preventive half of cross-source duplicate detection; its detective counterpart, cross_source_overlap, runs at rocky run time against the materialized tables.
Related Commands
Section titled “Related Commands”rocky plan– generate SQL from discovered sourcesrocky run– discover and execute in one step
rocky plan
Section titled “rocky plan”Generate the replication SQL Rocky would run, without running it. The command needs a replication pipeline; it refuses a transformation-only project. It connects to the source to discover tables, then prints the copy SQL for each one. It does not print a transformation model’s SQL. Add --model <name> for that, or use rocky emit-sql. Rocky writes the plan to .rocky/plans/<plan-id>.json and prints the plan_id. For a run plan that file holds the settings the run needs, not the SQL. A reviewer reads the printed SQL. Then rocky apply <plan-id> recompiles the project and executes it with those settings.
rocky plan plus rocky apply is the canonical path for production and for gating a pull request. Nothing touches the warehouse between the two steps. For local iteration, rocky run does the same work in one command and writes no plan file.
With --model, a compile error in that model or one reached through depends_on or a bare-name read refuses the plan. Without --model, Rocky compiles the whole models directory. An error in a model from another pipeline also refuses the plan. Rocky prints each relevant model and diagnostic code, exits with code 1, and writes no plan file or plan_id. This applies to text and JSON output. Warnings alone do not stop the plan.
rocky plan [flags]rocky plan promote <branch> [flags]The plan, review, apply lifecycle
Section titled “The plan, review, apply lifecycle” models/ + rocky.toml │ ▼ ┌──────────────┐ writes the plan ┌────────────────────────────┐ │ rocky plan │───────────────────►│ .rocky/plans/<plan-id>.json│ └───────┬──────┘ prints plan_id │ run plan: settings, not SQL│ │ └─────────────┬──────────────┘ prints replication SQL │ │ │ └──► a human reads it ────────────────────┤ ▼ ┌───────────────────────────┐ approval gate ───►│ rocky review <plan-id> │ (policy only) │ --approve │ └─────────────┬─────────────┘ │ review marker ▼ ┌───────────────────────────┐ │ rocky apply <plan-id> │ └─────────────┬─────────────┘ ▼ warehouseOn this path the gate is optional. rocky plan writes a run or a replication plan, and rocky plan promote writes a promote plan. None of those three is gated by its kind. A [policy] rule that resolves to require_review turns the gate on. Without a [policy] block, these plans go straight from rocky plan to rocky apply.
Four plan kinds are always gated, whatever the policy: ai_authored, backfill, gc, and restore. Other commands write them: the MCP propose tool, rocky backfill, rocky gc --derivable, and rocky restore. See rocky review.
Every flag below applies to the default rocky plan form, not to rocky plan promote. The set overlaps rocky run without matching it. rocky plan adds --semantic, --intent, --base and --cost-estimate, which rocky run does not have. rocky run has several flags that rocky plan does not, including --watch, a re-run loop with no plan to persist. --parallel also defaults to 1 here, against 4 for a rocky run without --dag and no default at all for one with it.
Rocky records the execution flags in the plan file, so rocky apply replays the same intent. The recorded set is:
- selection:
--filter,--pipeline,--model,--models,--all,--env - partitions:
--partition,--from,--to,--latest,--missing,--lookback,--parallel - routing:
--shadow,--shadow-suffix,--shadow-schema,--branch,--dag - other:
--governance-override,--resume,--resume-latest,--idempotency-key
--semantic and --base are not recorded. They only add the breaking_verdict field to the rocky plan output, so rocky apply never repeats that classification.
--intent is recorded as the intent key of the plan. The verdict that checks it is not recorded. It goes only into the intent_check field of the rocky plan output. A plan without --intent has no intent key, so its plan_id does not change.
| Flag | Type | Default | Description |
|---|---|---|---|
--filter <key=value> |
string |
Filter sources by component value (e.g., client=acme). |
|
--pipeline <NAME> |
string |
Pipeline name (required if multiple pipelines are defined). | |
--model <NAME> |
string |
Plan a single compiled model by name and skip replication. An alternative to --filter for model-only execution. |
|
--select <SELECTOR>..., -s / --exclude <SELECTOR>... / --state-ref <REF> |
string |
Node selection. The selection must resolve to exactly one model, which is then planned like --model. |
|
--models <PATH> |
PathBuf |
Models directory for transformation execution. | |
--all |
bool |
false |
Plan both replication and compiled models. |
--governance-override <JSON> |
string |
Additional governance config as inline JSON or @file.json, merged with the defaults. Resolved at plan time and stored in the plan. |
|
--resume <RUN_ID> |
string |
Resume a specific previous replication run from its last checkpoint. Mints a new run_id and records the prior one as resumed_from. Rejected with --dag, which does not replay the resume into its sub-runs (rejected at parse time). |
|
--resume-latest |
bool |
false |
Resume the most recent failed replication run from its last checkpoint. Which run that is gets resolved at apply time, not plan time. Rejected with --dag, which does not replay the resume into its sub-runs (rejected at parse time). |
--shadow |
bool |
false |
Write to shadow targets instead of production. |
--shadow-suffix <SUFFIX> |
string |
_rocky_shadow |
Requires --shadow. Appends a suffix to table names. Conflicts with --branch. --shadow alone uses _rocky_shadow. |
--shadow-schema <NAME> |
string |
Requires --shadow. Overrides the schema for shadow tables. Conflicts with --branch. |
|
--branch <NAME> |
string |
Plan against a branch created with rocky branch create. Equivalent to --shadow --shadow-schema <branch.schema_prefix>. Conflicts with --shadow, --shadow-schema, and --shadow-suffix. |
|
--partition <KEY> |
string |
Plan one partition by its canonical key (2026-04-07 for daily, 2026-04 for monthly). Errors if the format does not match the model’s granularity. Mutually exclusive with --from, --to, --latest, --missing. |
|
--from <KEY> |
string |
Lower bound of a closed partition range, inclusive. Requires --to. Both bounds must align to the model’s grain. |
|
--to <KEY> |
string |
Upper bound of a closed partition range, inclusive. Requires --from. |
|
--latest |
bool |
false |
Plan the partition containing now (UTC). The default for a time_interval model when no other selection flag is given. |
--missing |
bool |
false |
Plan the partitions missing from the state store, computed from the model’s first_partition up to now. Errors if first_partition is unset. Resolved against the state store at apply time. |
--lookback <N> |
integer |
Also recompute the previous N partitions. The flag overrides the model’s TOML lookback. This is the standard handling for late-arriving data. |
|
--parallel <N> |
integer |
1 |
Run N partitions at a time. Warehouse-query parallelism only: state writes serialize through the state store. |
--dag |
bool |
false |
Plan all pipelines as one DAG in dependency order. Each pipeline is a node, cross-pipeline depends_on edges set the order, and layers run in parallel. Mutually exclusive with --resume / --resume-latest: the DAG runner does not consume them, so a plan carrying both would apply as a fresh run of every pipeline. With --principal agent, the plan’s fingerprint and review cover every pipeline’s models. |
--idempotency-key <KEY> |
string |
$ROCKY_IDEMPOTENCY_KEY |
Opaque caller-supplied key that dedups this run against prior runs with the same key. Works on every state backend: local, valkey, tiered, s3 and gcs. The first claim is race-free. Recovery from a crashed claim is best-effort: two callers can both adopt one stale claim. Keys are stored as given, so never put a secret in one. |
--env <NAME> |
string |
Scope the governance preview (mask_actions) to one environment, so [mask.<env>] overrides overlay the workspace [mask] defaults. Classification tagging and retention policies are the same in every environment and are previewed regardless. |
|
--semantic |
bool |
false |
Also run the breaking-change classifier against --base and attach the change-impact verdict under breaking_verdict. Decision-support only — never gates the plan and never changes the exit code. |
--intent <INTENT> |
string |
Experimental. State what the change is meant to do, and check it on the data. The only value is refactor. See Check a refactor with --intent. |
|
--base <ref> |
string |
main |
Git ref the working tree is compared against. --semantic and --intent use it. The change classification that every run plan carries uses it too. |
--cost-estimate <MODE> |
string |
heuristic |
How cost_preview is estimated. heuristic is offline and never contacts the warehouse. adapter asks the warehouse with EXPLAIN. See Cost preview. Not recorded in the plan. |
Cost preview
Section titled “Cost preview”When the plan covers transformation models, the JSON output has a cost_preview object. It shows what applying the plan would rebuild, and roughly what it would cost, before any model runs.
| Field | Meaning |
|---|---|
is_estimate |
Always true. Every figure is an estimate. |
source |
heuristic, adapter, or mixed when the adapter could not estimate some models. Each row in models has its own source. |
models_to_rebuild |
The rebuild scope: the number of models the plan rebuilds. |
estimated_bytes_scanned, estimated_cost_usd |
Sums over every model. A total is absent when any model lacks the figure. |
previous_cost_usd |
The observed cost of the same models in the last successful production run, priced from the state store. Absent when any model has no such run. |
cost_delta_usd |
estimated_cost_usd - previous_cost_usd. Set only when every model has an adapter estimate and a previous cost. |
notes |
Why a figure is missing, for example a failed EXPLAIN. |
The default heuristic uses the same cost model as cost_hint in rocky compile. It assumes placeholder statistics for source tables, so its confidence is always low. Use it to compare plans, not to set a budget. A DuckDB target costs 0. --cost-estimate adapter runs EXPLAIN for each planned model, as rocky estimate does. A model the adapter cannot estimate falls back to the heuristic, with a note.
The preview is report-only. It never changes the planned models, the budget check or the exit code, and it is not part of plan_id.
The
--semanticverdict diffs output schema only and is blind to schema-stable value changes (aWHERE/JOIN-key /CASErewrite that changes values but not the schema). An emptyfindingslist is not a safety signal: the verdict’scaveatfield states this verbatim. See the CI/CD guide for the full flow and theplanschema for theSemanticPlanVerdictshape.
Check a refactor with --intent
Section titled “Check a refactor with --intent”Experimental. --intent refactor says that a change keeps the output of every changed model. Rocky checks that claim on the data and reports one verdict per model. A changed model is one whose compiled form differs between --base and the working tree.
--base ref ──► base SQL ──┐ one DuckDB transaction per model ├──► build base 6 times, head 2 times working tree ► head SQL ──┘ compare schema, then rows roll back: nothing is keptThe check passes when the schema is the same and the two outputs hold the same rows. The schema is the column names, types and order. Rows are compared as multisets (a duplicate row counts). Equality is exact, with no tolerance for floats.
| Verdict | Reason | Meaning |
|---|---|---|
match |
The same schema and the same rows on the current data. | |
mismatch |
schema_differs |
A column name, type or position changed. Both schemas are in the output. |
mismatch |
rows_differ |
The rows differ. The output gives the row counts and the rows only on each side. |
mismatch |
model_removed |
The model exists at --base and not in the working tree. A renamed model shows its old name here and its new name as no_base. |
unverified |
nondeterministic |
The SQL reads the clock, a random value, a UUID, a sequence, the session or the environment. Or it has a LIMIT with no ORDER BY. Or two builds of the same SQL differ. |
unverified |
no_base |
The model is new. |
unverified |
base_unavailable |
Rocky could not read or compile the models at --base. |
unverified |
adapter_unsupported |
The target is not DuckDB. Rocky runs no query. |
unverified |
unsupported_strategy |
The model is incremental, time_interval, microbatch or ephemeral. |
unverified |
build_failed |
A build or a compare query failed, or the SQL holds more than one statement. detail has the reason. |
Know these limits before you trust a match:
- A
matchmeans no difference on the current data. It is not a proof for other data. AWHEREchange that no current row reaches still givesmatch. - Rocky checks the
SELECToutput only. It does not check masks, classifications, targets, the strategy or other model config. When the target or the strategy also changed, amatchsays so indetail. - When both outputs are empty, a
matchsays so indetail. The current data did not exercise the model. - Each model reads the upstream tables that already exist in the warehouse. When an upstream model also changed, the verdict lists it in
upstream_changed. Per-model matches do not add up to a match for the whole chain. - The verdict is report-only. It relaxes no gate, it does not replace review, and it never changes the exit code. The flag itself can fail the command: with
--dag, with no models directory, or when the working tree does not compile. rocky planneeds a replication pipeline inrocky.toml, so the check runs only in a project that has one. It pays for the extra builds on your DuckDB database.- The plan records
intent: refactoras the author’s claim. The plan does not record the verdict, so a stored plan never shows the claim as checked.
The --intent flag is not the free-text intent = "..." key in a model sidecar. That key describes a model for the AI commands. --intent states what one change does.
rocky plan --intent refactor --base main --output jsonrocky plan promote
Section titled “rocky plan promote”Plan a branch promotion. Rocky runs the approval gate and the breaking-change gate now, then stores a promote plan that rocky apply <plan-id> executes later. The gates are not re-run at apply time, which is what makes “plan in the pull request, apply on merge” work.
rocky plan promote <branch> [flags]Arguments
Section titled “Arguments”| Argument | Type | Default | Description |
|---|---|---|---|
branch |
string |
(required) | Branch name to promote. |
| Flag | Type | Default | Description |
|---|---|---|---|
--base <ref> |
string |
main |
Git ref the breaking-change gate diffs against. |
--allow-breaking |
bool |
false |
Bypass the breaking-change gate. Always records a BreakingChangesAllowed audit event in the plan, so the override leaves a paper trail. |
--filter <key=value> |
string |
Filter the promote targets. A transformation pipeline supports the keys table, model, catalog, and schema. |
|
--pipeline <NAME> |
string |
Pipeline to plan against. Required when rocky.toml defines more than one pipeline. |
|
--models <PATH> |
PathBuf |
models |
Models directory used by the breaking-change gate. |
On success the output is a PlanOutput with plan_kind: "promote" and the plan_id you pass to rocky apply.
Examples
Section titled “Examples”Plan all SQL for a specific tenant:
rocky plan --filter client=acme{ "version": "1.6.0", "command": "plan", "filter": "client=acme", "statements": [ { "purpose": "create_catalog", "target": "acme_warehouse", "sql": "CREATE CATALOG IF NOT EXISTS acme_warehouse" }, { "purpose": "create_schema", "target": "acme_warehouse.staging__us_west__shopify", "sql": "CREATE SCHEMA IF NOT EXISTS acme_warehouse.staging__us_west__shopify" }, { "purpose": "incremental_copy", "target": "acme_warehouse.staging__us_west__shopify.orders", "sql": "SELECT *, CAST(NULL AS STRING) AS _loaded_by FROM source_catalog.src__acme__us_west__shopify.orders WHERE _fivetran_synced > TIMESTAMP '1970-01-01 00:00:00'" } ]}A model excluded from the preview or refused by compilation is listed in skipped:
"skipped": [ { "model": "stg_events", "reason": "invalid SQL generation request: model 'stg_events': `type = \"ephemeral\"` renders no statement of its own — it is inlined as a CTE into each model that reads it, so there is nothing to build (E038 when selected directly)" } ]The reason gives the compiler diagnostic or SQL generation error. An ephemeral model is always there: its SQL appears inside each consumer’s statement instead. A model that needs a live warehouse, such as a Snowflake dynamic table, is there too. The MCP plan_preview tool reports preview exclusions. The key is absent when nothing was skipped.
Plan with table output and a custom config:
rocky -c pipelines/prod.toml -o table plan --filter client=acmepurpose | target | sql (truncated)------------------+----------------------------------------------------+--------------------------create_catalog | acme_warehouse | CREATE CATALOG IF NOT...create_schema | acme_warehouse.staging__us_west__shopify | CREATE SCHEMA IF NOT...incremental_copy | acme_warehouse.staging__us_west__shopify.orders | SELECT *, CAST(NULL...incremental_copy | acme_warehouse.staging__us_west__shopify.customers | SELECT *, CAST(NULL...Plan for a specific pipeline:
rocky plan --filter client=acme --pipeline shopify_usPlan one model, then execute the stored plan. --filter is optional, so a model-scoped plan needs no source filter:
rocky plan --model fct_revenue --models models/rocky apply <plan-id>Related Commands
Section titled “Related Commands”rocky apply– execute a stored planrocky run– plan and execute in one steprocky emit-sql– read a transformation model’s SQL, whichrocky plandoes not printrocky validate– check config before planningrocky discover– see available sourcesrocky review– sign off on a gated plan
rocky apply
Section titled “rocky apply”Execute a plan that rocky plan, rocky compact, rocky archive, rocky backfill, rocky gc, or rocky restore already generated. Rocky reads .rocky/plans/<plan-id>.json and dispatches on the plan’s kind.
rocky apply <plan-id>Arguments
Section titled “Arguments”| Argument | Type | Default | Description |
|---|---|---|---|
plan-id |
string |
(required) | Plan identifier: the 64-character blake3 hex string the planning command printed. |
| Flag | Type | Default | Description |
|---|---|---|---|
--expect-spec-digest <DIGEST> |
string |
(none) | Refuse unless the plan payload’s spec_digest equals this value. The compare is an exact string compare; Rocky never parses the spec itself. |
The check is fail-closed in both directions, and it runs before any policy gate. A plan that carries a product identity (product_id + spec_digest in its payload) refuses a bare rocky apply — the flag is required for it. The flag against a plan with no spec_digest refuses too. A mismatch refuses and names both digests. A plan without product fields, applied without the flag, behaves exactly as before.
What each plan kind executes
Section titled “What each plan kind executes”The plan file carries its kind. Apply reads that kind and takes one path.
plan_kind |
Generated by | What apply does |
|---|---|---|
run |
rocky plan |
Re-executes the full pipeline with the flags the plan captured. |
replication |
rocky plan on a project with no compiled models |
Re-runs discovery and compares the result against the plan-time snapshot, then executes. See the staleness rules below. |
promote |
rocky plan promote |
Runs the promote statements. The approval and breaking-change gates ran at plan time and are not repeated. |
compact |
rocky compact |
Runs the OPTIMIZE and VACUUM statements. |
archive |
rocky archive |
Runs the DELETE and VACUUM statements. |
ai_authored |
an AI agent, via the propose MCP tool |
Same execution path as a run plan, but only after a review marker exists. |
backfill |
rocky backfill |
Rebuilds the scoped model set. Always needs a review marker. |
gc |
rocky gc --derivable |
Evicts each artifact. Always needs a review marker. |
restore |
rocky restore |
Rebuilds each evicted artifact. Always needs a review marker. |
What apply checks for staleness
Section titled “What apply checks for staleness”Staleness checking is per plan kind. The three kinds below differ in what they compare, and in whether a difference stops the apply.
replication. Apply re-runs discovery and compares the result against the snapshot taken at plan time. A difference aborts an unfiltered apply and tells you to re-plan. When the plan carries a --filter, a difference outside the filter scope logs a warning and the apply continues. A difference inside the filter scope still aborts.
run. Apply reloads rocky.toml and recompiles the current models, then executes those. It does not replay SQL stored in the plan. So it first checks the plan’s models. A plan records a fingerprint of the models it runs, and a second one of those models plus the config they run under. If a model was added, removed or edited after the plan was made, apply refuses with plan_models_changed: “models changed since this plan was made; plan again”. The same check covers ai_authored and backfill plans. It runs after the policy and review gates and before any warehouse statement.
- A person’s apply compares the models only. A plan made in one shell and applied from another environment (for example a
rocky servejob), or after an edit to another pipeline, still applies. It does not re-check that config ([run],[hook], governance), so a config edit made after approval applies too. The one exception is[mask]: where the run applies masks (a replication plan made with--allor--models), a change to the strategy of a tag those models classify with, as resolved for the plan’s--env, refuses withplan_models_changed. A${VAR}inside a model’s SQL or sidecar is part of the models: a different value refuses withplan_models_changed. The models can still change between this check and the run’s own compile. - An agent’s apply (
--principal agent, orROCKY_PRINCIPAL=agent) compares the models and the config. If only the config changed, it refuses withplan_config_changed. It also checks the routing identity, and repeats the fingerprint check when the models execute.ai_authoredandbackfillplans always apply as an agent. rocky review --approvecompares the models only, for every plan kind, the same way a person’s apply does. A plan approved from another environment, such as the browser UI’srocky serve, is not refused when its models did not change. If a model changed, the approval refuses: “the models changed since this plan was written”. The config of anai_authoredorbackfillplan is still checked when it applies, as an agent.- A plan whose models did not compile at plan time has no fingerprint and is not checked. Nor is a snapshot, load, quality or replication-only plan.
- A plan made before this release refuses with
plan_snapshot_missing. Plan it again, and review it again if it needs approval.
A review-gated --dag plan (agent-authored, or AI-authored) is checked across the whole DAG. Apply checks the models and the config they run under; rocky review --approve checks the models only. Its fingerprint and its approval cover every model the DAG runs: each transformation pipeline’s models, from that pipeline’s own directory and file glob. The fingerprint also covers the project’s seeds (data, sidecar and seed hooks). Before the DAG runs, apply recomputes that fingerprint and refuses if any model or seed was added, removed or changed. If the models are unchanged but the config they run under changed, it refuses with plan_config_changed. A credential such as token or password is not part of that check; a connection field such as host or path is, by its resolved ${VAR} value. An agent applying a --dag plan is still refused, because the DAG’s sub-runs are not yet policy-gated.
promote. The plan stores the branch-state hash that rocky plan promote computed. Apply does not recompute it, and it does not re-run the approval or breaking-change gates. Those ran at plan time, which is what makes “plan in the pull request, apply on merge” work. The [policy] gate described below still runs at apply time.
rocky apply gates on the identity the command runs under. Set that identity with the global --principal flag, which takes human or agent and defaults to human. The ROCKY_PRINCIPAL environment variable sets it too, and may only raise it to agent. Rocky combines the runtime identity most restrictively with the plan kind’s own default. The kinds ai_authored, backfill, and gc default to agent, so a policy rule scoped to agents applies to them even when nobody set a principal. That includes an interactive rocky gc run by a person. It never trusts the principal field stored inside the plan file. An agent running rocky apply is gated as an agent whatever that field says. Without a [policy] block in rocky.toml, the principal changes nothing.
Examples
Section titled “Examples”Plan, read the SQL, then apply:
rocky plan --filter client=acmerocky apply <plan-id>Apply an AI-authored plan. The bare apply is refused until an approval marker names that plan:
rocky review <plan-id> --approverocky apply <plan-id>Output
Section titled “Output”rocky apply writes no wrapping envelope. Each plan kind’s apply path prints its own output. Read the top-level command field to tell them apart.
command field |
Plan kinds | Shape |
|---|---|---|
run |
run, replication, ai_authored, backfill |
RunOutput |
compact apply |
compact |
CompactApplyOutput |
archive apply |
archive |
ArchiveApplyOutput |
branch promote |
promote |
BranchPromoteOutput |
apply (has evicted) |
gc |
GcApplyOutput |
apply (has restored) |
restore |
RestoreApplyOutput |
The gc and restore outputs share the command value apply. Tell them apart by which marker field is present.
Related Commands
Section titled “Related Commands”rocky plan– generate a run planrocky run– plan and apply in one steprocky review– sign off on a gated planrocky history– see what an apply recorded
rocky run
Section titled “rocky run”
rocky rundoes in one step whatrocky planplusrocky applydo in two. Use it for local iteration and automation. Use the two-step form for production and for gating a pull request, where someone needs to read the SQL first.
Run the whole pipeline end to end: discover the sources, detect schema drift, create catalogs and schemas, copy the data, apply governance, and run the quality checks.
rocky run [flags]| Flag | Type | Default | Description |
|---|---|---|---|
--filter <key=value> |
string |
Filter sources by component value (e.g., client=acme). |
|
--pipeline <NAME> |
string |
Pipeline name (required if multiple pipelines are defined). | |
--model <NAME> |
string |
Execute a single compiled model by name and skip replication. An alternative to --filter for model-only execution. |
|
--select <SELECTOR>..., -s / --exclude <SELECTOR>... / --state-ref <REF> / --state-working-tree |
string |
Build the selected models and skip replication. Unselected upstreams are read as they exist. Not with --dag, --watch, --all, --filter, or --resume. |
|
--contracts <PATH> |
PathBuf |
Contracts directory for this run. Default: the project contracts/ directory. With --model and --pipeline, the selected model must have a contract there. Not with --watch. |
|
--governance-override <JSON> |
string |
Additional governance config as inline JSON or @file.json, merged with defaults. |
|
--models <PATH> |
PathBuf |
Models directory for transformation execution. | |
--all |
bool |
false |
Execute both replication and compiled models. |
--resume <RUN_ID> |
string |
Resume a specific previous replication run from its last checkpoint; mints a new run_id and records the prior one as resumed_from. Rejected with --dag or --shadow. |
|
--resume-latest |
bool |
false |
Resume the most recent failed replication run from its last checkpoint; mints a new run_id and records the prior one as resumed_from. Rejected with --dag or --shadow. |
--shadow |
bool |
false |
Write to shadow targets, compare them with production, then drop them after any completed verdict. A failed threshold exits non-zero. |
--keep-shadow |
bool |
false |
Requires --shadow and conflicts with --watch. Keep shadow objects for a separate rocky compare. |
--shadow-suffix <SUFFIX> |
string |
_rocky_shadow |
Requires --shadow. Appends a suffix to table names. Conflicts with --branch. --shadow alone uses _rocky_shadow. |
--shadow-schema <NAME> |
string |
Requires --shadow. Overrides the schema for shadow tables. Conflicts with --branch. |
|
--branch <NAME> |
string |
Execute against a named branch previously registered with rocky branch create. Applies the branch’s schema_prefix to every target (internally equivalent to --shadow --shadow-schema <branch.schema_prefix>). Conflicts with --shadow, --shadow-schema, and --shadow-suffix. The run records <NAME> as rocky_branch in run history. |
|
--partition <KEY> |
string |
Run one partition by its canonical key (2026-04-07 for daily, 2026-04 for monthly). Mutually exclusive with --from, --to, --latest and --missing. |
|
--from <KEY> / --to <KEY> |
string |
Closed partition range, both bounds inclusive. Each requires the other. Both bounds must align to the model’s grain. | |
--latest |
bool |
false |
Run the partition that contains now (UTC). The default for a time_interval model when no other partition flag is given. |
--missing |
bool |
false |
Run the partitions missing from the state store, from the model’s first_partition up to now. Errors if first_partition is unset. |
--lookback <N> |
integer |
Also recompute the previous N partitions, for late-arriving data. Overrides the model’s TOML lookback. |
|
--dag |
bool |
false |
Run all pipelines as one DAG in dependency order. Each pipeline is a node, cross-pipeline depends_on edges set the order, and layers run in parallel. Rejected with --resume and --resume-latest. |
--idempotency-key <KEY> |
string |
$ROCKY_IDEMPOTENCY_KEY |
Skip this run when a run with the same key already succeeded (status = "skipped_idempotent"), or holds the key now (skipped_in_flight). Both exit 0. Works on every state backend. Recovery from a crashed claim is best-effort: two callers can both adopt one stale claim. Rejected with --resume and --resume-latest. Keys are stored as given, so never put a secret in one. See [state.idempotency]. |
--env <NAME> |
string |
Apply the [mask.<env>] overrides on top of the [mask] defaults in the post-run governance pass. Classification tags and retention are the same in every environment. |
|
--watch |
bool |
false |
Wrap the run in a filesystem watcher: re-execute the pipeline on every change to rocky.toml or any file under models/, debounced to 200 ms so editor save bursts coalesce into a single re-run. Failed runs do not exit the loop; Ctrl-C exits cleanly between runs. v0 limitations: mutually exclusive with --dag, --resume, --resume-latest, --idempotency-key, and --model (rejected at parse time). |
--defer |
bool |
false |
Build only the --model-selected models locally, resolving unbuilt upstream models to an existing (production) schema — the dbt-Core-style defer convenience. Takes effect only together with --model: a full run builds everything, so the flag is inert. Applies to transformation models; mutually exclusive with --dag. See the limitation note below. |
--defer-to <SCHEMA> |
string |
Schema the deferred upstream models resolve to. Requires --defer. Defaults to each unbuilt upstream’s own configured target schema (its production home); pass this to point every deferred reference at a single schema instead (catalog + table are preserved). |
|
--defer-to-state <PATH> |
PathBuf |
Saved production state store file the deferred upstreams resolve from. Requires --defer; conflicts with --defer-to. Each unbuilt upstream a selected model reads resolves to the table the newest successful production run recorded for it. Opened read-only. Refuses before any write when the store is missing, has an incompatible state schema version, or has no recorded table for a needed upstream. See Defer to a saved production state. |
|
--defer-run-id <RUN_ID> |
string |
Read deferred upstreams only from this production run in the --defer-to-state store. Requires --defer-to-state. |
|
--skip-unchanged |
bool |
false |
Turn on the model-skip gate for this invocation regardless of the [run] skip_unchanged config: skip re-materializing a transformation model whose logic and every upstream’s data both appear unchanged. Best-effort optimization, not a result-equivalence guarantee — non-deterministic SQL and models without provably-complete lineage (CTEs, subqueries, PIVOT/UNNEST, set operations) always rebuild. See [run] for the full eligibility rules. |
--force-rebuild |
bool |
false |
Force every selected model to build. Bypasses the --skip-unchanged gate and the content-addressed column-level skip. Use it after a change the IR hash cannot see, such as a UDF redefinition or a session setting. |
--no-reuse |
bool |
false |
Turn off content-addressed reuse for this run, even when [reuse] is on. Every content-addressed model builds. Also turns off the [reuse] column_level skip. |
--no-prune |
bool |
false |
Force a full replication pass: turn off prune_unchanged for this run, so every table is copied again. Use it after a manual change to a target. No effect when prune_unchanged is off. |
--full-refresh |
bool |
false |
Rebuild transformation incremental models with CREATE OR REPLACE TABLE ... AS. Every @incremental_filter becomes TRUE, so the table holds the model’s full result. Other strategies are unaffected: a merge or delete_insert model’s SQL often selects only recent rows, and rebuilding from it would drop history. The flag also turns off the --skip-unchanged gate. |
--refuse-hooks |
bool |
false |
Refuse the run when the config defines any [hook] command or [hook.webhooks] entry that would fire, before anything fires or any model builds. For CI jobs that run a config they do not trust, such as a pull request’s rocky.toml. Not supported with --watch. |
--var <name=value> |
string (repeatable) |
Bind a per-run variable substituted into model SQL wherever an @var(name) / @var(name, default) marker appears. Repeat for multiple variables. Distinct from config-time ${ENV} substitution: @var() resolves the run’s logical inputs at compile time, ${ENV} resolves connection/config values while parsing rocky.toml. A model that references @var(name) with no --var binding and no inline default fails to compile, naming the missing variable. See @var() run variables. |
|
--assume-fresh-state |
bool |
false |
Disaster recovery only, and audited. Treat a failed download of remote state as a deliberate fresh start, so the run proceeds with an empty ledger and uploads turned back on. Requires a remote [state] backend (s3, gcs, valkey or tiered). Rejected with --dag and --watch. Without it, a failed download refuses auto-applies and suppresses the run’s state uploads. |
--parallel <N> |
integer |
4 without --dag |
Models in a topological layer (and partitions of a time_interval model) run up to N at a time. Pass --parallel 1 to run one model or partition at a time. Under --dag the flag bounds how many pipeline nodes run at once, and it has no default there: left unset, a --dag run keeps its unbounded node fan-out. It does not bound a replication pipeline’s table fan-out, which comes from that pipeline’s [execution] concurrency (default 32), so --parallel 1 alone does not make a replication run serial. DuckDB always runs serially regardless of this flag (its adapter holds a single connection mutex); Snowflake and Databricks parallelize up to N. |
Recovering replication checkpoints
Section titled “Recovering replication checkpoints”--resume and --resume-latest refuse a checkpoint whose every planned table
copied but whose terminal run record is missing. Resuming that checkpoint would
skip its post-copy checks. Incomplete crash checkpoints remain resumable.
Follow the refusal’s recovery route. A matching fresh run copies targets from a complete checkpoint without a run record, even with unchanged source markers. It runs checks and writes a new run record. Rocky then marks the old checkpoint superseded, so the next run can prune unchanged targets. A recorded check failure also supersedes it.
Matching requires the same target endpoint and a plan containing every target in the checkpoint. An old checkpoint alone never fails a fresh run. With supported recovery records, Rocky re-derives target watermarks before copying. #2235 stays open for remaining recovery gaps.
For checkpoints from Rocky 1.75.0 or earlier, set strategy = "full_refresh"
for affected tables. Run rocky run --pipeline <name> --no-prune without a
resume flag. That replaces their data without duplicate appends. Keep full
refresh until rocky state reconcile-watermark --pipeline <name> sets the
cursor from the replacement target’s maximum timestamp. Use --dry-run to
preview it and repeat --table catalog.schema.table to select targets. A crash
under Rocky 1.75.0 or earlier may leave a stale cursor after an INSERT. The
first run after upgrading may append those rows again. Repair the cursor before
that run if the old flush is uncertain. See
Interrupted replication
for recovery routes, supported adapters and remote durability limits.
Contracts on every run
Section titled “Contracts on every run”Every rocky run reads the project contracts/ directory, with no flag. That is the contracts/ directory beside the models directory, usually next to rocky.toml. rocky compile, rocky ci and rocky test read the same directory.
The run checks each contract in the compile that supplies the model SQL. This applies to --dag, --pipeline, --all, --select and --model. When a model has a contract error (E010 to E013):
- Rocky does not write that model. Its existing table stays.
- Rocky does not run the models downstream of it.
- The run exits non-zero. Under
--dag, the failed node’serrornames the code, for examplefirst error: fct_orders: [E012] ....
A contract can declare a column type that Rocky cannot infer from the SQL alone, because the source table has no known schema. The compile reports that as I003. The run then reads that source’s columns from the warehouse and compiles again, so it checks the type before it writes. If the warehouse cannot describe the source, the type stays unchecked. With [contracts] strict = true in rocky.toml, a type that stays unchecked after that is the E059 error and the run refuses it before it writes.
--contracts <DIR> replaces the project directory for one run. A contract in the project directory for a model that the compile does not include is skipped, because one directory serves every pipeline. A contract in an explicit --contracts directory for an unknown model is still the W011 warning.
Guard one model with a contract
Section titled “Guard one model with a contract”Use --contracts with --model and --pipeline when you build one full_refresh transformation model and want the run to require its contract:
rocky run --pipeline transform --model int_order_lines --contracts contracts -o jsonThe directory must exist and contain int_order_lines.contract.toml. Rocky checks that contract in the compile that supplies the model SQL for this run. An error such as E010 fails the run before Rocky replaces the selected table. The failure JSON includes the compile error and has no materialization for that model.
This guard does not cover a branch or a deferred run. It refuses selected models that use a strategy other than full_refresh or add a surrogate_key after compilation. It also refuses idempotency and skip options that could report success without rebuilding. A failed contract can still update run history or state synchronization. I003 means Rocky could not infer a declared column type and did not check that type; required-column checks still run.
Pipeline Stages
Section titled “Pipeline Stages” ┌──────────────┐ ── enumerate the sources and their tables │ discover │ └──────┬───────┘ ▼ ┌──────────────┐ ── one catalog or schema at a time: create │ governance │ catalogs, apply tags, bind workspaces, │ setup │ grant permissions, create schemas └──────┬───────┘ ▼ ┌──────────────┐ ── up to `execution.concurrency` tables at │ tables │ once: drift detection, incremental copy, │ (parallel) │ tag application, watermark update └──────┬───────┘ ▼ ┌──────────────┐ ── row count, column match, and freshness, │ checks │ batched into one pass └──────┬───────┘ ▼ ┌──────────────┐ ── failed tables only, one at a time, up to │ retry │ `execution.table_retries` └──────────────┘Examples
Section titled “Examples”Run the pipeline for a specific tenant:
rocky run --filter client=acme{ "version": "1.6.0", "command": "run", "filter": "client=acme", "duration_ms": 45200, "tables_copied": 20, "tables_failed": 0, "materializations": [ { "asset_key": ["fivetran", "acme", "us_west", "shopify", "orders"], "rows_copied": null, "duration_ms": 2300, "metadata": { "strategy": "incremental", "watermark": "2026-03-30T10:00:00Z", "target_table_full_name": "acme_warehouse.staging__us_west__shopify.orders" } } ], "check_results": [], "errors": [], "excluded_tables": [], "permissions": { "grants_added": 3, "grants_revoked": 0, "catalogs_created": 0, "schemas_created": 1 }, "drift": { "tables_checked": 20, "tables_drifted": 1, "actions_taken": [] }, "anomalies": [], "partition_summaries": []}Run with a governance override file:
rocky run --filter client=acme --governance-override @overrides/acme.jsonRun both replication and model transformations:
rocky run --filter client=acme --models models/ --allResume the most recent failed replication run from its last checkpoint:
rocky run --filter client=acme --resume-latestRun in shadow mode. Rocky compares each shadow target with production before it drops the shadow object:
rocky run --filter client=acme --shadowKeep the shadow objects when you need a separate comparison:
rocky run --filter client=acme --shadow --keep-shadowrocky compare --filter client=acme--keep-shadow retains objects after a failed verdict too. A write or comparison query error may leave an object whose state Rocky cannot confirm.
When a shadow or branch run is refused
Section titled “When a shadow or branch run is refused”Shadow mode is only useful if it truly isolates the run from production. Rocky refuses the run rather than write a target it cannot isolate. A shadow or branch run fails closed in any of these cases.
- The selected transformation set contains a
content_addressedortime_intervalmodel. Both need extra storage or partition-state isolation that shadow mode does not give them. - The chosen suffix or schema would collide with a production target, or with another selected shadow target.
- All three of the following hold at once:
- the dialect treats identifier case as part of object identity (Snowflake and BigQuery);
- the run routes more than one model;
- a routed target differs from any other model’s target only by case.
That last case deserves a word. To such a warehouse the two targets are distinct objects. Rocky matches upstream references case-insensitively, so a read of either could land on the wrong one. The refusal does not depend on whether a model spells such a read today. Rename one of the targets, or scope the run so it routes only one of them.
This rule is not about whether the dialect quotes identifiers. Rocky renders Trino targets double-quoted, yet treats two Trino targets that differ only by case as one object, so such a run proceeds there.
--shadow and --branch isolate rocky run for transformation pipelines, and for replication pipelines in either mode. Rocky refuses both flags on rocky run --dag, and on quality, snapshot and load pipelines. A refused quality, snapshot or load run stops before it opens a warehouse adapter, writes to the state store, or claims an idempotency key, so it writes nothing. See Shadow mode for the reasons and for how to quality-check a branch.
Or run against a named branch:
rocky branch create fix_price --description "testing reprice migration"rocky run --filter client=acme --branch fix_priceRun in watch mode for the inner-loop developer workflow, where every save re-materializes the pipeline against the local DuckDB warehouse:
rocky run --watch--watch watches the parent directory of rocky.toml (filtered to rocky.toml itself) plus the resolved models/ directory recursively. The directory watch is FSEvents-safe on macOS: atomic-rename saves (vim’s :w, VSCode’s default) trigger correctly where a file-level watch can miss the new inode. Banner / “detected change” lines go to stderr so stdout stays parseable; with --output json, each iteration emits one RunOutput JSON object on stdout (newline-delimited).
Related Commands
Section titled “Related Commands”rocky plan– preview SQL before executionrocky state– inspect watermarks after a runrocky branch– manage named branchesrocky history– view past runs
rocky state
Section titled “rocky state”Show the watermarks stored in the embedded state file. A watermark is the newest source value Rocky has already loaded for a table, so the next run knows where to resume. The output lists every tracked table, its last watermark value, and the time Rocky recorded it.
rocky staterocky state show # the same, named explicitlyrocky state also has maintenance subcommands: reconcile-watermark, clear-schema-cache, retention sweep and schedule. See rocky state in Administration.
Examples
Section titled “Examples”Show watermarks with JSON output:
rocky state{ "version": "1.6.0", "command": "state", "watermarks": [ { "table": "acme_warehouse.staging__us_west__shopify.orders", "last_value": "2026-03-30T10:00:00Z", "updated_at": "2026-03-30T10:01:32Z" }, { "table": "acme_warehouse.staging__us_west__shopify.customers", "last_value": "2026-03-30T09:55:00Z", "updated_at": "2026-03-30T10:01:32Z" } ]}Show watermarks with table output using a custom state path:
rocky -o table --state-path /var/rocky/state.redb statetable | last_value | updated_at-----------------------------------------------------+---------------------------+---------------------------acme_warehouse.staging__us_west__shopify.orders | 2026-03-30T10:00:00Z | 2026-03-30T10:01:32Zacme_warehouse.staging__us_west__shopify.customers | 2026-03-30T09:55:00Z | 2026-03-30T10:01:32Zacme_warehouse.staging__eu_central__stripe.charges | 2026-03-29T22:15:00Z | 2026-03-30T10:01:32ZRelated Commands
Section titled “Related Commands”rocky run– update watermarks by executing the pipelinerocky history– view run history
rocky freshness
Section titled “rocky freshness”Check how fresh each declared source and model is, against the warehouse. This is Rocky’s form of dbt source freshness. It reads data only and writes nothing.
rocky freshness [flags]Rocky checks two kinds of declaration:
- Sources. Each
[[pipeline.NAME.sources]]entry with afreshnessblock. Rocky runsSELECT COUNT(*), MAX(loaded_at_field) FROM <table> [WHERE (<filter>)]. - Models. Each model with a
[freshness]block. With atime_column, Rocky readsMAX(time_column)from the model’s target table. Without one, Rocky uses the model’s last successful build from the state store.
The age is the check time minus the newest load time. Rocky grades it:
| Status | When |
|---|---|
pass |
The age is within every threshold. |
warn |
The age is greater than warn_after. Also when the newest load time is more than 60 seconds in the future: the age cannot be trusted. |
error |
The age is greater than error_after. |
runtime_error |
Rocky could not measure: invalid config, a failed query, or a value that does not read as a timestamp. |
A table with no rows, or with only NULL load times, counts as never loaded. The worst threshold it declares applies. A model’s max_lag_seconds is its warn threshold. Under severity = "error" it is the error threshold instead.
Exit code. 1 when any check is error or runtime_error. 0 otherwise, including when checks only warn.
rocky run does not call this command. To stop a run on stale sources, run rocky freshness first and stop on a non-zero exit.
A model can inherit time_column from _defaults.toml or the project [freshness] block. When Rocky cannot read an inherited column on that model, it measures the last successful build instead and says so in message. A time_column in the model’s own sidecar gets no fallback: a failed read is runtime_error.
Limits.
- Run history records a model build under its bare target table name. Two models whose targets share a table name in different schemas share that history. For a model without a
time_column, the newer build of the two can hide a stale one. Set atime_columnto measure the table itself. - With
[state] namespacing = "pipeline", the state-store fallback reads the global state file, so it may find no build. - Exit
1covers both a stale check and a command failure (for example a config that does not load). A command failure prints no JSON report. - A load time with no time zone is read as UTC. A column filled in local time is off by the zone’s offset. A SQL Server
DATETIMEfilled withGETDATE()is the common case. Fill it withSYSUTCDATETIME(), or use a zoned type such asDATETIMEOFFSET. - Snowflake temporal cells arrive as epoch numbers and are decoded as such. That decoding follows the documented SQL API format and is not yet verified against a live account.
| Flag | Type | Default | Description |
|---|---|---|---|
--pipeline <NAME> |
string |
Check one transformation pipeline. By default Rocky checks every transformation pipeline. A replication pipeline is refused: it checks freshness with [checks.freshness] during rocky run. |
Examples
Section titled “Examples”rocky freshness{ "version": "1.76.0", "command": "freshness", "checked_at": "2026-10-04T12:00:00Z", "sources": [ { "name": "raw.orders", "pipeline": "silver", "table": "raw.orders", "loaded_at_field": "_loaded_at", "measured_from": "warehouse", "max_loaded_at": "2026-10-03T23:00:00Z", "age_seconds": 46800, "warn_after_seconds": 43200, "error_after_seconds": 86400, "status": "warn" } ], "models": [], "summary": { "pass": 0, "warn": 1, "error": 0, "runtime_error": 0 }}Related Commands
Section titled “Related Commands”rocky compile– checks the declarations (E050,W050)rocky run– builds the models; does not check freshness
rocky branch
Section titled “rocky branch”Manage named virtual branches. A branch is the persistent, named form of shadow mode. Creating one records a schema_prefix in the state store. Every later run that names the branch applies that prefix to each model target. That holds whether you run rocky plan --branch <name> plus rocky apply <plan-id> or the one-step rocky run --branch <name>. Schema-prefix branches behave the same on every adapter today. Warehouse-native clones (Delta SHALLOW CLONE, Snowflake zero-copy CLONE) are a follow-up.
rocky plan accepts replication pipelines only. A transformation pipeline uses rocky run --branch <name>.
rocky branch create <name> [--description <text>]rocky branch delete <name>rocky branch listrocky branch show <name>rocky branch compare <name> [--filter <key=value>] [--pipeline <name>]rocky branch approve <name> [--message <text>] [--out <path>]rocky branch promote <name> [--allow-breaking] [--base-ref <ref>] [--models <path>] [--skip-approval] [--pipeline <name>] [--filter <key=value>]rocky branch promote <name> --plan <plan-id> [--pipeline <name>] # canonical: plan + applyBranch names accept 1–64 [A-Za-z0-9_] characters. Rocky refuses other characters at the command entry point and suggests underscores. The default schema prefix is branch__<name>. Deleting a branch removes its state-store entry but leaves its warehouse tables.
Target names have their own limit. branch promote quotes each name in the strategy’s CREATE OR REPLACE TABLE or VIEW statement. Quoting is not escaping. Promote refuses a name that contains the warehouse’s identifier quote and names the character.
| Warehouse | Identifier quote | Also refused |
|---|---|---|
| DuckDB, Snowflake, Trino | " |
|
| Databricks | ` |
|
| BigQuery | ` |
\ — BigQuery reads escape sequences inside a quoted identifier, so a trailing backslash consumes the closing quote |
No other character is refused by this quote check. A hyphen or a dot in a target name passes it. Branch names follow the stricter rule above. A backslash passes it everywhere except BigQuery.
Two limits sit outside this check and still apply:
- A transformation model that takes its schema from its group’s
schema_templatefollows[A-Za-z0-9_]too. This applies with or without a placeholder. A hyphen or a dot is refused while Rocky builds the plan, before promote quotes anything. - Promote also refuses a plan in which two steps replace the same production table, whatever the names look like.
The check runs when a promote plan is built and again when one is applied, because a plan stores its statement as ready-made text.
branch compare flags
Section titled “branch compare flags”| Flag | Type | Default | Description |
|---|---|---|---|
--filter <key=value> |
string |
(none) | Filter the compared targets. See the shared filter reference. |
--pipeline <name> |
string |
(none) | Which pipeline to compare, in a multi-pipeline project. Required when the project defines more than one pipeline. |
branch approve flags
Section titled “branch approve flags”| Flag | Type | Default | Description |
|---|---|---|---|
--message <text> |
string |
(none) | Optional free-form note persisted in the approval artifact. |
--out <path> |
PathBuf |
./.rocky/approvals/<branch>/<approval_id>.json |
Override the artifact destination path. |
Writes a content-addressed approval artifact that binds the approver’s git identity to the exact bytes of the branch’s models and project config. Editing, adding, or renaming any model after approval voids that approval, so rocky branch promote refuses to run unless the on-disk approvals still match the current state and satisfy the [branch.approval] policy.
Upgrade note (engine v1.43): approvals created before v1.43 bound to the project config only, not the model bytes. They no longer satisfy the gate after upgrading. Run
rocky branch approve <name>once to re-sign each branch against its current model contents.
branch promote flags
Section titled “branch promote flags”The canonical form is
rocky plan promote <name>followed byrocky apply <plan-id>(orrocky branch promote <name> --plan <plan-id>). The barerocky branch promote <name>form still works as an alias. It prints a one-line[deprecated]notice to stderr, whichROCKY_SUPPRESS_DEPRECATION=1silences.
| Flag | Type | Default | Description |
|---|---|---|---|
--plan <plan-id> |
string |
(none) | Execute a promote plan written by rocky plan promote. The gates already ran at plan time. |
--allow-breaking |
flag | off | Bypass the semantic breaking-change gate. Always emits a breaking_changes_allowed audit event so the override leaves a paper trail. |
--base-ref <ref> |
string |
main |
Git ref to diff against for the breaking-change gate. |
--models <path> |
PathBuf |
models |
Models directory used by the breaking-change gate. |
--skip-approval |
flag | off | Bypass the approval gate. Always emits an approval_skipped audit event so the bypass leaves a paper trail. |
--pipeline <name> |
string |
(none) | Which pipeline to promote, in a multi-pipeline project. Optional when the project defines a single pipeline, or when --plan names a promote plan that already recorded one — omit it to use the plan’s pipeline. A value that disagrees with the plan’s recorded pipeline is refused. |
--filter <key=value> |
string |
(none) | Filter the promote targets. Replication pipelines filter sources by schema-pattern component (e.g. --filter client=acme); transformation pipelines filter models by table, model, catalog, or schema. |
rocky branch promote enumerates the pipeline’s production targets. A replication pipeline finds the source connector’s tables through the schema-pattern templates. A transformation pipeline walks the configured models glob. Rocky runs the optional [branch.approval] gate and the semantic breaking-change gate against --base-ref.
Rocky promotes full_refresh tables by copying their branch results. It creates production views from the model SQL with production upstreams. It refuses all other strategies because their promotion is undefined. Replication promotion supports full_refresh only. Quality and snapshot pipelines are unsupported.
Before the first replacement, Rocky checks every branch source and production destination. It verifies that each destination schema exists. It also checks the kind of each existing destination object. These checks work on DuckDB, Databricks, Snowflake, BigQuery, and Trino. If a later write fails, Rocky does not roll back earlier replacements. Text and JSON output identify replaced, failed, and unattempted targets. The command exits non-zero.
The breaking-change gate vetoes the promote and exits non-zero when any finding has severity == "breaking", unless you pass --allow-breaking. Rocky records every gate decision in the audit trail: a block, an allow via override, and a fail-open when the gate could not run. To surface the same findings on every pull request without blocking, use rocky ci-diff --semantic.
Examples
Section titled “Examples”Create, list, run against, and delete a branch:
rocky branch create fix_price --description "testing reprice migration"{ "version": "1.11.0", "command": "branch create", "branch": { "name": "fix_price", "schema_prefix": "branch__fix_price", "created_by": "hugo", "created_at": "2026-04-20T14:22:11+00:00", "description": "testing reprice migration" }}rocky branch list{ "version": "1.11.0", "command": "branch list", "total": 2, "branches": [ { "name": "fix_price", "schema_prefix": "branch__fix_price", "created_by": "hugo", "created_at": "2026-04-20T14:22:11+00:00", "description": "testing reprice migration" }, { "name": "ingest_v2", "schema_prefix": "branch__ingest_v2", "created_by": "ci", "created_at": "2026-04-18T09:05:00+00:00", "description": null } ]}rocky run --filter client=acme --branch fix_pricerocky branch delete fix_priceDiff a branch’s materialized tables against production (row counts + schemas):
rocky branch compare fix_pricerocky branch compare fix_price --pipeline shopify_us # multi-pipeline projectInternally this is rocky compare pointed at the branch’s schema_prefix via ShadowConfig.schema_override, the same mechanism rocky run --branch uses for writes, so compare always hits exactly the tables the branch produced. Accepts the shared --filter flag, and --pipeline <name> to select the pipeline in a multi-pipeline project.
When Rocky confirms that production has no target, it reports verdict: "no_baseline". That target does not fail the run. An uncertain metadata read reports verdict: "error" and fails the command. Its unreadable row count is null, never 0. See rocky compare.
Related Commands
Section titled “Related Commands”rocky run– execute a pipeline against a branch viarocky run --branchrocky compare– diff an ad-hoc shadow against production (the generic formrocky branch comparespecialises)