Skip to main content
Scheduling lets workflows run automatically on a recurring basis without manual operator intervention. A workflow can have one or more schedule triggers — cron expressions, interval timers, or both — and the Convex scheduling evaluator fires them as configured. Schedules are separate from workflow definitions. Adding a schedule to a workflow doesn’t change its .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 the workflows table using the by_triggerType_status_nextScheduledRun index for any schedules that are:
  1. Active (status = "active")
  2. Due (nextScheduledRun <= now)
It batches up to 101 results per tick (.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

ConvexConvex cronevery 60 seconds

convex/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 the croner library running in the V8 runtime. Standard five-field POSIX cron syntax is supported:
┌───── minute (0-59)
│ ┌───── hour (0-23)
│ │ ┌───── day of month (1-31)
│ │ │ ┌───── month (1-12)
│ │ │ │ ┌───── day of week (0-7, 0 and 7 are Sunday)
│ │ │ │ │
* * * * *
Common patterns:
ExpressionMeaning
0 9 * * 1-5Weekdays 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 * * 1Every 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.
IntervalMilliseconds
Every 5 minutes300000
Every 30 minutes1800000
Every hour3600000
Every 6 hours21600000
The minimum supported interval is 5 minutes (300 000 ms). Shorter intervals are rejected at configuration time.

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.
Be careful with cron expressions during DST transitions. An expression like 0 2 * * * (2 AM daily) may skip or double-fire on transition days depending on the timezone. Prefer times outside the 1–3 AM window for timezone-sensitive schedules.

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.

When you disable a schedule, the 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 compute nextScheduledRun = 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:
nextScheduledRun = lastScheduledRun + intervalMs
For cron schedules, 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 stateSchedule behavior
activeFires normally
pausedSkips the run, advances to next scheduled time
deprecatedSkips the run, advances to next scheduled time
deletedSchedule 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:
ColumnDescription
Workflow / GatewayThe resource this schedule fires against
Typecron or interval, with the expression or period
TimezoneIANA timezone string (blank for UTC)
Statusenabled or disabled
Next runComputed next fire time, shown in operator’s local timezone
Last runTimestamp and status of the most recent triggered run
From this page you can enable, disable, or navigate to the source resource for any schedule without needing to open each workflow individually. This is useful for auditing which workflows are running on recurring schedules and verifying next-run alignment after a timezone or DST transition.

Reference

DetailValue
Evaluatorconvex/crons.tsworkflow_triggers.evaluate
Dispatchconvex/workflow_triggers.tsdispatchScheduledRun (internal mutation)
Cron parsercroner (V8 runtime)
Timezone APIIntl.DateTimeFormat
Batch size.take(101) per tick
Tick frequency60 seconds
Minimum interval300 000 ms (5 minutes)
Indexby_triggerType_status_nextScheduledRun on workflows
Cross-links: Workflow Execution | Observe & Schedules | .lobsterX Format