Skip to content

Add first-class codemode scheduler - #157

Merged
kentcdodds merged 9 commits into
mainfrom
cursor/codemode-job-scheduler-445e
Apr 12, 2026
Merged

kentcdodds merged 9 commits into
mainfrom
cursor/codemode-job-scheduler-445e

Conversation

@kentcdodds

@kentcdodds kentcdodds commented Apr 12, 2026 •

Copy link
Copy Markdown
Owner

Closes #156

Summary

  • add a per-user SchedulerDO with Durable Object storage, alarm scheduling, and codemode execution reuse
  • register first-class scheduler_upsert, scheduler_list, scheduler_get, scheduler_delete, and scheduler_run_now codemode capabilities
  • harden scheduler behavior for per-job caller context persistence, metadata-only updates that preserve nextRunAt, and normalized execution failures
  • align scheduler writes with existing upsert-style capability conventions and update scheduler docs/instructions/tests accordingly
  • stabilize scheduler MCP E2E coverage by passing runtime ids through execute params and using a dynamic future runAt timestamp

Testing

  • npm run typecheck
  • npx vitest run --project node-unit packages/worker/src/scheduler/schedule.node.test.ts packages/worker/src/scheduler/process-due-jobs.node.test.ts
  • npx vitest run --project workers-unit packages/worker/src/mcp/capabilities/build-capability-registry.workers.test.ts packages/worker/src/mcp/capabilities/build-capability-registry.workers.test.ts packages/worker/src/oauth-handlers.workers.test.ts
  • npx vitest run --project mcp-e2e packages/worker/src/mcp/mcp-server.mcp-e2e.test.ts -t "mcp server manages scheduled codemode jobs"
Open in Web Open in Cursor 

Summary by CodeRabbit

  • New Features

    • Added scheduled job management with five new operations: create/update, list, inspect, delete, and run immediately.
    • Support for recurring schedules using standard cron syntax and one-time schedules using ISO 8601 timestamps.
  • Documentation

    • Added comprehensive guide for using scheduled codemode jobs.

cursoragent and others added 2 commits April 12, 2026 19:34
createEnv omitted SCHEDULER_DO after EnvSchema required it, so getEnv()
threw and resolveSessionEmail swallowed the error as a null session.

Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
@coderabbitai

coderabbitai Bot commented Apr 12, 2026 •

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This pull request implements a first-class scheduler system that allows users to schedule codemode code execution on recurring (cron) or one-shot (ISO 8601) schedules. It adds a Durable Object (SchedulerDO) for job persistence and alarm-based execution, five new MCP capabilities for lifecycle management, supporting utilities for schedule parsing and validation, a client library for DO communication, and comprehensive tests.

Changes

