Repository navigation
Description
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-showcase boot, registerShowcaseApprovalDemo launches 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 approve decision was recorded on request
areq_6e629dda-…but its flow runrun_bbf65df7-…could not be resumed and is now stranded: resume of runrun_bbf65df7-…failed: Nodenotify_clearedfailed: notify: at least one recipient is required, but every recipient template resolved to nothing:{record.account.owner}. A flow template reads the trigger record as it was written — a relation field holds a scalar id, not an expanded record, so{record.<lookup>.<field>}resolves to nothing. Add the relation to the start node'sconfig.expandso the engine hydrates it, or address the id directly ({record.<lookup>}).
The decision itself lands — GET /api/v1/data/sys_approval_request shows "status": "approved" on the request — and the inbox count drops 3 → 2. The flow run is what is left stranded, so the invoice never reaches end_ok and the "Notify: Cleared" inbox message the demo promises is never delivered.
Cause
examples/app-showcase/src/automation/flows/index.ts:1095:
{
id: 'notify_cleared',
type: 'notify',
config: {
recipients: ['{record.account.owner}'], // ← account is a scalar FK here
…
},
},The start node (index.ts:1071) declares objectName / triggerType / condition and no config.expand, so record.account is the raw id. The request's own stored payload confirms it:
"account": "h8AhbJyB-W2ZgO81",
"owner": "grace@example.com",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:
- Add
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. - Address the invoice's own
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 verify and 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 typecheck and the coverage test see a well-formed notify node; 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
- objectstack
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).
Activity
claude commentedon Aug 10, 2026
Findings triage: promoted
findingtopm:queue, routeddomain:services, gradedtarget:v17.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).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.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.notify_clearedor this flow.本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
os-help commentedon Aug 10, 2026
Claim: PM loop round 5 (
domain:servicesseat #6021) —target:v17priority dispatch per the maintainer's standing "v17 优先派发" directive.Session:
session_015fkdTyGmMD5s8ZtEifvuGyBranch:
claude/issue-7381-showcase-approval-expandWorktree:
objectstack-issue-7381Domain:
domain:servicesFile 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: opusSerial 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
os-help commentedon Aug 10, 2026
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:
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.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
notify_clearedresolves{record.account.owner}to nothing (possible regression of #7381) #7524 as a duplicate of this issuefix(showcase): unstrand the Invoice Dual Sign-off approval demo (obje…