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..0915642397 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 duty itself | +| 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