# @keystrokehq/scheduler

**Layer:** `platform`

Job queue and cron/poll schedule sync for [keystroke](../../README.md). Enqueues workflow, agent, and trigger work; ticks due schedules into the queue. No HTTP, no attachment execution, no trigger source definitions.

## What it does

- **`createScheduler`** — queue backend from `SchedulerPlugin` (default: pg-boss on Postgres, Drizzle job table on SQLite), then expose `enqueue`, `startWorker`, `syncTriggerSchedules`, `startScheduleTicker`, and `fireDueSchedules`.
- **`SchedulerPlugin`** — swap the default scheduler for a custom queue backend.
- **`createMemoryJobQueue`** — in-process queue for tests (optional `sync: true` drain).
- **Job types** — `JobQueue`, `Scheduler`, `JobHandler`, `EnqueueInput`, `JobTrigger`, `StopFn` for server handlers and enqueue helpers.
- **Schedule sync** — `syncTriggerSchedules` (on `Scheduler`) upserts rows in `trigger_schedules` via `@keystrokehq/database`, resolving cron strings with **`resolveCronSchedule`** and next run times with **`nextTriggerRunAt`** from `@keystrokehq/trigger`.
- **Trigger firing** — pluggable `TriggerScheduler`. pg-boss (Postgres) and BullMQ register native cron schedules on `sync`, so there is no per-second poll; the SQLite/self-host DB queue uses the **schedule ticker** (background loop claims due schedules and enqueues `kind: "trigger"` jobs). `trigger_schedules` stays the source of truth, reconciled into the firing backend on every `sync`.

Queue implementations (`database-queue`, `pg-boss-queue`) and standalone `syncTriggerSchedules` / `resolveTriggerSchedule` stay internal unless a consumer needs them in a follow-up.

## What belongs somewhere else

- Trigger source defs (`defineCronSource`, poll cadence) → `@keystrokehq/trigger`
- Running attachments, webhooks, poll HTTP → `@keystrokehq/runtime`
- `trigger_schedules` / `jobs` schema and queries → `@keystrokehq/database`
- Workflow/agent run logic → `@keystrokehq/workflow` / `@keystrokehq/agent` (server wires handlers)

## Where it sits

```mermaid
flowchart TB
  scheduler["@keystrokehq/scheduler"]

  database["@keystrokehq/database"]
  trigger["@keystrokehq/trigger"]

  scheduler --> database
  scheduler --> trigger
```

**Allowed runtime deps:** `database`, `trigger`, `pg-boss`, `cron-parser` (via trigger).

**Forbidden:** `runtime`, `workflow`, `agent`, `action`, `config`, `build`, `cli`, integrations, HTTP frameworks. Scheduler must stay callable from runtime without circular imports.

**Depends on:** `database`, `trigger`, `pg-boss`, `cron-parser`.

**Used by:** `runtime` and `worker` (embedded worker + schedule ticker).

## Exports

Import from the package root (`@keystrokehq/scheduler`), not from `src/…`.

| Export                   | What it's for                                             |
| ------------------------ | --------------------------------------------------------- |
| `createScheduler`        | Production queue + schedule APIs                          |
| `SchedulerPlugin`        | Pluggable queue backend contract                          |
| `defaultSchedulerPlugin` | pg-boss (Postgres) / DB polling (SQLite)                  |
| `createSharedScheduler`  | Platform shared queue (honors `configureSharedScheduler`) |
| `createMemoryJobQueue`   | Test / dev in-process queue                               |
| `Scheduler`              | Full surface (`JobQueue` + schedule sync/ticker)          |
| `JobQueue`               | Enqueue + worker only (typing server overrides)           |
| `JobHandler`             | Process one claimed job payload                           |
| `EnqueueInput`           | Build enqueue calls from server                           |
| `JobTrigger`             | `"api"` \| `"cron"` \| `"webhook"` \| `"poll"` \| …       |
| `StopFn`                 | Stop worker or schedule ticker                            |
| `MemoryJobQueueOptions`  | `{ sync?: boolean }` for memory queue                     |

## Source layout

| File                        | What's in it                                                      |
| --------------------------- | ----------------------------------------------------------------- |
| `types.ts`                  | Job + scheduler types, retry backoff helper                       |
| `create-scheduler.ts`       | `createScheduler`, `createJobQueue`, `wrapJobQueueAsScheduler`    |
| `database-queue.ts`         | SQLite / generic Postgres job table worker                        |
| `pg-boss-queue.ts`          | Postgres pg-boss backend (exposes `boss` for native scheduling)   |
| `pg-boss-trigger-scheduler.ts` | pg-boss native cron `TriggerScheduler` (no poll loop)         |
| `database-trigger-scheduler.ts` | DB-ticker `TriggerScheduler` for the polling/SQLite backend  |
| `memory.ts`                 | In-memory `JobQueue`                                              |
| `sync-trigger-schedules.ts` | DB upsert for discovered cron/poll attachments                    |
| `resolve-schedule.ts`       | Override-aware cron string (uses trigger's `resolveCronSchedule`) |
| `schedule-ticker.ts`        | Claim due schedules → enqueue trigger jobs (polling backend)      |

## Adding a feature?

1. New queue backend or retry/concurrency behavior? → this package (keep types in `types.ts`).
2. New trigger schedule expression rules? → `@keystrokehq/trigger` (`nextTriggerRunAt`, `resolveCronSchedule`).
3. New table or claim query? → `@keystrokehq/database`.
4. Who runs the job (workflow step, agent prompt, attachment)? → `@keystrokehq/runtime` job handlers.

## Example

```typescript
import { createScheduler } from "@keystrokehq/scheduler";
import { initDatabase } from "@keystrokehq/database";

await initDatabase({ url: process.env.DATABASE_URL });

const scheduler = await createScheduler({ url: process.env.DATABASE_URL });

await scheduler.syncTriggerSchedules({
  schedules: [{ attachmentKey: "daily-report:run", kind: "cron", schedule: "0 9 * * *" }],
});

const stopWorker = await scheduler.startWorker(async (job) => {
  console.log(job.kind, job.targetId, job.trigger);
});

const stopTicker = await scheduler.startScheduleTicker();

// … later
await stopTicker();
await stopWorker();
```

## Tests

```bash
pnpm --filter @keystrokehq/scheduler test
pnpm --filter @keystrokehq/scheduler typecheck
```

Unit: `*.test.ts`. Integration (`queue.int.test.ts`): Postgres pg-boss via `describePostgres`; SQLite DB queue uses in-memory DB.

## See also

- [README.md](../../README.md) — platform layer map
- [packages/server/src/server.ts](../server/src/server.ts) — wires scheduler at listen
- [packages/trigger/](../trigger/) — cron parsing and `nextTriggerRunAt`
- [packages/database/src/scheduler/](../database/src/scheduler/) — jobs + `trigger_schedules` queries
