- CLI Reference
- Commands
- ironflow projection
ironflow projection
Manage projections: create, list, inspect, pause, resume, delete, rebuild, and wait for catch-up.
ironflow projection <subcommand> [flags]ironflow projection list
Section titled “ironflow projection list”List all registered projections with their mode, status, and event count.
ironflow projection list [flags]Flags:
| Flag | Short | Type | Default | Description |
|---|---|---|---|---|
--server | -s | string | Server URL override | |
--json | bool | false | Output as JSON |
Output:
NAME MODE STATUS EVENTS LAST SEQ UPDATEDorders-by-customer managed active 3 1523 2m agoinventory-tracker external rebuilding 2 0 15s agoironflow projection status
Section titled “ironflow projection status”Show detailed status for a projection, including rebuild progress if one is running.
ironflow projection status <name> [flags]Arguments:
| Argument | Required | Description |
|---|---|---|
name | Yes | The projection name |
Flags:
| Flag | Short | Type | Default | Description |
|---|---|---|---|---|
--server | -s | string | Server URL override | |
--json | bool | false | Output as JSON |
Output (active projection):
Name: orders-by-customerStatus: activeMode: managedLast Event: 1523Lag: 0Updated: 2m agoOutput (rebuilding projection):
Name: orders-by-customerStatus: rebuildingMode: managedLast Event: 0Lag: 1200Updated: 15s ago
Rebuild Job: Job ID: rebuild-abc123 Status: running Progress: 65.0% Processed: 65000 / 100000 events Started: 2m30s ago ETA: 1m15sironflow projection rebuild
Section titled “ironflow projection rebuild”Start a rebuild of a projection. Resets state and replays events from the event log.
ironflow projection rebuild <name> [flags]Arguments:
| Argument | Required | Description |
|---|---|---|
name | Yes | The projection to rebuild |
Flags:
| Flag | Short | Type | Default | Description |
|---|---|---|---|---|
--from | string | Start replay from this event ID | ||
--to | string | Stop replay at this event ID | ||
--partition | string | Only rebuild this partition | ||
--dry-run | bool | false | Estimate work without starting rebuild | |
--server | -s | string | Server URL override | |
--json | bool | false | Output as JSON |
Output:
Rebuild startedJob ID: rebuild-abc123Projection: orders-by-customerStatus: runningTotal Events: 100000Output (—dry-run):
Dry run — rebuild not startedJob ID: rebuild-abc123Projection: orders-by-customerStatus: dry_runTotal Events: 100000Examples:
# Full rebuildironflow projection rebuild orders-by-customer
# Partial rebuild from a specific eventironflow projection rebuild orders-by-customer --from evt_123
# Rebuild a specific partitionironflow projection rebuild orders-by-customer --partition customer-123
# Preview what a rebuild would doironflow projection rebuild orders-by-customer --dry-run
# Rebuild with event rangeironflow projection rebuild orders-by-customer --from evt_123 --to evt_456ironflow projection rebuild cancel
Section titled “ironflow projection rebuild cancel”Cancel an in-progress rebuild. Previous state is preserved.
ironflow projection rebuild cancel <name> [flags]Arguments:
| Argument | Required | Description |
|---|---|---|
name | Yes | The projection to cancel rebuild for |
Flags:
| Flag | Short | Type | Default | Description |
|---|---|---|---|---|
--server | -s | string | Server URL override |
Output:
Rebuild cancelled for projection "orders-by-customer" (status: cancelled)ironflow projection get
Section titled “ironflow projection get”Get the current state and metadata of a projection.
ironflow projection get <name> [flags]Arguments:
| Argument | Required | Description |
|---|---|---|
name | Yes | The projection name |
Flags:
| Flag | Short | Type | Default | Description |
|---|---|---|---|---|
--partition | string | Partition key | ||
--json | bool | false | Output as JSON | |
--server | -s | string | Server URL override |
Output:
Name: order-totalsMode: managedVersion: 15Last Event ID: evt_01HN8K3X5YLast Event At: 2m agoState: { "total_orders": 42, "total_revenue": 12500.00 }Examples:
ironflow projection get order-totalsironflow projection get order-totals --partition tenant-123ironflow projection get order-totals --jsonironflow projection pause
Section titled “ironflow projection pause”Pause a running projection. It will stop processing events until resumed.
ironflow projection pause <name> [flags]Arguments:
| Argument | Required | Description |
|---|---|---|
name | Yes | The projection name |
Flags:
| Flag | Short | Type | Default | Description |
|---|---|---|---|---|
--server | -s | string | Server URL override |
Output:
Paused projection "order-totals" (status: paused)ironflow projection resume
Section titled “ironflow projection resume”Resume a paused projection. It will continue processing events from where it left off.
ironflow projection resume <name> [flags]Arguments:
| Argument | Required | Description |
|---|---|---|
name | Yes | The projection name |
Flags:
| Flag | Short | Type | Default | Description |
|---|---|---|---|---|
--server | -s | string | Server URL override |
Output:
Resumed projection "order-totals" (status: active)ironflow projection delete
Section titled “ironflow projection delete”Unregister and delete a projection. Does not delete data written to external stores.
ironflow projection delete <name> [flags]Arguments:
| Argument | Required | Description |
|---|---|---|
name | Yes | The projection name |
Flags:
| Flag | Short | Type | Default | Description |
|---|---|---|---|---|
--server | -s | string | Server URL override |
Output:
Deleted projection "old-projection"ironflow projection create
Section titled “ironflow projection create”Create a new SQL projection that builds a read model from event streams.
ironflow projection create <name> [flags]Arguments:
| Argument | Required | Description |
|---|---|---|
name | Yes | The projection name |
Flags:
| Flag | Short | Type | Default | Description |
|---|---|---|---|---|
--sql | string | DDL for the projection (CREATE TABLE ..., plus optional CREATE INDEX / CREATE EXTENSION) (required unless --sql-file) | ||
--sql-file | string | Path to a file containing that DDL (required unless --sql) | ||
--event | string[] | Event name to subscribe to (repeatable) (required) | ||
--event-handler | string[] | Per-event SQL handler in event_name=SQL format (repeatable) (required) | ||
--description | string | Projection description | ||
--json | bool | false | Output as JSON | |
--server | -s | string | Server URL override |
One of --sql or --sql-file is required. Cannot use both. At least one --event and one --event-handler are also required.
The projection name becomes the table name, so it must be spellable as an
unquoted SQL identifier: lowercase letters, digits and underscores, not starting
with a digit, and at most 45 characters. order-totals is rejected at create
time — it would have produced proj_order-totals, which no unquoted
CREATE TABLE can name.
Uppercase is refused because PostgreSQL folds an unquoted identifier: Foo and
foo are two projections but one table, proj_foo, whose rows they would then
share. The 45-character bound is the same collision by another route — the
environment-scoped table proj_<envToken>_<name> is proj_ + 12 + _ + 45,
which is exactly PostgreSQL’s 63-byte identifier limit, past which it truncates
rather than failing. Both apply on SQLite too, so a projection created there
survives a move to PostgreSQL.
The name you write is the name you keep using — in the DDL, in every event
handler, and in ironflow projection get. The table the rows actually live in
carries an extra environment segment, proj_{envToken}_{name}, so two
environments that each own a projection called orders do not share one table
(#1676). Ironflow rewrites the identifier for you; the only time the physical
name matters is when you query the database directly instead of through the API,
and projection_registry is what maps it back to an environment.
The DDL is a script, not a single statement. It must contain exactly one
CREATE TABLE for the projection’s own proj_-prefixed table, and may add:
CREATE EXTENSION IF NOT EXISTS <name>before it —vectorandpg_trgmonly, and PostgreSQL only.CREATE INDEX/CREATE UNIQUE INDEXafter it, on that same table. The index name must start with the table name, because index names are schema-global.
Every other statement shape is rejected, including CREATE TABLE ... AS SELECT
and any statement naming a second table. A projection reads its own table and
nothing else. That includes a foreign key: any REFERENCES clause is
rejected, even one pointing at the projection’s own table. A foreign key to
another table would block deletes there until the projection is dropped, and a
self-referencing one would break a rebuild — events replay in stream order, so a
child row can arrive before its parent and fail the constraint. A table-element
LIKE is rejected for the same reason — CREATE TABLE proj_x (LIKE other)
copies another table’s column definitions and reads its catalog. The LIKE
operator is unaffected: CHECK (name LIKE 'a%') is accepted.
Two characters are refused anywhere in the script, comments and string literals
included: $ (dollar-quoting) and ` (SQLite’s backtick identifier quote).
Each opens a quoting mode the validator does not model, which would let it
disagree with the server about where a statement ends or what a statement says.
Quote an identifier with double quotes instead — "select" TEXT is accepted.
A block comment must not contain a nested /*. PostgreSQL nests them and SQLite
does not, so a nested comment is the one construct where the validator and the
server can disagree about where a comment — and therefore a statement — ends.
Ordinary /* ... */ comments are fine.
Output:
Created SQL projection "order_totals" (status: active)Examples:
ironflow projection create order_totals \ --sql "CREATE TABLE proj_order_totals (order_id TEXT PRIMARY KEY, amount NUMERIC)" \ --event "order.placed" \ --event-handler "order.placed=INSERT INTO proj_order_totals (order_id, amount) VALUES (:entity_id, :data.total)"
ironflow projection create order_totals \ --sql-file projection.sql \ --event "order.placed" \ --event-handler "order.placed=INSERT INTO proj_order_totals (order_id, amount) VALUES (:entity_id, :data.total)" \ --description "Order totals"
ironflow projection create board \ --sql "CREATE TABLE proj_board (id TEXT PRIMARY KEY, title TEXT, status TEXT)" \ --event "issue.created" --event "issue.status_changed" \ --event-handler "issue.created=INSERT INTO proj_board (id, title, status) VALUES (:entity_id, :data.title, 'OPEN')" \ --event-handler "issue.status_changed=UPDATE proj_board SET status = :data.to WHERE id = :entity_id" \ --jsonironflow projection watch
Section titled “ironflow projection watch”Watch real-time updates for a projection via WebSocket.
ironflow projection watch <name> [flags]Arguments:
| Argument | Required | Description |
|---|---|---|
name | Yes | The projection name to watch |
Flags:
| Flag | Short | Type | Default | Description |
|---|---|---|---|---|
--server | -s | string | ws://localhost:9123/ws (derived from IRONFLOW_SERVER_URL if set) | WebSocket server URL |
--replay | -r | int | 0 | Number of historical events to replay |
--json | -j | bool | false | Output events as JSON |
--metadata | -m | bool | false | Include event metadata |
Examples:
ironflow projection watch order-totalsironflow projection watch order-totals --replay 10ironflow projection watch order-totals --jsonironflow projection wait
Section titled “ironflow projection wait”Block until a projection has processed events up to --min-seq (NATS sequence on
the PUBSUB stream). Used for read-your-writes after a write returned a
sequence in AppendResult.
ironflow projection wait <name> --min-seq <n> [flags]Arguments:
| Argument | Required | Description |
|---|---|---|
name | Yes | The projection name |
Flags:
| Flag | Short | Type | Default | Description |
|---|---|---|---|---|
--server | -s | string | IRONFLOW_SERVER_URL | Server URL |
--min-seq | uint64 | — | Target NATS sequence the projection must reach (required) | |
--timeout | duration | 30s | Maximum wait duration. Server caps at 60s unary, 300s with --stream. | |
--partition | string | "" | Scope wait to a single partition (managed projections with partitioned state) | |
--stream | bool | false | Use streaming RPC (#476) — raises the cap to 300s and emits progress + heartbeat frames so long waits survive proxies/LBs with short idle timeouts. | |
--json | bool | false | Output as JSON |
Exit codes:
| Code | Meaning |
|---|---|
0 | Projection caught up within timeout |
1 | Timed out |
2 | Error (projection not found, paused/rebuilding, bad input, network) |
--partition is rejected (exit 2) on an external projection, and on a
projection that declares no partition key and holds no state row under that
partition. On a partitioned projection a partition that no event has reached
yet is accepted, so append-then-wait works for a brand-new key; the server
instead caps concurrent waits at 64 distinct partitions per projection. See
Partition on the wait methods.
Examples:
ironflow projection wait order-view --min-seq 1234ironflow projection wait order-view --min-seq 1234 --timeout 10sironflow projection wait order-view --min-seq 1234 --partition tenant-1 --json# Streaming — progress lines print as the cursor advances:ironflow projection wait order-view --min-seq 1234 --stream --timeout 5mironflow projection wait-for-event
Section titled “ironflow projection wait-for-event”Wait until the given projection has processed the event identified by
<event-id>. The server resolves the event ID to its NATS sequence via
events.nats_seq and waits on that.
ironflow projection wait-for-event <event-id> --projection <name> [flags]Arguments:
| Argument | Required | Description |
|---|---|---|
event-id | Yes | The event ID |
Flags:
| Flag | Short | Type | Default | Description |
|---|---|---|---|---|
--server | -s | string | IRONFLOW_SERVER_URL | Server URL |
--projection | string | — | Projection name to wait on (required) | |
--timeout | duration | 30s | Maximum wait duration. Server caps at 60s. | |
--partition | string | "" | Scope wait to a single partition (managed projections with partitioned state) | |
--json | bool | false | Output as JSON |
Exit codes: same as projection wait. Events written before migration 010
may have a NULL nats_seq and return exit code 2; in that case fall back to
projection wait --min-seq using a sequence from a fresh write.
Examples:
ironflow projection wait-for-event evt_abc --projection order-viewironflow projection wait-for-event evt_abc --projection order-view --timeout 10s --jsonironflow projection wait-batch
Section titled “ironflow projection wait-batch”Wait on multiple projections concurrently. All items share a single timeout and a single atomic capacity reservation on the server. Max 16 items per batch.
ironflow projection wait-batch --file <path> [flags]The input file is a JSON array of items:
[ {"name": "order-view", "minSeq": 42, "partition": "tenant-1"}, {"name": "inventory", "minSeq": 42}]Use --file - to read from stdin.
Flags:
| Flag | Short | Type | Default | Description |
|---|---|---|---|---|
--server | -s | string | IRONFLOW_SERVER_URL | Server URL |
--file | string | — | Path to JSON items file, or - for stdin (required) | |
--timeout | duration | 30s | Maximum wait duration. Server caps at 60s. | |
--json | bool | false | Output as JSON |
Exit codes:
| Code | Meaning |
|---|---|
0 | All items caught up within timeout |
1 | At least one item timed out (none errored) |
2 | At least one item errored, or bad input / network |
Examples:
ironflow projection wait-batch --file items.jsonjq '...' | ironflow projection wait-batch --file - --timeout 10s --jsonironflow projection durables prune
Section titled “ironflow projection durables prune”Reclaim the projection JetStream durables an upgrade past ADR 0050
(environment-ID-scoped pub/sub wire subjects) leaves behind
on the PUBSUB stream.
Projection durables are keyed by environment ID. A deployment upgraded from an
earlier version keeps its old, differently-keyed durable for every projection —
one per projection, forever. They are inert (PUBSUB uses limits retention, so
a stalled consumer pins no messages), but they persist and make
nats consumer ls PUBSUB misleading during triage.
Candidates are selected by their filter subject, never by their name. A durable
is spared when its filters begin with public.<environmentID>.events. for the
environment ID its own name is keyed on; only a subject that fails that test is
checked for the legacy public.<project>.<env>.events.… layout. A durable whose
generation cannot be determined is skipped rather than guessed at, and the
skipped ones are logged so you can inspect them by hand.
This talks to NATS directly, not to the Ironflow server.
By default it lists what it would delete and stops. Pass --delete to remove
them.
ironflow projection durables prune [flags]Flags:
| Flag | Type | Default | Description |
|---|---|---|---|
--nats-url | string | NATS_URL or nats://localhost:4222 | NATS URL |
--nats-creds | string | NATS_CREDS_FILE | NATS .creds file for JWT/NKey auth |
--delete | bool | false | Delete the listed durables instead of listing |
Examples:
# List what would be reclaimedironflow projection durables prune
# Reclaim themironflow projection durables prune --delete
# Against an external NATS clusterironflow projection durables prune --nats-url nats://nats:4222 --deleteRun this after an upgrade has settled, not during it. Rolling back to a
pre-ADR-0050 binary after pruning means the old durable no longer exists and
gets recreated under DeliverAllPolicy, replaying the stream’s whole retention
window.