Skip to content

Add one-off job scheduling capability - #237

Merged
kentcdodds merged 4 commits into
mainfrom
cursor/capability-one-off-jobs-4bb0
Apr 20, 2026
Merged

kentcdodds merged 4 commits into
mainfrom
cursor/capability-one-off-jobs-4bb0

Conversation

@kentcdodds

@kentcdodds kentcdodds commented Apr 20, 2026 •

Copy link
Copy Markdown
Owner

Summary

  • add a built-in job_schedule capability for scheduling standalone repo-backed jobs without creating a saved package first, including one-off, interval, and cron schedules
  • keep job_schedule_once as a compatibility wrapper for one-off schedules while routing scheduling logic through shared helpers
  • update job execution so standalone kody.json job sources run correctly while package-owned jobs keep using package manifests, and deduplicate the shared bundle/execute path
  • fix a startup import cycle in the shared jobs capability helper that was breaking preview/validate CI deployments
  • document the expanded standalone scheduling path in MCP server instructions and user docs

Testing

  • npm run test -- packages/worker/src/mcp/capabilities/jobs/job-schedule.node.test.ts packages/worker/src/jobs/service.node.test.ts
  • npx oxlint packages/worker/src/mcp/capabilities/jobs/shared.ts packages/worker/src/mcp/capabilities/jobs/job-schedule.ts packages/worker/src/mcp/capabilities/jobs/job-schedule-once.ts packages/worker/src/mcp/capabilities/jobs/job-schedule.node.test.ts packages/worker/src/jobs/service.ts
  • GitHub Actions: latest ✅ Validate and 🔎 Preview runs for cursor/capability-one-off-jobs-4bb0 now pass
Open in Web Open in Cursor 

Summary by CodeRabbit

Release Notes

  • New Features

    • Added job_schedule and job_schedule_once capabilities to schedule repo-backed jobs directly without creating saved packages.
    • Jobs now support interval, cron, and one-off scheduling with dedicated storage bindings.
  • Documentation

    • Updated guidance clarifying when to use scheduled jobs versus saved packages for recurring or ad-hoc work.

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

coderabbitai Bot commented Apr 20, 2026 •

Copy link
Copy Markdown

Warning

Rate limit exceeded

@cursor[bot] has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 49 minutes and 13 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 49 minutes and 13 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: cc347ba8-9985-4ffe-8585-c42bc6088efa

📥 Commits

Reviewing files that changed from the base of the PR and between 385f880 and c3b1c26.

📒 Files selected for processing (1)
  • packages/worker/src/mcp/capabilities/jobs/shared.ts
📝 Walkthrough

Walkthrough

The PR introduces standalone job scheduling via MCP capabilities (job_schedule and job_schedule_once), enabling scheduled work without creating saved packages. It refactors repo-backed job execution to distinguish standalone jobs from package-owned jobs, extends MCP domains with a new "jobs" domain, and updates documentation to clarify scheduling options.

Changes

Cohort / File(s) Summary
Documentation Updates
docs/use/execute.md, docs/use/first-steps.md, docs/use/packages.md
Expanded guidance on the shared "module-oriented runtime model" to include scheduled jobs, clarifying that packages are for reusable code while scheduled/recurring work should use job_schedule capability.
Job Execution Refactoring
packages/worker/src/jobs/service.ts
Added executeBundledJobModule helper to centralize bundling, building, and execution logic. Refactored runRepoBackedJob to detect standalone jobs (via entity_kind === 'job' or kody.json manifest), parse manifests, and delegate execution via the new helper with optional package context.
Job Execution Tests
packages/worker/src/jobs/service.node.test.ts
Added comprehensive test for executeJobOnce with repo-backed one-off jobs, verifying manifest reading, module execution, and storage tool configuration without repo checks.
MCP Jobs Domain & Capabilities
packages/worker/src/mcp/capabilities/jobs/domain.ts, packages/worker/src/mcp/capabilities/jobs/job-schedule.ts, packages/worker/src/mcp/capabilities/jobs/job-schedule-once.ts, packages/worker/src/mcp/capabilities/jobs/shared.ts
Introduced new "jobs" MCP domain with two capabilities: job_schedule (supports one-off, interval, cron schedules) and job_schedule_once (one-off convenience alias). Shared module provides Zod schemas for inputs/outputs, schedule conversion utilities, and createScheduledJobFromArgs orchestration function.
Jobs Capability Tests
packages/worker/src/mcp/capabilities/jobs/job-schedule.node.test.ts
Added test suite verifying scheduling capabilities correctly invoke createJob, sync job manager alarms, handle multiple schedule types, and reject unauthenticated requests.
Domain & Meta-Capability Integration
packages/worker/src/mcp/capabilities/builtin-domains.ts, packages/worker/src/mcp/capabilities/meta/meta-list-capabilities.ts, packages/worker/src/mcp/server-instructions.ts
Added jobsDomain to builtin domains, extended meta_list_capabilities domain filter to include jobs, and updated MCP server instructions to document the new scheduling tools and clarify when to use standalone scheduling versus package-owned jobs.
Formatting
packages/worker/src/mcp/capabilities/meta/meta-list-capabilities.node.test.ts
Added trailing newline.

Sequence Diagram

sequenceDiagram
    participant Client as Client/Agent
    participant MCP as MCP Handler
    participant JobService as Jobs Service
    participant JobManager as Job Manager
    participant RepoSession as Repo Session
    participant Storage as Storage

    Client->>MCP: job_schedule(name, code, schedule, params)
    activate MCP
    MCP->>JobService: createScheduledJobFromArgs(args)
    activate JobService
    JobService->>JobManager: createJob(JobCreateInput)
    activate JobManager
    JobManager->>Storage: Write job record with schedule
    JobManager-->>JobService: JobView
    deactivate JobManager
    JobService->>JobManager: syncJobManagerAlarm(userId)
    activate JobManager
    JobManager-->>JobService: (alarm synced)
    deactivate JobManager
    JobService-->>MCP: buildJobScheduleOutput(JobView)
    deactivate JobService
    MCP-->>Client: { job_id, name, schedule, next_run_at }
    deactivate MCP

    Note over Client,Storage: At scheduled time...
    Client->>JobService: executeJobOnce(jobId)
    activate JobService
    JobService->>RepoSession: readFile(kody.json)
    activate RepoSession
    RepoSession-->>JobService: manifest (kind, entrypoint)
    deactivate RepoSession
    JobService->>JobService: detectStandaloneJob(manifest)
    JobService->>JobService: executeBundledJobModule(repoContext, entrypoint)
    activate JobService
    JobService->>Storage: Read/write via storageId=job:jobId
    Storage-->>JobService: (storage bound)
    JobService-->>JobService: ExecuteResult
    deactivate JobService
    JobService-->>Client: { ok: true, result, logs }
    deactivate JobService
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 Hop hop, schedules bloom!
No packages needed now—just code and a room.
Jobs spring forth standalone, light and free,
Run once or repeat, as it's meant to be! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 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 (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Add one-off job scheduling capability' directly and clearly describes the main feature addition in the changeset: introducing the job_schedule_once capability for one-off job scheduling.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cursor/capability-one-off-jobs-4bb0

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.

@kentcdodds
kentcdodds marked this pull request as ready for review April 20, 2026 17:25
@github-actions

github-actions Bot commented Apr 20, 2026 •

Copy link
Copy Markdown
Contributor

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

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

Mocks:

@kentcdodds

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Apr 20, 2026

Copy link
Copy Markdown
✅ Actions performed

Full review triggered.

Comment thread packages/worker/src/jobs/service.ts Outdated
cursoragent and others added 2 commits April 20, 2026 17:40
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>

@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.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 385f880. Configure here.

onceScheduleSchema,
intervalScheduleSchema,
cronScheduleSchema,
])

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.

Redundant identical schema creates divergence risk

Low Severity

scheduledJobSummarySchema is defined as an identical copy of scheduledJobScheduleSchema — both are z.discriminatedUnion('type', [...]) with the exact same three variants. The summary schema is only used once in jobScheduleOutputSchema. Having two separate-but-identical schema objects risks them silently diverging if a future change updates one but not the other.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 385f880. Configure here.

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 (4)
packages/worker/src/mcp/server-instructions.ts (1)

50-50: Minor: run-on sentence.

Consider splitting for readability — the clause "Package jobs are owned by packages, ad hoc jobs can be scheduled with job_schedule, and package apps are optional package surfaces" packs three unrelated facts into one comma-joined run-on.

✏️ Suggested wording
-- Cross-package imports use specifiers such as \`kody:`@my-package/export-name`\`. Package jobs are owned by packages, ad hoc jobs can be scheduled with \`job_schedule\`, and package apps are optional package surfaces.
+- Cross-package imports use specifiers such as \`kody:`@my-package/export-name`\`. Package jobs are owned by packages; ad hoc jobs can be scheduled with \`job_schedule\`. Package apps are optional package surfaces.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/worker/src/mcp/server-instructions.ts` at line 50, The sentence
mentioning cross-package imports and package jobs is a run-on; split it into
clearer sentences and separate unrelated facts: keep the cross-package import
explanation using the specifier `kody:`@my-package/export-name`` as one sentence,
then create a second sentence stating that package jobs are owned by packages
and that ad hoc jobs can be scheduled with `job_schedule`, and optionally a
third sentence calling out that package apps are optional package surfaces;
update the string in server-instructions text accordingly to improve
readability.
docs/use/execute.md (1)

77-81: Storage ownership phrasing: minor awkwardness.

"bound storage is execute-, app-, package-, or job-owned durable state" is a bit dense. Consider rephrasing to something like "bound storage is durable state owned by an execute call, an app, a package, or a job." Non-blocking.

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

In `@docs/use/execute.md` around lines 77 - 81, Rewrite the awkward sentence
"bound storage is execute-, app-, package-, or job-owned durable state" to a
clearer phrasing; replace that exact phrase in the docs with something like
"bound storage is durable state owned by an execute call, an app, a package, or
a job" (or a similar concise wording) so the meaning is clearer and easier to
read.
packages/worker/src/mcp/capabilities/jobs/shared.ts (1)

65-75: Duplicate discriminated union definitions.

scheduledJobScheduleSchema and scheduledJobSummarySchema are identical. Consider aliasing the second to the first to reduce duplication and keep them in sync.

♻️ Suggested cleanup
 export const scheduledJobScheduleSchema = z.discriminatedUnion('type', [
 	onceScheduleSchema,
 	intervalScheduleSchema,
 	cronScheduleSchema,
 ])
 
-const scheduledJobSummarySchema = z.discriminatedUnion('type', [
-	onceScheduleSchema,
-	intervalScheduleSchema,
-	cronScheduleSchema,
-])
+const scheduledJobSummarySchema = scheduledJobScheduleSchema
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/worker/src/mcp/capabilities/jobs/shared.ts` around lines 65 - 75,
scheduledJobScheduleSchema and scheduledJobSummarySchema are duplicated
discriminated unions; replace the duplicate definition by aliasing
scheduledJobSummarySchema to scheduledJobScheduleSchema so they stay in
sync—locate the two constants (scheduledJobScheduleSchema,
scheduledJobSummarySchema) and change the latter to a direct reference to the
former instead of redefining the union.
packages/worker/src/jobs/service.ts (1)

758-802: Misleading variable name entrypoint refers to the manifest file, not the entrypoint.

In the standalone branch, entrypoint on line 758 holds the contents of kody.json (the manifest), while moduleFile on line 791 holds the actual entrypoint. This mirrors awkward naming that already existed in the package branch but is worth tightening up in the new code. Consider renaming to manifestFile for clarity — it also makes the error message on line 765 read more naturally.

♻️ Suggested rename
-			const entrypoint = await sessionClient.readFile({
+			const manifestFile = await sessionClient.readFile({
 				sessionId: session.id,
 				userId: input.callerContext.user.userId,
 				path: manifestPath,
 			})
-			if (!entrypoint.content) {
+			if (!manifestFile.content) {
 				return {
 					error: `Job manifest "${manifestPath}" was not found in repo session.`,
 					result: null,
 					logs: bypassLogs,
 				}
 			}
 			let manifest: ReturnType<typeof parseRepoManifest>
 			try {
 				manifest = parseRepoManifest({
-					content: entrypoint.content,
+					content: manifestFile.content,
 					manifestPath,
 				})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/worker/src/jobs/service.ts` around lines 758 - 802, Rename the local
variable entrypoint to manifestFile to accurately reflect that it contains the
manifest contents; update all usages (the sessionClient.readFile call result,
the content checks, and the parseRepoManifest invocation) to use manifestFile,
and adjust the related error message that currently says `Job manifest
"${manifestPath}" was not found in repo session.` if needed to read naturally
with manifestFile; ensure subsequent logic that calls parseRepoManifest,
getManifestEntrypointPath, and the later sessionClient.readFile for the actual
module file (moduleFile) remains unchanged.
🤖 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/jobs/service.node.test.ts`:
- Around line 754-812: The test calls createJob but never sets up the repo
mocks, causing repoMockModule.ensureEntitySource / syncArtifactSourceSnapshot to
be undefined when run in isolation; add a call to mockRepoPersistence() before
createJob in this test so the repoMockModule implementations (ensureEntitySource
returning an ensuredSource with an id) are registered; target the test block
using createJob and ensure mockRepoPersistence() is invoked earlier in that test
so createJob's access to ensuredSource.id won't throw.

---

Nitpick comments:
In `@docs/use/execute.md`:
- Around line 77-81: Rewrite the awkward sentence "bound storage is execute-,
app-, package-, or job-owned durable state" to a clearer phrasing; replace that
exact phrase in the docs with something like "bound storage is durable state
owned by an execute call, an app, a package, or a job" (or a similar concise
wording) so the meaning is clearer and easier to read.

In `@packages/worker/src/jobs/service.ts`:
- Around line 758-802: Rename the local variable entrypoint to manifestFile to
accurately reflect that it contains the manifest contents; update all usages
(the sessionClient.readFile call result, the content checks, and the
parseRepoManifest invocation) to use manifestFile, and adjust the related error
message that currently says `Job manifest "${manifestPath}" was not found in
repo session.` if needed to read naturally with manifestFile; ensure subsequent
logic that calls parseRepoManifest, getManifestEntrypointPath, and the later
sessionClient.readFile for the actual module file (moduleFile) remains
unchanged.

In `@packages/worker/src/mcp/capabilities/jobs/shared.ts`:
- Around line 65-75: scheduledJobScheduleSchema and scheduledJobSummarySchema
are duplicated discriminated unions; replace the duplicate definition by
aliasing scheduledJobSummarySchema to scheduledJobScheduleSchema so they stay in
sync—locate the two constants (scheduledJobScheduleSchema,
scheduledJobSummarySchema) and change the latter to a direct reference to the
former instead of redefining the union.

In `@packages/worker/src/mcp/server-instructions.ts`:
- Line 50: The sentence mentioning cross-package imports and package jobs is a
run-on; split it into clearer sentences and separate unrelated facts: keep the
cross-package import explanation using the specifier
`kody:`@my-package/export-name`` as one sentence, then create a second sentence
stating that package jobs are owned by packages and that ad hoc jobs can be
scheduled with `job_schedule`, and optionally a third sentence calling out that
package apps are optional package surfaces; update the string in
server-instructions text accordingly to improve readability.
🪄 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 Plus

Run ID: 6baa2953-1662-427a-9d7e-c058da47ad80

📥 Commits

Reviewing files that changed from the base of the PR and between eea39cc and 385f880.

📒 Files selected for processing (14)
  • docs/use/execute.md
  • docs/use/first-steps.md
  • docs/use/packages.md
  • packages/worker/src/jobs/service.node.test.ts
  • packages/worker/src/jobs/service.ts
  • packages/worker/src/mcp/capabilities/builtin-domains.ts
  • packages/worker/src/mcp/capabilities/jobs/domain.ts
  • packages/worker/src/mcp/capabilities/jobs/job-schedule-once.ts
  • packages/worker/src/mcp/capabilities/jobs/job-schedule.node.test.ts
  • packages/worker/src/mcp/capabilities/jobs/job-schedule.ts
  • packages/worker/src/mcp/capabilities/jobs/shared.ts
  • packages/worker/src/mcp/capabilities/meta/meta-list-capabilities.node.test.ts
  • packages/worker/src/mcp/capabilities/meta/meta-list-capabilities.ts
  • packages/worker/src/mcp/server-instructions.ts

Comment on lines +754 to +812
test('executeJobOnce runs repo-backed one-off jobs from kody.json manifests', async () => {
const db = createDatabase()
const env = {
APP_DB: db,
LOADER: {} as WorkerLoader,
REPO_SESSION: {} as DurableObjectNamespace,
STORAGE_RUNNER: {
idFromName(name: string) {
return name as unknown as DurableObjectId
},
get() {
return {
getValue: async () => ({ key: 'count', value: 2 }),
setValue: async () => ({ ok: true, key: 'count' }),
deleteValue: async () => ({ ok: true, key: 'count', deleted: true }),
clearStorage: async () => ({ ok: true }),
listValues: async () => ({
entries: [],
estimatedBytes: 0,
truncated: false,
nextStartAfter: null,
pageSize: 250,
}),
exportStorage: async () => ({
entries: [],
estimatedBytes: 0,
truncated: false,
nextStartAfter: null,
pageSize: 250,
}),
sqlQuery: async () => ({
columns: ['value'],
rows: [{ value: 2 }],
rowCount: 1,
rowsRead: 1,
rowsWritten: 0,
}),
}
},
},
} as unknown as Env
mockRepoPersistence()
const callerContext = createBaseCallerContext()

const jobView = await createJob({
env,
callerContext,
body: {
name: 'Capability-created one-off job',
code: 'export default async () => ({ ok: true, adHoc: true })',
params: {
step: 'lights-off',
},
schedule: {
type: 'once',
runAt: '2026-04-17T15:00:00Z',
},
},
})

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

Missing mockRepoPersistence() before createJob — test relies on state leaked from the previous test.

Unlike the adjacent test on line 594 (which calls mockRepoPersistence() at line 635 before createJob), this new test invokes createJob at line 798 without first configuring repoMockModule.ensureEntitySource / syncArtifactSourceSnapshot. Currently it only works because the mockImplementation set by an earlier test run is still in place (vi.restoreAllMocks() in afterEach does not reset implementations on non-spy vi.fn() mocks). Running this test in isolation, or reordering tests, will break it because ensureEntitySource would return undefined and ensuredSource.id in createJob would throw.

🛠️ Suggested fix
 	} as unknown as Env
+	mockRepoPersistence()
 	const callerContext = createBaseCallerContext()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/worker/src/jobs/service.node.test.ts` around lines 754 - 812, The
test calls createJob but never sets up the repo mocks, causing
repoMockModule.ensureEntitySource / syncArtifactSourceSnapshot to be undefined
when run in isolation; add a call to mockRepoPersistence() before createJob in
this test so the repoMockModule implementations (ensureEntitySource returning an
ensuredSource with an id) are registered; target the test block using createJob
and ensure mockRepoPersistence() is invoked earlier in that test so createJob's
access to ensuredSource.id won't throw.

@kentcdodds
kentcdodds merged commit f47397f into main Apr 20, 2026
9 checks passed
@kody-bot
kody-bot deleted the cursor/capability-one-off-jobs-4bb0 branch April 30, 2026 23:15
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.

2 participants