Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.deep.space/llms.txt

Use this file to discover all available pages before exploring further.

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
    INTERNAL_STORAGE_HMAC_SECRET?: string
    API_BASE_URL?: string
  },
  ownerUserId: string,
  roomId?: string,
): CronContext

interface CronContext {
  records: {
    query(
      collection: string,
      opts?: { where?: Record<string, unknown>; limit?: number },
    ): Promise<any[]>
    create(collection: string, data: Record<string, unknown>): Promise<any>
    update(collection: string, recordId: string, data: Record<string, unknown>): Promise<any>
    delete(collection: string, recordId: string): Promise<any>
  }
  integrations: {
    call(endpoint: string, params: Record<string, unknown>): Promise<any>
  }
  ownerUserId: string
}
roomId defaults to 'default'. Pass app:${env.APP_NAME} to target the per-app RecordRoom (the scaffold convention). The records.* methods are intentionally typed loosely (Promise<any> / Promise<any[]>) - they call the underlying RecordRoom tools API and surface its 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.
Shapes differ from server actions. ctx.records.query returns the unwrapped array directly (no ActionResult envelope). The methods throw on failure rather than returning a { success: false } envelope - wrap in try/catch if you need to handle denied writes inline.

Properties

  • ctx.records.* - RBAC-bypassing record operations (results already unwrapped from the ActionResult envelope).
  • ctx.integrations.call(endpoint, params) - proxies through the api-worker via signed internal HMAC, billed to the app owner (uses INTERNAL_STORAGE_HMAC_SECRET).
  • 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.APP_NAME}`)

  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