---
title: "Gatekeeper: Scheduler"
type: entity
tags: [gatekeepers, developer, operator, advanced, well-established]
created: 2026-08-05
updated: 2026-08-05
sources: ["raw/github_doc-packages-gatekeeper-scheduler-readme-md.md"]
confidence: high
cfos_snapshot: "2026-08-05"
---

## Overview

Scheduled Tasks (Scheduler) is an **ambient** gatekeeper — it doesn't connect to an external service but lets workspace code register persistent callbacks for elapsed intervals, wall-clock recurrences, and one-time runs. Each account also gets a read-only management app at `/gatekeepers/scheduler` for searching and inspecting schedules across the account.

## Characteristics

- **No external auth** — it is capability-authorized rather than identity-authorized: it does not receive Workshop user identity, assert its own ambient policy, or expose external network authority. Workshop's hook admission and observation authorization remain the security boundaries.
- **Agent API** (`ScheduleSession`, full contract in `src/types.d.ts`) exposes three registration methods:
  - `every(everyMs, callback, options)` — elapsed UTC time; minimum interval 60 seconds.
  - `calendarAt(rule, callback, options)` — local wall-clock time, requires explicit IANA timezone; supports hourly/daily/weekly rules.
  - `runAt(when, callback, options)` — runs once at an absolute epoch-ms timestamp or timezone-aware wall-clock time.
  Example:
  ```ts
  const callback = await ctx.restore({ type: "dailyBrief" });
  const scheduleId = await SCHEDULER.calendarAt(
    { timeZone: "America/Chicago", freq: "weekly", byDay: ["MO","TU","WE","TH","FR"], hour: 8, minute: 0 },
    callback,
    { title: "Daily brief", description: "Prepare the morning calendar and inbox brief.", occurrences: { count: 10 } },
  );
  ```
- **Bounds**: recurring calls take `occurrences: { count: N }` or `occurrences: { until: {...} }` (never both); `runAt()` accepts neither. Count bounds *due slots*, not successful runs — a slot consumes one count as soon as due, even if admission/delivery fails.
- **Callbacks** implement `ScheduledTaskHook.onSchedule()` and must be made persistent via `ctx.restore()` before registration. Each firing carries `scheduleId`, `runId` (stable across retries — use as idempotency key), `scheduledTime`, `actualTime`, `timeZone`. Failures retry up to 8 total attempts with exponential delay (starts at 1 minute, caps at 1 hour); exhausted schedules enter **Needs attention**.
- **Lifecycle**: registration only binds a disabled hook (writes no schedule row); enabling it in the Connections UI creates the account-driver row and arms the alarm; disabling removes the row/capabilities; re-enabling creates fresh active state but preserves recurrence phase (no catch-up of missed work). Disconnecting the Scheduler account revokes its driver, deletes state, and leaves a tombstone.
- **Architecture**: one SQLite-backed `ScheduleDriver` Durable Object and one alarm per account, covering all its workspaces. The alarm processes at most 20 due schedules per pass with 4 concurrent deliveries, persists state before crossing RPC boundaries, and uses stable `runId` fencing.
- **Limits (fixed policy, not deployment settings)**: 500 enabled/terminal schedule rows per account; 100 per workspace; 100 rows per management page; 20 due schedules per alarm pass / 4 concurrent deliveries; 8 callback attempts per occurrence; titles 200 chars, descriptions 2,000 chars.
- **Deployment requirement**: the `allow_irrevocable_stub_storage` compatibility flag is required while stored callback capabilities exist and must not be removed from an existing deployment. Add `GATEKEEPER_SCHEDULER` to the deployment's Workshop service bindings; vendor provisioning defaults to `optional`.

## How to Use

1. Ask the agent to create a scheduled task, or open **Scheduled** and choose a starter prompt; confirm cadence and IANA timezone (for wall-clock schedules) with the user explicitly — never infer timezone from locale.
2. The agent registers a persistent callback (`every`, `calendarAt`, or `runAt`), which returns a schedule ID but leaves the hook disabled.
3. Enable the hook in the Workshop's Connections UI to start it.
4. Use the **Scheduled** management app (All / Active / Needs attention / Finished tabs) to search and inspect schedules; it is read-only — editing, pausing, deletion, and run history stay in Connections.
5. Development: `pnpm install`, `pnpm --filter @gadgets/gatekeeper-scheduler test`, `pnpm --filter @gadgets/gatekeeper-scheduler build`, `pnpm run dev-server`, `pnpm run dev-client`.

Troubleshooting: "Scheduled missing from navigation" → check the binding/vendor/connected account; "new schedule not listed/running" → its hook is registered disabled, enable it; "Needs attention" → callback exhausted retries, fix and re-enable; blueprint-created workspaces never inherit schedules — register again.

## Related Entities

- [[concepts/gatekeepers]]
- [[concepts/writing-a-gatekeeper]]
- [[entities/gatekeeper-email]] — another ambient, hook-driven gatekeeper rather than an OAuth-connected external service
- [[entities/gatekeepers-catalog]]
