From 00fd4761e0c763edf13389e225161fe90d0638ca Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 31 Jul 2026 01:46:05 +0000 Subject: [PATCH 1/2] =?UTF-8?q?docs(automation):=20add=20a=20hooks-vs-flow?= =?UTF-8?q?s=20decision=20section=20=E2=80=94=20flow=20is=20the=20default?= =?UTF-8?q?=20surface,=20hook=20the=20backstop?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The framing settled while scoping #4271 existed only in an issue thread: flows are the default application surface (structural config that os validate checks; reviews as data; can pause), hooks are the system backstop (in-write mutation, cross-object invariants, read-side events — but a statically opaque body). Land it where authors look: a task→layer table plus the two structural reasons on hooks.mdx, a reverse link from flows.mdx Related. Docs only; empty changeset per repo convention. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_0147tNF4Snk7Ry1KGt4a5PY4 --- .changeset/hook-vs-flow-path-guidance.md | 4 +++ content/docs/automation/flows.mdx | 1 + content/docs/automation/hooks.mdx | 34 ++++++++++++++++++++++++ 3 files changed, 39 insertions(+) create mode 100644 .changeset/hook-vs-flow-path-guidance.md diff --git a/.changeset/hook-vs-flow-path-guidance.md b/.changeset/hook-vs-flow-path-guidance.md new file mode 100644 index 0000000000..ba4af4bc86 --- /dev/null +++ b/.changeset/hook-vs-flow-path-guidance.md @@ -0,0 +1,4 @@ +--- +--- + +docs(automation): add a "Hooks vs flows" decision section to `hooks.mdx` — flow as the default application surface, hook as the system backstop — with a task→layer table and the two structural reasons (a flow's writes are lint-checked metadata while a hook body's write set is statically opaque, per the accepted gap in `hook-body.zod.ts`; a flow reviews as data). Reverse link from `flows.mdx` Related. The framing settled in the #4271 discussion; documentation only, releases nothing. diff --git a/content/docs/automation/flows.mdx b/content/docs/automation/flows.mdx index f160e843e5..ead8dbe7e2 100644 --- a/content/docs/automation/flows.mdx +++ b/content/docs/automation/flows.mdx @@ -1123,6 +1123,7 @@ To branch on **which** event fired, test `previous` — it is empty on create ## Related +- [Hooks](/docs/automation/hooks) — data-layer interception below the node graph; [Hooks vs flows](/docs/automation/hooks#hooks-vs-flows) covers when to drop down to one - [Workflow Metadata](/docs/automation/workflows) — why there is no standalone Workflow Rule type, and what replaces it - [Object Metadata](/docs/data-modeling/objects) — Objects that flows operate on - [Validation Metadata](/docs/data-modeling/validation) — Data validation rules diff --git a/content/docs/automation/hooks.mdx b/content/docs/automation/hooks.mdx index 29687f5fb9..b5fd9e59b4 100644 --- a/content/docs/automation/hooks.mdx +++ b/content/docs/automation/hooks.mdx @@ -14,6 +14,40 @@ A hook targets one or more `object`s and subscribes to one or more lifecycle `beforeInsert`, `beforeUpdate`, `afterUpdate`, `afterDelete` (read-side events such as `beforeFind`/`afterFind` are also available). +## Hooks vs flows + +Hooks and [flows](/docs/automation/flows) overlap on record-triggered logic — +a flow's `record_change` trigger fires on the same lifecycle events — so pick +the layer before writing either. The working rule: **a flow is the default +surface for application logic; a hook is the backstop** for what a node graph +cannot express. + +| You need… | Reach for | +| :--- | :--- | +| Side effects after a save — create records, notify, call HTTP, request approval | **Flow** (`record_change`) | +| Anything that pauses: approvals, screens, timers, signals | **Flow** — a hook runs inline and cannot pause | +| Scheduled or date-relative sweeps ("30 days before `end_date`") | **Flow** (`schedule` / `timeRelative`) | +| Mutating the pending record in the same write, before it is saved | **Before hook** | +| An invariant enforced on every write path, across objects, no matter who writes | **Hook** — the backstop role | +| Read-side interception (`beforeFind` / `afterFind`) | **Hook** — flows have no read events | + +Two structural reasons to prefer the flow when either could work: + +- **A flow's writes are validatable metadata; a hook body is opaque code.** An + `update_record` node's `fields` is structural config that `os validate` + checks — readonly targets, template dialects, declared expression slots. A + hook body's write set is **not** statically checked (see + [Hook & Action Bodies](/docs/automation/hook-bodies#not-statically-checked-the-write-set)): + a misspelled field name runs green and writes nothing. +- **A flow reviews as data.** The node graph diffs field-by-field and renders + in the Console designer with per-node run history; a hook body reviews as + code only. + +Logic that genuinely needs code still doesn't have to be a hook: a flow +`script` node calling a function registered via `defineStack({ functions })` +keeps the orchestration in checked metadata and the code in one named, +testable unit. + ## Before Hook Mutate the incoming record before it is saved. The engine exposes the pending From e3b0a3f045cec3d633224c97103593289707b442 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 31 Jul 2026 01:48:15 +0000 Subject: [PATCH 2/2] =?UTF-8?q?docs:=20reword=20'the=20backstop=20role'=20?= =?UTF-8?q?=E2=80=94=20'role'=20is=20ADR-0090=20reserved=20vocabulary?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit check:role-word flags any NEW prose use of the word; rephrasing beats a baseline entry for a sentence that never meant authorization. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_0147tNF4Snk7Ry1KGt4a5PY4 --- content/docs/automation/hooks.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content/docs/automation/hooks.mdx b/content/docs/automation/hooks.mdx index b5fd9e59b4..0915642397 100644 --- a/content/docs/automation/hooks.mdx +++ b/content/docs/automation/hooks.mdx @@ -28,7 +28,7 @@ cannot express. | Anything that pauses: approvals, screens, timers, signals | **Flow** — a hook runs inline and cannot pause | | Scheduled or date-relative sweeps ("30 days before `end_date`") | **Flow** (`schedule` / `timeRelative`) | | Mutating the pending record in the same write, before it is saved | **Before hook** | -| An invariant enforced on every write path, across objects, no matter who writes | **Hook** — the backstop role | +| An invariant enforced on every write path, across objects, no matter who writes | **Hook** — the backstop duty itself | | Read-side interception (`beforeFind` / `afterFind`) | **Hook** — flows have no read events | Two structural reasons to prefer the flow when either could work: