Skip to content

Commit 73f5bf9

Browse files
committed
fix(example-todo): drop objectName from the two schedule start nodes
task_reminder and overdue_escalation stored objectName on a schedule start node. The schedule binding carries no object: deriveTriggerBinding lifts objectName into the record-change binding only, and ScheduleTrigger never reads it, so the key was stored and never acted on (the flow designer flags it "kept, not in effect"). Both sweeps already name todo_task on their get_record steps. Adds a pin over the shipped stack: no flow whose resolved trigger kind is schedule carries objectName on its start node. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018VqJPGA1NDNSCZsnecsChP
1 parent 9ffe8af commit 73f5bf9

2 files changed

Lines changed: 66 additions & 2 deletions

File tree

‎examples/app-todo/src/flows/task.flow.ts‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,12 @@ export const TaskReminderFlow: Flow = {
4949
],
5050

5151
nodes: [
52-
{ id: 'start', type: 'start', label: 'Start (Daily 8 AM)', config: { schedule: '0 8 * * *', objectName: 'todo_task' } },
52+
// [#24067] No `objectName` on a schedule start node: the schedule binding
53+
// carries no object (`AutomationEngine.deriveTriggerBinding` lifts
54+
// `objectName` into the record-change binding only), so the key would be
55+
// stored and never read. The sweep names its object where it reads, on the
56+
// `get_record` step below.
57+
{ id: 'start', type: 'start', label: 'Start (Daily 8 AM)', config: { schedule: '0 8 * * *' } },
5358
{
5459
id: 'get_upcoming_tasks', type: 'get_record', label: 'Get Tasks Due Tomorrow',
5560
// `limit > 1` is the declared way to make this a LIST read (`find`, not
@@ -158,7 +163,8 @@ export const OverdueEscalationFlow: Flow = {
158163
],
159164

160165
nodes: [
161-
{ id: 'start', type: 'start', label: 'Start (Daily 9 AM)', config: { schedule: '0 9 * * *', objectName: 'todo_task' } },
166+
// No `objectName` here either: see `task_reminder`'s start node.
167+
{ id: 'start', type: 'start', label: 'Start (Daily 9 AM)', config: { schedule: '0 9 * * *' } },
162168
{
163169
id: 'get_overdue_tasks', type: 'get_record', label: 'Get Severely Overdue Tasks',
164170
// `limit > 1` = LIST read; the undeclared `getAll` was never read (#4277).
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* [#24067] No schedule flow this example ships stores `objectName` on its
5+
* start node.
6+
*
7+
* A schedule binding carries no object. `AutomationEngine.deriveTriggerBinding`
8+
* lifts the start node's `config.objectName` into the record-change binding
9+
* (and, as a fallback behind `timeRelative.object`, into the time-relative
10+
* one); its `schedule` branch hands the trigger `schedule` and `organization`
11+
* only, and `ScheduleTrigger` never reads `objectName`. On a schedule start
12+
* node the key is stored and never acted on, and the console's flow designer
13+
* labels it "kept, not in effect". An example is what an AI copies, so the
14+
* example must not model it: a sweep names its object where it reads, on its
15+
* `get_record` step, as both daily sweeps here do.
16+
*
17+
* The flows are read off the stack the example ships (its default
18+
* `defineStack` export), never a hand list, so a schedule flow added later is
19+
* covered on the day it lands. "Schedule" is the spec's
20+
* `resolveFlowTriggerKind` answer, the one predicate the engine binds by: a
21+
* time-relative sweep, whose binding DOES fall back to `objectName`, is
22+
* correctly left out, and every other `type: 'schedule'` flow is in.
23+
*/
24+
25+
import { describe, it, expect } from 'vitest';
26+
import { resolveFlowTriggerKind } from '@objectstack/spec/automation';
27+
28+
import TodoApp from '../objectstack.config.js';
29+
30+
type NodeLike = { id?: string; type?: string; config?: Record<string, unknown> };
31+
type FlowLike = { name?: string; type?: string; nodes?: NodeLike[] };
32+
33+
const shippedFlows = ((TodoApp as unknown as { flows?: FlowLike[] }).flows ?? []);
34+
const scheduleFlows = shippedFlows.filter((flow) => resolveFlowTriggerKind(flow) === 'schedule');
35+
36+
function startConfig(flow: FlowLike): Record<string, unknown> {
37+
const start = (flow.nodes ?? []).find((node) => node.type === 'start');
38+
if (!start) throw new Error(`harness: flow '${flow.name}' has no start node`);
39+
return start.config ?? {};
40+
}
41+
42+
describe('#24067: a schedule start node carries no objectName', () => {
43+
it('reads real schedule flows off the shipped stack (anti-vacuity)', () => {
44+
expect(scheduleFlows.length).toBeGreaterThan(0);
45+
// Each start node read here is the one the schedule binding reads: it
46+
// carries the cadence that binding DOES act on.
47+
for (const flow of scheduleFlows) {
48+
expect(startConfig(flow), `flow '${flow.name}' start node`).toHaveProperty('schedule');
49+
}
50+
});
51+
52+
it('no schedule flow stores objectName on its start node', () => {
53+
const offenders = scheduleFlows
54+
.filter((flow) => Object.hasOwn(startConfig(flow), 'objectName'))
55+
.map((flow) => `flow '${flow.name}' · start node config.objectName`);
56+
expect(offenders).toEqual([]);
57+
});
58+
});

0 commit comments

Comments
 (0)