Appearance
Schema Reference
cyclone.Migrate creates this schema. The embedded files in migrations/ are the canonical source for a deployment review. Migrations lists what each file changes. This page describes the resulting schema, not one version of it.
Table Overview
| Object | Purpose |
|---|---|
cyclone_runs | One row per workflow or standalone run |
cyclone_steps | Durable step state, input/output, retries, waits, and leases |
cyclone_deps | Control/data dependency edges |
cyclone_step_inputs | Ordered multi-source input bindings |
cyclone_signals | Durable signal mailbox |
cyclone_schedules | Cron schedule definitions and scanner leases |
cyclone_schedule_fires | Deduplication record for each schedule slot |
cyclone_schema_version | Tern migration version tracking |
cyclone_runs
| Column | Type | Notes |
|---|---|---|
id | bigint identity | Primary key |
workflow | text | Workflow name or standalone kind |
def_version | text | DAG topology fingerprint |
input | jsonb, nullable | Submitted workflow input |
state | text | running, paused, completed, failed, or cancelled |
created_at, updated_at | timestamptz | Creation and last-update timestamps |
finished_at | timestamptz, nullable | Terminal timestamp |
deadline_at | timestamptz, nullable | Absolute run deadline (migration 6). Resume moves it forward by the length of the pause |
result_node | text, nullable | Node whose output is the run result, for a DefineResult workflow (migration 15) |
submit_key | text, nullable | SubmitOnce key (migration 16). NULL for all other runs |
has_fan_out | boolean | The run has a fan-out node, default false. The engine reads it with the run row lock (migration 17) |
paused_at | timestamptz, nullable | Time of the pause while the run is paused (migration 18). Resume clears it. A run that ends during the pause keeps it |
parent_run_id, parent_node_id | bigint / text, nullable | The parent run and the parent node of a child run (migration 19). NULL for a run with no parent. There is no foreign key. A purge can delete the parent run before the child run |
The primary key is id. A run starts in running. There is no persisted pending run state.
idx_runs_submit_key keeps (workflow, submit_key) unique for every run with a key, in every state. When Cyclone purges a run, its key is free again.
idx_runs_parent keeps (parent_run_id, parent_node_id) unique for every child run, in every state. Each child node therefore has at most one child run. A purge keeps a terminal child run until its parent run completes or is deleted.
cyclone_steps
| Column | Type | Notes |
|---|---|---|
id | bigint identity | Primary key and insertion-order tie breaker |
run_id | bigint | FK to cyclone_runs(id) with ON DELETE CASCADE |
node, kind | text | Node ID and registered handler kind |
state | text | pending, available, running, waiting_signal, waiting_timer, waiting_child, paused, completed, discarded, or cancelled |
input, output | jsonb, nullable | Persisted handler input and output |
input_from | text, nullable | Single-source upstream node |
queue | text | Default default; API and DB cap it at 256 bytes |
priority | integer | Higher values claim first |
attempt | integer | Failed executions consumed; a first-try success remains 0 |
max_attempts | integer | Default 5 |
scheduled_at | timestamptz | Earliest claim time or wait deadline |
timeout | interval, nullable | Per-execution timeout |
signal_name | text, nullable | WaitSignal/WaitN signal |
signal_names | text[], nullable | WaitAny candidates (migration 8) |
wait_for | interval, nullable | Timer or signal-wait duration |
wait_count | integer, nullable | Explicit WaitN quorum, including 1 |
on_dep_failure | text, nullable | NULL/fail-run or cancel_branch (migration 9) |
sequence_key | text, nullable | FIFO-per-key gate (migration 10) |
unique_key | text, nullable | Standalone uniqueness key; idx_steps_unique_active scopes it to active work |
parent_node, unique_until | text / timestamptz, nullable | Reserved by the initial schema. No query reads them or writes them. The parent link of a child run is on cyclone_runs, not in parent_node |
lease_owner | text, nullable | Worker holding the current lease |
lease_expiry, heartbeat_at | timestamptz, nullable | DB-clock lease timestamps |
lease_generation | bigint | Incremented on each claim; fences stale workers (migration 11) |
reclaims | integer | Expired-lease reclaims so far, default 0; bounded by WithMaxReclaims and reset by an operator retry (migration 14) |
fan_out_parallel | integer, nullable | Set on a fan-out node only: its WithMaxParallel limit, 0 for no limit (migration 17) |
fan_out_max_bytes | bigint, nullable | Set on the fan-out node of a step: the byte limit of the collected output. 0 means no limit. NULL on the fan-out node of an action, which collects no output (migration 17) |
item_of, item_index | text / integer, nullable | Set on an item task only: its fan-out node and the position of its item, from 0 (migration 17) |
child_workflow | text, nullable | Set on a child node only: the workflow that it starts. Its kind is @child (migration 19) |
child_needs_result | boolean, nullable | Set on a child node only. True for a ResultWorkflow.Child node, which needs the result of its child run. False for a Workflow.Child node (migration 19) |
error, error_kind | text, nullable | Last failure and classification |
panic_stack | text, nullable | Captured panic stack (migration 5) |
retention | interval, nullable | Per-step run retention override |
created_at, started_at, completed_at | timestamptz | Lifecycle timestamps; latter two nullable |
(run_id, node) is unique in addition to the id primary key. The node ID of an item task is the ID of its fan-out node plus the item index in brackets, for example process[3]. This constraint therefore gives each item task one identity.
cyclone_deps
| Column | Type | Notes |
|---|---|---|
run_id | bigint | FK to cyclone_runs(id) with cascade delete |
node | text | Dependent node |
dep_node | text | Upstream dependency |
dep_type | text | control by default; data edges are also recorded |
Primary key: (run_id, node, dep_node).
cyclone_step_inputs
| Column | Type | Notes |
|---|---|---|
run_id, node | bigint / text | FK to the consuming (run_id, node) step |
slot | integer | Stable source position |
dep_node | text | FK to the producing step in the same run |
assembly_mode | text | tuple or array (migration 3) |
Primary key: (run_id, node, slot).
cyclone_signals
| Column | Type | Notes |
|---|---|---|
id | bigint identity | Primary key and delivery order |
run_id | bigint | FK to cyclone_runs(id) with cascade delete |
name | text | Declared signal name |
payload | jsonb, nullable | Delivered value |
created_at | timestamptz | Mailbox insertion time |
consumed_by | text, nullable | Wait node that consumed the row |
consumed_at | timestamptz, nullable | Consumption time |
The Go API bounds pending mailbox rows to the useful capacity of the declared wait. The database does not cap payload byte size. Validate untrusted inputs at the application boundary.
Lifecycle
A signal row has no lifecycle of its own. It lives as long as its run. Only the ON DELETE CASCADE from cyclone_runs deletes it, and retention is what triggers that delete. Cyclone keeps a consumed row (consumed_by IS NOT NULL) for audit until it purges the run.
Depth has a bound per run. It is not unbounded. Signal refuses to insert once buffered >= limit (1 for WaitSignal or WaitAny, N for WaitN). A delivery that arrives after the wait has completed is not stored.
cyclone_schedule_fires shares only the cascade lifetime. It has no consumed_by and no mailbox bound. There is one row per fire, kept until Cyclone purges its run.
cyclone_schedules
| Column | Type | Notes |
|---|---|---|
id | text | Primary key |
kind, input | text / jsonb | Fired handler and input |
cron_spec, location | text | Cron expression and timezone (Local default) |
next_run_at | timestamptz | Next slot |
enabled | boolean | Scanner eligibility |
lease_owner, lease_expiry | text / timestamptz, nullable | Schedule scanner lease |
last_run_id | bigint, nullable | FK to last fired run, ON DELETE SET NULL |
last_error | text, nullable | Last scanner/fire error |
consecutive_failures | integer | Failure count |
max_attempts, timeout | integer / interval | Fired-step policy |
queue, priority, retention | text / integer / interval | Fired-step routing and retention |
def_version | text, nullable | Pinned workflow-definition fingerprint (migration 12). NULL for a step schedule or an action schedule |
created_at, updated_at | timestamptz | Lifecycle timestamps |
Queue values are capped at 256 bytes by migration 11.
cyclone_schedule_fires
The columns are schedule_id, scheduled_for, run_id, and created_at. The primary key (schedule_id, scheduled_for) stops two scanners from recording the same slot. Both IDs are foreign keys with cascade delete.
Views
cyclone_v_runs: run identity, state, and timestamps. It also has step counts for total, completed, discarded, running, signal-waiting, and timer-waiting. It has the parent link of a child run.cyclone_v_steps: step status, attempts, errors, lease metadata, computedlease_expired, timestamps, signal name, panic stack, and reclaim count. For an item task, it also has the fan-out node and the item index.cyclone_v_kinds: count, oldest available age, and expired leases by(kind, state).cyclone_v_queues: available, running, and waiting counts, oldest available age, and expired leases by queue.
Indexes
| Index | Definition / purpose |
|---|---|
idx_steps_claim | (queue, priority DESC, scheduled_at) for available claims |
idx_steps_lease | lease_expiry for running-step recovery |
idx_steps_timer | scheduled_at for timer waits |
idx_steps_signal | (run_id, signal_name) for signal waits |
idx_steps_signal_timeout | scheduled_at for capped signal waits |
idx_steps_timeout | started_at for running steps with a timeout |
idx_steps_signal_name_unique | unique (run_id, signal_name) when non-NULL |
idx_steps_unique_active | unique (kind, unique_key) for active standalone work, paused included (migration 18) |
idx_steps_sequence | (sequence_key, scheduled_at, id) for active sequenced work, paused included (migration 18) |
idx_signals_unconsumed | (run_id, name, id) for unconsumed mailbox rows |
idx_signals_run | (run_id) for the FK cascade retention triggers |
idx_schedule_fires_run | (run_id) for the FK cascade retention triggers |
idx_deps_depnode | (run_id, dep_node) for downstream traversal |
idx_step_inputs_depnode | (run_id, dep_node) for fan-in lookup |
idx_schedules_due | next_run_at for enabled schedules |
idx_schedules_lease | lease_expiry for leased schedules |
idx_runs_state_id | (state, id DESC) for run listing |
idx_runs_deadline | deadline_at for running runs with deadlines |
idx_runs_submit_key | unique (workflow, submit_key) when submit_key is non-NULL |
idx_steps_fan_out | (run_id) for pending fan-out nodes |
idx_steps_items | (run_id, item_of, state, item_index) for item tasks |
idx_runs_parent | unique (parent_run_id, parent_node_id) for child runs (migration 19) |
idx_steps_waiting_child | (run_id) for waiting child nodes (migration 19) |
Migration and Compatibility Notes
A pre-1.0 schema has no backward-compatibility guarantee. Review each embedded migration for locks and for mixed-version safety. In particular, do not deploy migration 11 next to v0.1.0 workers. Stop those workers and drain them, migrate, then start the upgraded workers.
Migrate uses a session-level advisory lock. Connect directly to Postgres, or through session-mode PgBouncer. Transaction-mode PgBouncer is not safe for the migration connection.