feat(trigger-record-change): record-after-write fires one flow on create OR update (#3427)#3446
Merged
Merged
Conversation
…ate OR update (#3427) A record_change flow's start node bound to exactly one lifecycle event via `triggerType`, so a rule meant to run on both insert and update forced authors to duplicate the whole flow into two near-identical definitions that drift. Add `record-after-write` / `record-before-write` — the create-OR-update union (delete excluded: a write persists field data, a delete removes the row). One start node binds both lifecycle hooks (afterInsert + afterUpdate) under the same flow; exactly one fires per mutation (a write is an insert xor an update), so it is not a double run. To branch on which event fired, test `previous` (empty on create, populated on update). - New `triggerTypeToHookEvents` (plural) is the canonical mapper, expanding `write` to both events; `triggerTypeToHookEvent` (singular) is kept for back-compat and returns null for the multi-event write tokens. - The engine already forwards any `record-*` token to this trigger, so no engine/lint/spec change is needed — the trigger owns the vocabulary. - Unit + end-to-end tests (a single write flow fires on both create and update, self-write does not loop); docs under Automation > Flows and the README. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018939yJ413zG3irLzcTtqaa
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
📓 Docs Drift CheckNo hand-written docs reference the 1 changed package(s). ✅ |
os-zhuang
marked this pull request as ready for review
July 24, 2026 15:05
This was referenced Jul 24, 2026
fix(lint): flag every never-firing
record--prefixed trigger token, incl. record-change (#3427)
#3469
Merged
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Jul 25, 2026
…ation lint, showcase flow, create-leg `previous` fix (objectstack-ai#3427) (objectstack-ai#3467) * feat(lint,showcase): flag never-firing record trigger tokens; add record-after-write showcase flow (objectstack-ai#3427) Follow-ups to the record-after-write feature (objectstack-ai#3446). Lint (item 3): new `flow-trigger-unknown-event` rule in validateFlowTriggerReadiness flags a start node whose `triggerType` is record-lifecycle-shaped (`record-before|after-<op>`) but names an op the record-change trigger cannot map — a typo like `record-after-updated`. The engine still routes any `record-` token to the record trigger, which then binds to NO hook and never fires (only a runtime warn), so this surfaces the never-fire defect at authoring/`os validate` time. Warning severity, consistent with the file's existing rules. Bare `record-<noun>` shapes (e.g. `record-change`) are out of scope for this rule. Showcase (item 2): add `UrgentTaskAlertFlow`, a single `record-after-write` flow that fires when a task is created Urgent OR escalated to Urgent — using the `previous == null` create/update discrimination the write trigger enables. Plus a durable integration test booting a real kernel that verifies the create leg (`previous == null` → fires on afterInsert), the non-urgent create (no fire), and the escalation leg (fires on afterUpdate). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018939yJ413zG3irLzcTtqaa * fix(service-automation): bind `previous` as null on the create leg; fix showcase notify recipient (objectstack-ai#3427) Dogfooding the record-after-write showcase flow in a live app surfaced two bugs: 1. The engine bound `previous` into the flow condition scope only when truthy, so on a record insert `previous` was an unknown CEL variable — the documented `previous == null` create-discrimination threw "Unknown variable: previous" and failed the whole start condition, dropping the run. `previous` is now always bound (null when there is no prior row), making the create/update discrimination the record-after-write docs + Studio designer advertise actually work. Verified end-to-end (integration test + a live showcase boot: the flow fires on create-urgent and on escalation, and correctly skips a non-urgent create). 2. The showcase `UrgentTaskAlertFlow` notify node had no recipient, so every run failed "at least one recipient is required". Now notifies the assignee, falling back to the triggering user (`{$User.Id}`) so an unassigned urgent task still pings someone. Live-verified: a `task.urgent` notification is delivered. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018939yJ413zG3irLzcTtqaa * chore: add changeset for the flow-trigger-unknown-event lint rule Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018939yJ413zG3irLzcTtqaa --------- Co-authored-by: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #3427.
A
record_changeflow'sstartnode bound to exactly one lifecycle event viatriggerType(record-after-create,record-after-update, …). A rule meant to run on both create and update — "recompute the SLA whenever a case is created or its priority changes" — forced authors to duplicate the whole flow into two near-identical definitions that drift, or drop out of the visual-flow layer into an object lifecycle hook.This adds
record-after-write(and itsrecord-before-writebefore-phase form): the create-OR-update union. Onestartnode, both lifecycle events — mirroring Salesforce's "created or updated" record-trigger option and Rails'after_save.writeis exactly insert + update — the two mutations that persist field data.deleteis deliberately excluded (a write persists a row, a delete removes it). Onestartnode binds both hooks (afterInsert+afterUpdate) under the same flow; exactly one fires per mutation (a write is an insert xor an update), so it is not a double run.To branch on which event fired inside the flow, test
previous— empty on create, populated on update. e.g. edge conditionprevious == nullselects the create path.How it works
The engine's
resolveTriggerBindingalready forwards anyrecord-*token straight through to this trigger, so no engine, lint, or spec change is needed — the trigger owns the record-event vocabulary. The change is localized to@objectstack/trigger-record-change:triggerTypeToHookEvents(triggerType)(new, plural) is now the canonical mapper: it returns the list of ObjectQL hook events a token binds, expandingwrite→['afterInsert', 'afterUpdate'].triggerTypeToHookEvent(singular) is kept for back-compat and now returnsnullfor the multi-eventwritetokens rather than silently dropping a binding.RecordChangeTrigger.startregisters one hook per mapped event, all under the same per-flowpackageId, so a singlestop()(unregisterHooksByPackage) tears both down together.record-after-writeflow whose action updates its own trigger record does not loop: the engine's existingflowName::recordIdre-entrancy guard suppresses the self-fire (now covered by an end-to-end test).Tests
record-change-trigger.test.ts):triggerTypeToHookEventsmapswrite→ both events for both phases; the singular mapper returnsnullforwrite;start()binds bothafterInsert+afterUpdate(same object, same packageId), fires on each, andstop()tears both down.record-change-integration.test.ts): a singlerecord-after-writeflow booted on a real kernel fires on both an insert and a subsequent update, and its own write-back does not loop.All 33 tests in the package pass; the package builds clean (DTS typecheck).
Docs
triggerType→ event table and theprevious-based create/update discrimination pattern.README.mdupdated.Changeset
minorbump for@objectstack/trigger-record-change.Companion UI PR (objectui): the Studio flow designer's start-node trigger picker gains a "Record created or updated" option, with
record+previouscorrectly in scope for it.Generated by Claude Code