Skip to content

Latest commit

 

History

History
199 lines (160 loc) · 9.85 KB

File metadata and controls

199 lines (160 loc) · 9.85 KB

ADR-0041: Flow trigger family — taxonomy, naming, and the open-source core set

Status: Proposed (2026-06-12) Deciders: ObjectStack Protocol Architects Builds on: ADR-0018 (open node/executor registry — descriptors as the engine-owned contract surface), ADR-0030 (outbox-backed delivery), ADR-0012 (notification platform / messaging) Consumers: @objectstack/service-automation (the FlowTrigger seam), @objectstack/cli (serve auto-wiring), runtime presets, Studio flow designer (trigger pickers), future marketplace packaging


TL;DR

A trigger is an event-source adapter that wakes the one automation engine: it normalizes an external or internal signal into a flow execution, via the engine's FlowTrigger extension seam. Triggers are how business events become user-editable logic — their downstream is a flow a non-developer drew in Studio, which is what distinguishes them from the two other wake-up mechanisms the platform already has (kernel hooks: in-process lifecycle code; jobs: a clock for plugin-owned code).

This ADR decides three things:

  1. Taxonomy & naming — triggers become a first-class package family trigger-* (@objectstack/trigger-schedule, …), dropping the plugin- prefix, consistent with the existing driver-* / connector-* / embedder-* / knowledge-* families.
  2. The open-source core set — three triggers are table stakes for every project and ship Apache-2.0 in the default runtime preset: trigger-record-change (shipped), trigger-schedule (shipped), and trigger-api (inbound webhook/HTTP — the one gap to build now; the flow schema already reserves type: 'api' with no runtime behind it).
  3. Everything else is deliberately deferred — to an on-demand open-source tier or to marketplace distribution — with the criteria for promotion written down, so the family grows by demand instead of by speculation.

Context — current state (verified 2026-06-12, against the code)

The seam exists and two triggers are real. The engine exposes registerTrigger({type, start(binding, cb), stop(flowName)}) (service-automation/src/engine.ts); when a flow is registered/enabled it parses the start node into a FlowTriggerBinding ({flowName, object?, event?, condition?, schedule?, config?}) and hands it to the trigger registered for that type. The CLI's serve auto-loads two concrete triggers (cli/src/commands/serve.ts):

  • plugin-trigger-record-change — data events (record-after-update, …). Battle-tested: the showcase budget-approval flow runs on it.
  • plugin-trigger-schedule — bridges the seam to IJobService (spec/contracts/job-service.ts), normalizing authoring shorthands (bare cron string, {every}, {at}) into canonical JobSchedules and managing job lifecycle with the flow (flow-schedule:<flowName>, cancel on disable).

Both follow the same decoupling pattern: structural mirrors of the engine types (no import of service-automation) and structural service surfaces (no import of the job adapter). Triggers, the engine, and their backing services version independently; a missing trigger means the flow simply never activates.

The declared-but-unwired surface. The flow schema's type enum is 'autolaunched' | 'record_change' | 'schedule' | 'screen' | 'api'api has no trigger implementation and no inbound REST route. This is the platform's normal contract-first pattern (cf. recalled before recall(), ApprovalEscalationSchema before any scheduler), but it is also a Studio trap: the designer renders from engine descriptors, and a user can author an api flow that will never run.

Naming is inconsistent. packages/plugins/ already contains four families named by capability domain rather than by loading mechanism (driver-*, connector-*, embedder-*, knowledge-*); the two triggers are the outliers carrying a plugin- prefix.

Decision

1. What is (and is not) a trigger

A trigger adapts one class of business event into flow executions. The test for "should X be a trigger": does this event need a non-developer to orchestrate the response in Studio?

