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

vercelBot commented Jul 25, 2026

Copy link
Copy Markdown

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

ProjectDeploymentActionsUpdated (UTC)
specBuildingBuildingPreview, CommentJul 25, 2026 3:29am

Request Review

@github-actionsgithub-actionsBot 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.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

1 participant

@os-zhuang