Skip to content

fix(approvals): resolve field/manager approvers against the live record at node entry (#3447)#3495

Merged
os-zhuang merged 1 commit into
mainfrom
fix/3447-approver-live-record
Jul 25, 2026
Merged

fix(approvals): resolve field/manager approvers against the live record at node entry (#3447)#3495
os-zhuang merged 1 commit into
mainfrom
fix/3447-approver-live-record

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

What

Approval nodes resolved field / manager approvers against the trigger snapshot the flow froze into $record at submit time. Once a run was in flight, nothing an earlier step wrote could reach a later approval node's approver resolution — so dynamic routing (one step decides the next step's approvers), dynamic co-sign, and "pick the co-review departments mid-flow" were all impossible to express declaratively. Fixes #3447.

Root cause

Resolution timing was already lazy / per-node — each approval node resolves on entry via openNodeRequestexpandApprovers. The defect was the data source: the node reads variables.get('$record'), seeded once at flow start and carried verbatim through every suspend / resume, never re-read from the DB. {type:'field'} just indexed that frozen object.

Tell-tale that this was a leftover and not a design choice: in the same node, team / department / position approvers query the graph live, while field / manager read the frozen snapshot.

Change (P1)

  • Re-read the record's live state by id at node entry (loadLiveRecord, system identity — approver routing is a platform concern and the record is the flow's own subject, so submitter RLS/FLS must not narrow it). field / manager now bind to current data; graph types were already live.
  • Falls back to the trigger snapshot (warn, don't throw) when the record can't be re-read — hard-deleted mid-flow, or a backend that can't serve a point read. Matches the existing "warn but proceed" stance (Approval routed to an empty position permanently locks the record (no admin override, no recovery) #3424) and the "record is gone" enrichment fallback that already relies on the payload snapshot.
  • payload_json still snapshots input.record — its contract is "the record as submitted", deliberately unchanged.

Two latent defects fixed on the same path

  • Multi-select fan-out was working only by coincidence: String(['u1','u2'])'u1,u2' happened to match the pending_approvers CSV format. Now fans out explicitly into one slot per user.
  • OOO delegation silently no-op'd for multi-value fields — the whole 'u1,u2' string was looked up as one delegator id. Now applied per fanned-out user.

Tests

6 new tests. Verified they go red against the pre-fix implementation (stash the impl, keep the tests): 5 red (live-read, live-wins-over-stale-snapshot, array fan-out, CSV fan-out, per-user OOO) and 1 green (snapshot fallback — behaviour intentionally unchanged). Full suite 142 passed; tsup DTS build (type-check) green.

Not in this PR

P2 (type: 'expression' approvers evaluated over flow variables + a node-level empty-approver policy) and P3 (resolver hook) are the more general evolutions; both are written up on #3447 for follow-up. This PR is the minimal P1 that fixes the reported scenario.

🤖 Generated with Claude Code

…rd at node entry (#3447)

Approval nodes resolved `field` / `manager` approvers against the trigger
snapshot the flow froze into `$record` at submit time, so an earlier step could
not set the field a later step routes on — dynamic routing / dynamic co-sign
were impossible to express. Resolution was already lazy per-node; the defect was
the data source (a frozen snapshot, never re-read), betrayed by graph approvers
resolving live in the same node while field/manager read the snapshot.

Re-read the record's live state by id at node entry under system identity, with
a warn-and-fall-back-to-snapshot path when the record is gone. Also fan a
multi-select user field into one approver slot per user (previously stringified
to one bogus id) and apply OOO delegation per fanned-out user (previously
silently skipped for multi-value fields). payload_json still snapshots the
submitted record — its contract is unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 25, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
spec Building Building Preview, Comment Jul 25, 2026 3:29am

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Jul 25, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/plugin-approvals.

4 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/automation/approvals.mdx (via @objectstack/plugin-approvals)
  • content/docs/plugins/packages.mdx (via @objectstack/plugin-approvals)
  • content/docs/releases/implementation-status.mdx (via @objectstack/plugin-approvals)
  • content/docs/releases/v9.mdx (via @objectstack/plugin-approvals)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@os-zhuang
os-zhuang merged commit d75edb9 into main Jul 25, 2026
15 of 16 checks passed
@os-zhuang
os-zhuang deleted the fix/3447-approver-live-record branch July 25, 2026 03:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

审批流无法在「运行中」动态确定审批人——审批人在提交时即被 $trigger 快照锁定(动态路由 / 动态会签受阻)

1 participant