Skip to content

fix(example-todo): task_completion computes a real next recurrence due date instead of writing a dead DATEADD(...) string - #7227

Merged
os-help merged 3 commits into
mainfrom
claude/issue-7037-recurrence-due-date
Aug 10, 2026
Merged

fix(example-todo): task_completion computes a real next recurrence due date instead of writing a dead DATEADD(...) string#7227
os-help merged 3 commits into
mainfrom
claude/issue-7037-recurrence-due-date

Conversation

@os-help

@os-help os-help commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

Fixes #7037

examples/app-todo's TaskCompletionFlow spawned the next occurrence of a recurring task with a literal DATEADD(...) template in create_record's due_date. Two independent faults stacked: DATEADD exists nowhere in the platform, and a create_record node's fields values are TEMPLATE-interpolated rather than evaluated — the holes are filled and the surrounding text passes through verbatim. So the engine received the literal string DATEADD(2026-08-10, 1, "daily") and the field's own coercion refused it.

Premise re-verified against origin/main

Still live at examples/app-todo/src/flows/task.flow.ts:201, exactly as the card described.

Measured BEFORE — real kernel, real record-change trigger, sqlite

One recurring task transitioned into completed, driven the way the #6882 suite drives it:

run.status      = failed
run.error       = "Node 'create_next_task' failed: create_record(todo_task) failed: Due Date must be a valid date (ISO-8601)"
steps           = start:success -> get_task:success -> check_recurring:success -> create_next_task:failure
spawned         = NO

Verbatim the failure the card reported.

The route, and why the alternatives are not available

The triage ruling was to use whatever date arithmetic the platform actually ships. Measured, not assumed:

