Development Commands
These commands support local development. Build a sample project, import a dbt project or package, or scaffold an adapter. Run the HTTP server with its browser UI, or the language server. Check the project’s health, and list what it holds.
rocky playground
Section titled “rocky playground”Create a self-contained sample project that runs on DuckDB in process. It needs no warehouse credentials and no external service. Use it to learn Rocky, to try model logic, and to prototype.
rocky playground [path]Arguments
Section titled “Arguments”| Argument | Type | Default | Description |
|---|---|---|---|
path |
string |
rocky-playground |
Directory name for the playground project. |
| Flag | Type | Default | Description |
|---|---|---|---|
--template <TEMPLATE> |
string |
quickstart |
Sample project template. quickstart is the only one. The removed ecommerce and showcase templates fail with a message that lists the remaining templates. |
Examples
Section titled “Examples”Create a playground with the default name:
rocky playground Rocky Playground
Created sample project at ./rocky-playground/ Template: Quickstart (3 models) Using DuckDB (local, no warehouse needed)
Try: cd rocky-playground rocky compile # type-check the models rocky test # run models on an in-memory DuckDB rocky run # materialize the model DAG rocky preview rows --model customer_orders # peek at materialized rowsCreate a playground with a custom name:
rocky playground my-experiment Rocky Playground
Created sample project at ./my-experiment/ Template: Quickstart (3 models) Using DuckDB (local, no warehouse needed)
Try: cd my-experiment rocky compile rocky test rocky runRelated Commands
Section titled “Related Commands”rocky init– create a production projectrocky test– run tests in the playgroundrocky compile– compile playground models
rocky import-dbt
Section titled “rocky import-dbt”Import an existing dbt project and emit a Rocky project you can run. The importer reads dbt_project.yml and profiles.yml, and translates each .sql model body. It writes a self-contained directory that rocky compile and rocky plan plus rocky apply accept as is.
Translation expands {{ ref(...) }} and {{ source(...) }} into plain identifiers. A model with a Jinja statement tag ({% if %}, {% for %}, {% set %}) is refused on a no-manifest import and listed under “Failed models” with the reason. Run dbt compile --full-refresh and import with the manifest. A {{ ... }} expression the importer cannot lower is left as a /* TODO: unsupported Jinja expression */ comment, so nothing is dropped silently.
rocky import-dbt --dbt-project <PATH> [flags]| Flag | Type | Default | Description |
|---|---|---|---|
--dbt-project <PATH> |
PathBuf |
(required) | Path to the dbt project directory (containing dbt_project.yml). |
--output-dir <PATH> |
PathBuf |
rocky-out |
Output directory for the emitted Rocky repo (rocky.toml + models/ + seeds/ + MIGRATION-NOTES.md). Refuses to write into a non-empty directory unless --overwrite is set. |
--target-adapter <KIND> |
string |
profile-derived | Override the Rocky adapter type. Accepts duckdb, databricks, snowflake, bigquery. Defaults to whatever <dbt_project>/profiles.yml declares — or duckdb when the profile can’t be parsed or maps to an unsupported warehouse. |
--overwrite |
bool |
false |
Replace contents of --output-dir if it already exists and is non-empty. Without this flag, the importer refuses to write into a non-empty directory so it never silently clobbers existing work. |
--manifest <PATH> |
PathBuf |
auto-detect | Path to target/manifest.json. When present, the importer uses dbt’s compiled manifest (Jinja already resolved). Auto-detected from <dbt_project>/target/ if omitted. |
--no-manifest |
bool |
false |
Force regex-based import (skip manifest.json even if available). Useful when the manifest is stale or you want to verify the regex path. |
--microbatch-as <KIND> |
string |
merge |
How to map dbt materialized='microbatch' models. merge reuses the dbt unique_key for an idempotent key-upsert; time_interval maps the batch onto Rocky’s partition-window strategy. |
--skip-unit-tests |
bool |
false |
Don’t convert dbt manifest.unit_tests (1.8+) into Rocky [[test]] blocks. The unit-test counters still report what was found and skipped. Useful when you want to port unit tests by hand. |
Emitted layout
Section titled “Emitted layout”<output-dir>/├── rocky.toml derived from dbt_project.yml + profiles.yml├── models/│ ├── _defaults.toml catalog + schema defaults from dbt_project.yml│ ├── <name>.sql translated model body (Jinja stripped or kept│ │ as a comment)│ └── <name>.toml [strategy] + [target] sidecar├── seeds/ verbatim copy of <dbt_project>/seeds/└── MIGRATION-NOTES.md counts, known limitations, required env varsThe importer never inlines a connection secret. Passwords, API tokens, and service-account JSON come out as ${VAR} placeholders in rocky.toml. MIGRATION-NOTES.md lists every variable you must export before rocky plan plus rocky apply.
How materialized maps
Section titled “How materialized maps”dbt materialized |
Rocky strategy |
|---|---|
view |
view |
table |
full_refresh |
incremental with a unique_key |
merge |
incremental without a unique_key |
full_refresh, with a warning |
microbatch |
merge, or time_interval when you pass --microbatch-as time_interval. Without a unique_key, full_refresh with a warning |
| anything else | full_refresh, plus a TODO line in MIGRATION-NOTES.md |
An incremental model whose SQL uses dbt’s standard watermark filter imports as a Rocky incremental model. The filter must be one {% if is_incremental() %} block of this shape, with no {% else %}:
{% if is_incremental() %} WHERE updated_at > (SELECT MAX(updated_at) FROM {{ this }}){% endif %}The block becomes WHERE @incremental_filter (or AND @incremental_filter), and the sidecar gets timestamp_column = "updated_at". A qualified or renamed left side (o._synced_at > (SELECT MAX(updated_at) ...)) also sets filter_column. Only a strict > is recognised. A >= compares differently, so it is not converted. The unique_key carries over when incremental_strategy is unset or 'merge'; 'append' drops it. Other strategies, 'delete+insert' included, keep their previous mapping: a keyed MERGE does not delete every target row of a non-unique key. on_schema_change maps fail and append_new_columns directly, sync_all_columns to append_new_columns, and ignore to fail, each change with a warning.
On the raw path, the other Jinja in the file must convert too. From a manifest, the importer rebuilds the SQL from raw_code when the block is its only statement tag. Otherwise it keeps the full-refresh compiled_code with no placeholder, and Rocky filters the model’s output column. rocky compile then refuses the model with E046 unless that column is a direct passthrough.
On the raw path, an incremental model that uses is_incremental() in any other way is still imported. The block is commented out under a -- TODO: dbt is_incremental() block not translated: line, and the sidecar has no watermark. rocky compile refuses the model with E037 until you add timestamp_column and the placeholder.
Any other append-style dbt model falls back to full_refresh, which cannot duplicate rows. The fallback covers four cases:
incrementalwith nounique_keyincremental_strategy = 'append', even with aunique_keyincremental_strategy = 'merge'with nounique_key- an
incremental_strategythe importer does not recognise, even with aunique_key
A unique_key maps a model to merge only when incremental_strategy is unset or 'merge'. An explicit 'append' keeps the fallback.
Where the fallback is reported depends on the import path. From a manifest, each model is listed in MIGRATION-NOTES.md under “Items to translate manually”, and under “Warnings”. With --no-manifest, the importer keeps only the warning text, so the model appears under “Warnings” alone. That includes a model with no config() block that inherits materialized = 'incremental' from dbt_project.yml.
Some fallbacks are refused instead. A manifest import keeps dbt’s compiled SQL, and dbt compiles is_incremental() as true against an existing table. So the compiled SQL can keep an incremental filter, and as full_refresh each run would replace the table with only the recent rows. When an incremental or microbatch model would fall back to full_refresh and its raw SQL uses is_incremental(), the importer does not import it. The model is listed under failed_details. Rewrite it by hand: remove the is_incremental() filter, then use merge with a unique_key, or a time_interval model.
Apart from the incremental conversion above, Rocky refuses a model whose raw Jinja calls is_incremental() on either raw-SQL path: --no-manifest, or a manifest node with no compiled_code. Without compiled SQL, Rocky cannot preserve dbt’s first-run versus later-run distinction. Each refused model is listed under failed_details.
A profile type Rocky does not support natively stubs a DuckDB [adapter], so the emitted project still compiles. MIGRATION-NOTES.md records the original type under “Not Translated”.
When it reads profiles.yml, the importer resolves YAML anchors and aliases (&anchor, *alias) plus {{ env_var('VAR', 'default') }} expressions in the adapter type field. A profile that templates its warehouse type therefore detects the right adapter instead of stubbing DuckDB. --target-adapter overrides the detection entirely.
Known limitations
Section titled “Known limitations”Listed in every emitted MIGRATION-NOTES.md:
- Singular dbt tests (custom SQL files in
tests/): not translated. - Macros and
dbt_packages/: skipped. The hybrid-dbt-packages POC is the documented escape hatch. - dbt model contracts (
contract: {enforced}): written to<model>.contract.tomlnext to the model. Column types carry over, read for the manifest’s dbt adapter where a name such asfloatortimestampdiffers by warehouse. A column type Rocky has no name for (or one it reads differently on that warehouse), thenot_nullandprimary_keyconstraints (Rocky cannot prove NOT NULL from the sources), and theunique,check,foreign_keyandcustomconstraints do not. A model with such a part is reported with a warning and counted incontracts_dropped(JSON output andMIGRATION-NOTES.md).
The four built-in dbt generic tests (unique, not_null, accepted_values, relationships) translate to native Rocky [[tests]] on the matching per-model sidecar, as do several common dbt_utils / dbt_expectations tests. Tests with no native equivalent, or that reference columns Rocky didn’t translate, are surfaced as UnsupportedTest warnings and listed in MIGRATION-NOTES.md rather than silently dropped. See the full generic test mapping.
Examples
Section titled “Examples”Import a dbt project end-to-end. The default output dir is ./rocky-out/:
rocky import-dbt --dbt-project ~/projects/acme-dbt{ "version": "1.30.0", "command": "import-dbt", "import_method": "regex", "project_name": "demo_project", "imported": 1, "failed": 0, "warnings": 0, "imported_models": ["orders"], "warning_details": [], "failed_details": [], "emission": { "out_dir": "rocky-out", "rocky_toml_path": "rocky-out/rocky.toml", "migration_notes_path": "rocky-out/MIGRATION-NOTES.md", "models_translated_count": 1, "models_skipped_count": 0, "seeds_copied_count": 0, "adapter_type": "duckdb", "original_dbt_adapter_type": "duckdb", "required_env_vars": [] }}import_method is "manifest" when a compiled target/manifest.json was found (pre-resolved Jinja), or "regex" when Rocky parsed the raw .sql files directly. The emission block is populated whenever --output-dir writes succeeded. Model-level import failures use the command’s partial-import contract and do not make the process exit non-zero, so automation must check failed and failed_details before consuming the emitted repository.
Import into a custom directory and override the target adapter (e.g. migrating a Postgres dbt project to Rocky-on-Snowflake):
rocky import-dbt --dbt-project ~/projects/acme-dbt --output-dir ./acme-rocky --target-adapter snowflakeRe-run after fixing the dbt source, replacing the previous emit:
rocky import-dbt --dbt-project ~/projects/acme-dbt --output-dir ./acme-rocky --overwriteEnd-to-end migration in one shot (emit, then compile to verify the result is loadable):
rocky import-dbt --dbt-project ~/projects/acme-dbt --output-dir ./acme-rocky --overwrite \ && rocky compile --models ./acme-rocky/models/Related Commands
Section titled “Related Commands”rocky compile– type-check the emitted modelsrocky validate-migration– cross-check a dbt project against its imported Rocky formrocky init– scaffold a fresh Rocky project (alternative to importing)
rocky package
Section titled “rocky package”Vendor a dbt Hub package, such as a Fivetran connector package, as Rocky models. dbt runs once per add or update to compile the package. After that, Rocky owns the models. See Vendor dbt Packages for the walkthrough.
rocky package add <namespace>/<name>[@<version-spec>] [flags]rocky package update [<name>] [flags]rocky package listrocky package remove <name> [--force]add and update run dbt deps and dbt compile --full-refresh in a temporary dbt project, with a cleared environment plus an allowlist. Credentials reach dbt as DBT_ENV_SECRET_ROCKY_* variables. With --build-empty, dbt run --empty --full-refresh runs between them. The profile comes from the rocky.toml adapter (duckdb, snowflake, databricks, bigquery or postgres). Rocky writes the package’s models to models/packages/<package>/ and records them in rocky-packages.lock at the project root.
Flags (add and update)
Section titled “Flags (add and update)”| Flag | Type | Default | Description |
|---|---|---|---|
--vars <KEY=VALUE> |
string (repeatable) |
lockfile vars on update |
A dbt var for the package. The value is read as YAML. Recorded in the lockfile and reused by update. |
--adapter <NAME> |
string |
the only warehouse adapter | The rocky.toml adapter dbt compiles against. |
--target-schema <NAME> |
string |
the warehouse default schema (main on DuckDB, public on Postgres, PUBLIC on Snowflake, default on Databricks) |
Schema the vendored models build into. Required on BigQuery. |
--dbt <PATH> |
PathBuf |
dbt on PATH |
The dbt executable. A relative path is resolved against the current directory. |
--dbt-timeout <SECONDS> |
u64 |
1800 |
Stop a dbt step (deps, run --empty, compile) that runs longer. |
--allow-secret-var |
bool |
false |
Accept --vars names that look like credentials (*_key, *_token, *_password, …). Vars are stored in clear text in the lockfile. |
--compiled <DIR> |
PathBuf |
Import an already-compiled dbt project (<DIR>/target/manifest.json + <DIR>/package-lock.yml) instead of running dbt. |
|
--build-empty[=BOOL] |
bool |
false on add; the lockfile’s mode on update |
Run dbt run --empty --full-refresh before compiling, so macros that read upstream columns see them. Writes empty rocky_package_build* schemas to the warehouse and runs the package’s hooks. Without it, a package whose compile came out with all-NULL columns or a placeholder * is refused with E055. Conflicts with --compiled. |
remove --force also deletes vendored files you edited.
The lockfile records the build mode: compile-only, build-empty or compiled. update reuses it. A compiled package needs --compiled <dir> or --build-empty on update.
Update rules
Section titled “Update rules”update never overwrites a file you edited. When the package also changed that file, the new version goes to <file>.incoming and W055 names it. Unedited files are written, deleted or restored to match the package. See Update a package.
E055: refused. A bad spec,dbtnot found, an adapter with no profile mapping, a faileddbt depsordbt compile, a compile that came out wrong because upstream relations did not exist (all-NULL columns or a placeholder*, without--build-empty), a model name or[target]table the project or another package already owns (ignoring case), a vendored model that reads a model that was not vendored or a dbt seed, vendored SQL that does not parse, a previously vendored model that no longer imports (onupdate), a dbt project name already vendored from another Hub package, Jinja in a var, a credential-like var name without--allow-secret-var, a dbt step past--dbt-timeout, a symlink undermodels/packages/, orremoveof edited files without--force. Nothing is written.W055: vendored, with something to review. An.incomingfile, an edited file upstream removed, a model that could not be vendored, an incremental model that fell back to full refresh, dropped dbt tests, or adbt run --emptyfailure.
JSON output
Section titled “JSON output”--output json prints PackageAddOutput (command: "package_add"), PackageUpdateOutput, PackageListOutput or PackageRemoveOutput. The schemas are in schemas/package_*.schema.json.
{ "version": "1.76.0", "command": "package_add", "lockfile": "./rocky-packages.lock", "package": { "name": "stripe", "hub": "fivetran/stripe", "version": "1.10.1", "version_spec": ">=1.0.0,<2.0.0", "dbt_version": "1.12.5", "adapter": "duckdb", "target_schema": "main", "vars_hash": "blake3:af13…", "mode": "build-empty", "includes": [], "models": ["int_stripe__account_daily", "..."], "models_added": ["int_stripe__account_daily", "..."], "models_removed": [], "sources": [{ "name": "stripe.charge", "catalog": "dev", "schema": "stripe", "table": "charge" }], "tests_mapped": 15, "tests_dropped": [], "incremental_fallbacks": [], "failed_models": [], "files_written": ["models/packages/stripe/int_stripe__account_daily.sql", "..."], "files_incoming": [], "files_deleted": [], "files_kept_edited": [], "files_unchanged": 0 }, "diagnostics": []}rocky serve
Section titled “rocky serve”Start an HTTP server for the project. It answers under /api/v1 with typed, schema-backed payloads: models, lineage and the DAG, runs and schedules, products, the review queue, and the governor’s brief, audit and custody reads. A route that matches a CLI command returns what that command’s --output json prints. The routes with no CLI counterpart, such as /models and /dag/layers, have their own shapes. It also runs run, plan and apply as background jobs. --ui adds the browser UI, and --scheduler adds the resident scheduler. The Embedding guide covers the routes, and the OpenAPI document lists every one.
rocky serve [flags]Security defaults
Section titled “Security defaults”rocky serve binds 127.0.0.1 (loopback) by default. Whether a request needs a Bearer token depends on one thing: whether a token is configured.
- No token configured (the loopback default) — nothing checks a Bearer token: the middleware passes every request to its handler. Any process on the machine can read model SQL, run history and the governor ledger, and can call the mutating routes. A handler’s own checks still apply — a body it cannot parse is
400, a secondrunwhile one is in flight is409, and the webhook route still verifies its HMAC whenROCKY_WEBHOOK_SECRETis set. This is the development posture. It is refused on a non-loopback bind, so a misconfigured network cannot expose the server this way. A web page in a browser on the machine cannot use it either: the host check below refuses its requests. - A token configured —
--token <secret>, or theROCKY_SERVE_TOKENenv var. The Bearer check runs on every request whose path is not exempt. Two paths are exempt, whatever the method:/api/v1/health, and/api/v1/hooks/trigger/{pipeline}(exactly one path segment after the prefix), where the handler verifies anX-Rocky-SignatureHMAC instead — and accepts an unsignedPOSTonly on a loopback bind with--schedulerand noROCKY_WEBHOOK_SECRET(the edge below). Two more things carry no token by design: a CORS preflight (OPTIONS) is answered before the check runs, and the browser UI’s own files under/ui/are public — they carry no data. - Non-loopback bind —
--host 0.0.0.0(or any non-loopback address) requires a token;rocky serverefuses to start otherwise.--uirequires a read-only token on a non-loopback bind. On a loopback bind with no token configured,--uigenerates one for the process (see the browser UI). - The host check — on every loopback bind, with or without
--ui, and on every--uibind, a request whoseHostis not a loopback name (localhost,127.0.0.1,[::1], any port), the bind host, or an--allowed-hostentry is refused421 host_not_allowedbefore routing. A presentOriginthat is neither this server’s own nor an--allowed-originentry is refused403 origin_not_allowed. This stops DNS rebinding: a web page cannot point its own domain at127.0.0.1and then callPOST /api/v1/jobs/runon a server with no token.GET /api/v1/healthskips the check. A non-loopback bind without--uiruns no host check: it is reached by names it cannot know, such as a container’s service name or a pod IP, and it must carry a token. To reach a loopback server through a reverse proxy that sends its own name asHost, pass that name with--allowed-host.
A token is full-scope by default: it reaches every route. --token-scope read-only narrows it. A read-only token authenticates the same way, then gets 403 on any non-exempt request that is not GET, HEAD, or OPTIONS. Give that token to a browser UI on a shared server, so a leaked token cannot start a run:
read-only token ├─ GET /api/v1/dag → 200 ├─ GET /api/v1/compile → 200 ├─ POST /api/v1/jobs/run → 403 forbidden_read_only_token └─ POST /api/v1/compile → 403 forbidden_read_only_tokenThe check reads the HTTP method, not a list of paths, so a route added later with a mutating method is refused the moment it exists. Two limits are worth knowing: the check classifies the METHOD, not the handler, so a GET that mutates would pass; and it only covers routes registered inside the authenticated router, not one added after that layer. Two things it does not touch: GET /api/v1/health stays open, and the webhook route POST /api/v1/hooks/trigger/{pipeline} keeps its own HMAC check.
The webhook exemption has three more clauses, and they are stated here rather than left to be assembled from three sections.
On a loopback bind with --scheduler and no ROCKY_WEBHOOK_SECRET, the route accepts an unsigned POST and queues work for the scheduler. No token is involved, so a read-only token does not stop it. This is the dev-convenience path.
On a non-loopback bind with no secret, the route is refused outright — 404 webhook_disabled, not accepted-unsigned. So the unsigned path cannot be reached from the network by binding wider; widening the bind closes it rather than exposing it.
rocky serve --ui --scheduler refuses to start without ROCKY_WEBHOOK_SECRET. A browser can reach the webhook route, so the combination that would let page script queue unsigned work is rejected at startup rather than served.
--scheduler, no ROCKY_WEBHOOK_SECRET: loopback bind → unsigned POST ACCEPTED (dev convenience) non-loopback bind → 404 webhook_disabled (refused, not unsigned) loopback bind + --ui → refuses to STARTSet ROCKY_WEBHOOK_SECRET whenever a browser or an untrusted process can reach the server.
CORS is empty-by-default. Browser apps must declare every allowed origin via --allowed-origin <ORIGIN>. Permitted methods: GET, POST, OPTIONS. Permitted headers: Authorization, Content-Type. CORS authenticates nothing: it decides what a browser may read from a response, and it answers OPTIONS before the token check runs.
| Flag | Type | Default | Description |
|---|---|---|---|
--models <PATH> |
PathBuf |
models |
Directory containing model files. When omitted and rocky.toml declares a transformation pipeline, the server compiles every transformation pipeline’s own models set, the same set GET /api/v1/dag and rocky dag read. If those models cannot be loaded (for example two files with one model name in two pipelines, or a malformed sidecar), the server compiles models/ alone and reports the load error as a W014 warning that names the pipeline and file; /models and /dag then show only models/ until the error is fixed. If models/ is absent or has no model, the server answers engine_not_ready with the load error instead. When given, only this directory is compiled. |
--contracts <PATH> |
PathBuf |
Directory containing data contract definitions. | |
--host <HOST> |
String |
127.0.0.1 |
Bind host. Non-loopback (0.0.0.0, etc.) requires --token. |
--port <PORT> |
u16 |
8080 |
Port to listen on. |
--token <SECRET> |
String |
Bearer token. When set, the check runs on every request whose path is not exempt (/api/v1/health, and the HMAC-checked webhook route); the UI’s own files are public. When unset, no request is asked for a token — see Security defaults. Falls back to ROCKY_SERVE_TOKEN env var when omitted. Required when --host is non-loopback, including with --ui. With --ui on loopback and no token, the server generates a per-process token: full scope (operator mode), or read-only with --read-only or when --allowed-host/--allowed-origin is given. |
|
--token-scope <SCOPE> |
full | read-only |
full |
What --token may do. read-only allows GET, HEAD, and OPTIONS only; anything else gets 403 forbidden_read_only_token. Falls back to ROCKY_SERVE_TOKEN_SCOPE. Setting a scope without a token is an error. The one exception is --ui on loopback, which generates a token with that scope. With --ui on loopback and --allowed-host or --allowed-origin, full is refused. |
--read-only |
bool |
false |
Alias for --token-scope read-only. Conflicts with --token-scope full. Use it for a view-only UI. |
--allowed-origin <ORIGIN> |
String (repeatable) |
[] |
Add an origin to the CORS allowlist. Repeat for multiple origins (e.g. --allowed-origin http://localhost:5173 --allowed-origin https://dashboard.example.com). |
--ui |
bool |
false |
Serve the browser UI at /ui/. Release binaries carry it; from source, build with --features ui. On loopback with no token, the server generates one for the process: full scope (operator mode), or read-only with --read-only. On any other host, pass --token with --read-only. A server with --allowed-host or --allowed-origin is shared, so it stays read-only. Requires ROCKY_WEBHOOK_SECRET with --scheduler. Prints the address to open, token included. |
--allowed-host <HOST> |
String (repeatable) |
[] |
An extra Host header value to accept, for a reverse proxy in front of the server. Loopback names and the bind host are always accepted; any other Host is refused 421 on a loopback bind or a --ui bind (see Security defaults). A non-loopback bind without --ui checks no Host. With --ui, makes the server count as shared, so the UI token stays read-only. |
--watch |
bool |
false |
Watch the models directory, functions/ beside it and the bound rocky.toml, and recompile on a change. Without --models, also watch each transformation pipeline’s models directory that exists at startup. A pipeline root that does not exist at startup, or a pipeline added to rocky.toml later, is compiled but watched only after a restart. A missing default models/ is skipped when pipeline roots exist. |
--scheduler |
bool |
false |
Also run the resident scheduler: a timer loop that evaluates every pipeline’s [schedule] and runs what is due, in-process. On SIGTERM or Ctrl-C the server drains a running scheduled child before it exits. Run one instance per project directory. Experimental. |
--poll-interval-seconds <SECONDS> |
u64 |
15 |
Seconds between scheduler ticks. Must be at least 1. Only meaningful with --scheduler. |
--drain-timeout-seconds <SECONDS> |
u64 |
60 |
Seconds a running scheduled child may keep going after a shutdown signal before Rocky terminates it. Only meaningful with --scheduler. |
--open |
bool |
false |
With --ui: open the printed address in the default browser once the listener is bound — after the startup sweep, never before. The address, token included, is handed to the system opener (open, xdg-open, rundll32) as an argument, so it is visible in the process list while the opener runs. At full scope, another local OS user could act with it. On a shared machine, pass --token and --read-only. A missing opener, or one that exits non-zero, is a warning; the server still starts and still prints the address. Refused without --ui. |
The browser UI
Section titled “The browser UI”rocky serve --ui serves a browser UI at /ui/, from files built into the release binaries (cargo feature ui; a plain cargo build has no UI and refuses the flag with the build command to run).
On your own machine, --ui alone is enough:
rocky serve --ui# Rocky UI: http://127.0.0.1:8080/login?t=<64 hex characters>With no token configured on a loopback bind (127.0.0.1, ::1, localhost), the server generates a per-process token. It works for every request until the server stops, and each start makes a new one. Once the listener is bound, the server prints the Rocky UI: address with the token on stdout, and a note on stderr.
The generated token has full scope. This is operator mode: the UI can run, plan, approve and apply as the OS user who started the server, like the VS Code extension. The stderr note says so. For a view-only UI, add --read-only. A server with --allowed-host or --allowed-origin is shared, so it generates a read-only token and refuses --token-scope full.
An SSH -L tunnel or kubectl port-forward to a loopback port cannot be detected: the server still looks local. Use --read-only whenever someone else can reach your port. With --open, the opener’s command line holds the address and its token, and other local users can read it with ps. The address is also printed on stdout, so it lands in anything that captures stdout (a terminal log, docker logs, a service journal) and in browser history. On a shared host, choose the token yourself. Do the same to keep one token across restarts, or to serve on any other host:
rocky serve --ui --token s3cret --read-only# Rocky UI: http://127.0.0.1:8080/login?t=s3cret--ui, no --token and no ROCKY_SERVE_TOKEN: loopback, not fronted → generates a full token (operator mode) loopback, not fronted, --read-only → generates a read-only token loopback, fronted → generates a read-only token loopback, fronted, scope full → refuses to start non-loopback bind → refuses to start("fronted" = --allowed-host or --allowed-origin given)The rules, each refused at start with its fix:
--uineeds a token. On a non-loopback bind, the token must be read-only and you must pass it. On loopback, a server with a token you gave acceptsfull(or no scope, which means full) unless it is fronted. A fronted server refuses a full-scope token, because writes from the UI need a token for each person. One token can plan, approve and apply, as the CLI user can: approving in the UI is not a second person’s sign-off.- The printed address is
/login?t=<token>. The server checks the token and answers303to/ui/with a session cookie,rocky_ui_<tag>(the tag differs per server;HttpOnly,SameSite=Strict, a session cookie;Securebehind a TLS proxy that sendsX-Forwarded-Proto: https). The cookie holds a keyed hash of the token, so a restart ends every session. The server logs no request URIs, and the answers sendno-storeandno-referrer. A reverse proxy may log the query of/login: configure it not to. A bad token gets a401page with a token field that posts toPOST /login, which needs an allowedOrigin. - The page and its files are public: they carry no data. Every API call the page makes carries the cookie. A route accepts
Authorization: Bearer <token>or the cookie. A write by cookie must also carry anOriginthat names this server exactly (or an--allowed-originentry) andX-Rocky-UI: 1, or the answer is403 ui_write_not_from_ui. The read-only scope applies to cookie sessions too. - With
--ui, a request whoseHostis not a loopback name, the bind host, or an--allowed-hostentry is refused421 host_not_allowedbefore routing. A presentOriginthat is neither this server’s own nor an--allowed-originentry is refused403 origin_not_allowed. Both refusals carry the error envelope. The same checks run on a loopback bind without--ui; only a non-loopback bind without--uiskips them (see Security defaults).GET /api/v1/healthskips both checks, so a load balancer or a Kubernetes probe that sends the pod IP asHoststill gets200. The route carries no data. - Every UI response carries a Content Security Policy that allows scripts, styles, images, fonts and connections from this server only and forbids framing. The page loads nothing from any other host.
--ui --schedulerrefuses to start withoutROCKY_WEBHOOK_SECRET: a browser can reach the webhook route.
A reverse proxy in front of the UI names itself with --allowed-host proxy.internal; a page served from another origin lists it with --allowed-origin https://app.example.
The browser UI guide shows what each screen displays.
Ask the server what it is doing
Section titled “Ask the server what it is doing”GET /api/v1/settings reports the posture of the running server. Read it when you want to check what a server is actually enforcing, rather than what its start command looked like.
curl -H "Authorization: Bearer $ROCKY_SERVE_TOKEN" \ http://127.0.0.1:8080/api/v1/settingsIt reports the bind host, the CORS allowlist, the Host values the host check accepts, whether the scheduler and the UI are on, the token’s scope, and the state backend.
No secret is in the response. The token appears as its name and scope. The webhook secret appears only as whether it can sign a webhook:
webhook_secret what it means ------------------ -------------------------------------------------- present webhook requests must carry a signature absent loopback accepts UNSIGNED; non-loopback answers 404 set_but_unusable blank or not valid UTF-8 -- --scheduler will REFUSE to startThat field is reported even when the scheduler is off. It tells you what will happen when you turn the scheduler on, which is the point.
Two fields have a different freshness from the rest. state_backend and concurrency_control come from rocky.toml, and are read once on the first request to this route, then fixed for the life of the process. They are not read at startup on purpose. On a plain rocky serve, nothing on the path to binding the listener reads a file, and reading rocky.toml eagerly would put one there — letting a rocky.toml on a stalled mount stop the server binding at all. (--scheduler without an explicit --poll-interval already reads the config before binding; that is unchanged.) When concurrency_control is unset, the field reports the backend default (cas on s3, gcs, tiered; off on local, valkey). It is the requested mode: the writers’ startup probe does not run here, so a store that falls back to off shows up in rocky doctor instead.
That read runs one at a time under a 5 second deadline, so a stuck file cannot starve the server either:
503 engine_busy another settings read is in flight -- retry 504 settings_config_timeout the read blew its deadline; usually a file that will not return, so retrying will not helpEvery other field on the route is unaffected by either.
So a config edited after that first request is not reflected here, while the scheduler — which re-reads the file every tick — acts on the new one. They are null when there was no readable config, and config_status says which:
loaded read and parsed absent no rocky.toml -- an ordinary fact, not a failure unreadable present but would not parse; the scheduler will skip every tick until it doesallowed_hosts and allowed_origins both report what is enforced, not what was typed. --allowed-host only becomes a check on a loopback or --ui bind, so that list is empty on a non-loopback bind without --ui. --allowed-origin values that are not valid header values are dropped when the CORS layer is built, and an origin that could not be installed grants nothing, so they are absent here too.
Examples
Section titled “Examples”Start the server with defaults (loopback, token via env var):
export ROCKY_SERVE_TOKEN=$(openssl rand -hex 32)rocky serveCompiled 14 models in 42msListening on http://127.0.0.1:8080Endpoints: GET /api/v1/models - List all compiled models GET /api/v1/models/:name - Get model details GET /api/v1/models/:name/lineage - Column-level lineage GET /api/v1/dag - Full dependency graphCall an authenticated endpoint:
curl -H "Authorization: Bearer $ROCKY_SERVE_TOKEN" http://127.0.0.1:8080/api/v1/modelsStart with file watching on a custom port:
rocky serve --port 3000 --watch --token "$ROCKY_SERVE_TOKEN"Compiled 14 models in 42msWatching models/ for changes...Listening on http://127.0.0.1:3000Expose to the LAN with explicit token + origin allowlist:
rocky serve \ --host 0.0.0.0 \ --token "$ROCKY_SERVE_TOKEN" \ --allowed-origin https://dashboard.internal \ --port 9090Start with contracts:
rocky serve --models src/models --contracts src/contracts --port 9090 --watchRelated Commands
Section titled “Related Commands”rocky lsp– IDE integration via Language Server Protocolrocky compile– one-shot compilation without the serverrocky lineage– CLI lineage (the server exposes the same data via HTTP)
rocky lsp
Section titled “rocky lsp”Start the language server for editors. It gives diagnostics, completions, hover information and go-to-definition for Rocky models. It talks over stdin and stdout. An editor starts it; you rarely run it by hand.
rocky lspNo command-specific flags. A hidden --stdio flag is accepted for editors that pass it. Stdio is always the transport.
In VS Code, the Rocky extension starts rocky lsp for you. rocky.server.path sets the binary (default rocky), and rocky.server.extraArgs adds arguments.
In Neovim, with lspconfig:
require('lspconfig').rocky.setup({ cmd = { "rocky", "lsp" }, filetypes = { "sql" }, root_dir = function(fname) return require('lspconfig.util').root_pattern('rocky.toml')(fname) end,})Related Commands
Section titled “Related Commands”rocky serve– the HTTP API and browser UIrocky compile– the LSP uses the same compiler
rocky init-adapter
Section titled “rocky init-adapter”Scaffold a new warehouse adapter crate under crates/rocky-<name>/. Run it from the engine workspace root. It refuses when the directory already exists.
rocky init-adapter <name>| Argument | Type | Default | Description |
|---|---|---|---|
name |
string |
(required) | Adapter name, for example redshift. The crate is rocky-<name>. |
rocky init-adapter redshiftCreated adapter scaffold at crates/rocky-redshift/
crates/rocky-redshift/ ├── Cargo.toml ├── src/ │ ├── lib.rs │ ├── dialect.rs ← SqlDialect trait (TODO: implement methods) │ ├── adapter.rs ← WarehouseAdapter trait (TODO: implement) │ └── types.rs ← TypeMapper trait └── tests/ └── integration.rs ← Live tests (#[ignore])The command then prints the next steps: add the crate to the workspace, choose literal_escape in dialect.rs (the crate does not compile until you do), implement the traits, and register the adapter in registry.rs.
Related Commands
Section titled “Related Commands”rocky test-adapter– run the conformance suite against the adapterrocky validate– check a config that uses it
rocky hooks
Section titled “rocky hooks”List the lifecycle hooks configured in rocky.toml, or fire a test event at them.
rocky hooks listrocky hooks test <event>| Argument | Type | Description |
|---|---|---|
event |
string |
Event name for hooks test, for example on_pipeline_start or on_materialize_error. |
hooks test builds a synthetic context for the event, prints it, and runs every hook configured for that event.
$ rocky hooks test on_pipeline_startFiring test event: on_pipeline_startContext JSON:{ ... }
All hooks passed.See Hooks for the events and the config.
rocky validate-migration
Section titled “rocky validate-migration”Compare imported dbt model names with a Rocky project. The command also reports dbt test conversion. It does not compile Rocky models or compare warehouse data.
rocky validate-migration --dbt-project <PATH> [flags]| Flag | Type | Default | Description |
|---|---|---|---|
--dbt-project <PATH> |
PathBuf |
(required) | Path to the dbt project directory. |
--rocky-project <PATH> |
PathBuf |
Path to the Rocky project directory. Optional, for a side-by-side comparison. | |
--sample-size <N> |
usize |
Accepted but ignored. This command does not sample rows or compare warehouse data. |
Examples
Section titled “Examples”$ rocky validate-migration --dbt-project ~/dbt-projectMigration Validation Report===========================
Models: 12 imported, 0 failed
Results: 12 models validated successfullyRelated Commands
Section titled “Related Commands”rocky import-dbt– produce the Rocky project this command checksrocky compile– type-check the imported models
rocky adapter
Section titled “rocky adapter”Discover and inspect the adapters available on your system. Rocky follows the cargo-subcommand convention: any executable on your PATH whose name starts with rocky- is treated as a process adapter named after the suffix, so a rocky-snowplow binary registers as the adapter snowplow.
rocky adapter list
Section titled “rocky adapter list”Walks PATH, runs each rocky-* candidate, and prints one row per discovered adapter.
rocky adapter list [--output json]The table shows NAME, VERSION, DIALECT, and PATH. An adapter that fails to initialize still appears in the listing with its error, so a broken install is visible rather than silently skipped. The bundled rocky-lsp language server is filtered out. Passing --output json returns each adapter’s full manifest.
rocky adapter info <name>
Section titled “rocky adapter info <name>”Resolves rocky-<name> on PATH, runs it, and prints its manifest (name, version, SQL dialect, and the path it resolved to).
rocky adapter info snowplowRelated Commands
Section titled “Related Commands”rocky test-adapter– run the conformance suite against a discovered adapterrocky init-adapter– scaffold a new adapter crate
rocky test-adapter
Section titled “rocky test-adapter”Run the conformance suite against an adapter. Pass --adapter or --command. With neither, the command fails.
rocky test-adapter --adapter <NAME> [--adapter-config <JSON>]rocky test-adapter --command <PATH> [--adapter-config <JSON>]| Flag | Type | Default | Description |
|---|---|---|---|
--adapter <NAME> |
string |
Adapter name. Built-in names are databricks, snowflake, duckdb, postgres, redshift, clickhouse, sqlserver and spark. Any other name resolves to a rocky-<name> executable on your PATH. |
|
--command <PATH> |
string |
Path to a process adapter binary. | |
--adapter-config <JSON> |
string |
Adapter config as a JSON string. |
$ rocky test-adapter --adapter duckdbAdapter Conformance: duckdb (SDK 1.x)==================================================...Most specs report skipped today. The suite declares 26 specs. Three of them (format_table_ref, watermark_where and row_hash) run real dialect calls, and only against a process adapter. On a built-in adapter, those three are skipped too. A skipped spec never counts as a pass. A run that verified nothing prints a warning. It still exits 0, because the exit status keys on failures only.
Related Commands
Section titled “Related Commands”rocky adapter– list the process adapters on yourPATHrocky init-adapter– scaffold a new adapter crate
rocky doctor
Section titled “rocky doctor”Run health checks on the project and report each result. It exits 3 when any check is critical, and 0 otherwise.
rocky doctor [--check <NAME>] [--verbose]| Flag | Type | Default | Description |
|---|---|---|---|
--check <NAME> |
string |
(all) | Run only the check with this name, from the table below. A name that matches no check runs nothing. |
--verbose |
bool |
false |
Add context under each check. In JSON, each check gains a details array of [key, value] pairs. Without the flag, details is left out. |
| Check | What it checks |
|---|---|
config |
rocky.toml parses, and its adapters and pipelines are valid. |
state |
The local state store is readable and not corrupted. |
state_schema |
The state store’s schema version. critical when the store was written by a newer engine than this binary. |
adapters |
Each adapter’s configuration, without connecting. |
pipelines |
Schema patterns, templates and governance config. |
state_sync |
The configured remote state backend (type only). |
state_rw |
A put, get and delete of a marker object on the state backend, so access and reachability problems show at start-up. No-op for local; tiered probes both legs. |
state_concurrency |
Lost-update exposure of a remote [state] backend. See below. Silent for local. |
auth, auth/<adapter> |
Pings each warehouse and discovery adapter to check credentials and connectivity. |
scheduler |
critical when the scheduler’s .rocky/tick.lock is held with a stale heartbeat. A warning when no tick ran for twice the shortest cron interval. Silent when no pipeline has a [schedule]. |
spool |
Webhook demands that a tick quarantined as unreadable. Silent when the spool does not exist. A spool that exists but cannot be read is a warning. |
state_concurrency. Rocky resolves the concurrency mode as a writer does at start-up. An unset concurrency_control takes the backend default, and "cas" (compare-and-swap) runs a conditional-write probe.
- Healthy: the mode resolves to
"cas"and the store supports it. The end-of-run upload and every ledger write (rocky policy,rocky gc,rocky restore,rocky apply) commit by compare-and-swap. - Critical:
"cas"is explicit, and the probe shows the store ignores conditional writes. - Warning, each with its own message: the mode is
"off"but thecas-requiredmarker exists, so uploads will be refused; an unset mode fell back to"off"; the probe was inconclusive;"off"is explicit; or"cas"is set on a backend with no compare-and-swap write. --verboseaddsresolved,cas_probeandcas_required_marker.
With --verbose, the other checks add the config path, the state file size, each adapter’s type and credential signal, each pipeline’s kind, and the state backend. The credential signal is one of token, oauth_client, oauth_token, key_pair, password, service_account, adc, env or none.
rocky doctor --check auth{ "command": "doctor", "overall": "warning", "checks": [ { "name": "config", "status": "healthy", "message": "rocky.toml valid", "duration_ms": 4 }, { "name": "state", "status": "healthy", "message": "state store readable", "duration_ms": 2 }, { "name": "adapters", "status": "warning", "message": "adapter.fivetran: API key not set", "duration_ms": 120 } ], "suggestions": [ "Set FIVETRAN_API_KEY to enable the Fivetran discovery adapter." ]}overall is critical when any check is critical, warning when any check warns, and healthy otherwise.
rocky list
Section titled “rocky list”List what the project holds, without running anything.
rocky list pipelines # pipeline definitionsrocky list adapters # adapter configurationsrocky list models # transformation modelsrocky list sources # replication source configurationsrocky list deps <model> # what a model depends onrocky list consumers <model> # what depends on a model| Flag | Type | Default | Description |
|---|---|---|---|
--models <PATH> |
PathBuf |
models |
Models directory. Accepted by bare rocky list, models, deps and consumers. |
--select, --exclude, --state-ref, --state-working-tree |
string |
(all) | Node selection. Accepted by bare rocky list and rocky list models. |
Bare rocky list lists models, so rocky list --select +fct_orders works like dbt ls. Its JSON is the same as rocky list models. Rocky finds models in the models directory and its direct subdirectories, which covers the common models/{layer}/ layout.
$ rocky -o table list pipelinesNAME TYPE TARGET SOURCE DEPENDS ONplayground replication default default -{ "version": "1.80.0", "command": "list_pipelines", "pipelines": [ { "name": "playground", "pipeline_type": "replication", "target_adapter": "default", "source_adapter": "default", "depends_on": [], "concurrency": "16" } ]}Related Commands
Section titled “Related Commands”rocky dag– the same dependencies as a graphrocky catalog– export the compiled graph as a lineage snapshot