Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions docs/use/execute.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,23 @@ To run persisted user code by name, use **`meta_run_skill`** with **`name`** and
optional **`params`**. To inspect source, use **`meta_get_skill`**. You can also
inline saved skill code into **execute** when that fits the workflow.

## Scheduled jobs

Kody can persist scheduled codemode jobs per user:

- **`scheduler_upsert`** — create a new one-shot or recurring cron job, or
update an existing one when you pass **`id`**
- **`scheduler_list`** — list jobs with next run time, last run status, and a
human-readable schedule summary
- **`scheduler_get`** — inspect one job
- **`scheduler_delete`** — remove a job
- **`scheduler_run_now`** — trigger a job immediately without changing its normal
schedule

Cron schedules use standard **5-field cron syntax** and may include an IANA
timezone such as **`America/New_York`**. One-shot schedules use an ISO 8601 UTC
timestamp such as **`2026-04-17T15:00:00Z`**.

## Long-term memory

Kody can surface a small number of relevant long-term memories when you pass a
Expand Down
20 changes: 20 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@
"acorn": "^8.16.0",
"agents": "^0.7.6",
"bonjour": "^3.5.1",
"croner": "^10.0.1",
"get-port": "^7.1.0",
"remix": "3.0.0-alpha.3",
"workers-ai-provider": "^3.1.2",
Expand Down
6 changes: 6 additions & 0 deletions packages/worker/src/env-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,12 @@ export const EnvSchema = object({
'COOKIE_SECRET must be at least 32 characters for session signing.',
),
APP_DB: d1DatabaseSchema,
SCHEDULER_DO: createSchema<unknown, DurableObjectNamespace>((value, context) => {
if (value) {
return { value: value as DurableObjectNamespace }
}
return fail('Missing SCHEDULER_DO binding for scheduled codemode jobs.', context.path)
}),
APP_BASE_URL: optionalUrlStringSchema,
APP_COMMIT_SHA: optionalCommitShaSchema,
CLOUDFLARE_EMAIL_FROM: optionalNonEmptyStringSchema,
Expand Down
10 changes: 9 additions & 1 deletion packages/worker/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { ChatAgent } from './chat-agent.ts'
import { HomeConnectorSession } from './home/session.ts'
import { HomeMCP } from './home/mcp.ts'
import { MCP } from './mcp/index.ts'
import { SchedulerDO } from './scheduler/scheduler-do.ts'
import { chatAgentBasePath } from '@kody-internal/shared/chat-routes.ts'
import { getWorkerSentryOptions } from './sentry-options.ts'
import { handleRequest } from '#app/handler.ts'
Expand Down Expand Up @@ -38,7 +39,14 @@ import {
parseConnectorRoutePath,
} from './remote-connector/connector-session-key.ts'

export { ChatAgent, CodemodeFetchGateway, HomeConnectorSession, HomeMCP, MCP }
export {
ChatAgent,
CodemodeFetchGateway,
HomeConnectorSession,
HomeMCP,
MCP,
SchedulerDO,
}

const claudeWidgetDomainSuffix = '.claudemcpcontent.com'

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,23 @@ test('buildCapabilityRegistry rejects duplicate domain registration', () => {
)
})

test('builtin capability domains include scheduler', async () => {
const { builtinDomains } = await import('./builtin-domains.ts')
expect(builtinDomains.some((domain) => domain.name === 'scheduler')).toBe(true)
const schedulerDomain = builtinDomains.find(
(domain) => domain.name === 'scheduler',
)
expect(
schedulerDomain?.capabilities.map((capability) => capability.name),
).toEqual([
'scheduler_upsert',
'scheduler_list',
'scheduler_get',
'scheduler_delete',
'scheduler_run_now',
])
})

test('defineDomain rejects duplicate capability names within one domain', () => {
const one = defineDomainCapability(capabilityDomainNames.apps, {
name: 'dup',
Expand Down
2 changes: 2 additions & 0 deletions packages/worker/src/mcp/capabilities/builtin-domains.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { appsDomain } from './apps/domain.ts'
import { codingDomain } from './coding/domain.ts'
import { metaDomain } from './meta/domain.ts'
import { schedulerDomain } from './scheduler/domain.ts'
import { secretsDomain } from './secrets/domain.ts'
import { valuesDomain } from './values/domain.ts'

Expand All @@ -16,6 +17,7 @@ export const builtinDomains = [
appsDomain,
codingDomain,
metaDomain,
schedulerDomain,
secretsDomain,
valuesDomain,
] as const
1 change: 1 addition & 0 deletions packages/worker/src/mcp/capabilities/domain-metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export const capabilityDomainNames = {
home: 'home',
math: 'math',
meta: 'meta',
scheduler: 'scheduler',
secrets: 'secrets',
values: 'values',
} as const
Expand Down
21 changes: 21 additions & 0 deletions packages/worker/src/mcp/capabilities/scheduler/domain.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { defineDomain } from '../define-domain.ts'
import { capabilityDomainNames } from '../domain-metadata.ts'
import { schedulerDeleteCapability } from './scheduler-delete.ts'
import { schedulerGetCapability } from './scheduler-get.ts'
import { schedulerListCapability } from './scheduler-list.ts'
import { schedulerRunNowCapability } from './scheduler-run-now.ts'
import { schedulerUpsertCapability } from './scheduler-upsert.ts'

export const schedulerDomain = defineDomain({
name: capabilityDomainNames.scheduler,
description:
'Create, upsert, inspect, delete, and trigger scheduled codemode jobs that run once at a UTC timestamp or recur on a timezone-aware cron schedule.',
keywords: ['schedule', 'scheduler', 'cron', 'datetime', 'run later', 'job'],
capabilities: [
schedulerUpsertCapability,
schedulerListCapability,
schedulerGetCapability,
schedulerDeleteCapability,
schedulerRunNowCapability,
],
})
28 changes: 28 additions & 0 deletions packages/worker/src/mcp/capabilities/scheduler/scheduler-delete.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { defineDomainCapability } from '#mcp/capabilities/define-domain-capability.ts'
import { capabilityDomainNames } from '#mcp/capabilities/domain-metadata.ts'
import { type CapabilityContext } from '#mcp/capabilities/types.ts'
import { schedulerDelete } from '#worker/scheduler/client.ts'
import {
scheduledJobIdInputSchema,
schedulerDeleteOutputSchema,
} from './shared.ts'
import { requireMcpUser } from '../meta/require-user.ts'

export const schedulerDeleteCapability = defineDomainCapability(
capabilityDomainNames.scheduler,
{
name: 'scheduler_delete',
description:
'Delete a scheduled job for the signed-in user by id and update the Durable Object alarm to the next pending run.',
keywords: ['scheduler', 'delete', 'job', 'remove'],
readOnly: false,
idempotent: false,
destructive: true,
inputSchema: scheduledJobIdInputSchema,
outputSchema: schedulerDeleteOutputSchema,
async handler(args, ctx: CapabilityContext) {
const user = requireMcpUser(ctx.callerContext)
return schedulerDelete(ctx.env, user.userId, args.id)
},
},
)
24 changes: 24 additions & 0 deletions packages/worker/src/mcp/capabilities/scheduler/scheduler-get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { defineDomainCapability } from '#mcp/capabilities/define-domain-capability.ts'
import { capabilityDomainNames } from '#mcp/capabilities/domain-metadata.ts'
import { requireMcpUser } from '#mcp/capabilities/meta/require-user.ts'
import { schedulerGet } from '#worker/scheduler/client.ts'
import { scheduledJobIdInputSchema, schedulerJobViewSchema } from './shared.ts'

export const schedulerGetCapability = defineDomainCapability(
capabilityDomainNames.scheduler,
{
name: 'scheduler_get',
description:
'Get one scheduled codemode job by id for the signed-in user, including next run time and last execution state.',
keywords: ['schedule', 'scheduler', 'job', 'get'],
readOnly: true,
idempotent: true,
destructive: false,
inputSchema: scheduledJobIdInputSchema,
outputSchema: schedulerJobViewSchema,
async handler(args, ctx) {
const user = requireMcpUser(ctx.callerContext)
return schedulerGet(ctx.env, user.userId, args.id)
},
},
)
29 changes: 29 additions & 0 deletions packages/worker/src/mcp/capabilities/scheduler/scheduler-list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { z } from 'zod'
import { defineDomainCapability } from '#mcp/capabilities/define-domain-capability.ts'
import { capabilityDomainNames } from '#mcp/capabilities/domain-metadata.ts'
import { type CapabilityContext } from '#mcp/capabilities/types.ts'
import { schedulerList } from '#worker/scheduler/client.ts'
import {
requireSchedulerUser,
schedulerCapabilityKeywords,
schedulerJobViewSchema,
} from './shared.ts'

export const schedulerListCapability = defineDomainCapability(
capabilityDomainNames.scheduler,
{
name: 'scheduler_list',
description:
'List all scheduled jobs for the signed-in user, including next run time, last run status, and a human-readable schedule summary.',
keywords: [...schedulerCapabilityKeywords],
readOnly: true,
idempotent: true,
destructive: false,
inputSchema: z.object({}),
outputSchema: z.array(schedulerJobViewSchema),
async handler(_args, ctx: CapabilityContext) {
const user = requireSchedulerUser(ctx)
return schedulerList(ctx.env, user.userId)
},
},
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { defineDomainCapability } from '#mcp/capabilities/define-domain-capability.ts'
import { capabilityDomainNames } from '#mcp/capabilities/domain-metadata.ts'
import { type CapabilityContext } from '#mcp/capabilities/types.ts'
import { requireMcpUser } from '#mcp/capabilities/meta/require-user.ts'
import { schedulerRunNow } from '#worker/scheduler/client.ts'
import {
scheduledJobIdInputSchema,
schedulerRunNowOutputSchema,
} from './shared.ts'

export const schedulerRunNowCapability = defineDomainCapability(
capabilityDomainNames.scheduler,
{
name: 'scheduler_run_now',
description:
'Trigger a scheduled job immediately outside its normal schedule and return the execution result plus the updated job state.',
keywords: ['schedule', 'run now', 'trigger', 'job'],
readOnly: false,
idempotent: false,
destructive: false,
inputSchema: scheduledJobIdInputSchema,
outputSchema: schedulerRunNowOutputSchema,
async handler(args, ctx: CapabilityContext) {
const user = requireMcpUser(ctx.callerContext)
return schedulerRunNow(ctx.env, user.userId, {
callerContext: ctx.callerContext,
body: { id: args.id },
})
},
},
)
56 changes: 56 additions & 0 deletions packages/worker/src/mcp/capabilities/scheduler/scheduler-upsert.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { type CapabilityContext } from '#mcp/capabilities/types.ts'
import { defineDomainCapability } from '#mcp/capabilities/define-domain-capability.ts'
import { capabilityDomainNames } from '#mcp/capabilities/domain-metadata.ts'
import { schedulerCreate, schedulerUpdate } from '#worker/scheduler/client.ts'
import {
requireSchedulerUser,
schedulerJobViewSchema,
schedulerUpsertInputSchema,
} from './shared.ts'

export const schedulerUpsertCapability = defineDomainCapability(
capabilityDomainNames.scheduler,
{
name: 'scheduler_upsert',
description:
'Create a new scheduled codemode job when id is omitted, or update an existing scheduled job when id is provided. Supports one-shot UTC timestamps and recurring 5-field cron expressions with IANA timezone interpretation.',
keywords: ['scheduler', 'upsert', 'create', 'update', 'cron', 'datetime'],
readOnly: false,
idempotent: false,
destructive: false,
inputSchema: schedulerUpsertInputSchema,
outputSchema: schedulerJobViewSchema,
async handler(args, ctx: CapabilityContext) {
const user = requireSchedulerUser(ctx)
if (args.id === undefined) {
return schedulerCreate(ctx.env, user.userId, {
callerContext: ctx.callerContext,
body: {
name: args.name ?? '',
code: args.code ?? '',
...(args.params !== undefined && args.params !== null
? { params: args.params }
: {}),
schedule: args.schedule!,
...(args.timezone !== undefined && args.timezone !== null
? { timezone: args.timezone }
: {}),
...(args.enabled !== undefined ? { enabled: args.enabled } : {}),
},
})
}
return schedulerUpdate(ctx.env, user.userId, {
callerContext: ctx.callerContext,
body: {
id: args.id,
...(args.name !== undefined ? { name: args.name } : {}),
...(args.code !== undefined ? { code: args.code } : {}),
...(args.params !== undefined ? { params: args.params } : {}),
...(args.schedule !== undefined ? { schedule: args.schedule } : {}),
...(args.timezone !== undefined ? { timezone: args.timezone } : {}),
...(args.enabled !== undefined ? { enabled: args.enabled } : {}),
},
})
},
},
)
Loading
Loading