Summary
Move all trigger functionality out of the immutable Workflow Definition and into a
unified, detached triggers resource — managed like Secrets, portable across
instances via an import/export trigger-config file, and editable from both CLI and UI.
This epic supersedes the cron-only overlay (ADR-0010, PR #870) and generalizes it to
manual, webhook, and cron triggers (event reserved, no runtime yet).
This issue was scoped out via /grill-with-docs. It is the umbrella; each child issue
below is independently shippable in the stated order.
Decision — ADR-0011 (supersedes ADR-0010)
Author this as docs/adr/0011-triggers-detached-unified-resource.md in Issue 1.
Draft below.
Title: Triggers are detached resources in a unified table; the Workflow Definition is trigger-free
Status: proposed (supersedes ADR-0010)
A Trigger (manual, webhook, cron) is a first-class mutable resource keyed by
(namespace, workflowName, triggerName), stored in one unified triggers table
discriminated by type, and attached to a Workflow independently of its versioned
Definition. The Workflow Definition no longer declares triggers at all. Triggers are
managed from CLI and UI and are portable via an importable/exportable trigger-config file.
Webhook callable URLs are derived from (host, namespace, workflow, path) and never
stored, so import re-derives them for the target instance; cron fire-cursors anchor to
now on import so a materialized schedule never back-fires.
Driver: workflows must be portable across instances without baking instance state
into the spec; operators must add / modify / stop any trigger type without registering a
new Definition version; and triggers should behave like Secrets — detached resources a
workflow uses, not fields embedded in the spec.
Considered options
- Keep triggers in the Definition (status quo). Rejected: couples operational toggles
to immutable versions, not portable, bakes instance-specific webhook state into the spec.
- Capability flags in the Definition + detached wiring. Rejected: confusing to declare
every type yes while only one is wired; still mixes spec and operations.
- Cron-only overlay (ADR-0010). Superseded: solved only cron;
manual/webhook stayed
embedded, leaving two divergent mechanisms.
- One table per trigger type. Rejected: import/export and the unified Triggers tab
fan out across tables for no benefit; partial indexes give type-specific constraints on
a single table.
Consequences
- Supersedes ADR-0010; the
cron_trigger_state overlay is generalized into triggers;
heartbeat, CLI, and UI rename cron→trigger.
triggers.min(1) and the triggers array leave the Definition schema; register / import /
validate stop reading them; existing definitions' declared triggers migrate into the table.
ManualTrigger and WebhookRouter resolve start-permission and routing against the
triggers table, not the Definition.
- The Definition becomes fully portable; import materializes triggers from a separate file
and re-derives webhook URLs; cron cursors anchor to now.
- Documentation (CONTEXT.md glossary, ADR-0010 superseded note, authoring docs, examples)
flips when the Definition becomes trigger-free (Issue 5).
Target model
One table:
triggers(namespace, workflow_name, trigger_name, type, enabled,
config jsonb, last_triggered_at nullable, created_at, updated_at)
type discriminates. config holds the type payload:
cron → { schedule } (+ last_triggered_at fire cursor)
webhook → { method, path, payloadSchema? } (last_triggered_at null)
manual → {} (last_triggered_at null)
- Attaching a
manual trigger is what makes a Workflow hand-startable; attaching a
webhook trigger is what makes its derived endpoint resolve; a cron trigger is the
live schedule the heartbeat enumerates.
- Type-specific constraints via partial indexes (e.g.
UNIQUE (namespace, workflow_name, (config->>'path')) WHERE type='webhook').
Sequencing
Hard constraint: the Definition cannot become trigger-free (Issue 5) until all three
start paths are already table-backed (Issues 2, 3, 4) — otherwise workflows become
unstartable. Issues 2/3/4 are mutually independent once Issue 1 lands.
┌──> #929 cron (re-homes PR #870)
#928 core ─┼──> #930 manual ──┐
└──> #931 webhook ──┴──> #932 definition trigger-free ──> #933 import/export
Child issues
What happens to PR #870
Close without merging. Its surface is cron-specific (cron_trigger_state,
mediforce.cronTriggers.*, cron-add/cron-list, "Schedules" tab) — the wrong shape for
the end state. It never landed, so there is no rename migration to pay: author the unified
migration fresh. Harvest its commits into Issues 1 and 2 (heartbeat rewrite, CRUD
handlers, contract, route adapters, CLI, UI, L2/L3/L4 tests — generalized cron→trigger).
Summary
Move all trigger functionality out of the immutable Workflow Definition and into a
unified, detached
triggersresource — managed like Secrets, portable acrossinstances via an import/export trigger-config file, and editable from both CLI and UI.
This epic supersedes the cron-only overlay (ADR-0010, PR #870) and generalizes it to
manual,webhook, andcrontriggers (eventreserved, no runtime yet).This issue was scoped out via
/grill-with-docs. It is the umbrella; each child issuebelow is independently shippable in the stated order.
Decision — ADR-0011 (supersedes ADR-0010)
Title: Triggers are detached resources in a unified table; the Workflow Definition is trigger-free
Status: proposed (supersedes ADR-0010)
A Trigger (
manual,webhook,cron) is a first-class mutable resource keyed by(namespace, workflowName, triggerName), stored in one unifiedtriggerstablediscriminated by
type, and attached to a Workflow independently of its versionedDefinition. The Workflow Definition no longer declares triggers at all. Triggers are
managed from CLI and UI and are portable via an importable/exportable trigger-config file.
Webhook callable URLs are derived from
(host, namespace, workflow, path)and neverstored, so import re-derives them for the target instance; cron fire-cursors anchor to
nowon import so a materialized schedule never back-fires.Driver: workflows must be portable across instances without baking instance state
into the spec; operators must add / modify / stop any trigger type without registering a
new Definition version; and triggers should behave like Secrets — detached resources a
workflow uses, not fields embedded in the spec.
Considered options
to immutable versions, not portable, bakes instance-specific webhook state into the spec.
every type
yeswhile only one is wired; still mixes spec and operations.manual/webhookstayedembedded, leaving two divergent mechanisms.
fan out across tables for no benefit; partial indexes give type-specific constraints on
a single table.
Consequences
cron_trigger_stateoverlay is generalized intotriggers;heartbeat, CLI, and UI rename cron→trigger.
triggers.min(1)and the triggers array leave the Definition schema; register / import /validate stop reading them; existing definitions' declared triggers migrate into the table.
ManualTriggerandWebhookRouterresolve start-permission and routing against thetriggerstable, not the Definition.and re-derives webhook URLs; cron cursors anchor to
now.flips when the Definition becomes trigger-free (Issue 5).
Target model
One table:
typediscriminates.configholds the type payload:cron→{ schedule }(+last_triggered_atfire cursor)webhook→{ method, path, payloadSchema? }(last_triggered_atnull)manual→{}(last_triggered_atnull)manualtrigger is what makes a Workflow hand-startable; attaching awebhooktrigger is what makes its derived endpoint resolve; acrontrigger is thelive schedule the heartbeat enumerates.
UNIQUE (namespace, workflow_name, (config->>'path')) WHERE type='webhook').Sequencing
Hard constraint: the Definition cannot become trigger-free (Issue 5) until all three
start paths are already table-backed (Issues 2, 3, 4) — otherwise workflows become
unstartable. Issues 2/3/4 are mutually independent once Issue 1 lands.
Child issues
triggerstable + core model (+ commit ADR-0011)What happens to PR #870
Close without merging. Its surface is cron-specific (
cron_trigger_state,mediforce.cronTriggers.*,cron-add/cron-list, "Schedules" tab) — the wrong shape forthe end state. It never landed, so there is no rename migration to pay: author the unified
migration fresh. Harvest its commits into Issues 1 and 2 (heartbeat rewrite, CRUD
handlers, contract, route adapters, CLI, UI, L2/L3/L4 tests — generalized cron→trigger).