Skip to content

JSON Output

Rocky’s JSON output is the contract between Rocky and whatever calls it — Dagster, a CI job, your own script. Every command that accepts --output json is backed by one typed Rust struct. The generated Python and TypeScript bindings come from that same struct, and the codegen-drift CI check fails a pull request where they drift apart. See JSON contract for the stability rules.

Every response carries a top-level version field naming the engine release that produced it. Rocky sets it at compile time from the binary’s own version, so rocky --output json always reports the version you are running. The examples on this page pin a representative version string for readability; your output shows whichever engine you installed.

Compatibility contract:

  • New fields ship in a minor release and do not break an existing consumer.
  • Removing or renaming a field is breaking, and happens only in a major release.
  • Parse defensively and ignore fields you do not recognize.

The canonical machine-readable schemas live in schemas/*.schema.json, written by rocky export-schemas. Validate against those.

Read the exit code alongside the JSON, because a run can be part-successful:

Code Meaning
0 Success.
1 Hard failure — bad config, unreachable warehouse.
2 rocky run: the run finished and something in it failed. Either some models failed, or an error-severity check failed on a run that had already copied data. rocky tick: a run it launched failed or came back partial. rocky fulfill: the loop is blocked and needs a human. rocky product verify: the product failed verification.
3 rocky doctor: at least one health check is critical. rocky fulfill: the loop is parked at applying_unknown.
4 rocky fulfill only: the plan applied and its output is failing a check the product declared.
130 rocky run only: you interrupted the run with Ctrl-C.

A quality pipeline’s failed check gate exits 1, not 2.

rocky ci and exit_code. The JSON exit_code is the code the process exits with: 0 or 1. Advisory warnings do not change it. To act on warnings, read the "severity": "Warning" entries in diagnostics.

Exit 2 still writes valid JSON to stdout, so parse the payload rather than treating a non-zero code as no output.

Throughout the output, asset_key arrays follow the format:

[source_type, ...component_values, table_name]

For example, a Fivetran source with tenant acme, region us_west, connector shopify, and table orders produces:

["fivetran", "acme", "us_west", "shopify", "orders"]

The key maps straight onto an orchestrator’s own asset identifier, such as Dagster’s AssetKey.


discover (a read) and run (a write) are shown in full below as representative payloads. Every other command returns the same versioned envelope; see Every other command for where each one is documented.

Returns all discovered sources and their tables.

{
"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 },
{ "name": "products", "row_count": null }
]
}
],
"failed_sources": [
{
"id": "connector_flaky456",
"schema": "src__acme__us_west__hubspot",
"source_type": "fivetran",
"error_class": "transient",
"message": "schema fetch failed: 503 Service Unavailable"
}
],
"checks": {
"freshness": { "threshold_seconds": 86400 }
},
"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"]
}
]
}

Field reference:

Field Type Description
sources[].id string Connector identifier from the source system.
sources[].components object Parsed schema pattern components.
sources[].source_type string Source adapter type, such as "fivetran" or "manual".
sources[].last_sync_at string or null ISO 8601 timestamp of the last successful sync. Null if unknown.
sources[].tables array List of tables in this source.
sources[].tables[].name string Table name.
sources[].tables[].row_count integer or null Row count if available, otherwise null.
failed_sources array or absent Sources the adapter attempted to fetch metadata for and failed on. Absent when empty. Distinct from sources (succeeded) and excluded_tables (filtered post-success).
failed_sources[].id string Connector or namespace identifier.
failed_sources[].schema string Source schema string (when known).
failed_sources[].source_type string Source type ("fivetran", "iceberg", etc.).
failed_sources[].error_class string One of "transient", "timeout", "rate_limit", "auth", "unknown". Lets consumers branch on operating-mode without parsing message.
failed_sources[].message string Free-form error detail for human inspection.
excluded_tables array Tables the discovery adapter reported but that do not exist in the source warehouse. Same shape as on rocky run. Empty when nothing was filtered.
schemas_cached integer or absent Schema-cache entries written by rocky discover --with-schemas. Omitted without that flag.
checks object or absent Pipeline-level check configuration, when [checks] is declared in rocky.toml.
checks.freshness.threshold_seconds integer Freshness threshold in seconds.
new_sources array or absent Source schemas seen for the first time since the prior snapshot. Present only when […source.discovery] report_new_sources = true; absent (omitted) otherwise. The first discover of a pipeline establishes the baseline and reports none.
collision_candidates array or absent Cross-source collisions — the same external object onboarded under more than one schema. Present only when […source.discovery] on_collision is "warn" or "error"; absent otherwise. With "error", discover also exits non-zero.
collision_candidates[].external_object_id string The shared external object id (e.g. an ad-account id) found under more than one schema.
collision_candidates[].sources array The distinct source schemas that resolve to this object id.

Consumers diffing successive discover snapshots must treat ids that appear in failed_sources but not in sources as “unknown state, do not delete”; that’s the contract that distinguishes a fetch failure from a deletion.

new_sources and collision_candidates are the discover-time signals for cross-source duplicate detection; both are opt-in and omitted from the payload when their feature is off.


Note: the canonical, auditable form is rocky plan followed by rocky apply <plan-id>. The rocky run single-step alias fuses plan + apply into one invocation for local iteration and automation; the JSON output shape below is the same on both apply and run (the command field reflects which verb was invoked).

Returns a complete summary of the pipeline execution.

{
"version": "1.6.0",
"command": "run",
"status": "Success",
"interrupted": false,
"pipeline_type": "replication",
"filter": "tenant=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",
"sql_hash": "a1b2c3d4e5f67890",
"column_count": 12,
"compile_time_ms": 8
}
}
],
"check_results": [
{
"asset_key": ["fivetran", "acme", "us_west", "shopify", "orders"],
"checks": [
{
"name": "row_count",
"passed": true,
"source_count": 15000,
"target_count": 15000
},
{
"name": "column_match",
"passed": true,
"missing": [],
"extra": []
},
{
"name": "freshness",
"passed": true,
"lag_seconds": 300,
"threshold_seconds": 86400
}
]
}
],
"permissions": {
"grants_added": 3,
"grants_revoked": 0,
"catalogs_created": 1,
"schemas_created": 2
},
"drift": {
"tables_checked": 20,
"tables_drifted": 1,
"actions_taken": [
{
"table": "acme_warehouse.staging__us_west__shopify.line_items",
"action": "drop_and_recreate",
"reason": "column 'status' changed STRING -> INT"
}
]
},
"execution": {
"concurrency": 8,
"tables_processed": 20,
"tables_failed": 0
},
"metrics": {
"tables_processed": 20,
"tables_failed": 0,
"statements_executed": 45,
"retries_attempted": 1,
"retries_succeeded": 1,
"anomalies_detected": 0,
"table_duration_p50_ms": 1200,
"table_duration_p95_ms": 4500,
"table_duration_max_ms": 8200,
"query_duration_p50_ms": 800,
"query_duration_p95_ms": 3200,
"query_duration_max_ms": 7100
},
"errors": [],
"anomalies": [],
"anomaly_evaluated": [
{
"table": "acme_warehouse.staging__us_west__shopify.orders",
"evaluated": true
},
{
"table": "acme_warehouse.staging__us_west__shopify.line_items",
"evaluated": false,
"not_evaluated_reason": "no row count was measured for this table, so there is nothing to compare against its history"
}
]
}

Top-level fields:

Field Type Description
status string Terminal status: "Success", "PartialFailure", "Failure", or, for --idempotency-key, "SkippedIdempotent" / "SkippedInFlight". Always present.
interrupted boolean true when Ctrl-C cancelled the run. Always present.
check_gate_failed boolean true when an error-severity check failed and fail_on_error is on. It is not counted in tables_failed.
contained array Models withheld after an upstream failure. Omitted when empty. See Failure containment.
pipeline_type string or absent Pipeline type executed (e.g., "replication").
filter string The filter applied to this run. Empty string when no filter was set.
duration_ms integer Total pipeline execution time in milliseconds.
tables_copied integer Number of tables that were copied (full or incremental).
tables_failed integer Number of tables that failed during processing.
tables_skipped integer Number of tables skipped (omitted when 0).
resumed_from string or absent Run ID this run resumed from, if --resume was used.
shadow boolean True when running in shadow mode (omitted when false).
shadow_comparison object or absent The in-run shadow comparison, before cleanup. Uses the rocky compare result shape. tables_no_baseline counts confirmed missing production targets.
errors array Error details for tables that failed. Each entry has asset_key, error, and a typed failure_kind discriminator (kebab-case, e.g. query-rejected, transient, compile-error) so consumers can branch without parsing the free-form string. See Per-table error containment.
execution object Concurrency and throughput summary.
metrics object or null Counters and percentile histograms for the run.
anomalies array Row count anomalies detected by historical baseline comparison.
anomaly_evaluated array One entry per table the run considered for anomaly detection: table, evaluated (boolean), and not_evaluated_reason (set exactly when evaluated is false). Omitted when empty. Read it with anomalies: an empty anomalies list alone means both “the detector found nothing” and “the detector never ran”. See Anomaly detection.
partition_summaries array Per-model partition execution summaries (present for time_interval models).
cost_summary object or absent Per-run cost rollup: total_cost_usd (float or null), adapter_type (string), total_bytes_scanned (integer or null), total_duration_ms (integer), and per_model (array of {asset_key, duration_ms, cost_usd}). Absent only for unbilled source adapters (fivetran/airbyte); present otherwise — including DuckDB, which reports total_cost_usd 0, and billed adapters that computed no cost, where total_cost_usd is null. See [budget] for how cost limits are enforced.
budget_breaches array Populated when [budget] limits tripped. Each entry has limit_type ("max_usd" / "max_duration_ms" / "max_bytes_scanned"), limit, and actual (both floats). Empty array when within budget or no limits configured.

The table above is not every field. run.schema.json also defines verify_after_failed, quarantine, model_decisions, idempotency_key, skipped_by_run_id, scheduling_warnings, override_warnings, consumer_diagnostics and excluded_tables.

A transformation model that fails to compile during a run counts as a failure, not a silent skip. It lands on tables_failed, gets an errors[] entry with failure_kind: "compile-error" carrying the diagnostic, and the run reports Failure — or PartialFailure when other models succeeded — with exit code 1 or 2.

materializations[]:

Field Type Description
asset_key array of strings Unique asset identifier.
notes array of strings Run notes for this materialization, including a completed kind switch or why drop_existing_kind was not used after a failed kind probe. Empty when there is nothing to report.
rows_copied integer or null Number of rows inserted. Null if the warehouse does not report this.
duration_ms integer Time spent copying this table in milliseconds.
metadata.strategy string Replication strategy used ("incremental" or "full_refresh").
metadata.watermark string or null The watermark value after this copy. Null for full refresh.
metadata.target_table_full_name string or absent Fully-qualified target table (catalog.schema.table).
metadata.sql_hash string or absent 16-char hex hash of the generated SQL.
metadata.column_count integer or absent Number of columns in the materialized table.
metadata.compile_time_ms integer or absent Compile time in milliseconds for derived models.
cost_usd float or absent Observed cost of this materialization in USD, computed post-hoc from the adapter’s cost formula. Rolls up into cost_summary.total_cost_usd at the run level.
job_ids array of strings Warehouse-side job IDs for the statements this materialization issued, accumulated alongside bytes_scanned / bytes_written. Lets orchestrators cross-check rocky-reported figures against the warehouse console (bq show -j, Snowflake query history, Databricks SQL warehouse history). Empty [] for adapters that don’t surface a job id.
partition object or absent Partition window info for time_interval materializations.

The schema also carries attempts, bytes_scanned, bytes_written, started_at and tenant on each entry. See run.schema.json.

Cross-checking BigQuery cost against bq show -j

Section titled “Cross-checking BigQuery cost against bq show -j”

job_ids lets operators reconcile rocky’s reported bytes_scanned against BigQuery’s own job statistics.

Terminal window
# Capture the first job id from a run.
rocky --config rocky.toml run --output json \
| jq -r '.materializations[].job_ids[]' \
| head -1
# → bquxjob_5f3c4e2a_19a1b6d3e21
# Fetch the same job via the BigQuery REST API and read the billed bytes.
bq show -j --location=EU --format=prettyjson bquxjob_5f3c4e2a_19a1b6d3e21 \
| jq '.statistics.query.totalBytesBilled'
# → "10485760"

The number returned by bq show -j is the same value the BigQuery console displays under “Bytes billed” and matches materializations[].bytes_scanned in rocky’s JSON output. --location must match the dataset’s region (EU, US, us-east1, …): BigQuery jobs are region-scoped and bq show -j returns Not found: Job if the location is wrong.

check_results[]:

Field Type Description
asset_key array of strings The table this check applies to.
checks[].name string Check name. See the list below.
checks[].passed boolean Whether the check passed.
checks[].severity string "error" or "warning". An error-severity failure fails the run when the pipeline’s fail_on_error is on.
checks[].not_evaluated string Present only when Rocky could not run the check, carrying the reason. Such a result always has passed: false and severity: "error", whatever the config asked for. One case passes instead: a cross-source group where exactly one sibling carries the key, so there is nothing to compare. A group where no sibling carries it fails.

A check name is one of:

  • row_count, column_match, freshness — the per-table aggregate checks.
  • null_rate:<column> — one per configured column.
  • cross_source_overlap:<source_type>.<table> — one per sibling group.
  • quarantine:compile — a quarantine plan Rocky refused to compile.
  • quarantine:execute — a quarantine statement that failed at the warehouse. not_evaluated names the statement’s role and the warehouse error.

A failing quarantine:compile or quarantine:execute also counts the table in tables_failed and lists it in errors. That is what fails the run. With fail_on_error off, the check result itself is still let through, but the failed table fails the run anyway. A refused plan has failure_kind compile-error. A failed write keeps the warehouse error’s failure_kind and cooldown_seconds.

  • schema_expansion — a [[tables]] entry naming a whole schema that could not be listed.
  • the name you gave a [[checks.custom]] block.
  • an assertion’s name, or {kind}:{column} when you did not set one ({kind}:- when the kind has no column).

Additional fields vary by check type:

  • row_count: source_count (integer), target_count (integer)
  • column_match: missing (list of column names missing from target), extra (list of unexpected columns in target)
  • freshness: lag_seconds (integer), threshold_seconds (integer)
  • null_rate: column (string), null_rate (float), threshold (float)
  • assertion: kind (string, e.g. "not_null"), column (string, when the kind has one), failing_rows (integer)
  • custom: query (string), result_value (integer), threshold (integer)
  • cross_source_overlap: overlap_count (integer), contributing_tables (list of table names), sample (list of overlapping keys)

When not_evaluated is set, these numbers are placeholders, not measurements.

permissions:

Field Type Description
grants_added integer Number of declared catalog permissions Rocky sent this run. It counts what was sent, not what changed.
grants_revoked integer Always 0. Rocky does not revoke grants.
catalogs_created integer Number of catalogs created during this run.
schemas_created integer Number of schemas created during this run.

drift:

Field Type Description
tables_checked integer Total tables inspected for schema drift.
tables_drifted integer Number of tables where drift was detected.
actions_taken[].table string Fully qualified table name.
actions_taken[].action string Action taken (e.g., "drop_and_recreate").
actions_taken[].reason string Human-readable explanation of the drift.

Every other --output json command returns the same versioned envelope. Rather than restate each shape here and let it rot, this page points you at two places that stay current:

  • Machine-readable schemas — schemas/*.schema.json, one file per command, written by rocky export-schemas. These same files generate the Python and TypeScript bindings, so validate against them.
  • Worked examples — each command’s entry in the CLI Reference and in the category pages under Reference → Commands.

The three shapes below are the ones consumers most often get wrong. The authoritative shape for each still lives in the generated schema.

When a model declares a fixture-driven [[test]] block, rocky test --output json carries a unit_tests object alongside the top-level total / passed / failed counts. It’s present only when at least one model declares such a block, and is distinct from declarative (the [[tests]] summary, present only under --declarative).

Field Type Description
unit_tests.total integer Number of fixture-driven unit tests run.
unit_tests.passed integer Number that passed.
unit_tests.failed integer Number that failed.
unit_tests.results array Per-test outcomes.
unit_tests.results[].model string Model under test.
unit_tests.results[].test string Test name.
unit_tests.results[].passed boolean Whether the test passed.
unit_tests.results[].error string or null Failure message. Null when the test passed.
unit_tests.results[].mismatches array Row-level diffs between expected and actual output, for diagnosing a failure. Empty on pass. See the generated schema for the per-row shape.

On both rocky test and rocky ci, the top-level failures field is an array of objects, each { "name": "...", "error": "..." }, not positional [name, error] tuples. JSON Schema can’t represent positional tuples cleanly, so the engine emits named fields. See test.schema.json and ci.schema.json for the exact shape.

rocky compile --output json includes a models_detail[] array, one entry per compiled model. Each entry’s tags object carries the model’s resolved governance tags: the model’s own sidecar [tags] merged over its config-group [tags] baseline, with the sidecar winning per key. So a domain set once on a config group is visible on every member model’s models_detail[].tags without being repeated. The authoritative models_detail[] shape lives in compile.schema.json, and the tag-resolution rules are documented under Group tags.