Surface Can it compute a value mid-flow?
assignment node Nologic-nodes.ts calls interpolate() on each value. It substitutes, it does not evaluate.
Any node config expression slot NoFLOW_NODE_EXPRESSION_PATHS in @objectstack/spec/automation lists every declared slot: screen.fields[].visibleWhen and decision.conditions[].expression are PREDICATES, loop.collection / map.collection are flow-template REFERENCES. Plus the structural predicate surfaces config.condition and edge.condition. There is no value-producing expression slot in the vocabulary.
script node → registered function Yesconfig.function resolves through defineStack({ functions }) (#1870); pure, takes input, returns a value, a later declarative node persists it (#4396).
A formula field on todo_task Would work (packages/formula does ship addDays / addMonths), but src/objects/task.object.ts is sibling #7036's declared region this round, and it adds a permanent schema surface for one flow's use.

So the value has to be computed before the create node, and the script node is the shipped way — the same shape showcase_task_completed already uses. No shipped-capability gap; the ruling's STOP-and-report branch did not trigger.

The change

  • src/functions/task.functions.ts (new) — computeNextTaskDueDate, covering all four authored cadences (daily / weekly / monthly / yearly × recurrence_interval). It clamps a monthly shift to the target month's last day exactly as @objectstack/formula's addMonths does, so the example cannot teach a recurrence semantic that disagrees with the platform's own formula function. Unknown recurrence_type, or an interval min: 1 forbids, refuse loudly rather than guessing a cadence; an absent interval takes the field's own declared defaultValue: 1; a completed task with no due_date yields null rather than an invented day.
  • src/flows/task.flow.ts — a compute_next_due_date script node on the recurring branch, due_date: '{nextDueDate}' as a whole-string token (the one form interpolateString returns the raw value for), edge e3 retargeted and e6 added. The check_recurring gate is unchanged, so the non-recurring path still skips both nodes.
  • objectstack.config.tsfunctions: todoFunctions on defineStack.
  • test/task-recurrence.test.ts (new — 14 it declarations, 16 cases once the parameterized end-to-end block expands).

DATEADD survives in no authored value anywhere; the only remaining mentions are comments and test prose describing the removed defect, and a test asserts the name's absence from JSON.stringify(allFlows).

Measured AFTER — same harness, same record

run.status      = completed
run.error       = null
steps           = start:success -> get_task:success -> check_recurring:success -> compute_next_due_date:success -> create_next_task:success -> end:skipped
spawned         = yes
spawned.due_date= 2026-08-11

Reverse verification, committed

The suite rebuilds the pre-fix shape from the LIVE flow (computation node removed, recurring edge pointed straight at the create, a function call left in the field value) and asserts it fails inside create_next_task with the date refusal — and explicitly not with no function named …, because nothing ever tried to call one. The value was never a call; it was always just text. Non-vacuous: the real flow on the same kernel and same record completes and lands 2026-08-11.

The invented name in that fixture is deliberately not the historical one. The failure was never specific to DATEADD — any function-call text in a create_record field value reaches the driver as a literal string — so the fixture pins the class and keeps the dead name out of the repo, which is the other half of the card. A separate class-level guard asserts no write node in any of the app's flows leaves function-call text in a field value; that is the check that would have caught the original defect at authoring time.

Verification

  • pnpm --filter '@objectstack/example-todo^...' build — dependency closure (prefix ^... = upstream deps), green.
  • pnpm --filter @objectstack/example-todo typecheck — green.
  • pnpm --filter @objectstack/example-todo test99 passed (99), 3 files. Re-run after merging origin/main.
  • pnpm --filter @objectstack/example-todo build (objectstack build) — green, Bundling 1 handler. The 2 remaining author-time warnings are pre-existing flow-draft-status-ambiguous on the two schedule flows, deliberately out of scope per examples/app-todo task_completion declares type: 'record_change' with no triggerType — the flow is dead, and its trigger condition is written to a key nothing reads #6882's note in the same file.
  • npx eslint on the changed files — clean.
  • node scripts/check-nul-bytes.mjs — OK; plus a control-byte self-scan of the changed files, clean.
  • CI converged green on all 24 checks, including ESLint (where this repo's family gates run), TypeScript Type Check, Check Changeset, Test Core and the Dogfood Regression Gate.

Changeset

Included, @objectstack/example-todo: patch. The policy does cover examples/** here: the package is versioned by changesets (it carries a version and a CHANGELOG.md), .changeset/config.json sets no ignore, and the sibling example-only PR for #6882 carried .changeset/todo-task-completion-trigger-armed.md on the same package. So no skip-changeset is needed — and Check Changeset confirms it, green.

Region discipline

Sibling #7036 owns src/objects/task.object.ts, src/hooks/task.hook.ts, src/actions/task.handlers.ts and test/task-completion-trigger.test.ts this round — none are touched (git diff origin/main --stat covers only src/flows/task.flow.ts, objectstack.config.ts, the new src/functions/, the new test, and the changeset). The repro is driven the way #6882's suite drives it (completed_date seeded on CREATE), without editing that file. origin/main merged before opening; happy to merge again after #7036 lands.


Generated by Claude Code

claude added 3 commits August 10, 2026 02:58
…ate instead of writing a dead DATEADD string (#7037)

`create_next_task` wrote `DATEADD({completedTask.due_date}, ...)` into `due_date`.
`DATEADD` exists nowhere in the platform, and a `create_record` node's `fields` are
template-interpolated rather than evaluated, so the driver received the literal string
`DATEADD(2026-08-10, 1, "daily")` and refused it with `Due Date must be a valid date
(ISO-8601)`. Dead while the flow was unbound; a failed run on every recurring completion
once #6882 armed it.

No flow node evaluates a value-producing expression (the only expression slots are
predicates and `flow-template` references -- `FLOW_NODE_EXPRESSION_PATHS`), so the date is
now computed before the create by a `script` node calling `computeNextTaskDueDate`,
registered via `defineStack({ functions })`.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015fkdTyGmMD5s8ZtEifvuGy
…p's authored metadata (#7037)

Named explicitly rather than only covered by the function-call-shape guard: an
invented function name in a SHIPPED example is what an AI author copies, so its
absence is enforced instead of merely current.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015fkdTyGmMD5s8ZtEifvuGy
@vercel

vercel Bot commented Aug 10, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 10, 2026 3:12am

Request Review

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

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

2 participants