Explicit non-goals, recorded to keep the family honest:

  • screen flows are user-launched, not event-triggered — no trigger.
  • Approval outcomes continue down the approval node's out-edges — no "approval trigger".
  • Plugin-internal clocks (SLA scanner #1742, cleanup jobs) call IJobService directly — a trigger's downstream must be a flow, not plugin code.

2. Naming: the trigger-* family

Rename, keeping class names (the *Plugin suffix accurately describes the kernel loading mechanism):

today becomes
plugin-trigger-record-change @objectstack/trigger-record-change
plugin-trigger-schedule @objectstack/trigger-schedule

Migration: workspace rename + final plugin-trigger-* versions published as deprecation stubs re-exporting the new packages; serve nameMatch lists already match both spellings.

3. The tiers

Tier 1 — core, Apache-2.0, ships in the default runtime preset. Every project needs these; they are the Zapier/Power-Automate baseline trio (data changed / time passed / someone called us):

trigger status scope
trigger-record-change ✅ shipped data events
trigger-schedule ✅ shipped cron / interval / once
trigger-api build next — the only Tier-1 gap inbound webhook/HTTP starts a flow

trigger-api acceptance criteria (the short-term work item):

  • Per-flow inbound endpoint (POST /api/v1/automation/hooks/:flowName/:hookId) with a per-flow secret; HMAC signature verification (GitHub/Stripe style) and a constant-time compare.
  • Request body → flow variables via the binding's config mapping; the raw payload available as $payload.
  • Queue-backed ingestion from day one: inbound HTTP is the first trigger whose event rate is not under our control. The handler enqueues (service-queue / outbox per ADR-0030) and ACKs 202; a consumer executes the flow. At-least-once delivery; flows must be authored idempotently (documented; a dedupKey passthrough supported).
  • Activates the reserved type: 'api' — closing the Studio trap (§4).

Tier 2 — open source, on demand. Built when a driving scenario lands, not before. Each bridges an existing service, so the cost is one adapter:

  • trigger-email-inbound — email-to-case; needs per-deployment inbound mail infra (plugin-email is outbound-only today).
  • trigger-queue — consume integration events from service-queue / external brokers; becomes urgent the moment two ObjectStack deployments (or an external ESB) need to talk.
  • trigger-lifecycle — platform events (user created, package installed) promoted from kernel hooks to Studio-orchestratable events.

Tier 3 — marketplace / vertical (future, possibly commercial). Naturally per-ecosystem packages, distributed and versioned like connectors:

  • trigger-connector-event family — third-party SaaS events (Slack message, Stripe payment, GitHub PR), one package per ecosystem, pairing with the outbound connector-* actions (ADR-0022); supports both webhook-subscribe and poll modes.
  • trigger-threshold — metric-crossing alerts on service-analytics.
  • trigger-file — storage-bucket events.
  • trigger-iot — MQTT/device telemetry (vertical).

Promotion rule: a Tier-2/3 trigger moves up when (a) two independent real projects request it, or (b) it unblocks a Tier-1 acceptance criterion. Nothing is built speculatively.

4. Discoverability: descriptors carry maturity

Triggers self-describe to the engine the way node executors do (ADR-0018): a descriptor with type, display name, config schema for the start-node form, and a maturity: 'ga' | 'beta' | 'reserved' field. Studio renders trigger pickers from the live registry and greys out reserved — flow types whose runtime hasn't shipped (today: api) become visible-but- disabled instead of silent traps. The same audit marks escalation and other contract-ahead-of-runtime surfaces.

5. Delivery guarantees, stated per tier

  • In-process, low-rate triggers (record-change, schedule, lifecycle): direct engine callback, as today.
  • Boundary triggers (api, queue, connector-event, email): must ingest through the outbox/queue path — a slow flow execution may not drop or block inbound events. This is decided now so trigger-api is built toward it rather than retrofitted.

Consequences

  • Two package renames (deprecation stubs, one changeset) — mechanical.
  • One real build item short-term: trigger-api, including the queue-backed ingestion seam it forces us to establish for every future boundary trigger.
  • Studio gains a registry-driven trigger picker and loses the api trap; the maturity field gives the platform a standing way to ship contracts ahead of runtimes without misleading authors.
  • The marketplace gets a well-shaped unit of distribution (trigger-<ecosystem>), aligned with how connectors are already framed.
  • Deferred lists are recorded with promotion criteria, so "why don't we have an email trigger?" has a written answer instead of a re-litigated debate.

References

  • Engine seam: packages/services/service-automation/src/engine.ts (FlowTrigger, registerTrigger, activateFlowTrigger)
  • Shipped triggers: packages/plugins/plugin-trigger-record-change, packages/plugins/plugin-trigger-schedule
  • Reserved surface: FlowSchema.type enum (packages/spec/src/automation/flow.zod.ts)
  • Auto-wiring: packages/cli/src/commands/serve.ts (trigger nameMatch table)
  • Related: ADR-0018 (descriptor pattern this extends), ADR-0030 (outbox), approvals SLA scanner issue #1742 (the "jobs, not trigger" precedent)