Edgenivo Schedules: JS SDK Reference
Use the @vercel/schedules package to create and manage dynamic schedules from your code, and to handle runs in a function. To manage schedules from the terminal instead, use vercel schedules. To call the API without the SDK, see the API Reference.
npm i @vercel/schedulesOn Edgenivo, the SDK authenticates with your deployment's OIDC token. You don't need to configure anything. Each call applies to the environment or branch of the deployment that runs it:
| Code runs in | Calls apply to |
|---|---|
| A production deployment | Production schedules |
| A Preview deployment | Schedules for that deployment's branch |
| A custom environment deployment | Schedules for that custom environment |
vercel dev | The local Schedules service |
The package exports a default client, Schedules, that's ready to use:
import { Schedules } from '@vercel/schedules';
const schedule = await Schedules.get({ name: 'cleanup' });To pass options, create your own SchedulesClient. It has the same methods as Schedules:
import { SchedulesClient } from '@vercel/schedules';
const schedules = new SchedulesClient({
token: process.env.VERCEL_OIDC_TOKEN,
});| Option | Type | Description |
|---|---|---|
token | string | Token to authenticate with. Defaults to the OIDC token from @vercel/oidc. |
baseUrl | string | Schedules API URL. Defaults to https://vercel-schedules.com. |
fetch | typeof fetch | Custom fetch implementation |
headers | HeadersInit | Extra headers to send with every request. Can't override authorization or content-type. |
The default Schedules client reads its configuration when you import the package. If you change environment variables at runtime, create a new SchedulesClient afterward.
Schedules.create() creates a dynamic schedule that publishes to a queue topic. It takes effect immediately.
import { Schedules } from '@vercel/schedules';
const schedule = await Schedules.create({
name: 'cleanup',
target: { topic: 'scheduled-cleanup' },
expression: { type: 'cron', cron: '0 * * * *' },
timezone: 'America/New_York',
jitter: 5,
payload: { maxAgeDays: 30 },
});
console.log(schedule.scheduleId, schedule.state);| Parameter | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Unique within the namespace. See naming rules. |
namespace | string | No | Groups schedules, for example by tenant. Defaults to default. |
expression | ScheduleExpression | Yes | { type: 'cron', cron: string } for a recurring schedule, or { type: 'single', at: string } for a one-time schedule |
target | { topic: string } | Yes | Queue topic to publish to. Dynamic schedules only support queue topics. |
timezone | string | No | IANA timezone for the expression. Defaults to UTC. |
jitter | number | No | Jitter window in minutes, from 1 to 15. On Hobby, omit it or pass 15. |
payload | unknown | No | JSON value, up to 16 KB, that Edgenivo includes in the payload field of each queue message |
Returns the new Schedule. Throws a SchedulesApiError with code schedule_already_exists if a schedule with the same name exists in the namespace.
Pass { type: 'single', at } to run once at a local date and time. Set timezone separately, or omit it to use UTC:
await Schedules.create({
name: 'send-reminder',
target: { topic: 'reminders' },
expression: { type: 'single', at: '2026-11-01T09:00' },
timezone: 'America/New_York',
});The at value uses YYYY-MM-DDTHH:mm format. See One-time expressions for the full format rules.
Schedules.list() returns static and dynamic schedules in one namespace, one page at a time:
const { data, cursor } = await Schedules.list({
namespace: 'default',
limit: 50,
});
for (const schedule of data) {
console.log(schedule.name, schedule.state, schedule.target.type);
}| Parameter | Type | Required | Description |
|---|---|---|---|
namespace | string | No | Namespace to list. Defaults to default. |
limit | number | No | Page size, from 1 to 100. Defaults to 20. |
cursor | string | No | cursor from the previous page |
Returns { data: Schedule[], cursor: string | null }. When cursor is null, there are no more pages. To read every page:
let cursor: string | undefined;
do {
const page = await Schedules.list({ namespace: 'default', cursor });
for (const schedule of page.data) {
console.log(schedule.name);
}
cursor = page.cursor ?? undefined;
} while (cursor);Static schedules can target a function or a path, so check target.type before you read the target:
import type { Schedule } from '@vercel/schedules';
function describeTarget(schedule: Schedule): string {
switch (schedule.target.type) {
case 'queue':
return `topic ${schedule.target.topic}`;
case 'function':
return `function ${schedule.target.function}`;
case 'path':
return `path ${schedule.target.path}`;
}
}Schedules.get() returns one static or dynamic schedule by name:
const schedule = await Schedules.get({ name: 'cleanup' });| Parameter | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Schedule name |
namespace | string | No | Defaults to default |
Throws a SchedulesApiError with code not_found if the schedule doesn't exist.
Schedules.update() changes a dynamic schedule. Properties you leave out keep their current values:
await Schedules.update({
name: 'cleanup',
expression: { type: 'cron', cron: '0 2 * * *' },
payload: { maxAgeDays: 60 },
});| Parameter | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Schedule name |
namespace | string | No | Defaults to default |
expression | ScheduleExpression | No | New expression of the same type. You can't change a cron schedule to a one-time schedule, or the reverse. |
target | { topic: string } | No | New queue topic |
timezone | string | No | New IANA timezone |
jitter | number or null | No | New jitter window. Pass null to return to the default. On Hobby, the window stays at 15 minutes. |
payload | unknown | No | New payload. Pass null to remove it. |
Returns the updated Schedule. You can't update a static schedule. Edit your configuration and redeploy instead. To change a schedule's expression type, delete it and create it again.
Schedules.enable() and Schedules.disable() change whether a static or dynamic schedule runs on its cadence:
await Schedules.disable({ name: 'cleanup' });
await Schedules.enable({ name: 'cleanup' });Pass until to enable() to enable the schedule until a set time:
await Schedules.enable({
name: 'cleanup',
until: new Date(Date.now() + 2 * 60 * 60 * 1000),
});| Parameter | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Schedule name |
namespace | string | No | Defaults to default |
until | Date or number | No | enable() only. Time to disable the schedule again, as a Date or Unix timestamp in milliseconds. Must be in the future. |
Both methods return the updated Schedule. When you pass until:
- The schedule's
activeUntilfield holds the end time until it passes. - Calling
enable()again withuntilreplaces the end time. - Calling
enable()withoutuntilkeeps the schedule active indefinitely. - If the schedule is already active indefinitely, the call fails with code
schedule_already_active. Disable it first.
Schedules.invoke() runs a static or dynamic schedule now, without changing its state or cadence:
await Schedules.invoke({ name: 'cleanup' });| Parameter | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Schedule name |
namespace | string | No | Defaults to default |
The method returns once Edgenivo accepts the run. Your target receives it shortly afterward, the same way as a scheduled run. You can invoke an inactive schedule.
Schedules.delete() deletes a dynamic schedule and stops its future runs:
await Schedules.delete({ name: 'cleanup' });| Parameter | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Schedule name |
namespace | string | No | Defaults to default |
You can't delete a static schedule. Remove it from your configuration and redeploy instead.
After you delete a schedule, you can create a new one with the same name right away. You can delete a one-time schedule before it runs or after its run is dispatched, but not while the run is being dispatched. See Delete a schedule.
The package also exports helpers for functions that a schedule targets:
| Export | Description |
|---|---|
handleSchedule(handler) | Wraps your handler in a route handler. Returns 400 for requests that aren't schedule runs and 200 when your handler finishes. |
parseScheduleEvent(request) | Returns the run's ScheduleMetadata, or throws a ScheduleEventParseError |
import { handleSchedule } from '@vercel/schedules';
export const POST = handleSchedule(async (metadata, request) => {
console.log(`${metadata.name} run ${metadata.executionId}`);
await deleteExpiredRecords();
});See Handling scheduled runs for function, path, and queue examples.
Failed API calls throw a SchedulesApiError:
| Property | Type | Description |
|---|---|---|
status | number | HTTP status code |
code | string | Machine-readable error code |
message | string | Error message |
retryAfter | number | Seconds to wait before retrying, when the API sends a Retry-After header |
Common codes:
| Code | Cause |
|---|---|
not_found | The schedule doesn't exist in this namespace |
not_enabled | Schedules isn't enabled for your team |
forbidden | Your role can't make this change in this environment. See Permissions. |
feature_blocked | Your Hobby team went over its usage limits for this billing period |
schedule_already_exists | A schedule with this name already exists in this namespace |
schedule_limit_exceeded | Your team reached its schedule limit |
invalid_cron_expression | The cron expression isn't valid. See Cron expressions. |
invalid_jitter | The jitter window isn't allowed on your plan |
jitter_exceeds_cadence | The jitter window is longer than the shortest interval between runs |
single_at_in_past | The one-time schedule's at time isn't far enough in the future |
payload_too_large | The payload is larger than 16 KB |
schedule_type_change_not_allowed | You tried to change a cron schedule to a one-time schedule, or the reverse |
schedule_already_active | You passed until to a schedule that's already active indefinitely |
active_until_in_past | The until time isn't in the future |
static_schedule | You tried to update or delete a static schedule |
schedule_single_at_passed | You tried to enable, disable, or update a one-time schedule after its scheduled time, or to delete it while its run is being dispatched |
rate_limited | You exceeded an API request limit. Wait retryAfter seconds. |
service_unavailable | The Schedules API is temporarily unavailable. Retry later. |
import { Schedules, SchedulesApiError } from '@vercel/schedules';
async function findSchedule(name: string) {
try {
return await Schedules.get({ name });
} catch (error) {
if (error instanceof SchedulesApiError && error.code === 'not_found') {
return null;
}
throw error;
}
}create(), get(), list(), update(), enable(), and disable() return Schedule objects:
| Field | Type | Description |
|---|---|---|
scheduleId | string | Schedule ID |
ownerId | string | ID of the team that owns the schedule |
projectId | string | ID of the project |
name | string | Schedule name |
namespace | string | Schedule namespace |
expression | ScheduleExpression | { type: 'cron', cron } or { type: 'single', at } |
timezone | string | IANA timezone |
jitter | number | Jitter window in minutes, when set. Omitted when the schedule uses the plan default. |
target | ScheduleTarget | See the next table |
state | 'active' or 'inactive' | Whether the schedule runs on its cadence. A one-time schedule stays active after it runs. |
activeUntil | number | Unix timestamp in milliseconds when a temporarily enabled schedule becomes inactive. Only present while that window is open. |
source | 'static' or 'dynamic' | Whether the schedule comes from your configuration or the API |
createdAt | number | Unix timestamp in milliseconds |
updatedAt | number | Unix timestamp in milliseconds |
target has one of these shapes:
type | Shape | Available for |
|---|---|---|
queue | { type: 'queue', topic: string } | Static and dynamic schedules |
function | { type: 'function', function: string, service?: string } | Static schedules |
path | { type: 'path', path: string, requestMethod?: 'GET' | 'POST' } | Static schedules |
handleSchedule and parseScheduleEvent return ScheduleMetadata:
| Field | Type | Description |
|---|---|---|
scheduleId | string | Schedule ID |
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 | Date | Time the expression matched. Doesn't include jitter. |
source | string | static or dynamic |
expression | string | The cron expression, or the local datetime of a one-time schedule |
A schedule with a queue target publishes a ScheduleQueueMessage to its topic each time it runs. Pass your payload's type as the parameter:
import { handleCallback } from '@vercel/queue';
import type { ScheduleQueueMessage } from '@vercel/schedules';
export const POST = handleCallback<ScheduleQueueMessage<{ reportType: string }>>(
async (message) => {
await generateReport(message.payload?.reportType ?? 'summary');
},
);| Field | Type | Description |
|---|---|---|
scheduleId | string | Schedule ID |
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. |
expression | string | The cron expression, or the local datetime of a one-time schedule |
source | 'static' or 'dynamic' | Whether the schedule comes from your configuration or the API |
payload | TPayload | The schedule's payload. Omitted when the schedule has none. |
Was this helpful?