Skip to main content

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:

  1. Install the SDK:

    npm i @vercel/schedules
  2. Wrap the handler in handleSchedule:

    api/cleanup.ts
    import { handleSchedule } from "@vercel/schedules";
    import { deleteExpiredSessions } from "../lib/sessions";
    
    export const POST = handleSchedule(async (metadata) => {
      await deleteExpiredSessions({ runId: metadata.executionId });
    });
  3. Mark the function and declare the schedule in vercel.json:

    vercel.json
    {
      "functions": {
        "api/cleanup.ts": {
          "experimentalTriggers": [{ "type": "schedule/v1beta" }]
        }
      },
      "schedules": {
        "cleanup": {
          "schedule": "0 2 * * *",
          "timezone": "America/New_York",
          "function": "api/cleanup"
        }
      }
    }

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:

TargetWhat a run does
functionSends a POST straight to a private function marked with schedule/v1beta
pathRequests a path on your deployment, like a visitor. requestMethod is GET or POST, POST by default
topicPublishes 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 -:

FieldDescription
scheduleA five-field cron expression
timezoneAn IANA time zone, such as Europe/Paris. UTC by default
jitterMinutes, from 1 to 15, that a run may start after its scheduled time. 1 by default
function, path, or topicThe 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:

app/api/reminders/route.ts
import { Schedules } from "@vercel/schedules";

export async function POST(request: Request) {
  const { userId, at } = await request.json();
  await Schedules.create({
    name: "send-reminder",
    namespace: userId,
    expression: { type: "single", at },
    timezone: "America/New_York",
    target: { topic: "reminders" },
    payload: { userId },
  });
  return new Response(null, { status: 204 });
}

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:

vercel.json
{
  "schedules": {
    "daily-report": {
      "schedule": "0 5 * * *",
      "path": "/api/cron",
      "requestMethod": "GET"
    }
  }
}

Leave out timezone to keep UTC. Route code doesn't change: runs still send vercel-cron/1.0 and CRON_SECRET.

Next steps