Repository navigation
[showcase][approvals] Approving the Invoice Dual Sign-off demo strands its flow run — notify_cleared reads {record.account.owner} with no expand on the start node #7381
Description
Activity
Findings triage: promoted
findingtopm:queue, routeddomain:services, gradedtarget:v17.- Landing:
examples/app-showcase/src/automation/flows/index.ts— verified onorigin/main@60f0dd8:notify_clearedat :1095 reads{record.account.owner}and the start node config declares noexpand(read directly at :1069-1077). Examples route to the subsystem they exercise (approvals/flow, sodomain:services). - Grading: concrete defect + exact repro + the engine diagnostic already names both fixes, so queue, not hold. The card's option 1 (
expand: ['account']) also demonstrates the hydration path a kitchen-sink example should teach; the dev run should sweep the sibling showcase flows for the same resume-time pattern before closing, per the card. - Release grading:
target:v17— class-4: the flagship approvals demo dead-ends at its payoff moment on a stock boot; a first-hour-experience failure that release notes would have to apologize for. Cheap to fix, cheap to veto off the board if the maintainer disagrees. - Dedup: no open issue/PR names
notify_clearedor this flow.
本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
- Landing:
Claim: PM loop round 5 (
domain:servicesseat #6021) —target:v17priority dispatch per the maintainer's standing "v17 优先派发" directive.
Session:session_015fkdTyGmMD5s8ZtEifvuGy
Branch:claude/issue-7381-showcase-approval-expand
Worktree:objectstack-issue-7381
Domain:domain:services
File surface:examples/app-showcase/src/automation/flows/index.ts+ a flow-resume regression test in the same app (new file or the flows' existing suite). ⛔ NOTtest/hook-body-persisted-writes.test.ts(#7311's file, just merged). (Stop on breach; explain in the report.)
Container & model: M,mode:subagent,model: opus
Serial constraints cleared: PR #7311 (app-showcase, test-only, disjoint files) MERGED; no in-flight claim touches app-showcase. Anchors re-verified by triage @60f0dd8(:1095recipients template,:1069-1077start node withoutexpand).
Generated by Claude Code
ACCEPT. PR #7395, reviewed by the
domain:servicesseat (#6021, sessionsession_015fkdTyGmMD5s8ZtEifvuGy). Marking ready and queueing —target:v17priority.Verified against GitHub rather than the report: 5 files (flows/index.ts, the new 412-line real-kernel resume suite, four dev-only harness deps + lockfile, changeset), all inside
examples/app-showcase. 25/25 check runssuccess/skipped (ESLint 09:25:55Z, TypeScript Type Check 09:37:19Z, by job conclusion).Fixes #7381correct — the sweep is complete and in the PR body as required.Two things worth the record:
- The dispatch prompt's preferred route (option 1 verbatim) was falsified by measurement and correctly overridden under its own escape clause:
showcase_accounthas NOownerfield (people-ish keys arebilling_email+ injectedowner_id), so hydration alone strands identically — asserted in a test that reads the persisted snapshot and pins the absent key. What landed keeps BOTH halves of the option's intent with fields that exist: recipient →{record.owner}(the invoice's own RLS anchor),expand: ['account']kept LIVE via{record.account.name}in the message body. The PM's suggested route being wrong and the dev proving it is the system working. - The mandated sweep found a second live instance —
showcase_task_done_notify_ownerran EVERY task completion to a failure on main ({record.project.owner}unhydrated into the subflow's notify). Fixed in the same PR per the same-class clause; 29 flows swept, table in the PR body, the other 27 make no relation hops.
The reverse verification reproduces the issue's reported toast verbatim (
RESUME_FAILED … {record.account.owner}) and the quoted scalar-id payload, with the pre-fix case asserting diagnostic content rather than a bare throw. The resume-time mechanism (expander mutates the run record in place → survives the suspend/restore round-trip) is documented at both fix sites.
Generated by Claude Code
- The dispatch prompt's preferred route (option 1 verbatim) was falsified by measurement and correctly overridden under its own escape clause:
- added a commit that references this issue
on Aug 17, 2026
Found while browser-verifying the #7213 epic's converged approvals surface. Unrelated to that epic — this is a defect in the showcase example's own flow authoring — but it makes the showcase's marquee approval demo dead-end at the moment it is supposed to pay off.
What happens
On a fresh
examples/app-showcaseboot,registerShowcaseApprovalDemolaunches three real pending requests so the Approvals Inbox has something to act on. Approving Invoice Dual Sign-off (parallel approval) (INV-1010) from the inbox produces this error toast:The decision itself lands —
GET /api/v1/data/sys_approval_requestshows"status": "approved"on the request — and the inbox count drops 3 → 2. The flow run is what is left stranded, so the invoice never reachesend_okand the "Notify: Cleared" inbox message the demo promises is never delivered.Cause
examples/app-showcase/src/automation/flows/index.ts:1095:The
startnode (index.ts:1071) declaresobjectName/triggerType/conditionand noconfig.expand, sorecord.accountis the raw id. The request's own stored payload confirms it:The engine's diagnostic is exact and already names both fixes; nothing about the runtime behaviour is in question here.
Suggested shape (not prescriptive)
Two candidate fixes, and they are not equivalent for a showcase:
expand: ['account']to the start node's config — keeps{record.account.owner}and demonstrates theconfig.expandhydration path, which is arguably the thing a kitchen-sink example should be teaching.ownerfield ({record.owner}) — simpler, but then the flow no longer exercises relation hydration at all.Whichever is chosen, the same pattern is worth a sweep across the other showcase flows before closing: the failure only surfaces at resume time, well after
pnpm verifyand after the seed, so a second instance of it would sit undetected in exactly the same way.Why this went unnoticed
Nothing static catches it.
pnpm typecheckand the coverage test see a well-formednotifynode; the seed loader suppresses record-change flows (#2661), so the request is opened imperatively by the demo bootstrap and the node in question is only reached when a human approves. It takes a real click in a real browser — which is how it was found.Environment
main@88154bee1,@objectstack/*@17.0.0-rc.5, vendored console at objectui8aad9fd50b16,objectstack dev --ui --seed-adminon a fresh sqlite DB, zh-CN, dev admin (who holdsfinance+legal, so one click satisfies bothunanimousslots and drives the run straight intonotify_cleared).