Skip to content

[Epic] Detach triggers from the Workflow Definition into a unified triggers resource #927

Description

@Griphu

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).

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions