Skip to content

Commit 5f8b628

Browse files
os-zhuangclaude
andcommitted
docs(adr): ADR-0041 — flow trigger family: taxonomy, naming, open-source core set
Decides three things about the trigger layer: 1. Taxonomy: a trigger adapts one class of business event into flow executions via the engine's FlowTrigger seam — distinct from kernel hooks (in-process lifecycle) and IJobService (plugin-owned clocks). Non-goals recorded: screen flows are launched not triggered, approval outcomes ride out-edges, plugin clocks (SLA scanner) call jobs directly. 2. Naming: triggers become a first-class `trigger-*` package family (matching driver-/connector-/embedder-/knowledge-), dropping the plugin- prefix; migration via deprecation stubs. 3. Tiers: record-change + schedule (shipped) + trigger-api (the one gap to build next — flow type 'api' is schema-reserved with no runtime, a Studio trap) form the Apache-2.0 core set every project needs. email-inbound / queue / lifecycle defer to on-demand open source; connector-event / threshold / file / iot to marketplace distribution, with a written promotion rule. Descriptors gain a maturity field ('ga'|'beta'|'reserved') so Studio greys out contract-ahead-of-runtime surfaces; boundary triggers must ingest via the outbox/queue path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 3e2a3d2 commit 5f8b628

1 file changed

Lines changed: 199 additions & 0 deletions

File tree

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
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

Comments
 (0)