Run code on a schedule
Run functions, paths, or queue topics on a cron expression or once at a set time, with @vercel/schedules and vercel.json.
Schedules run your code on a cron expression, in the time zone you choose, or once at a set time. A schedule can call a private function, request a path on your deployment, or publish to a Queues topic.
Easel implements the same contract as
Vercel Schedules, so schedules in
vercel.json and the @vercel/schedules
SDK work unchanged. Schedules is in beta.
Schedule a function
A scheduled function has no public URL, so only its schedule can run it:
-
Install the SDK:
-
Wrap the handler in
handleSchedule:api/cleanup.ts -
Mark the function and declare the schedule in
vercel.json:vercel.json
A schedule's function is the function's path in the build output, without
its extension. Easel checks every schedule at build time, and a problem fails
the build with the schedule's name and the reason.
Other targets
Set exactly one target on each schedule:
| Target | What a run does |
|---|---|
function | Sends a POST straight to a private function marked with schedule/v1beta |
path | Requests a path on your deployment, like a visitor. requestMethod is GET or POST, POST by default |
topic | Publishes a message to a Queues topic, which gets Queues' retries |
A path run goes through routing and middleware, and carries
user-agent: vercel-cron/1.0. When the deployment sets CRON_SECRET, the run
sends it as Authorization: Bearer <CRON_SECRET>, so check it to reject other
callers.
Schedule fields
Each schedule is keyed by its name, which can use letters, numbers, ., _,
and -:
| Field | Description |
|---|---|
schedule | A five-field cron expression |
timezone | An IANA time zone, such as Europe/Paris. UTC by default |
jitter | Minutes, from 1 to 15, that a run may start after its scheduled time. 1 by default |
function, path, or topic | The target |
Cron expressions
The five fields are minute, hour, day of month, month, and day of week, where
Sunday is 0. Each field takes *, a number, a range such as 1-5, a step
such as */15, or a comma-separated list of those.
Easel doesn't accept month or day names (MON, JAN), macros such as
@daily, or L, W, #, and ?. You can set day of month or day of week,
but not both.
Expressions run in the schedule's time zone, following daylight saving time. A time skipped by a spring-forward change runs at the first moment after the gap. A time that happens twice in a fall-back change runs once, at the first occurrence, unless the expression runs every hour.
Create schedules at runtime
Create one-time or recurring schedules from your code with Schedules.create.
Runtime schedules publish to a queue topic, with a JSON payload of up to 16 KB:
at is a local date and time such as 2026-12-01T09:00, in the schedule's
time zone. The SDK also lists, updates, enables, disables, runs, and deletes
schedules, as on Vercel.
How runs are delivered
Each scheduled time is one run, with one executionId. Design handlers for
these guarantees:
- At least once. A run can arrive more than once with the same
executionId. Record it, and skip a run you've already completed. - No retries for functions and paths. A run that fails isn't retried.
If a path run's connection fails before your code answers, Easel sends it
once more, with the same
executionId. Publish to a topic when you need retries. - Independent runs. A slow run doesn't hold back the next one, so runs can overlap. Take a lock in your database if they mustn't.
- Late runs. A run that can't start on time is still delivered until its jitter window plus one interval has passed, or 10 minutes, whichever is longer. After a long outage, Easel delivers at most the latest 10 missed runs from the past 7 days.
Each run is bound by its function's maximum duration.
Production and previews
Schedules belong to an environment: production, or a single preview branch. A deployment's schedules take effect when it becomes that environment's current deployment: the current production deployment, or the branch's newest ready preview. A rollback restores the schedules of the deployment you roll back to.
- In production, a new schedule starts enabled.
- On preview branches, a new schedule starts disabled, so a preview doesn't run production work on its own.
- Deleting a Git branch deletes its schedules.
Enabling or disabling a schedule lasts across deploys. When a deployment changes a schedule's expression, time zone, or jitter, the new timing starts from that deployment: runs the new expression would have made in the past don't fire. Runs skip while a project is paused.
Manage schedules
Open the project's Schedules tab to see every schedule, its next run, and its error rate. Open a schedule to:
- Choose Run Schedule to run it now.
- Enable or Disable it, or enable it for a set time.
- See its runs from the last 90 days, and open a run's logs.
Limits and pricing
Hobby runs a schedule at most every 15 minutes, always with 15 minutes of jitter, and caps runs per billing cycle. See Schedules limits for every limit.
Schedules have no charge of their own. Function and path runs bill as function invocations, and topic runs as Queues operations.
Migrate from Vercel Cron Jobs
Easel doesn't run crons in vercel.json; the build log warns about them
instead. Move each entry to schedules with a name. Cron Jobs sent GET
requests, so set requestMethod:
Leave out timezone to keep UTC. Route code doesn't change: runs still send
vercel-cron/1.0 and CRON_SECRET.
Next steps
Queues
Publish messages to durable topics with @vercel/queue and process them in your functions, with retries, delays and at-least-once delivery.
Workflows
Build workflows with the Workflow SDK that survive restarts, retry steps, sleep for days and wait on hooks, with every run traced in the dashboard.