Skip to content

Commit dad73e9

Browse files
os-helpclaude
andauthored
fix(showcase): unstrand the Invoice Dual Sign-off approval demo (#7381) (#7395)
`notify_cleared` addressed `{record.account.owner}` with no `expand` on the start node, so the hop read a scalar FK and resolved to nothing. The notify node refuses a run with zero recipients, so approving the demo recorded the decision and stranded the flow run at its payoff moment. The hop was unfixable as written: `showcase_account` has no `owner` field. The notice now addresses `showcase_invoice.owner`, while the start node declares `expand: ['account']` and the body reads `{record.account.name}`, so the demo still teaches relation hydration with a field the account has. Sweep: `showcase_task_done_notify_owner` carried the same resume-time pattern (`{record.project.owner}` into a subflow notify, no expand) and is fixed too. Claude-Session: https://claude.ai/code/session_015fkdTyGmMD5s8ZtEifvuGy Co-authored-by: Claude <noreply@anthropic.com>
1 parent eaed2ee commit dad73e9

5 files changed

Lines changed: 481 additions & 4 deletions

File tree

.changeset/quiet-pumas-invent.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
'@objectstack/example-showcase': patch
3+
---
4+
5+
showcase: stop the Invoice Dual Sign-off approval demo stranding its flow run
6+
7+
`showcase_invoice_signoff`'s `notify_cleared` node addressed `{record.account.owner}`
8+
while its `start` node declared no `config.expand`, so the hop read a scalar foreign key
9+
and resolved to nothing. The notify node refuses a run with no recipients, so approving
10+
the showcase's marquee approval demo recorded the decision and then stranded the flow
11+
run — the "Notify: Cleared" inbox message never arrived.
12+
13+
The hop was unfixable as written: `showcase_account` has no `owner` field, so hydrating
14+
the relation would not have helped. The notice now addresses `showcase_invoice.owner`
15+
(the seeded rep, and the object's own row-level-security anchor), while the start node
16+
declares `expand: ['account']` and the message body reads `{record.account.name}` — so
17+
the demo still teaches the relation-hydration path, with a field the account really has.
18+
19+
The same resume-time pattern is fixed in `showcase_task_done_notify_owner`, which hopped
20+
`{record.project.owner}` into a subflow's notify with no `expand` on its start node.

examples/app-showcase/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,12 @@
3535
},
3636
"devDependencies": {
3737
"@objectstack/cli": "workspace:*",
38+
"@objectstack/core": "workspace:*",
3839
"@objectstack/formula": "workspace:*",
3940
"@objectstack/objectql": "workspace:*",
41+
"@objectstack/plugin-approvals": "workspace:*",
42+
"@objectstack/service-automation": "workspace:*",
43+
"@objectstack/service-messaging": "workspace:*",
4044
"@playwright/test": "^1.62.1",
4145
"typescript": "^6.0.3",
4246
"vitest": "^4.1.10"

examples/app-showcase/src/automation/flows/index.ts

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,11 @@ export const TaskCompletedFlow = defineFlow({
5757
type: 'notify',
5858
label: 'Notify the assignee',
5959
config: {
60-
// A field ON the record: the flow record carries `project` as a scalar
61-
// id, so `{record.project.owner}` would resolve to an empty string.
60+
// A field ON the record — deliberately, so this flow stays the simple
61+
// specimen. The flow record carries `project` as a scalar id, so
62+
// `{record.project.owner}` would resolve to an empty string unless the
63+
// start node declared `expand: ['project']`; the sibling
64+
// `showcase_task_done_notify_owner` is where that hop is demonstrated.
6265
recipients: '{record.assignee}',
6366
title: '✅ Task done: {record.title}',
6467
message: '{summary}',
@@ -674,6 +677,14 @@ export const TaskDoneNotifyOwnerFlow = defineFlow({
674677
objectName: 'showcase_task',
675678
triggerType: 'record-after-update',
676679
condition: 'status == "done" && previous.status != "done"',
680+
// The SAME resume-time trap this flow's sibling hit (#7381): the node
681+
// below hops `{record.project.owner}`, and a flow record carries
682+
// `project` as a scalar FK. Un-hydrated it resolved to nothing, the
683+
// subflow's `notify` refused for want of a recipient, and every
684+
// completion of a task ran this flow to a failure. Unlike the invoice
685+
// case the hop is sound — `showcase_project.owner` is a real, seeded
686+
// field — so the relation only needed declaring.
687+
expand: ['project'],
677688
},
678689
},
679690
{
@@ -1074,6 +1085,13 @@ export const InvoiceDualSignoffFlow = defineFlow({
10741085
objectName: 'showcase_invoice',
10751086
triggerType: 'record-after-update',
10761087
condition: 'status == "sent" && previous.status != "sent"',
1088+
// #3475 opt-in single-hop hydration. A flow record carries `account` as
1089+
// a scalar FK, so `{record.account.*}` reads nothing unless the relation
1090+
// is declared here; the engine re-reads it once, before the run starts,
1091+
// and the expanded object is part of the run state that survives the
1092+
// approval pause — which is why `notify_cleared` can still read it at
1093+
// RESUME time, hours or days later (#7381).
1094+
expand: ['account'],
10771095
},
10781096
},
10791097
{
@@ -1097,11 +1115,22 @@ export const InvoiceDualSignoffFlow = defineFlow({
10971115
label: 'Notify: Cleared',
10981116
config: {
10991117
topic: 'invoice.signoff',
1100-
recipients: ['{record.account.owner}'],
1118+
// The INVOICE's own owner, not `{record.account.owner}` (#7381).
1119+
// `showcase_account` has no `owner` field at all — its people-ish keys
1120+
// are `billing_email` and the injected `owner_id` — so that hop resolved
1121+
// to nothing however the relation was hydrated, and the notify node
1122+
// refuses a run with no recipients: approving the demo stranded its run
1123+
// instead of delivering this message. `showcase_invoice.owner` is the
1124+
// seeded rep (the same RLS anchor the contributor permission set uses),
1125+
// which is who "your invoice cleared sign-off" is addressed to anyway.
1126+
recipients: ['{record.owner}'],
11011127
channels: ['inbox'],
11021128
severity: 'info',
1129+
// The expanded relation is read HERE — `{record.account.name}` is what
1130+
// makes the start node's `expand: ['account']` live rather than inert,
1131+
// and it is the hydration path this kitchen-sink flow exists to teach.
11031132
title: 'Invoice cleared: {record.name}',
1104-
message: 'Invoice "{record.name}" passed finance + legal sign-off and is on its way.',
1133+
message: 'Invoice "{record.name}" for {record.account.name} passed finance + legal sign-off and is on its way.',
11051134
actionUrl: '/showcase_invoice/{record.id}',
11061135
},
11071136
},

0 commit comments

Comments
 (0)