Skip to content

Pre-GA Design Partner and Early Access only. Request access

Helix Docs

Scheduled functions

Run functions on a cron schedule with defineSchedule: five-field cron expressions, IANA timezones, build-time validation, and at-least-once delivery.

For developers Updated Sep 18, 2026
View as Markdown

Scheduled functions run on a cron schedule instead of an HTTP request. Declare one with defineSchedule in functions/_scheduled/, where the underscore keeps the file out of routing, and deploy: the platform registers the schedule and fires your handler at the times the cron describes. Schedules run in production only: helix dev warns about _scheduled/ files and does not run them locally.

Defining a schedule

// functions/_scheduled/daily-report.ts
import { defineSchedule } from '@trayai/helix-sdk';

export default defineSchedule({
  cron: '0 9 * * MON-FRI',   // 9am on weekdays
  timezone: 'Europe/London',
  concurrent: false,         // whether runs of this schedule may overlap
  handler: async (ctx) => {
    const kv = ctx.kv();
    const signups = (await kv.getItem<number>('signups:today')) ?? 0;

    ctx.log.info('Daily signup report', { signups });

    await ctx.http.post('https://hooks.slack.com/services/...', {
      json: { text: `Daily report: ${signups} new signups` },
    });

    await kv.setItem('signups:today', 0);
  },
});

The handler receives the platform context: ctx.kv(), ctx.http, ctx.log, and the rest. What it doesn’t have is request data, so ctx.input, ctx.method, and ctx.headers are not available. The return value is discarded.

Configuration

FieldTypeRequiredDescription
cronstringYesFive-field standard cron expression: minute, hour, day, month, weekday
timezonestringNoNamed IANA timezone, for example 'Europe/London' or 'America/New_York'. Defaults to UTC.
concurrentbooleanNoWhether a fire may overlap the previous run of the same schedule. Defaults to false.
handlerfunctionYesThe function to execute. Receives ctx with all platform services. The return value is discarded.

Validation happens at build time

Schedules are validated during the deploy build, and validation is fail-closed: one invalid schedule file fails the whole build, with every problem listed.

  • Five-field standard cron only. Month and weekday names are accepted (JANDEC, SUNSAT, case-insensitive), and ? is treated as * in both day fields. Six-field expressions are rejected.
  • Day-of-month and day-of-week cannot both be constrained. Standard cron’s either-day union is not supported, so put * (or ?) in one of the two day fields, or split the schedule into two files.
  • The timezone must be a named IANA zone starting with a letter. Offset forms like '+05:00' are rejected.
  • At most 20 schedule files per project.

Overlapping runs

concurrent declares whether a fire may overlap the previous run of the same schedule. It defaults to false. The setting is per schedule, so it never affects the other schedules in a project, and the dashboard marks the jobs that set it to true.

Delivery is at-least-once either way, so a handler whose work must not happen twice needs to be idempotent regardless.

The trigger payload

Inside the handler, ctx.trigger describes why this execution happened:

FieldTypeMeaning
type'schedule'Always 'schedule' for scheduled functions
scheduledAtDateWhen the cron said it should fire
firedAtDateWhen it actually started, stamped at invoke time, so dispatch latency, cold starts, and retries all widen the gap
isManualbooleanReserved for manual triggering, which is planned. Always false today.

The gap between scheduledAt and firedAt reflects dispatch and cold-start latency. Log it if you care about precision. The execution ID lives on ctx.executionId, as in every function.

Nothing in the payload marks an execution as a retry: a handler cannot tell a retried run from a first run, so write handlers to be idempotent instead of gating side effects on how the run started.

Retries and failures

Delivery is at-least-once. A run that throws fails the invocation and is retried once, starting five minutes after the failed run started; the delay is not configurable per schedule. If the retry also fails, the tick is dropped and never replayed, because a stale cron tick must not fire at a time nothing asked for. Both attempts appear in the project’s Logs tab as errored runs, each with its own trace.

A retry means the same tick can run twice, and overlapping runs mean two ticks can run at once. Design for both: make the work idempotent, and use the key-value store when a handler needs to remember what it already processed.

Limits

These limits are enforced for scheduled functions today.

LimitValue
Schedule files per project20 maximum, enforced at build time
Fastest cadenceOnce per minute
Run duration3 minutes maximum per run, not configurable
RetriesOne retry, starting five minutes after the failed run started, then the tick is dropped
OverlapRuns can still overlap. concurrent is accepted at deploy time but not enforced yet

Five-field cron has minute resolution, so a schedule cannot fire more than once per minute. A run still going at 3 minutes fails and follows the same retry path as a thrown error, so break work that could run long into smaller ticks: store a cursor in the key-value store and let each run pick up where the last one stopped.

Platform-wide limits, including everything the handler itself is subject to, are collected on the limits reference.

How production scheduling works

helix deploy uploads your source and the platform does the schedule work; the CLI never parses or registers schedules itself.

  1. The build evaluates each file in functions/_scheduled/ and extracts its cron, timezone, and overlap setting. This is where validation runs.
  2. When the build completes, the platform reconciles the extracted set of schedules: new and changed schedules are registered in a disabled state, the new code is published, then every schedule is activated and schedules whose files are gone are deleted. Registering them disabled first means a failed deploy can’t leave a schedule firing against unpublished code.
  3. When a cron fires, the platform invokes your project’s deployed functions and dispatches to the matching handler.

The schedule set is declarative, so the live state always matches the deployed code, and a schedule always runs the latest deployment’s code without being re-registered.

Local development

helix dev does not run schedules: it notices functions/_scheduled/ at startup and warns that the files won’t fire locally. A local scheduler and a manual-run command (helix trigger) are planned. Until they land, keep the schedule’s logic in a shared module that an HTTP function also calls, and exercise it through the HTTP route while developing.

Viewing and managing schedules

The dashboard is where you see and control schedules after a deploy. There is no helix schedules command; the CLI’s part ends at helix deploy.

The project’s Scheduled Jobs tab

Open a project and pick Scheduled Jobs to see every schedule its live deployment declares:

ColumnContents
JobThe schedule’s file name, the description your code carries for it, and a marker when the job allows overlapping runs
ScheduleThe cadence in words, such as “At 09
, Monday through Friday”. Hover for the cron expression. A job that fires every 15 minutes or more often is flagged as high frequency
TimezoneThe schedule’s timezone
Next runA countdown and the local date and time of the next fire. Approximate, since it is derived from the cron expression, and empty for a paused job
StatusActive or Paused

Search by job name or description, filter by All, Active, or Paused, and read the active and paused counts beside the filter.

Pausing and resuming

Pause on a row stops a job firing and leaves it in place; Resume starts it again. Pausing applies to future fires and does not stop a run already going, and the paused state survives redeploys. The control appears only for people who can change the project, so a viewer sees the list without it.

Pause is the control to reach for when a job is causing trouble: removing the file also stops it, but that needs a deploy.

Following what a job did

View logs in a row’s actions menu opens the project’s Logs tab filtered to that job’s runs. Each run is one entry with the SCHEDULE trigger type, and opening an entry gives the same execution trace an HTTP request gets. A retried tick puts each attempt in the log as its own entry.

The organization-wide view

Org admins get every schedule in the organization on one page: Scheduled Jobs in the Admin console. It carries the same job, cadence, timezone, and status columns, plus the project and workspace each job belongs to. Search there covers project names as well as job names and descriptions, so you can pull up every schedule in one project without opening it. Pause and resume stay on the project’s own tab, which the rows link to.

Loading search…

Jump to a section

tab to move · esc to close