|
| 1 | +# ADR-0041: Flow trigger family — taxonomy, naming, and the open-source core set |
| 2 | + |
| 3 | +**Status**: Proposed (2026-06-12) |
| 4 | +**Deciders**: ObjectStack Protocol Architects |
| 5 | +**Builds on**: [ADR-0018](./0018-unified-node-action-registry.md) (open node/executor registry — descriptors as the engine-owned contract surface), [ADR-0030](./0030-notification-platform-convergence.md) (outbox-backed delivery), [ADR-0012](./0012-notification-platform.md) (notification platform / messaging) |
| 6 | +**Consumers**: `@objectstack/service-automation` (the `FlowTrigger` seam), `@objectstack/cli` (`serve` auto-wiring), runtime presets, Studio flow designer (trigger pickers), future marketplace packaging |
| 7 | + |
| 8 | +--- |
| 9 | + |
| 10 | +## TL;DR |
| 11 | + |
| 12 | +A **trigger** is an event-source adapter that wakes the *one* automation |
| 13 | +engine: it normalizes an external or internal signal into a flow execution, |
| 14 | +via the engine's `FlowTrigger` extension seam. Triggers are how **business |
| 15 | +events become user-editable logic** — their downstream is a flow a |
| 16 | +non-developer drew in Studio, which is what distinguishes them from the two |
| 17 | +other wake-up mechanisms the platform already has (kernel hooks: in-process |
| 18 | +lifecycle code; `jobs`: a clock for plugin-owned code). |
| 19 | + |
| 20 | +This ADR decides three things: |
| 21 | + |
| 22 | +1. **Taxonomy & naming** — triggers become a first-class package family |
| 23 | + `trigger-*` (`@objectstack/trigger-schedule`, …), dropping the `plugin-` |
| 24 | + prefix, consistent with the existing `driver-*` / `connector-*` / |
| 25 | + `embedder-*` / `knowledge-*` families. |
| 26 | +2. **The open-source core set** — three triggers are *table stakes for every |
| 27 | + project* and ship Apache-2.0 in the default runtime preset: |
| 28 | + `trigger-record-change` (shipped), `trigger-schedule` (shipped), and |
| 29 | + **`trigger-api` (inbound webhook/HTTP — the one gap to build now**; the |
| 30 | + flow schema already reserves `type: 'api'` with no runtime behind it). |
| 31 | +3. **Everything else is deliberately deferred** — to an on-demand |
| 32 | + open-source tier or to marketplace distribution — with the criteria for |
| 33 | + promotion written down, so the family grows by demand instead of by |
| 34 | + speculation. |
| 35 | + |
| 36 | +## Context — current state (verified 2026-06-12, against the code) |
| 37 | + |
| 38 | +**The seam exists and two triggers are real.** The engine exposes |
| 39 | +`registerTrigger({type, start(binding, cb), stop(flowName)})` |
| 40 | +(`service-automation/src/engine.ts`); when a flow is registered/enabled it |
| 41 | +parses the start node into a `FlowTriggerBinding` |
| 42 | +(`{flowName, object?, event?, condition?, schedule?, config?}`) and hands it |
| 43 | +to the trigger registered for that `type`. The CLI's `serve` auto-loads two |
| 44 | +concrete triggers (`cli/src/commands/serve.ts`): |
| 45 | + |
| 46 | +- `plugin-trigger-record-change` — data events (`record-after-update`, …). |
| 47 | + Battle-tested: the showcase budget-approval flow runs on it. |
| 48 | +- `plugin-trigger-schedule` — bridges the seam to `IJobService` |
| 49 | + (`spec/contracts/job-service.ts`), normalizing authoring shorthands (bare |
| 50 | + cron string, `{every}`, `{at}`) into canonical `JobSchedule`s and managing |
| 51 | + job lifecycle with the flow (`flow-schedule:<flowName>`, cancel on |
| 52 | + disable). |
| 53 | + |
| 54 | +Both follow the same decoupling pattern: structural mirrors of the engine |
| 55 | +types (no import of `service-automation`) and structural service surfaces |
| 56 | +(no import of the job adapter). Triggers, the engine, and their backing |
| 57 | +services version independently; a missing trigger means the flow simply |
| 58 | +never activates. |
| 59 | + |
| 60 | +**The declared-but-unwired surface.** The flow schema's type enum is |
| 61 | +`'autolaunched' | 'record_change' | 'schedule' | 'screen' | 'api'` — `api` |
| 62 | +has **no trigger implementation and no inbound REST route**. This is the |
| 63 | +platform's normal contract-first pattern (cf. `recalled` before `recall()`, |
| 64 | +`ApprovalEscalationSchema` before any scheduler), but it is also a Studio |
| 65 | +trap: the designer renders from engine descriptors, and a user can author an |
| 66 | +`api` flow that will never run. |
| 67 | + |
| 68 | +**Naming is inconsistent.** `packages/plugins/` already contains four |
| 69 | +families named by *capability domain* rather than by *loading mechanism* |
| 70 | +(`driver-*`, `connector-*`, `embedder-*`, `knowledge-*`); the two triggers |
| 71 | +are the outliers carrying a `plugin-` prefix. |
| 72 | + |
| 73 | +## Decision |
| 74 | + |
| 75 | +### 1. What is (and is not) a trigger |
| 76 | + |
| 77 | +A trigger adapts **one class of business event** into flow executions. The |
| 78 | +test for "should X be a trigger": *does this event need a non-developer to |
| 79 | +orchestrate the response in Studio?* |
| 80 | + |
| 81 | +Explicit non-goals, recorded to keep the family honest: |
| 82 | + |
| 83 | +- **`screen` flows** are user-*launched*, not event-*triggered* — no trigger. |
| 84 | +- **Approval outcomes** continue down the approval node's out-edges — no |
| 85 | + "approval trigger". |
| 86 | +- **Plugin-internal clocks** (SLA scanner #1742, cleanup jobs) call |
| 87 | + `IJobService` directly — a trigger's downstream must be a flow, not plugin |
| 88 | + code. |
| 89 | + |
| 90 | +### 2. Naming: the `trigger-*` family |
| 91 | + |
| 92 | +Rename, keeping class names (the `*Plugin` suffix accurately describes the |
| 93 | +kernel loading mechanism): |
| 94 | + |
| 95 | +| today | becomes | |
| 96 | +|---|---| |
| 97 | +| `plugin-trigger-record-change` | `@objectstack/trigger-record-change` | |
| 98 | +| `plugin-trigger-schedule` | `@objectstack/trigger-schedule` | |
| 99 | + |
| 100 | +Migration: workspace rename + final `plugin-trigger-*` versions published as |
| 101 | +deprecation stubs re-exporting the new packages; `serve` `nameMatch` lists |
| 102 | +already match both spellings. |
| 103 | + |
| 104 | +### 3. The tiers |
| 105 | + |
| 106 | +**Tier 1 — core, Apache-2.0, ships in the default runtime preset.** |
| 107 | +Every project needs these; they are the Zapier/Power-Automate baseline trio |
| 108 | +(*data changed / time passed / someone called us*): |
| 109 | + |
| 110 | +| trigger | status | scope | |
| 111 | +|---|---|---| |
| 112 | +| `trigger-record-change` | ✅ shipped | data events | |
| 113 | +| `trigger-schedule` | ✅ shipped | cron / interval / once | |
| 114 | +| `trigger-api` | **build next — the only Tier-1 gap** | inbound webhook/HTTP starts a flow | |
| 115 | + |
| 116 | +`trigger-api` acceptance criteria (the short-term work item): |
| 117 | + |
| 118 | +- Per-flow inbound endpoint (`POST /api/v1/automation/hooks/:flowName/:hookId`) |
| 119 | + with a per-flow secret; HMAC signature verification (GitHub/Stripe style) |
| 120 | + and a constant-time compare. |
| 121 | +- Request body → flow variables via the binding's `config` mapping; the |
| 122 | + raw payload available as `$payload`. |
| 123 | +- **Queue-backed ingestion from day one**: inbound HTTP is the first trigger |
| 124 | + whose event rate is not under our control. The handler enqueues |
| 125 | + (`service-queue` / outbox per ADR-0030) and ACKs 202; a consumer executes |
| 126 | + the flow. At-least-once delivery; flows must be authored idempotently |
| 127 | + (documented; a `dedupKey` passthrough supported). |
| 128 | +- Activates the reserved `type: 'api'` — closing the Studio trap (§4). |
| 129 | + |
| 130 | +**Tier 2 — open source, on demand.** Built when a driving scenario lands, |
| 131 | +not before. Each bridges an existing service, so the cost is one adapter: |
| 132 | + |
| 133 | +- `trigger-email-inbound` — email-to-case; needs per-deployment inbound mail |
| 134 | + infra (plugin-email is outbound-only today). |
| 135 | +- `trigger-queue` — consume integration events from `service-queue` / |
| 136 | + external brokers; becomes urgent the moment two ObjectStack deployments |
| 137 | + (or an external ESB) need to talk. |
| 138 | +- `trigger-lifecycle` — platform events (user created, package installed) |
| 139 | + promoted from kernel hooks to Studio-orchestratable events. |
| 140 | + |
| 141 | +**Tier 3 — marketplace / vertical (future, possibly commercial).** |
| 142 | +Naturally per-ecosystem packages, distributed and versioned like connectors: |
| 143 | + |
| 144 | +- `trigger-connector-event` family — third-party SaaS events (Slack message, |
| 145 | + Stripe payment, GitHub PR), one package per ecosystem, pairing with the |
| 146 | + outbound `connector-*` actions (ADR-0022); supports both webhook-subscribe |
| 147 | + and poll modes. |
| 148 | +- `trigger-threshold` — metric-crossing alerts on `service-analytics`. |
| 149 | +- `trigger-file` — storage-bucket events. |
| 150 | +- `trigger-iot` — MQTT/device telemetry (vertical). |
| 151 | + |
| 152 | +**Promotion rule**: a Tier-2/3 trigger moves up when (a) two independent |
| 153 | +real projects request it, or (b) it unblocks a Tier-1 acceptance criterion. |
| 154 | +Nothing is built speculatively. |
| 155 | + |
| 156 | +### 4. Discoverability: descriptors carry maturity |
| 157 | + |
| 158 | +Triggers self-describe to the engine the way node executors do (ADR-0018): |
| 159 | +a descriptor with `type`, display name, config schema for the start-node |
| 160 | +form, and a **`maturity: 'ga' | 'beta' | 'reserved'`** field. Studio renders |
| 161 | +trigger pickers from the live registry and greys out `reserved` — flow |
| 162 | +types whose runtime hasn't shipped (today: `api`) become visible-but- |
| 163 | +disabled instead of silent traps. The same audit marks `escalation` and |
| 164 | +other contract-ahead-of-runtime surfaces. |
| 165 | + |
| 166 | +### 5. Delivery guarantees, stated per tier |
| 167 | + |
| 168 | +- In-process, low-rate triggers (record-change, schedule, lifecycle): |
| 169 | + direct engine callback, as today. |
| 170 | +- Boundary triggers (api, queue, connector-event, email): **must** ingest |
| 171 | + through the outbox/queue path — a slow flow execution may not drop or |
| 172 | + block inbound events. This is decided now so `trigger-api` is built toward |
| 173 | + it rather than retrofitted. |
| 174 | + |
| 175 | +## Consequences |
| 176 | + |
| 177 | +- Two package renames (deprecation stubs, one changeset) — mechanical. |
| 178 | +- One real build item short-term: `trigger-api`, including the queue-backed |
| 179 | + ingestion seam it forces us to establish for every future boundary |
| 180 | + trigger. |
| 181 | +- Studio gains a registry-driven trigger picker and loses the `api` trap; |
| 182 | + the `maturity` field gives the platform a standing way to ship contracts |
| 183 | + ahead of runtimes without misleading authors. |
| 184 | +- The marketplace gets a well-shaped unit of distribution |
| 185 | + (`trigger-<ecosystem>`), aligned with how connectors are already framed. |
| 186 | +- Deferred lists are recorded with promotion criteria, so "why don't we |
| 187 | + have an email trigger?" has a written answer instead of a re-litigated |
| 188 | + debate. |
| 189 | + |
| 190 | +## References |
| 191 | + |
| 192 | +- Engine seam: `packages/services/service-automation/src/engine.ts` |
| 193 | + (`FlowTrigger`, `registerTrigger`, `activateFlowTrigger`) |
| 194 | +- Shipped triggers: `packages/plugins/plugin-trigger-record-change`, |
| 195 | + `packages/plugins/plugin-trigger-schedule` |
| 196 | +- Reserved surface: `FlowSchema.type` enum (`packages/spec/src/automation/flow.zod.ts`) |
| 197 | +- Auto-wiring: `packages/cli/src/commands/serve.ts` (trigger `nameMatch` table) |
| 198 | +- Related: ADR-0018 (descriptor pattern this extends), ADR-0030 (outbox), |
| 199 | + approvals SLA scanner issue #1742 (the "jobs, not trigger" precedent) |
0 commit comments