Worker cron reference
`CronRoom`, `CronTask`, and `buildCronContext`.
On this page
import { CronRoom, buildCronContext } from 'deepspace/worker'
import type { CronTask, CronExecution, CronRoomConfig, CronContext } from 'deepspace/worker'
CronRoom<E>#
See Rooms reference → CronRoom for the class signature. The scaffolded AppCronRoom extends this and overrides onTask.
CronTask#
interface CronTask {
name: string
intervalMinutes?: number // mutually exclusive with schedule
schedule?: string // 5-field cron expression
timezone?: string // IANA timezone (required with schedule)
paused?: boolean // start disabled
}
Each task declares either intervalMinutes OR schedule + timezone. Declaring both or neither throws at DO construction.
Cron mode is DST-aware - the wall-clock comparison happens after the timezone shift.
CronExecution#
interface CronExecution {
taskName: string
startedAt: string
/** Null while the task is still running. */
completedAt: string | null
success: boolean
durationMs: number
error?: string
}
Stored in the DO's history and streamed to useCronMonitor subscribers. The lastRunAt field on a task state row is similarly nullable until the task fires at least once.
buildCronContext(env, ownerUserId, roomId?)#
Returns a context for use inside runTask. Operations run as the app owner, bypassing RBAC.
function buildCronContext(
env: {
RECORD_ROOMS: DurableObjectNamespace
/** Optional in the type, but `ctx.integrations.call(...)` throws at runtime if missing. */
APP_OWNER_JWT?: string
API_WORKER?: Fetcher
API_WORKER_URL?: string
},
ownerUserId: string,
roomId?: string,
): CronContext
interface CronContext {
records: {
query(
collection: string,
opts?: { where?: Record<string, unknown>; limit?: number },
): Promise<unknown[]>
create(collection: string, data: Record<string, unknown>): Promise<unknown>
update(collection: string, recordId: string, data: Record<string, unknown>): Promise<unknown>
delete(collection: string, recordId: string): Promise<unknown>
}
integrations: {
call(endpoint: string, params?: Record<string, unknown>): Promise<unknown>
}
ownerUserId: string
}
roomId defaults to 'default'. Pass app:${env.DEEPSPACE_APP_ID} to target the per-app RecordRoom - the scaffold convention. Scope ids key to the immutable app id - never the app name (why).
The records.* methods return unknown because the collection shape belongs to the app. Narrow or validate the result at the call site. They surface the RecordRoom tools API's data field directly. At runtime:
queryresolves to an array of record envelopes ({ recordId, data, createdAt, updatedAt, ... }).create/updateresolve to{ recordId, record }(the envelope of the row that was written).deleteresolves to a delete confirmation payload.
There is no records.get method on CronContext - use records.query with a where clause when you need a single row.
Properties#
ctx.records.*- RBAC-bypassing record operations (results already unwrapped from theActionResultenvelope).ctx.integrations.call(endpoint, params)- proxies through the api-worker as the app owner (signed withAPP_OWNER_JWT), billed to the app owner. Returns the unwrappeddatafield; throws on non-2xx orsuccess: false.ctx.ownerUserId- convenience accessor for the owner's user ID.
Pattern#
// src/cron.ts
import type { CronTask } from 'deepspace/worker'
import { buildCronContext } from 'deepspace/worker'
export const tasks: CronTask[] = [
{ name: 'heartbeat', intervalMinutes: 1 },
{ name: 'daily-digest', schedule: '0 9 * * *', timezone: 'America/New_York' },
]
export async function runTask(name: string, env: Env): Promise<void> {
const ctx = buildCronContext(env, env.OWNER_USER_ID, `app:${env.DEEPSPACE_APP_ID}`)
if (name === 'heartbeat') {
const settings = await ctx.records.query('settings', { where: { key: 'lastHeartbeat' } })
if (settings.length > 0) {
await ctx.records.update('settings', settings[0].recordId, {
value: new Date().toISOString(),
})
}
}
}
Worker wiring#
// worker.ts
export class AppCronRoom extends CronRoom<Env> {
constructor(state: DurableObjectState, env: Env) {
super(state, env, { tasks: cronTasks })
}
protected async onTask(name: string): Promise<void> {
await runCronTask(name, this.env)
}
}
Don't edit the WebSocket route or DO binding wiring - add tasks in src/cron.ts and the DO picks them up at construction.
Outbound calls#
Use ctx.integrations.call(...) for third-party APIs (billed to the owner):
const data = await ctx.integrations.call('resend/send-email', {
to: user.data.email,
subject: 'Your digest',
text: '...',
})
For autonomous LLM calls via the AI SDK, use createDeepSpaceAI without authToken:
import { createDeepSpaceAI } from 'deepspace/worker'
import { generateText } from 'ai'
const ai = createDeepSpaceAI(env, 'anthropic') // owner pays
const { text } = await generateText({ model: ai('claude-haiku-4-5'), prompt: '...' })
See also#
- Scheduled jobs guide - patterns and worked examples
- Rooms reference - class signature
- Real-time reference - client-side monitor