Skip to content

docs(research): cron and scheduling landscape#4

Merged
tannerlinsley merged 2 commits into
mainfrom
research/scheduling
May 22, 2026
Merged

docs(research): cron and scheduling landscape#4
tannerlinsley merged 2 commits into
mainfrom
research/scheduling

Conversation

@tannerlinsley
Copy link
Copy Markdown
Member

@tannerlinsley tannerlinsley commented May 22, 2026

Adds `research/SCHEDULING.md` — a comparison of how Inngest, Trigger.dev, Temporal, Cloudflare Workflows, Hatchet, DBOS, Restate, AWS Step Functions, Vercel WDK, Mastra, and LangGraph.js handle cron / recurring execution.

Why

Came up while answering "how do other libraries handle cron?" — worth saving as a sketchpad entry rather than relitigating later.

Key takeaways

  • Every mature engine separates the cron metadata from the workflow body and fires fresh invocations per tick.
  • Nobody runs production recurring workflows as "loop forever with sleep" — log growth, replay cost, and overlap-policy visibility all push toward fresh-invocation.
  • Temporal's six-mode overlap-policy enum is the gold standard worth copying.
  • Restate's self-rescheduling pattern is interesting but probably not the primary model.

Implications

The closure engine needs no changes for cron. A future `@tanstack/workflow-cron` package would be small (schedule definitions + a polling driver). Recommend deferring until at least one durable storage adapter ships.

Test plan

  • Docs-only change, no source touched
  • `pnpm format` clean
  • CI green

Summary by CodeRabbit

  • Documentation

    • Clarified the research README to frame design notes as historical context and decision-point sketches
    • Added a research note on recurring scheduling/cron patterns and proposed package architecture
    • Introduced a user-facing Scheduling concept doc with concrete external-scheduler recipes and overlap strategies
    • Updated site navigation to surface Scheduling documentation
  • Tests

    • Added end-to-end tests exercising external cron scheduling and overlap-handling scenarios (skip-overlap, buffer-one)

Review Change Stack

…hape

Captures research on how major workflow engines (Inngest, Trigger.dev,
Temporal, Cloudflare Workflows, Hatchet, DBOS, Restate, AWS Step
Functions, Vercel WDK) model recurring execution. Five consistent
patterns surface; informs the shape of a future @tanstack/workflow-cron
package.

Key finding: every mature engine separates the cron from the workflow
body and fires fresh invocations per tick. Nobody runs production
workflows as "loop forever with sleep." The closure engine already
supports the fresh-per-tick model — a scheduler is purely additive.

Also touches up research/README.md to call the directory a sketchpad
for upcoming work, not strictly historical.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 22, 2026

📝 Walkthrough

Walkthrough

Research documentation is added to the TanStack Workflow repository with a new scheduling and cron landscape analysis. The research directory README is clarified to better characterize design notes, and a new SCHEDULING.md document is cataloged that surveys recurring execution patterns across major workflow engines.

Changes

Workflow Scheduling Research and Documentation

