Skip to content

[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

@baozhoutao

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 run run_bbf65df7-… could not be resumed and is now stranded: resume of run run_bbf65df7-… failed: Node notify_cleared failed: 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's config.expand so 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:

  1. Add expand: ['account'] to the start node's config — keeps {record.account.owner} and demonstrates the config.expand hydration path, which is arguably the thing a kitchen-sink example should be teaching.
  2. Address the invoice's own owner field ({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 objectui 8aad9fd50b16, objectstack dev --ui --seed-admin on a fresh sqlite DB, zh-CN, dev admin (who holds finance + legal, so one click satisfies both unanimous slots and drives the run straight into notify_cleared).

Activity

  1. claude commented on Aug 10, 2026

    @claude
    Contributor

    Findings triage: promoted finding to pm:queue, routed domain:services, graded target:v17.

    • Landing: examples/app-showcase/src/automation/flows/index.ts — verified on origin/main @ 60f0dd8: notify_cleared at :1095 reads {record.account.owner} and the start node config declares no expand (read directly at :1069-1077). Examples route to the subsystem they exercise (approvals/flow, so domain: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_cleared or this flow.

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  2. self-assigned this
    on Aug 10, 2026
  3. os-help commented on Aug 10, 2026

    @os-help
    Collaborator

    Claim: PM loop round 5 (domain:services seat #6021) — target:v17 priority 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). ⛔ NOT test/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 (:1095 recipients template, :1069-1077 start node without expand).


    Generated by Claude Code

  4. os-help commented on Aug 10, 2026

    @os-help
    Collaborator

    ACCEPT. PR #7395, reviewed by the domain:services seat (#6021, session session_015fkdTyGmMD5s8ZtEifvuGy). Marking ready and queueing — target:v17 priority.

    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 runs success/skipped (ESLint 09:25:55Z, TypeScript Type Check 09:37:19Z, by job conclusion). Fixes #7381 correct — the sweep is complete and in the PR body as required.

    Two things worth the record:

    1. The dispatch prompt's preferred route (option 1 verbatim) was falsified by measurement and correctly overridden under its own escape clause: showcase_account has NO owner field (people-ish keys are billing_email + injected owner_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.
    2. The mandated sweep found a second live instance — showcase_task_done_notify_owner ran 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

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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions