Skip to main content
Documentation

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'
ts

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
}
ts

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
}
ts

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
}
ts

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:

  • query resolves to an array of record envelopes ({ recordId, data, createdAt, updatedAt, ... }).
  • create / update resolve to { recordId, record } (the envelope of the row that was written).
  • delete resolves 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 the ActionResult envelope).
  • ctx.integrations.call(endpoint, params) - proxies through the api-worker as the app owner (signed with APP_OWNER_JWT), billed to the app owner. Returns the unwrapped data field; throws on non-2xx or success: 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(),
      })
    }
  }
}
ts

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)
  }
}
ts

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: '...',
})
ts

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: '...' })
ts

See also#