Layer / File(s) Summary
README clarification and SCHEDULING.md catalog entry
research/README.md
Design notes disclaimer reworded to emphasize they are decision-point snapshots, not living documentation. Contents table extended with a new entry for SCHEDULING.md describing forward-looking cron and recurring execution research.
Concepts: external-scheduler recipes and overlap policies
docs/concepts/scheduling.md
Adds the external-scheduler model and concrete recipes for Node (node-cron), Cloudflare Worker cron, Vercel cron HTTP routes, and AWS EventBridge → Lambda. Describes skip-overlap and buffer-one overlap policies, schedule-store polling guidance, and references the test example.
Docs navigation update
docs/config.json
Inserted a "Scheduling" navigation child under Concepts pointing to concepts/scheduling.
External-cron tests: header and imports
packages/workflow-core/tests/examples.external-cron.test.ts
New Vitest test module and imports documenting the external-scheduler pattern.
Test: one-shot workflow factory
packages/workflow-core/tests/examples.external-cron.test.ts
Shared one-shot workflow factory used in tests defining input validation, generation and email steps, and returned output.
Test: scheduler helper abstraction
packages/workflow-core/tests/examples.external-cron.test.ts
Minimal setInterval-based scheduler helper used to drive periodic ticks under fake timers.
Test: three-tick fresh-run behavior
packages/workflow-core/tests/examples.external-cron.test.ts
Test that verifies each tick starts a fresh workflow run with distinct runIds and produces expected finished outputs.
Test: skip-overlap behavior
packages/workflow-core/tests/examples.external-cron.test.ts
Test simulating deterministic runId checks to skip starting a second run while a previous run is still in-flight.
Test: buffer-one behavior
packages/workflow-core/tests/examples.external-cron.test.ts
Test that collapses multiple ticks occurring during a long-running execution into a single buffered follow-up run.
SCHEDULING.md research document
research/SCHEDULING.md
New research document surveying timer primitives, cross-system cron implementations, extracted recurring patterns, a proposed cron-package architecture with deployment options, and open design questions; marked research-only and links to user recipes and the test example.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐰 A research scroll unfurls,
Cron patterns swirl and dance,
Temporal gears, schedules twirled—
No engines changed, just glance!
Sweet documentation's gift, ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately captures the main change: adding research documentation comparing how various workflow engines handle cron and scheduling patterns.
Description check ✅ Passed The PR description is largely complete with clear motivation, key takeaways, and implications, but is missing the standard checklist items and release impact sections from the template.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@research/SCHEDULING.md`:
- Line 70: The bullet starting with "A driver — polls a schedule store, computes
'next fire time,' fires `runWorkflow`." is a sentence fragment; update it to
include an explicit subject and verb so it's a complete sentence (e.g., "A
driver polls a schedule store, computes the next fire time, and fires
`runWorkflow`."). Edit the line in SCHEDULING.md to replace the fragment with a
full sentence while preserving the examples (Durable Object alarm, AWS Lambda +
EventBridge, Cloudflare Cron Trigger) and backtick formatting for `runWorkflow`.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7b864385-b4cb-47cc-af0a-77ec90021ada

📥 Commits

Reviewing files that changed from the base of the PR and between 214ac78 and 30e46ba.

📒 Files selected for processing (2)
  • research/README.md
  • research/SCHEDULING.md

Comment thread research/SCHEDULING.md
Two pieces:

- **Schedule definitions** — declarative, sit next to workflow definitions.
- **A driver** — polls a schedule store, computes "next fire time," fires `runWorkflow`. Can be deployed as a long-running worker, Durable Object alarm, AWS Lambda + EventBridge, Cloudflare Cron Trigger, etc.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix sentence fragment for readability.

Line 70 reads as a fragment after the previous sentence. Add an explicit subject to keep the bullet grammatically complete.

Suggested edit
-- **A driver** — polls a schedule store, computes "next fire time," fires `runWorkflow`. Can be deployed as a long-running worker, Durable Object alarm, AWS Lambda + EventBridge, Cloudflare Cron Trigger, etc.
+- **A driver** — polls a schedule store, computes "next fire time," and fires `runWorkflow`. It can be deployed as a long-running worker, Durable Object alarm, AWS Lambda + EventBridge, Cloudflare Cron Trigger, etc.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- **A driver** — polls a schedule store, computes "next fire time," fires `runWorkflow`. Can be deployed as a long-running worker, Durable Object alarm, AWS Lambda + EventBridge, Cloudflare Cron Trigger, etc.
- **A driver** — polls a schedule store, computes "next fire time," and fires `runWorkflow`. It can be deployed as a long-running worker, Durable Object alarm, AWS Lambda + EventBridge, Cloudflare Cron Trigger, etc.
🧰 Tools
🪛 LanguageTool

[style] ~70-~70: To form a complete sentence, be sure to include a subject.
Context: ... "next fire time," fires runWorkflow. Can be deployed as a long-running worker, D...

(MISSING_IT_THERE)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@research/SCHEDULING.md` at line 70, The bullet starting with "A driver —
polls a schedule store, computes 'next fire time,' fires `runWorkflow`." is a
sentence fragment; update it to include an explicit subject and verb so it's a
complete sentence (e.g., "A driver polls a schedule store, computes the next
fire time, and fires `runWorkflow`."). Edit the line in SCHEDULING.md to replace
the fragment with a full sentence while preserving the examples (Durable Object
alarm, AWS Lambda + EventBridge, Cloudflare Cron Trigger) and backtick
formatting for `runWorkflow`.

Concrete companion to research/SCHEDULING.md — shows the bring-your-
own-scheduler pattern that's possible with what shipping today.

docs/concepts/scheduling.md
- "Mental model in one paragraph" — external scheduler + fresh
  workflow invocation per tick, never loop with sleep
- Six recipes: node-cron, Cloudflare Worker cron, Vercel Cron Job,
  AWS EventBridge → Lambda, skip-overlap policy via deterministic
  runId, buffer-one policy via local queue
- Sketch of a writing-your-own schedule store + worker

packages/workflow-core/tests/examples.external-cron.test.ts
- vitest-fake-timer-driven scheduler against the engine, three
  scenarios:
   - fresh runId per tick, multiple ticks finish independently
   - skip-overlap via deterministic runId + state check
   - buffer-one via local pending flag, third tick collapses into
     the buffer

docs/config.json — adds Concepts → Scheduling nav entry.
research/SCHEDULING.md — points at the new recipes + test under
"Status" so the research and the user-facing material cross-link.

Verification: tsc clean, eslint clean, 105 / 22 tests pass
(+3 new), tsdown build clean.

NOTE: PR CI is currently blocked by a GitHub Actions billing issue
on the TanStack org — no jobs are starting. Push for visibility;
will merge once billing resolves.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/workflow-core/tests/examples.external-cron.test.ts`:
- Around line 179-188: The test uses vi.advanceTimersByTimeAsync(0) to try to
ensure tick1 reached the held step, which is flaky; change it to use an explicit
"step started" latch like the existing pattern used elsewhere in the file: add a
promise/resolver pair (e.g., stepStartedPromise and resolveStepStarted) inside
the held step callback that is resolved when the step begins, await that promise
after creating tick1 (instead of awaiting vi.advanceTimersByTimeAsync(0)), then
create tick2 via tickWithSkipOverlap(), and only call releaseFirstStep() after
the latch ensures the first step is actually running; reference the existing
latch implementation in the file to mirror naming and setup.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f1db5e9b-bc53-4b35-8901-f96f814b97da

📥 Commits

Reviewing files that changed from the base of the PR and between 30e46ba and 7f45a55.

📒 Files selected for processing (4)
  • docs/concepts/scheduling.md
  • docs/config.json
  • packages/workflow-core/tests/examples.external-cron.test.ts
  • research/SCHEDULING.md
✅ Files skipped from review due to trivial changes (1)
  • research/SCHEDULING.md

Comment on lines +179 to +188
const tick1 = tickWithSkipOverlap()
// Yield so tick1 progresses to inside the step fn and is "running".
await vi.advanceTimersByTimeAsync(0)

// Fire tick2 — same minute window, first is still running.
const tick2 = tickWithSkipOverlap()
await vi.advanceTimersByTimeAsync(0)

// Release the first tick.
releaseFirstStep!()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid timer-yield synchronization in skip-overlap test; use an explicit “step started” latch.

await vi.advanceTimersByTimeAsync(0) does not reliably guarantee tick1 reached the held step, so releaseFirstStep!() can race and make this test flaky. Use a promise latch (same pattern as Line 207-Line 209) before firing tick2.

Proposed deterministic fix
+    let firstStepStarted: (() => void) | null = null
+    const firstStepStartedPromise = new Promise<void>((r) => {
+      firstStepStarted = r
+    })
     let releaseFirstStep: (() => void) | null = null
     workFn.mockImplementationOnce(async () => {
       invocations++
+      firstStepStarted!()
       await new Promise<void>((r) => {
         releaseFirstStep = r
       })
       return { summary: 'first' }
     })

     const tick1 = tickWithSkipOverlap()
-    // Yield so tick1 progresses to inside the step fn and is "running".
-    await vi.advanceTimersByTimeAsync(0)
+    // Deterministically wait until tick1 is inside the held step.
+    await firstStepStartedPromise

     // Fire tick2 — same minute window, first is still running.
     const tick2 = tickWithSkipOverlap()
-    await vi.advanceTimersByTimeAsync(0)

     // Release the first tick.
     releaseFirstStep!()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/workflow-core/tests/examples.external-cron.test.ts` around lines 179
- 188, The test uses vi.advanceTimersByTimeAsync(0) to try to ensure tick1
reached the held step, which is flaky; change it to use an explicit "step
started" latch like the existing pattern used elsewhere in the file: add a
promise/resolver pair (e.g., stepStartedPromise and resolveStepStarted) inside
the held step callback that is resolved when the step begins, await that promise
after creating tick1 (instead of awaiting vi.advanceTimersByTimeAsync(0)), then
create tick2 via tickWithSkipOverlap(), and only call releaseFirstStep() after
the latch ensures the first step is actually running; reference the existing
latch implementation in the file to mirror naming and setup.

@tannerlinsley tannerlinsley merged commit 721d546 into main May 22, 2026
3 of 9 checks passed
@tannerlinsley tannerlinsley deleted the research/scheduling branch May 22, 2026 03:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant