Skip to content
Docs

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/schedules

On 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 inCalls apply to
A production deploymentProduction schedules
A Preview deploymentSchedules for that deployment's branch
A custom environment deploymentSchedules for that custom environment
vercel devThe 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,
});
OptionTypeDescription
tokenstringToken to authenticate with. Defaults to the OIDC token from @vercel/oidc.
baseUrlstringSchedules API URL. Defaults to https://vercel-schedules.com.
fetchtypeof fetchCustom fetch implementation
headersHeadersInitExtra 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);
ParameterTypeRequiredDescription
namestringYesUnique within the namespace. See naming rules.
namespacestringNoGroups schedules, for example by tenant. Defaults to default.
expressionScheduleExpressionYes{ type: 'cron', cron: string } for a recurring schedule, or { type: 'single', at: string } for a one-time schedule
target{ topic: string }YesQueue topic to publish to. Dynamic schedules only support queue topics.
timezonestringNoIANA timezone for the expression. Defaults to UTC.
jitternumberNoJitter window in minutes, from 1 to 15. On Hobby, omit it or pass 15.
payloadunknownNoJSON 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);
}
ParameterTypeRequiredDescription
namespacestringNoNamespace to list. Defaults to default.
limitnumberNoPage size, from 1 to 100. Defaults to 20.
cursorstringNocursor 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' });
ParameterTypeRequiredDescription
namestringYesSchedule name
namespacestringNoDefaults 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 },
});
ParameterTypeRequiredDescription
namestringYesSchedule name
namespacestringNoDefaults to default
expressionScheduleExpressionNoNew expression of the same type. You can't change a cron schedule to a one-time schedule, or the reverse.
target{ topic: string }NoNew queue topic
timezonestringNoNew IANA timezone
jitternumber or nullNoNew jitter window. Pass null to return to the default. On Hobby, the window stays at 15 minutes.
payloadunknownNoNew 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),
});
ParameterTypeRequiredDescription
namestringYesSchedule name
namespacestringNoDefaults to default
untilDate or numberNoenable() 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 activeUntil field holds the end time until it passes.
  • Calling enable() again with until replaces the end time.
  • Calling enable() without until keeps 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' });
ParameterTypeRequiredDescription
namestringYesSchedule name
namespacestringNoDefaults 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' });
ParameterTypeRequiredDescription
namestringYesSchedule name
namespacestringNoDefaults 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:

ExportDescription
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
api/cleanup.ts
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:

PropertyTypeDescription
statusnumberHTTP status code
codestringMachine-readable error code
messagestringError message
retryAfternumberSeconds to wait before retrying, when the API sends a Retry-After header

Common codes:

CodeCause
not_foundThe schedule doesn't exist in this namespace
not_enabledSchedules isn't enabled for your team
forbiddenYour role can't make this change in this environment. See Permissions.
feature_blockedYour Hobby team went over its usage limits for this billing period
schedule_already_existsA schedule with this name already exists in this namespace
schedule_limit_exceededYour team reached its schedule limit
invalid_cron_expressionThe cron expression isn't valid. See Cron expressions.
invalid_jitterThe jitter window isn't allowed on your plan
jitter_exceeds_cadenceThe jitter window is longer than the shortest interval between runs
single_at_in_pastThe one-time schedule's at time isn't far enough in the future
payload_too_largeThe payload is larger than 16 KB
schedule_type_change_not_allowedYou tried to change a cron schedule to a one-time schedule, or the reverse
schedule_already_activeYou passed until to a schedule that's already active indefinitely
active_until_in_pastThe until time isn't in the future
static_scheduleYou tried to update or delete a static schedule
schedule_single_at_passedYou 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_limitedYou exceeded an API request limit. Wait retryAfter seconds.
service_unavailableThe 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:

FieldTypeDescription
scheduleIdstringSchedule ID
ownerIdstringID of the team that owns the schedule
projectIdstringID of the project
namestringSchedule name
namespacestringSchedule namespace
expressionScheduleExpression{ type: 'cron', cron } or { type: 'single', at }
timezonestringIANA timezone
jitternumberJitter window in minutes, when set. Omitted when the schedule uses the plan default.
targetScheduleTargetSee the next table
state'active' or 'inactive'Whether the schedule runs on its cadence. A one-time schedule stays active after it runs.
activeUntilnumberUnix 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
createdAtnumberUnix timestamp in milliseconds
updatedAtnumberUnix timestamp in milliseconds

target has one of these shapes:

typeShapeAvailable 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:

FieldTypeDescription
scheduleIdstringSchedule ID
executionIdstringID of this run. Stays the same if the run is delivered more than once.
namestringSchedule name
namespacestringSchedule namespace
scheduledAtDateTime the expression matched. Doesn't include jitter.
sourcestringstatic or dynamic
expressionstringThe 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');
  },
);
FieldTypeDescription
scheduleIdstringSchedule ID
executionIdstringID of this run. Stays the same if the run is delivered more than once.
namestringSchedule name
namespacestringSchedule namespace
scheduledAtstringISO 8601 time the expression matched. Doesn't include jitter.
expressionstringThe 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
payloadTPayloadThe schedule's payload. Omitted when the schedule has none.
Last updated October 26, 2026

Was this helpful?

supported.