.lobsterX definition; it only adds a trigger that calls startWorkflow on your behalf.
How the Evaluator Works
A dedicated Convex cron job runs every 60 seconds. Each tick, it queries theworkflows table using the by_triggerType_status_nextScheduledRun index for any schedules that are:
- Active (
status = "active") - Due (
nextScheduledRun <= now)
.take(101)). If the batch hits 101, the evaluator immediately self-reschedules an additional tick to catch the overflow — the standard “page size + 1” pattern that prevents silent drops under load.
For each due schedule, the evaluator calls dispatchScheduledRun which in turn calls startWorkflowInternal — the same internal path that a manual operator trigger follows. The run is attributed to the schedule trigger with the schedule ID as the initiating actor.
Why a polling cron instead of self-chaining
scheduler.runAt? A self-chaining scheduler would compute the next run time and schedule a future Convex function call for it. That design creates a chain that can break: if one link fails (function error, deployment, transient issue), all future runs for that schedule are silently lost. A polling evaluator has no chain to break. Even if a tick fails, the next tick — guaranteed by the Convex cron — will catch up on any overdue schedules.SCHEDULE EVALUATION LOOP
every 60 secondsconvex/crons.ts → workflow_triggers.evaluate
↓
Index scan
by_triggerType_status_nextScheduledRun — active schedules with nextScheduledRun now, .take(101)
↓
Overflow guard
If batch = 101 → self-reschedule immediately to catch remaining due schedules
↓
dispatchScheduledRun
Per-schedule: validate workflow still active → call startWorkflowInternal → compute and write nextScheduledRun
↓
lobsterXInterpreter
Durable workflow starts — same execution path as any manual run
Schedule Types
Cron Schedules
Cron expressions give you precise control over run timing: day-of-week, time-of-day, monthly cadence. Cron expressions are parsed by thecroner library running in the V8 runtime. Standard five-field POSIX cron syntax is supported:
| Expression | Meaning |
|---|---|
0 9 * * 1-5 | Weekdays at 9 AM |
0 0 * * * | Daily at midnight |
0 */4 * * * | Every 4 hours |
30 8 1 * * | 1st of every month at 8:30 AM |
0 9 * * 1 | Every Monday at 9 AM |
Interval Schedules
Interval schedules fire on a fixed cadence measured in milliseconds. Use intervals for high-frequency or sub-hourly automation where a cron expression would be cumbersome.| Interval | Milliseconds |
|---|---|
| Every 5 minutes | 300000 |
| Every 30 minutes | 1800000 |
| Every hour | 3600000 |
| Every 6 hours | 21600000 |
Timezone Support
Both cron and interval schedules accept an IANA timezone string (e.g.America/New_York, Europe/London, Asia/Tokyo). Timezone resolution uses Intl.DateTimeFormat in the V8 runtime.
When a timezone is set on a cron schedule, the expression is evaluated in that timezone. 0 9 * * 1-5 with America/New_York fires at 9 AM Eastern on weekdays, automatically adjusting for daylight saving transitions.
Interval schedules store nextScheduledRun as an absolute UTC timestamp, so timezone has no effect on intervals — they fire at fixed millisecond offsets regardless of clock changes.
Schedule Lifecycle
SCHEDULE STATES
Enabled
nextScheduledRun set.
Appears in index.
Evaluator fires on due date.
⇄toggle
Disabled
nextScheduledRun cleared to undefined.
Falls out of index.
Evaluator never sees it.
→remove
Removed
Schedule document deleted.
Workflow triggerType reverts to “manual”.
No future runs.
nextScheduledRun field is cleared. Because the index includes nextScheduledRun as a component, documents with undefined in that position fall outside the index range the evaluator queries. Disabling a schedule is instantaneous — the evaluator cannot see it on the next tick even if a run would have been due.
Re-enabling a schedule recomputes nextScheduledRun from the current wall clock time, not from the last skipped run time. If a schedule was disabled for three days and re-enabled, it does not catch up on missed fires.
Drift Prevention
A naive interval implementation might computenextScheduledRun = Date.now() + interval. This causes drift: each run takes non-zero time, so the effective period gradually shifts later. Forge prevents drift by computing the next run from the scheduled time, not the wall clock at dispatch:
croner computes the next occurrence strictly from the cron expression and the previous scheduled time. The result is that cron schedules stay anchored to the expression, not to variable Convex function execution latency.
Drift prevention applies within a single day. If the Convex platform is unavailable for an extended period (hours), the evaluator will find multiple overdue ticks on recovery and fire them in sequence, catching up until
nextScheduledRun is back in the future. Each recovered tick produces one run, up to the catch-up limit.Paused Workflows
Scheduling respects workflow pause state.dispatchScheduledRun checks the workflow’s current status before calling startWorkflowInternal. If the workflow is paused, deprecated, or deleted, the scheduled run is suppressed and nextScheduledRun is still advanced — the schedule stays alive, it just doesn’t fire while the workflow is ineligible.
This means:
| Workflow state | Schedule behavior |
|---|---|
active | Fires normally |
paused | Skips the run, advances to next scheduled time |
deprecated | Skips the run, advances to next scheduled time |
deleted | Schedule is cleaned up; no future fires |
Gateway Cron Management
Gateways can declare their own cron-style health and maintenance jobs. These are separate from workflow schedules — they are gateway-scoped operations managed on the gateway’s detail page under Platform > Gateways. Gateway crons are managed via the Bridge client REST API (cronList, cronAdd, cronUpdate, cronRun, cronRemove) — they run on the gateway runtime, not in Convex. They appear in the Observe > Schedules cross-cutting view alongside workflow schedules.
Typical gateway cron uses:
- Periodic capability refresh (re-advertise model support)
- Health probe on a schedule independent of the platform health check
- Maintenance window execution (clear caches, rotate credentials)
Observe > Schedules
The Observe > Schedules page provides a cross-cutting view of all schedules in the platform:| Column | Description |
|---|---|
| Workflow / Gateway | The resource this schedule fires against |
| Type | cron or interval, with the expression or period |
| Timezone | IANA timezone string (blank for UTC) |
| Status | enabled or disabled |
| Next run | Computed next fire time, shown in operator’s local timezone |
| Last run | Timestamp and status of the most recent triggered run |
Reference
| Detail | Value |
|---|---|
| Evaluator | convex/crons.ts → workflow_triggers.evaluate |
| Dispatch | convex/workflow_triggers.ts → dispatchScheduledRun (internal mutation) |
| Cron parser | croner (V8 runtime) |
| Timezone API | Intl.DateTimeFormat |
| Batch size | .take(101) per tick |
| Tick frequency | 60 seconds |
| Minimum interval | 300 000 ms (5 minutes) |
| Index | by_triggerType_status_nextScheduledRun on workflows |

