Handling Scheduled Runs
When a schedule fires, Edgenivo delivers a run to the schedule's target. This page shows how to write the code that receives the run for each target type.
| Target | Use it when | Retries | Available for |
|---|---|---|---|
| Function | You want a private function that nobody else can call. | No | Static schedules |
| Path | You're migrating a Cron Job, or the route must stay public. | No | Static schedules |
| Queue topic | Failed runs must be retried, or you need a payload or dynamic schedules. | Yes | Static and dynamic schedules |
A function target runs a Edgenivo Function that has no public URL. Only Edgenivo's scheduler can invoke it, so you don't need to check a secret.
-
Install the SDK:
npm i @vercel/schedules -
Create the function and wrap your task in
handleSchedule:api/cleanup.tsimport { handleSchedule } from '@vercel/schedules'; export const POST = handleSchedule(async (metadata, request) => { console.log(`${metadata.name} run ${metadata.executionId}`); await deleteExpiredRecords(); }); -
Add a
schedule/v1betatrigger to the function and point a schedule at it invercel.json:vercel.json{ "functions": { "api/cleanup.ts": { "experimentalTriggers": [{ "type": "schedule/v1beta" }] } }, "schedules": { "cleanup": { "schedule": "0 * * * *", "function": "api/cleanup" } } }The
functionvalue is the function's build output path, not its source filename. See Function identifiers. For a function in a service, also setservice.
A function with the schedule/v1beta trigger can't also have a queue trigger.
handleSchedule reads the run's details from the request headers and passes them to your callback with the original Request:
- If the request isn't a valid schedule run, it returns a
400response and doesn't call your callback. - If your callback finishes, it returns a
200response. - If your callback throws, the error propagates to your framework or runtime, which typically returns a
500response.
The response status shows up in your function logs and the schedule's execution history. It doesn't affect the schedule, and Edgenivo doesn't retry the run.
The callback's first argument is ScheduleMetadata:
| Field | Type | Description |
|---|---|---|
scheduleId | string | ID of the schedule |
executionId | string | ID of this run. Stays the same if the run is delivered more than once. |
name | string | Schedule name |
namespace | string | Schedule namespace. Static schedules use default. |
scheduledAt | Date | Time the expression matched. Doesn't include jitter, so the run can arrive later. |
source | string | static or dynamic |
expression | string | The cron expression, or the local datetime of a one-time schedule |
To read the details without wrapping your handler, call parseScheduleEvent(request). It returns the same ScheduleMetadata, or throws a ScheduleEventParseError if the request isn't a schedule run.
Several schedules can target the same function. Use metadata.name to decide what to run:
import { handleSchedule } from '@vercel/schedules';
export const POST = handleSchedule(async (metadata) => {
switch (metadata.name) {
case 'hourly-cleanup':
await deleteExpiredRecords();
break;
case 'nightly-report':
await sendReport();
break;
}
});A path target sends an HTTP request to a route on your deployment, like a Cron Job. The route is public, so anyone can call it. Protect it with a secret.
-
Add a schedule with a
pathinvercel.json. Path schedules send aPOSTrequest by default. SetrequestMethodtoGETif your route handlesGET:vercel.json{ "schedules": { "daily-report": { "schedule": "0 9 * * *", "path": "/api/daily-report", "requestMethod": "GET" } } } -
Add a
CRON_SECRETenvironment variable to your project. Use a random string of at least 16 characters. Edgenivo sends it in theAuthorizationheader of every path request. -
Check the header in your route before doing any work:
api/daily-report.tsexport async function GET(request: Request) { const authHeader = request.headers.get('authorization'); const cronSecret = process.env.CRON_SECRET; if (!cronSecret || authHeader !== `Bearer ${cronSecret}`) { return new Response('Unauthorized', { status: 401 }); } await sendReport(); return new Response(null, { status: 200 }); }
Path requests include the same headers as Cron Job requests, plus the schedule headers that function targets receive. That means:
- A route that already handles a Cron Job keeps working.
- You can wrap a path route in
handleScheduleto readScheduleMetadata.handleScheduleaccepts bothGETandPOST. It doesn't checkCRON_SECRET, so keep the check.
See Request headers for the full list.
A queue target publishes a message to a Edgenivo Queues topic each time the schedule fires. A consumer on that topic processes the message. If the consumer fails, the queue retries it.
Queue topics are the only target for dynamic schedules, and the only target that supports a payload.
-
Install the Queues and Schedules SDKs:
npm i @vercel/queue @vercel/schedules -
Create a consumer with
handleCallback:api/daily-reports.tsimport { handleCallback } from '@vercel/queue'; import type { ScheduleQueueMessage } from '@vercel/schedules'; type ReportPayload = { reportType: string }; export const POST = handleCallback<ScheduleQueueMessage<ReportPayload>>( async (message) => { console.log(`Running ${message.name}, scheduled for ${message.scheduledAt}`); await generateReport(message.payload?.reportType ?? 'summary'); }, );ScheduleQueueMessagetypes the message. Its parameter types the payload. -
Subscribe the consumer to a topic and point a schedule at the same topic:
vercel.json{ "functions": { "api/daily-reports.ts": { "experimentalTriggers": [ { "type": "queue/v2beta", "topic": "daily-reports" } ] } }, "schedules": { "daily-report": { "schedule": "0 9 * * *", "topic": "daily-reports" } } }
To create a queue schedule at runtime instead, use Schedules.create with target: { topic: 'daily-reports' }.
The message body is a JSON object with the schedule's details at the top level:
{
"scheduleId": "sch_abc123",
"executionId": "schx_def456",
"name": "daily-report",
"namespace": "default",
"scheduledAt": "2026-09-15T09:00:00.000Z",
"source": "dynamic",
"payload": {
"reportType": "summary"
}
}| Field | Type | Description |
|---|---|---|
scheduleId | string | ID of the schedule |
executionId | string | ID of this run. Stays the same if the run is delivered more than once. |
name | string | Schedule name |
namespace | string | Schedule namespace |
scheduledAt | string | ISO 8601 time the expression matched. Doesn't include jitter. |
source | "static" or "dynamic" | How the schedule was defined |
payload | unknown | The payload you set on a dynamic schedule. Omitted when the schedule has no payload. Static schedules never include it. |
A run can arrive more than once, and a delayed schedule can catch up with several runs in a row. Queue targets also retry failed deliveries. Write your handler so a repeated run doesn't repeat its side effects.
The execution ID is the same for every delivery of a run, so you can use it to detect a run you've already processed. Function targets get it as metadata.executionId, and queue targets get it as message.executionId.
Function and path targets receive a request with an empty body. The run's details are in the headers. handleSchedule and parseScheduleEvent read these for you.
| Header | Sent to | Value |
|---|---|---|
ce-specversion | Function and path | 1.0 |
ce-type | Function and path | com.vercel.schedule.v1beta |
ce-source | Function and path | /schedule/{namespace}/{name} |
ce-id | Function and path | Execution ID of this run |
ce-vssscheduleid | Function and path | Schedule ID |
ce-vssschedulename | Function and path | Schedule name |
ce-vssnamespace | Function and path | Schedule namespace |
ce-vssscheduledat | Function and path | ISO 8601 time the expression matched, without jitter |
ce-vssschedulesource | Function and path | static or dynamic |
ce-vssexpression | Function and path | The cron expression, or the local datetime of a one-time schedule |
user-agent | Path only | vercel-cron/1.0 |
x-vercel-cron-schedule | Path only | The cron expression |
authorization | Path only | Bearer <CRON_SECRET>, when you set CRON_SECRET |
- Concepts: Learn about retries, duplicates, and late runs.
- Configuration reference: Look up every schedule property.
- Testing schedules locally: Run your handler with
vercel dev.
Was this helpful?