-- Loops: durable recurring topology executions. A loop pairs a stored -- topology graph + task template with a trigger configuration (any of -- cron / on_completion / webhook) and a repeat policy (infinite / N iters / -- until condition). Every iteration writes a normal topology_runs row with -- loop_id + iteration + parent_run_id set so the existing SSE observer, -- audit log, and gated approvals surface transparently. -- -- Cross-iteration state: iteration N+1's run driver reads parent_run_id and -- injects the parent's final assistant message into the first system prompt -- as prior-iteration context (refine-over-time by default). -- -- Missed schedules: if the loop scheduler wakes and finds next_fire_at in -- the past (a restart or long stall), it fires ONCE and computes the next -- fire from the current cron expression. Missed backlog is not drained. CREATE TABLE loops ( id UUID PRIMARY KEY, workspace_id UUID NOT NULL REFERENCES workspaces (id) ON DELETE CASCADE, title TEXT NOT NULL, description TEXT NOT NULL, -- Same shape as topology_runs.graph: JSON-encoded agent/node connectivity -- from the wizard's topology builder (or a template pick). graph JSONB NOT NULL, -- Text prompt used as the task for every iteration. Templating is out of -- scope for v1 — the same string runs each time (with prior-iteration -- output prepended by the run driver for refine-over-time). task_template TEXT NOT NULL, -- {cron?: '0 */6 * * *', on_completion?: bool, webhook_enabled?: bool} -- All three can be enabled simultaneously; whichever fires first wins. triggers JSONB NOT NULL DEFAULT '{}'::JSONB, -- {kind: 'infinite' | 'iters' | 'until', spec: } -- iters: {n: 10} — stop after N iterations -- until: {event: 'ok', within_iters: 20} — stop when N events match repeat_policy JSONB NOT NULL DEFAULT '{"kind":"infinite"}'::JSONB, enabled BOOLEAN NOT NULL DEFAULT true, -- Computed by the scheduler from `triggers.cron` after each fire. NULL -- when no cron trigger is set (loop runs only on webhook / on_completion -- / manual). next_fire_at TIMESTAMPTZ, last_run_id UUID REFERENCES topology_runs (id) ON DELETE SET NULL, -- Opaque secret ID for the webhook endpoint. HMAC-SHA256 verification of -- the request body uses webhook_signing_key. Both are NULL when the -- webhook trigger is disabled; both are set when it's on. Rotation: -- disable trigger, re-enable to regenerate both. webhook_token TEXT UNIQUE, webhook_signing_key TEXT, created_by UUID NOT NULL REFERENCES users (id) ON DELETE RESTRICT, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX loops_workspace_idx ON loops (workspace_id, updated_at DESC); -- Scheduler tick query: SELECT ... WHERE enabled AND next_fire_at <= now(). -- Partial index keeps the scanner tight; disabled + no-cron loops stay out. CREATE INDEX loops_next_fire_idx ON loops (next_fire_at) WHERE enabled AND next_fire_at IS NOT NULL; -- Back-refs from durable runs. loop_id groups all iterations of a loop; -- iteration is 1-indexed per loop; parent_run_id chains iteration N+1 back -- to N so the run driver can pull prior context in one hop. ALTER TABLE topology_runs ADD COLUMN loop_id UUID REFERENCES loops (id) ON DELETE SET NULL, ADD COLUMN iteration INT, ADD COLUMN parent_run_id UUID REFERENCES topology_runs (id) ON DELETE SET NULL; -- Listing "recent iterations of loop X" hits this. CREATE INDEX topology_runs_loop_idx ON topology_runs (loop_id, iteration DESC) WHERE loop_id IS NOT NULL;