Quickstart
Everything runs on your machine: a sqlite-backed Iceberg catalog, a local
file warehouse, DuckDB compute. No S3, no credentials, no Docker. The same
steps work against a real catalog (Glue, Polaris, Nessie, Hive, REST) by
pointing reble.yml at it.
Install
Section titled “Install”pip install reble # or: uv tool install reble / pipx install reblePython 3.10+.
First branch in ten minutes
Section titled “First branch in ten minutes”The example below lives in the repo as
examples/orders-lakehouse
— copy it or follow along.
mkdir orders && cd orders && mkdir modelsCreate three SQL files — a model is just Reble’s word for one SQL file that produces one table, and the file name is the table name:
-- kind: table-- key: order_id-- Upstream input (not a model): raw_events, where ingestion lands events-- as they arrive.with latest as ( select *, row_number() over (partition by order_id order by event_ts desc) as _rn from raw_events),typed as ( select cast(order_id as bigint) as order_id, cast(user_id as bigint) as user_id, lower(status) as status, cast(amount as decimal(12, 2)) as amount, cast(event_ts as timestamp) as event_ts from latest where _rn = 1)select *from typedwhere status = 'paid' and amount > 0-- kind: table-- key: order_idselect order_id, user_id, amount, round(amount * 0.0825, 2) as tax_amount, round(amount + amount * 0.0825, 2) as total_with_tax, date_trunc('day', event_ts) as order_datefrom stg_ordersWrite the project config and seed the upstream input table:
reble init --catalog sql --namespace analyticsimport datetime as dt
import pyarrow as pa, yamlfrom pyiceberg.catalog import load_catalog
cfg = yaml.safe_load(open("reble.yml"))cat = load_catalog("reble", **cfg["warehouse"]["catalog"])cat.create_namespace_if_not_exists("analytics")cat.create_table( "analytics.raw_events", schema=pa.schema([ ("order_id", pa.int64()), ("user_id", pa.int64()), ("status", pa.string()), ("amount", pa.float64()), ("event_ts", pa.timestamp("us")), ]),).append(pa.table({ "order_id": [1, 2, 3], "user_id": [101, 102, 103], "status": ["paid", "paid", "paid"], "amount": [10.0, 20.0, 30.0], "event_ts": [dt.datetime(2026, 8, 30, 14, 22, 5), dt.datetime(2026, 8, 31, 9, 14, 33), dt.datetime(2026, 9, 1, 18, 3, 47)],}))python seed.pyFirst run. A fresh project has no run history, so there are no SQL hashes to compare against and the inferred scope is empty — declare it explicitly this once:
reble run --models stg_orders,mart_orders,report_dailyscope (edited) 3 mart_orders, report_daily, stg_ordersscope (downstream) 0 -pinned inputs 1 reble_pin__local__raw_eventsengine duckdb (local)stg_orders: ran (3 rows, 113ms)mart_orders: ran (3 rows, 50ms)report_daily: ran (3 rows, 48ms)There is no --models all; use reble run --force when you mean everything.
Every later run infers its own scope.
Now make a real change and give it a branch:
git switch -c tighten-filter$EDITOR models/stg_orders.sql # amount > 0 → amount > 15
reble run # scope inferred: stg_orders + everything downstreamscope (edited) 1 stg_ordersscope (downstream) 2 mart_orders, report_dailypinned inputs 1 reble_pin__local__raw_eventsstg_orders: ran (2 rows, 145ms)mart_orders: ran (2 rows, 50ms)report_daily: ran (2 rows, 48ms)You edited one model; Reble worked out the other two.
reble diff mart_ordersdiffing analytics.mart_orders … +2 -0 ~0 (0.1s)detail: .reble/diffs/local (per-table JSON; --rows N / --full for more)The terminal prints the summary; the changed rows themselves are written to
.reble/diffs/<change-set>/<table>.json. Use --rows N to save more per
category, or --full for all of them.
reble status # clean — nothing driftedreble promote # fast-forward mainNo git repo? Same verbs, keyed by an explicit change-set:
reble run --change-set agent-42 --models ..., then pass
--change-set agent-42 to diff/status/promote. See
Git, optional.
Watch the guardrails
Section titled “Watch the guardrails”Simulate production moving under you — append a row to the input on main:
python - <<'PY'import pyarrow as pa, yamlfrom pyiceberg.catalog import load_catalogcfg = yaml.safe_load(open("reble.yml"))cat = load_catalog("reble", **cfg["warehouse"]["catalog"])cat.load_table("analytics.raw_events").append( pa.table({ "order_id": [4], "user_id": [104], "status": ["paid"], "amount": [40.0], "event_ts": [__import__("datetime").datetime(2026, 9, 2, 11, 0, 0)], }))PYreble status # exit code 3: a pinned input no longer matches mainreble promote --ff-only # exit code 4: refusesreble promote # re-pins, re-runs the scope, emits the authoritative # promote-time diff, then fast-forwardsPromote is legal only when every pinned input still equals production. Exit codes are a contract —
3 = drift, 4 = promote blocked, 5 = empty scope, 6 = lineage error,
7 = missing diff key. CI can (and should) branch on them.
Where to go next
Section titled “Where to go next”- How Reble works — the whole loop in one page.
- Quickstart on AWS — the same walkthrough against Glue + S3.
- CLI reference — every command and flag.