Cohort / File(s) Summary
Documentation
docs/use/execute.md
Added "Scheduled jobs" section documenting scheduler capabilities and accepted schedule formats (5-field cron with optional IANA timezone, ISO 8601 UTC timestamps).
Type Definitions & Schemas
packages/worker/src/scheduler/types.ts, packages/worker/src/mcp/capabilities/scheduler/shared.ts
Introduced comprehensive type definitions for jobs (ScheduledJob, ScheduledJobView), schedules (cron/once discriminated union), execution results, and input/output validation schemas including conditional field requirements for upsert operations.
Schedule Utilities
packages/worker/src/scheduler/schedule.ts, packages/worker/src/scheduler/process-due-jobs.ts
Implemented schedule parsing, normalization, timezone validation (IANA), next-run computation using croner, and job processing logic that handles execution, status recording, and one-shot job deletion with error resilience.
Scheduler Core & Storage
packages/worker/src/scheduler/scheduler-do.ts, packages/worker/src/scheduler/client.ts
Built Durable Object for multi-tenant job persistence with HTTP routing, alarm-based execution triggering, and typed client library (schedulerCreate, schedulerList, schedulerGet, schedulerUpdate, schedulerDelete, schedulerRunNow) for DO communication.
MCP Capabilities
packages/worker/src/mcp/capabilities/scheduler/*.ts (5 capability files), packages/worker/src/mcp/capabilities/domain-metadata.ts, builtin-domains.ts
Registered five new scheduler capabilities (scheduler_upsert, scheduler_list, scheduler_get, scheduler_delete, scheduler_run_now) under new scheduler domain with appropriate metadata (readonly/idempotent flags), input/output schema wiring, and user authentication via requireMcpUser/requireSchedulerUser.
MCP Infrastructure & Instructions
packages/worker/src/mcp/tools/execute.ts, search.ts, server-instructions.ts, packages/worker/src/index.ts
Extended execute/search tool descriptions to reference scheduler capabilities; added server instructions for upsert/list/get/delete/run-now guidance; exported SchedulerDO from worker module.
Configuration & Bindings
packages/worker/src/env-schema.ts, packages/worker/wrangler.jsonc, package.json
Added required SCHEDULER_DO DurableObjectNamespace environment binding validation; configured Durable Object in wrangler.jsonc with migration tag v4 for all environments; added croner dependency (^10.0.1).
Testing
packages/worker/src/mcp/capabilities/build-capability-registry.workers.test.ts, process-due-jobs.node.test.ts, schedule.node.test.ts, mcp-server.mcp-e2e.test.ts, oauth-handlers.workers.test.ts
Added unit tests for schedule parsing/normalization, job processing with error handling, cron/timezone computation; added comprehensive E2E test exercising full upsert/list/run-now/update/get/delete workflow; updated OAuth test env setup with mocked SCHEDULER_DO namespace.

Sequence Diagram(s)

sequenceDiagram
    participant User as User/MCP Client
    participant Cap as Scheduler Capability
    participant Client as Scheduler Client
    participant DO as SchedulerDO
    participant Storage as DO Storage
    participant Pipeline as Codemode Pipeline

    User->>Cap: scheduler_upsert (name, code, schedule)
    Cap->>Cap: requireSchedulerUser()
    Cap->>Client: schedulerCreate/Update()
    Client->>DO: POST /jobs (callerContext, job data)
    DO->>DO: Parse & normalize job (schedule, timezone)
    DO->>DO: Compute nextRunAt
    DO->>Storage: Save job:<jobId> & job-context:<jobId>
    DO->>DO: syncAlarm() - set to earliest nextRunAt
    DO-->>Client: Return ScheduledJobView
    Client-->>Cap: Return ScheduledJobView
    Cap-->>User: Return ScheduledJobView

    Note over DO: [Time passes, alarm fires]
    DO->>Storage: Load all jobs
    DO->>DO: Filter jobs where enabled && nextRunAt <= now
    DO->>DO: processDueJobs(jobs, executeJob callback)
    loop For each due job
        DO->>Pipeline: runCodemodeWithRegistry (code, params, callerContext)
        Pipeline-->>DO: result or error
        DO->>DO: Record lastRunAt, lastRunStatus, lastRunError
        alt Cron schedule
            DO->>DO: computeNextRunAt()
            DO->>Storage: Save updated job
        else Once schedule
            DO->>Storage: Delete job:<jobId> & job-context:<jobId>
        end
    end
    DO->>DO: syncAlarm() - set to next earliest nextRunAt
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 Hop, hop, the scheduler springs to life,
Cron and ISO, no more strife!
One-shots and repeats, all persisted with care,
The Durable Object alarm rings fair! 🔔
Jobs at dawn, jobs at night,
Kody now schedules all just right!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'Add first-class codemode scheduler' clearly and concisely summarizes the primary change: introducing scheduler capabilities for executing codemodes on recurring or one-time schedules.
Linked Issues check ✅ Passed The PR successfully implements all coding requirements from issue #156: SchedulerDO with alarm handler, job persistence, cron/once scheduling with IANA timezone support, five capabilities (upsert, list, get, delete, run_now), per-job error handling, nextRunAt precomputation, one-shot cleanup, and alarm updates after mutations.
Out of Scope Changes check ✅ Passed All changes are directly aligned with the scheduler feature scope: job persistence schema, cron/once parsing, capability implementations, MCP integration, end-to-end tests, and documentation updates. No unrelated modifications detected.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cursor/codemode-job-scheduler-445e

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
@kentcdodds
kentcdodds marked this pull request as ready for review April 12, 2026 19:42
@github-actions

github-actions Bot commented Apr 12, 2026 •

Copy link
Copy Markdown
Contributor

🔎 Preview deployed: https://kody-pr-157.kentcdodds.workers.dev

Worker: kody-pr-157
D1: kody-pr-157-db
KV: kody-pr-157-oauth-kv

Mocks:

Comment thread packages/worker/src/scheduler/schedule.ts
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
packages/worker/src/scheduler/process-due-jobs.node.test.ts (1)

44-54: Prefer order-agnostic assertions for saveJobs.

These assertions currently depend on array order. If processing order changes, this can fail despite correct behavior.

Proposed refactor
-	expect(result.saveJobs[0]).toMatchObject({
+	const firstSaved = result.saveJobs.find((job) => job.id === 'job-1')
+	expect(firstSaved).toMatchObject({
 		id: 'job-1',
 		lastRunStatus: 'error',
 		lastRunError: 'boom',
 		lastRunAt: now.toISOString(),
 	})
-	expect(result.saveJobs[1]).toMatchObject({
+	const secondSaved = result.saveJobs.find((job) => job.id === 'job-2')
+	expect(secondSaved).toMatchObject({
 		id: 'job-2',
 		lastRunStatus: 'success',
 		lastRunAt: now.toISOString(),
 	})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/worker/src/scheduler/process-due-jobs.node.test.ts` around lines 44
- 54, The test currently asserts result.saveJobs by index which is
order-dependent; update it to be order-agnostic by asserting the array contains
the expected job objects regardless of order—e.g., replace the two index-based
expects on result.saveJobs[...] with a single assertion using
expect.arrayContaining and expect.objectContaining for the two job shapes (id
'job-1' with lastRunStatus 'error' and lastRunError 'boom', and id 'job-2' with
lastRunStatus 'success'), or alternatively map result.saveJobs by id and assert
each mapped entry matches the expected properties.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/worker/src/mcp/mcp-server.mcp-e2e.test.ts`:
- Around line 233-260: The test hard-codes a future timestamp causing flakiness
when that date passes; instead generate a dynamic future runAt (e.g., Date.now()
+ some offset) before calling mcpClient.client.callTool and pass that ISO
timestamp into the codemode.scheduler_update arguments (refer to jobId and
codemode.scheduler_update in the call), then assert against that generated ISO
string (use the same value for both expected schedule.runAt and nextRunAt when
inspecting updateStructured) so the test always uses a valid future time.

In `@packages/worker/src/scheduler/scheduler-do.ts`:
- Around line 23-24: The current key callerContextStorageKey
('scheduler:caller-context') is global to the Durable Object and causes caller
context (baseUrl, connector refs, storageContext) to be overwritten across jobs;
change all uses to persist caller context per-job by composing the storage key
from jobStorageKeyPrefix and the job id (e.g., jobStorageKeyPrefix + jobId +
':caller-context') so each job stores/reads its own context. Update every
read/write that references callerContextStorageKey (including the block around
lines 287-297) to use the per-job key and ensure functions that load or apply
caller context accept/derive jobId when constructing the storage key.
- Around line 172-199: The current update always calls computeNextRunAt and
overwrites existing.nextRunAt; change this to preserve existing.nextRunAt unless
the schedule or timezone actually changed or the job is being re-enabled:
compare the new schedule (from normalizeScheduledJobSchedule) and new timezone
(from normalizeSchedulerTimezone) to existing.schedule/timezone, and only call
computeNextRunAt({schedule, timezone}) when they differ or when
payload.body.enabled transitions from false to true; otherwise set
updated.nextRunAt = existing.nextRunAt. Use the existing symbols
(normalizeScheduledJobSchedule, normalizeSchedulerTimezone, computeNextRunAt,
payload.body.enabled, existing) to locate where to add the conditional.

---

Nitpick comments:
In `@packages/worker/src/scheduler/process-due-jobs.node.test.ts`:
- Around line 44-54: The test currently asserts result.saveJobs by index which
is order-dependent; update it to be order-agnostic by asserting the array
contains the expected job objects regardless of order—e.g., replace the two
index-based expects on result.saveJobs[...] with a single assertion using
expect.arrayContaining and expect.objectContaining for the two job shapes (id
'job-1' with lastRunStatus 'error' and lastRunError 'boom', and id 'job-2' with
lastRunStatus 'success'), or alternatively map result.saveJobs by id and assert
each mapped entry matches the expected properties.
🪄 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: 45491ff8-6081-4270-8081-b85dc07990c8

📥 Commits

Reviewing files that changed from the base of the PR and between 8104ec5 and db29733.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (29)
  • docs/use/execute.md
  • package.json
  • packages/worker/src/env-schema.ts
  • packages/worker/src/index.ts
  • packages/worker/src/mcp/capabilities/build-capability-registry.workers.test.ts
  • packages/worker/src/mcp/capabilities/builtin-domains.ts
  • packages/worker/src/mcp/capabilities/domain-metadata.ts
  • packages/worker/src/mcp/capabilities/scheduler/domain.ts
  • packages/worker/src/mcp/capabilities/scheduler/index.ts
  • packages/worker/src/mcp/capabilities/scheduler/scheduler-create.ts
  • packages/worker/src/mcp/capabilities/scheduler/scheduler-delete.ts
  • packages/worker/src/mcp/capabilities/scheduler/scheduler-get.ts
  • packages/worker/src/mcp/capabilities/scheduler/scheduler-list.ts
  • packages/worker/src/mcp/capabilities/scheduler/scheduler-run-now.ts
  • packages/worker/src/mcp/capabilities/scheduler/scheduler-update.ts
  • packages/worker/src/mcp/capabilities/scheduler/shared.ts
  • packages/worker/src/mcp/mcp-server.mcp-e2e.test.ts
  • packages/worker/src/mcp/server-instructions.ts
  • packages/worker/src/mcp/tools/execute.ts
  • packages/worker/src/mcp/tools/search.ts
  • packages/worker/src/oauth-handlers.workers.test.ts
  • packages/worker/src/scheduler/client.ts
  • packages/worker/src/scheduler/process-due-jobs.node.test.ts
  • packages/worker/src/scheduler/process-due-jobs.ts
  • packages/worker/src/scheduler/schedule.node.test.ts
  • packages/worker/src/scheduler/schedule.ts
  • packages/worker/src/scheduler/scheduler-do.ts
  • packages/worker/src/scheduler/types.ts
  • packages/worker/wrangler.jsonc

Comment thread packages/worker/src/mcp/mcp-server.mcp-e2e.test.ts
Comment thread packages/worker/src/scheduler/scheduler-do.ts Outdated
Comment thread packages/worker/src/scheduler/scheduler-do.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (2)
packages/worker/src/scheduler/scheduler-do.ts (2)

24-25: ⚠️ Potential issue | 🟠 Major

Persist caller context per job, not per Durable Object.

This still shares one execution context across every job in the user's DO, so a later create/update/run-now can overwrite baseUrl, connector refs, or storageContext for older jobs. Store caller context under a job-specific key and load it per job during alarm(); use a separate namespace such as job-context:${jobId} so the job: prefix scan in listStoredJobs() keeps returning only ScheduledJob records.

Also applies to: 122-126, 299-310

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/worker/src/scheduler/scheduler-do.ts` around lines 24 - 25, The
callerContextStorageKey currently stores caller context on the DO level causing
later operations to overwrite context for other jobs; change to persist caller
context per job by using a job-scoped key pattern (e.g. job-context:${jobId})
instead of callerContextStorageKey, update storage writes where
callerContextStorageKey is set (e.g. in create/update/run-now flows) to write
under job-context:${jobId}, and update alarm() and listStoredJobs() readers to
load the job-specific context by reading job-context:${jobId} when handling a
ScheduledJob; ensure the jobStorageKeyPrefix ('job:') remains reserved for
ScheduledJob records so listStoredJobs() scanning logic continues to return only
ScheduledJob entries.

182-209: ⚠️ Potential issue | 🟠 Major

Preserve nextRunAt on metadata-only updates.

Line 206 still recomputes nextRunAt for every PATCH. Renaming a job or editing code/params can silently push an already pending run out to a later time. Keep existing.nextRunAt unless the normalized schedule/timezone changed or enabled is transitioning from false to true.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/worker/src/scheduler/scheduler-do.ts` around lines 182 - 209, The
code always recomputes nextRunAt (via computeNextRunAt) when building the
updated ScheduledJob, which can move a pending run; change the logic in the
update block that constructs updated (around schedule, timezone, enabled) so
that nextRunAt is set to existing.nextRunAt unless one of three conditions is
true: the normalized schedule changed (compare new schedule vs
existing.schedule), the normalized timezone changed (compare timezone vs
existing.timezone), or enabled is transitioning from false to true
(existing.enabled === false && (payload.body.enabled ?? existing.enabled) ===
true); only in those cases call computeNextRunAt({ schedule, timezone }) to
assign nextRunAt. Reference symbols: normalizeScheduledJobSchedule,
normalizeSchedulerTimezone, payload.body.enabled, existing.nextRunAt,
computeNextRunAt, and the updated: ScheduledJob construction.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/worker/src/scheduler/scheduler-do.ts`:
- Around line 235-242: The call to executeJob in the run-now path can throw
(e.g., runCodemodeWithRegistry throws) and currently bypasses the normal error
shape; wrap the executeJob(...) invocation in a try/catch and convert any thrown
exception into a unified failure result object (e.g., { ok: false, error: string
| Error, ... }) so the subsequent code can always read execution.ok and
execution.error; make the same change for the other direct executeJob call noted
(lines ~261-279) so both paths persist lastRunStatus/lastRunError consistently.

---

Duplicate comments:
In `@packages/worker/src/scheduler/scheduler-do.ts`:
- Around line 24-25: The callerContextStorageKey currently stores caller context
on the DO level causing later operations to overwrite context for other jobs;
change to persist caller context per job by using a job-scoped key pattern (e.g.
job-context:${jobId}) instead of callerContextStorageKey, update storage writes
where callerContextStorageKey is set (e.g. in create/update/run-now flows) to
write under job-context:${jobId}, and update alarm() and listStoredJobs()
readers to load the job-specific context by reading job-context:${jobId} when
handling a ScheduledJob; ensure the jobStorageKeyPrefix ('job:') remains
reserved for ScheduledJob records so listStoredJobs() scanning logic continues
to return only ScheduledJob entries.
- Around line 182-209: The code always recomputes nextRunAt (via
computeNextRunAt) when building the updated ScheduledJob, which can move a
pending run; change the logic in the update block that constructs updated
(around schedule, timezone, enabled) so that nextRunAt is set to
existing.nextRunAt unless one of three conditions is true: the normalized
schedule changed (compare new schedule vs existing.schedule), the normalized
timezone changed (compare timezone vs existing.timezone), or enabled is
transitioning from false to true (existing.enabled === false &&
(payload.body.enabled ?? existing.enabled) === true); only in those cases call
computeNextRunAt({ schedule, timezone }) to assign nextRunAt. Reference symbols:
normalizeScheduledJobSchedule, normalizeSchedulerTimezone, payload.body.enabled,
existing.nextRunAt, computeNextRunAt, and the updated: ScheduledJob
construction.
🪄 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: 5e366617-5a37-4001-a079-1f3fa4f79dd7

📥 Commits

Reviewing files that changed from the base of the PR and between db29733 and 61a7542.

📒 Files selected for processing (1)
  • packages/worker/src/scheduler/scheduler-do.ts

Comment thread packages/worker/src/scheduler/scheduler-do.ts Outdated
Comment thread packages/worker/src/scheduler/scheduler-do.ts Outdated
Comment thread packages/worker/src/mcp/capabilities/scheduler/index.ts Outdated
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Comment thread packages/worker/src/scheduler/scheduler-do.ts
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>

export function requireSchedulerUser(ctx: CapabilityContext) {
return requireMcpUser(ctx.callerContext)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Inconsistent auth helper usage across scheduler capabilities

Low Severity

requireSchedulerUser in shared.ts is a trivial wrapper around requireMcpUser(ctx.callerContext), but only scheduler-list and scheduler-update use it. The other four capabilities (scheduler-create, scheduler-delete, scheduler-get, scheduler-run-now) call requireMcpUser(ctx.callerContext) directly. This split makes it unclear whether the wrapper exists for a reason or is leftover from a refactor, increasing maintenance confusion.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 393e02b. Configure here.

Interpolating memory_id into generated codemode source triggered a workerd
SWC internal error; using execute's params keeps user code stable.

Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (3)
packages/worker/src/scheduler/scheduler-do.ts (2)

241-242: ⚠️ Potential issue | 🟠 Major

Normalize thrown codemode execution failures in executeJob.

If runCodemodeWithRegistry throws, handleRunNow currently bubbles into the top-level fetch catch and skips lastRunStatus/lastRunError persistence.

Suggested fix
-		const execution = await runCodemodeWithRegistry(
-			this.env,
-			callerContext,
-			job.code,
-			job.params,
-			workerExports,
-		)
+		let execution
+		try {
+			execution = await runCodemodeWithRegistry(
+				this.env,
+				callerContext,
+				job.code,
+				job.params,
+				workerExports,
+			)
+		} catch (error) {
+			return {
+				ok: false,
+				error: formatSchedulerError(error),
+				logs: [],
+			}
+		}

Also applies to: 272-279

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/worker/src/scheduler/scheduler-do.ts` around lines 241 - 242,
executeJob currently lets exceptions from runCodemodeWithRegistry bubble up
(causing handleRunNow to skip persisting lastRunStatus/lastRunError); update
executeJob to catch errors from runCodemodeWithRegistry (and any
codemode-related calls), normalize them into a structured result (e.g., {
success: false, error: <message|stack|code> }) and return that instead of
throwing so handleRunNow can always write lastRunStatus and lastRunError;
specifically modify executeJob to wrap the runCodemodeWithRegistry call in a
try/catch, populate a failure result, and ensure handleRunNow uses that result
to update ScheduledJob.lastRunStatus and ScheduledJob.lastRunError (also apply
the same pattern to the similar block around lines 272-279).

24-25: ⚠️ Potential issue | 🟠 Major

Persist caller context per job, not per Durable Object.

A single scheduler:caller-context key is still shared across all jobs in the user DO, so newer mutations overwrite execution context for older jobs.

Suggested fix
-const callerContextStorageKey = 'scheduler:caller-context'
 const jobStorageKeyPrefix = 'job:'

 function getJobStorageKey(jobId: string) {
 	return `${jobStorageKeyPrefix}${jobId}`
 }
+
+function getCallerContextStorageKey(jobId: string) {
+	return `${getJobStorageKey(jobId)}:caller-context`
+}
@@
-		const callerContext = await this.getPersistedCallerContext()
 		const result = await processDueJobs({
 			jobs: dueJobs,
 			now,
-			executeJob: async (job) => this.executeJob(job, callerContext),
+			executeJob: async (job) =>
+				this.executeJob(job, await this.getPersistedCallerContext(job.id)),
 		})
@@
-		await this.persistCallerContext(payload.callerContext)
+		await this.persistCallerContext(job.id, payload.callerContext)
@@
-		await this.persistCallerContext(payload.callerContext)
+		await this.persistCallerContext(jobId, payload.callerContext)
@@
-		await this.persistCallerContext(payload.callerContext)
+		await this.persistCallerContext(jobId, payload.callerContext)
@@
-	private async persistCallerContext(
+	private async persistCallerContext(
+		jobId: string,
 		callerContext: PersistedSchedulerCallerContext,
 	) {
-		await this.ctx.storage.put(callerContextStorageKey, callerContext)
+		await this.ctx.storage.put(getCallerContextStorageKey(jobId), callerContext)
 	}
 
-	private async getPersistedCallerContext() {
+	private async getPersistedCallerContext(jobId: string) {
 		return (
 			(await this.ctx.storage.get<PersistedSchedulerCallerContext>(
-				callerContextStorageKey,
+				getCallerContextStorageKey(jobId),
 			)) ?? null
 		)
 	}

Also applies to: 122-123, 310-320

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/worker/src/scheduler/scheduler-do.ts` around lines 24 - 25, The code
currently stores caller context under a single key (callerContextStorageKey) so
newer jobs overwrite older ones; change the storage scheme to persist context
per job by composing the key from jobStorageKeyPrefix + jobId (e.g.,
`${jobStorageKeyPrefix}${jobId}:caller-context`) and update all places that
read/write callerContextStorageKey to use this per-job key; ensure all usages
(reads, writes, deletes) that reference callerContextStorageKey are replaced so
each job uses its own storage key derived from jobId.
packages/worker/src/mcp/mcp-server.mcp-e2e.test.ts (1)

233-260: ⚠️ Potential issue | 🟠 Major

Avoid hard-coded future runAt in scheduler update E2E.

This timestamp will become stale and eventually break the test when once.runAt must be in the future.

Suggested fix
+	const futureRunAt = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString()
+
 	const updateResult = await mcpClient.client.callTool({
 		name: 'execute',
 		arguments: {
 			code: `async () => {
 				return await codemode.scheduler_update({
 					id: ${JSON.stringify(jobId)},
 					enabled: false,
-					schedule: { type: 'once', runAt: '2026-04-18T15:00:00Z' },
+					schedule: { type: 'once', runAt: ${JSON.stringify(futureRunAt)} },
 				})
 			}`,
 		},
 	})
@@
 	expect(updateStructured?.result?.schedule).toEqual({
 		type: 'once',
-		runAt: '2026-04-18T15:00:00.000Z',
+		runAt: futureRunAt,
 	})
-	expect(updateStructured?.result?.nextRunAt).toBe('2026-04-18T15:00:00.000Z')
+	expect(updateStructured?.result?.nextRunAt).toBe(futureRunAt)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/worker/src/mcp/mcp-server.mcp-e2e.test.ts` around lines 233 - 260,
The test uses a hard-coded future timestamp for codemode.scheduler_update which
will become stale; instead compute a dynamic future ISO timestamp when building
the callTool code (e.g., now + some minutes) and inject that value into the code
string passed to mcpClient.client.callTool, then update the assertions that
reference updateStructured?.result?.schedule.runAt and nextRunAt to expect the
computed ISO (with millisecond precision) rather than the fixed literal; locate
the call in the test where callTool is invoked and the subsequent expectations
for updateStructured to make these changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/worker/src/scheduler/scheduler-do.ts`:
- Around line 182-186: The type-narrowing for payload.body.schedule fails
because the boolean flag hasScheduleUpdate prevents TypeScript from tracking the
narrowed type; replace that pattern by extracting payload.body.schedule into a
local variable (e.g., const newSchedule = payload.body.schedule), use a direct
check on that variable (newSchedule !== undefined) and call
normalizeScheduledJobSchedule(newSchedule) when present, otherwise fall back to
existing.schedule; update the code that defines schedule to reference this local
variable and normalizeScheduledJobSchedule to satisfy typechecking.

---

Duplicate comments:
In `@packages/worker/src/mcp/mcp-server.mcp-e2e.test.ts`:
- Around line 233-260: The test uses a hard-coded future timestamp for
codemode.scheduler_update which will become stale; instead compute a dynamic
future ISO timestamp when building the callTool code (e.g., now + some minutes)
and inject that value into the code string passed to mcpClient.client.callTool,
then update the assertions that reference
updateStructured?.result?.schedule.runAt and nextRunAt to expect the computed
ISO (with millisecond precision) rather than the fixed literal; locate the call
in the test where callTool is invoked and the subsequent expectations for
updateStructured to make these changes.

In `@packages/worker/src/scheduler/scheduler-do.ts`:
- Around line 241-242: executeJob currently lets exceptions from
runCodemodeWithRegistry bubble up (causing handleRunNow to skip persisting
lastRunStatus/lastRunError); update executeJob to catch errors from
runCodemodeWithRegistry (and any codemode-related calls), normalize them into a
structured result (e.g., { success: false, error: <message|stack|code> }) and
return that instead of throwing so handleRunNow can always write lastRunStatus
and lastRunError; specifically modify executeJob to wrap the
runCodemodeWithRegistry call in a try/catch, populate a failure result, and
ensure handleRunNow uses that result to update ScheduledJob.lastRunStatus and
ScheduledJob.lastRunError (also apply the same pattern to the similar block
around lines 272-279).
- Around line 24-25: The code currently stores caller context under a single key
(callerContextStorageKey) so newer jobs overwrite older ones; change the storage
scheme to persist context per job by composing the key from jobStorageKeyPrefix
+ jobId (e.g., `${jobStorageKeyPrefix}${jobId}:caller-context`) and update all
places that read/write callerContextStorageKey to use this per-job key; ensure
all usages (reads, writes, deletes) that reference callerContextStorageKey are
replaced so each job uses its own storage key derived from jobId.
🪄 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: 546b4d15-14e5-4ca7-a6ff-31ff7a609ca9

📥 Commits

Reviewing files that changed from the base of the PR and between 61a7542 and 6b5faf3.

📒 Files selected for processing (2)
  • packages/worker/src/mcp/mcp-server.mcp-e2e.test.ts
  • packages/worker/src/scheduler/scheduler-do.ts

Comment thread packages/worker/src/scheduler/scheduler-do.ts Outdated
Comment thread packages/worker/src/scheduler/scheduler-do.ts
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/worker/src/scheduler/scheduler-do.ts`:
- Around line 66-69: The normalizeOptionalParams function currently returns any
JSON value; update it to enforce that params must be a plain JSON object per the
ScheduledJob.params contract: if params is null/undefined return undefined,
otherwise validate that typeof params === "object" and Array.isArray(params) ===
false (and not null) and throw a TypeError (or reject) when the value is an
array or a non-object primitive so invalid inputs are rejected up front;
reference the function name normalizeOptionalParams and ensure the thrown error
message clearly states it expects an object for ScheduledJob.params.
- Around line 111-112: The current catch in the scheduler handler
indiscriminately maps all exceptions to a 400 using formatSchedulerError; change
it so only explicit client/request validation errors (the specific error
type/class that formatSchedulerError expects) are returned as new
Response(formatSchedulerError(error), { status: 400 }), and all other exceptions
are rethrown (or allowed to bubble) so they surface as 5xx and are captured by
the outer Sentry wrapper around the handler (see Sentry wrapper near lines
~397-400); locate the catch in scheduler-do.ts and use type/instance checks
against the client-error type or a predicate to decide whether to return 400 vs
rethrow.
🪄 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: d109ad26-fb42-4604-9d07-79b9f8be4b8f

📥 Commits

Reviewing files that changed from the base of the PR and between 6b5faf3 and 73552e4.

📒 Files selected for processing (3)
  • packages/worker/src/mcp/mcp-server.mcp-e2e.test.ts
  • packages/worker/src/scheduler/process-due-jobs.node.test.ts
  • packages/worker/src/scheduler/scheduler-do.ts
✅ Files skipped from review due to trivial changes (1)
  • packages/worker/src/scheduler/process-due-jobs.node.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/worker/src/mcp/mcp-server.mcp-e2e.test.ts

Comment on lines +66 to +69
function normalizeOptionalParams(
params: Record<string, unknown> | null | undefined,
): Record<string, unknown> | undefined {
return params === null || params === undefined ? undefined : params

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

Validate params as a JSON object before persisting it.

request.json() is untyped at runtime, so this currently accepts arrays, strings, numbers, etc. That violates the ScheduledJob.params contract and pushes bad input into scheduled execution instead of rejecting it up front.

Suggested fix
-function normalizeOptionalParams(
-	params: Record<string, unknown> | null | undefined,
-): Record<string, unknown> | undefined {
-	return params === null || params === undefined ? undefined : params
+function normalizeOptionalParams(
+	params: unknown,
+): Record<string, unknown> | undefined {
+	if (params === null || params === undefined) {
+		return undefined
+	}
+	if (typeof params !== 'object' || Array.isArray(params)) {
+		throw new Error('Scheduled job params must be a JSON object.')
+	}
+	return params as Record<string, unknown>
 }
📝 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
function normalizeOptionalParams(
params: Record<string, unknown> | null | undefined,
): Record<string, unknown> | undefined {
return params === null || params === undefined ? undefined : params
function normalizeOptionalParams(
params: unknown,
): Record<string, unknown> | undefined {
if (params === null || params === undefined) {
return undefined
}
if (typeof params !== 'object' || Array.isArray(params)) {
throw new Error('Scheduled job params must be a JSON object.')
}
return params as Record<string, unknown>
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/worker/src/scheduler/scheduler-do.ts` around lines 66 - 69, The
normalizeOptionalParams function currently returns any JSON value; update it to
enforce that params must be a plain JSON object per the ScheduledJob.params
contract: if params is null/undefined return undefined, otherwise validate that
typeof params === "object" and Array.isArray(params) === false (and not null)
and throw a TypeError (or reject) when the value is an array or a non-object
primitive so invalid inputs are rejected up front; reference the function name
normalizeOptionalParams and ensure the thrown error message clearly states it
expects an object for ScheduledJob.params.

Comment on lines +111 to +112
} catch (error) {
return new Response(formatSchedulerError(error), { status: 400 })

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

Don't collapse server failures into HTTP 400s.

This catch turns everything—including storage faults and unexpected bugs—into a client error. It also swallows exceptions before the Sentry wrapper at Lines 397-400 can see them. Restrict 4xx responses to explicit request errors and let unexpected failures surface as 500s.

Suggested direction
-		} catch (error) {
-			return new Response(formatSchedulerError(error), { status: 400 })
+		} catch (error) {
+			if (error instanceof SchedulerNotFoundError) {
+				return new Response(error.message, { status: 404 })
+			}
+			if (error instanceof SchedulerValidationError) {
+				return new Response(error.message, { status: 400 })
+			}
+			throw error
 		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/worker/src/scheduler/scheduler-do.ts` around lines 111 - 112, The
current catch in the scheduler handler indiscriminately maps all exceptions to a
400 using formatSchedulerError; change it so only explicit client/request
validation errors (the specific error type/class that formatSchedulerError
expects) are returned as new Response(formatSchedulerError(error), { status: 400
}), and all other exceptions are rethrown (or allowed to bubble) so they surface
as 5xx and are captured by the outer Sentry wrapper around the handler (see
Sentry wrapper near lines ~397-400); locate the catch in scheduler-do.ts and use
type/instance checks against the client-error type or a predicate to decide
whether to return 400 vs rethrow.

Comment thread packages/worker/src/scheduler/schedule.ts
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/worker/src/mcp/capabilities/scheduler/scheduler-upsert.ts (1)

29-35: Avoid defaulting required create fields to empty strings.

Line 29 and Line 30 silently coerce missing create inputs into ''. It’s safer to fail fast and preserve the required-field contract.

Proposed fix
 		async handler(args, ctx: CapabilityContext) {
 			const user = requireSchedulerUser(ctx)
 			if (args.id === undefined) {
+				if (
+					args.name === undefined ||
+					args.code === undefined ||
+					args.schedule === undefined
+				) {
+					throw new Error(
+						'scheduler_upsert create requires name, code, and schedule.',
+					)
+				}
 				return schedulerCreate(ctx.env, user.userId, {
 					callerContext: ctx.callerContext,
 					body: {
-						name: args.name ?? '',
-						code: args.code ?? '',
+						name: args.name,
+						code: args.code,
 						...(args.params !== undefined && args.params !== null
 							? { params: args.params }
 							: {}),
-						schedule: args.schedule!,
+						schedule: args.schedule,

Based on learnings: Follow MCP capabilities specifications as documented in docs/contributing/adding-capabilities.md and mcp-apps-spec-notes.md

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/worker/src/mcp/capabilities/scheduler/scheduler-upsert.ts` around
lines 29 - 35, The code in scheduler-upsert.ts is masking missing required
create fields by defaulting args.name and args.code to empty strings; instead,
preserve the required-field contract by removing the "?? ''" defaults and
enforce presence (e.g., throw or validate) before building the payload. Update
the builder that uses args.name and args.code to either (a) perform an explicit
null/undefined check and throw a clear error referencing "name" or "code", or
(b) use the non-null asserted values expected by the create flow so missing
inputs fail early; keep the existing conditional handling for params and
timezone and leave schedule as-is.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/worker/src/mcp/capabilities/scheduler/shared.ts`:
- Around line 22-25: The runAt schema currently only checks non-empty strings,
so update the runAt Zod validator (the runAt field in the schema in shared.ts)
to validate an ISO 8601 UTC timestamp: replace .min(1) with a refinement that
asserts the string matches an ISO8601 UTC pattern (for example using a regex
like /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z/ or use
z.string().datetime() if your Zod version supports it) and provide a clear error
message (e.g. "must be an ISO 8601 UTC timestamp like 2023-01-01T00:00:00Z") so
malformed timestamps are rejected at the schema boundary.

---

Nitpick comments:
In `@packages/worker/src/mcp/capabilities/scheduler/scheduler-upsert.ts`:
- Around line 29-35: The code in scheduler-upsert.ts is masking missing required
create fields by defaulting args.name and args.code to empty strings; instead,
preserve the required-field contract by removing the "?? ''" defaults and
enforce presence (e.g., throw or validate) before building the payload. Update
the builder that uses args.name and args.code to either (a) perform an explicit
null/undefined check and throw a clear error referencing "name" or "code", or
(b) use the non-null asserted values expected by the create flow so missing
inputs fail early; keep the existing conditional handling for params and
timezone and leave schedule as-is.
🪄 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: 08236ecc-a61d-4de0-a740-e2be1ffe6096

📥 Commits

Reviewing files that changed from the base of the PR and between 73552e4 and b43e833.

📒 Files selected for processing (10)
  • docs/use/execute.md
  • packages/worker/src/mcp/capabilities/build-capability-registry.workers.test.ts
  • packages/worker/src/mcp/capabilities/scheduler/domain.ts
  • packages/worker/src/mcp/capabilities/scheduler/scheduler-upsert.ts
  • packages/worker/src/mcp/capabilities/scheduler/shared.ts
  • packages/worker/src/mcp/mcp-server.mcp-e2e.test.ts
  • packages/worker/src/mcp/server-instructions.ts
  • packages/worker/src/mcp/tools/execute.ts
  • packages/worker/src/mcp/tools/search.ts
  • packages/worker/src/scheduler/types.ts
✅ Files skipped from review due to trivial changes (7)
  • packages/worker/src/mcp/tools/search.ts
  • packages/worker/src/mcp/capabilities/build-capability-registry.workers.test.ts
  • packages/worker/src/mcp/tools/execute.ts
  • packages/worker/src/mcp/server-instructions.ts
  • docs/use/execute.md
  • packages/worker/src/mcp/capabilities/scheduler/domain.ts
  • packages/worker/src/scheduler/types.ts

Comment on lines +22 to +25
runAt: z
.string()
.min(1)
.describe('ISO 8601 UTC timestamp for a one-shot run.'),

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

Validate once.runAt at the schema boundary.

Line 22 only enforces non-empty text, so malformed timestamps can pass capability validation and fail deeper in scheduler execution.

Proposed fix
  z.object({
    type: z.literal('once'),
    runAt: z
      .string()
      .min(1)
+     .refine(
+       (value) => !Number.isNaN(Date.parse(value)) && value.endsWith('Z'),
+       'runAt must be an ISO 8601 UTC timestamp ending with "Z".',
+     )
      .describe('ISO 8601 UTC timestamp for a one-shot run.'),
  }),
📝 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
runAt: z
.string()
.min(1)
.describe('ISO 8601 UTC timestamp for a one-shot run.'),
runAt: z
.string()
.min(1)
.refine(
(value) => !Number.isNaN(Date.parse(value)) && value.endsWith('Z'),
'runAt must be an ISO 8601 UTC timestamp ending with "Z".',
)
.describe('ISO 8601 UTC timestamp for a one-shot run.'),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/worker/src/mcp/capabilities/scheduler/shared.ts` around lines 22 -
25, The runAt schema currently only checks non-empty strings, so update the
runAt Zod validator (the runAt field in the schema in shared.ts) to validate an
ISO 8601 UTC timestamp: replace .min(1) with a refinement that asserts the
string matches an ISO8601 UTC pattern (for example using a regex like
/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z/ or use z.string().datetime() if
your Zod version supports it) and provide a clear error message (e.g. "must be
an ISO 8601 UTC timestamp like 2023-01-01T00:00:00Z") so malformed timestamps
are rejected at the schema boundary.

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Fix All in Cursor

Reviewed by Cursor Bugbot for commit b43e833. Configure here.

} else {
await this.ctx.storage.put(getJobStorageKey(jobId), updated)
await this.persistCallerContext(jobId, payload.callerContext)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Missing syncAlarm in handleRunNow recurring branch

Low Severity

The handleRunNow method's else branch (recurring jobs) writes to storage but omits the syncAlarm() call that every other storage-mutating handler includes (handleCreateJob, handleUpdateJob, handleDeleteJob, the once-type branch of handleRunNow, and alarm()). While handleRunNow doesn't currently change nextRunAt or enabled, the missing call breaks the self-healing pattern and means a previously-lost alarm won't be re-established by this code path.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit b43e833. Configure here.

@kentcdodds
kentcdodds merged commit b24d13a into main Apr 12, 2026
9 checks passed
@kentcdodds
kentcdodds deleted the cursor/codemode-job-scheduler-445e branch April 17, 2026 00:59
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.

feat: first-class scheduler — run codemode code on cron or datetime

2 participants