fed-sx-types Phase 8: blog-publish-digest e2e + flow:wait
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 58s

The motivating end-to-end demonstration (fed-sx-triggers-loop.md Phase
4): one trigger arriving in the pipeline drives a multi-step business
flow with a branch, a timer suspension, an injected effect, and a
follow-up activity emit — all in the kernel's own runtime.

- flow.erl: flow:wait/1 — a timer-style suspend that PRESERVES the value
  on resume (vs flow:suspend/1, which returns the logged result), so a
  "wait until morning" step lets the env flow through to later steps.
- next/flow/flows/blog_publish_digest.erl: the flow. Branches on the
  article :category (newsletter -> wait-until-morning -> send + emit;
  urgent -> send + emit now; else -> skip), fetches followers (injected),
  builds a digest email per follower, and emits a DigestSent activity
  OBJECT. Effect-as-data: a flow can't call kernel gen_servers from
  inside the drive (a blocking call there deadlocks the scheduler), so
  it returns the emails + DigestSent object for a driver to dispatch and
  append — which can then trigger downstream flows, closing the loop.

Test: triggers_e2e.sh (10) — urgent completes in one cycle with 3 emails
+ a DigestSent object; newsletter suspends on the morning timer, then
resumes to the same on "advancing the clock"; draft takes the else
branch (no emails); a non-Article note is rejected by the guard; a
duplicate activity fires once. flow:wait covered in next/flow (36/36).

plans/fed-sx-design.md §13.10 documents the trigger fan-out as a
kernel convention. lib/erlang 771/771.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-30 18:31:26 +00:00
parent 6b4850b34e
commit 6c9b96390f
5 changed files with 267 additions and 1 deletions

View File

@@ -1296,6 +1296,32 @@ inbox + pull from outbox. SSE is convenience, not protocol.
unknown verbs are stored-but-not-projected — safe by default, with explicit
operator control over what extensions load.
### 13.10 Activity-driven flow triggers (kernel convention)
Beyond projections (which fold an activity into read-model state), the kernel
supports firing **durable business flows** off arriving activities — the
"something happened → here is what we DO about it" half of the model. The
convention (substrate landed in `loops/fed-sx-types`, Phases 58):
- A `DefineTrigger{activity-type, flow-name, guard?, actor-scope?}` activity binds
an activity-type to a named flow. `trigger_registry` hydrates from a fold over
these (restart-safe, same content-addressing as `define-registry`).
- Fan-out runs **after** the kernel append, as the last pipeline step (§14):
`envelope → signature → activity-type schema → object schema → append → trigger
fan-out`. Only accepted activities fire flows; rejected ones never trigger.
- Fan-out is deduped per `{activity-cid, trigger-cid}` (federation can deliver the
same activity twice via different peers) using the actor's `:triggers_fired`
field, and is failure-isolated: one flow's failure never blocks the append or
the other flows.
- Flows run on **flow-on-erlang** (`next/flow/`), a native Erlang-on-SX durable
workflow engine (deterministic-replay suspend/resume; combinator algebra
mirrored from the Scheme `lib/flow`). It runs in the kernel's own runtime, so
the fan-out is a direct call — no cross-guest bridge. Because a flow runs inside
the engine's drive (where a blocking kernel call would deadlock the cooperative
scheduler), flows are **pure and describe effects as data** (their output, or a
`suspend`); a driver outside the flow performs IO and appends any follow-up
activity — which can in turn trigger further flows.
## 14. Validation pipeline
Every activity entering the substrate (whether published locally or received from a