Schedules
Trigger workflows on a cron expression or interval, with overlap policy and run options.
A schedule automatically triggers workflows at defined times or intervals. Use schedules for recurring jobs like daily reports, hourly syncs, or cron-based maintenance tasks.
Creating a Schedule
import { client } from "@aikirun/client";
import { schedule } from "@aikirun/workflow";
import { dailyReportWorkflowV1 } from "./workflows";
const aikiClient = client({
url: "http://localhost:9850",
apiKey: "your-api-key",
});
const dailyReport = schedule({
type: "cron",
expression: "0 9 * * *", // Every day at 9 AM UTC
});
const handle = await dailyReport.activate(
aikiClient,
dailyReportWorkflowV1,
{ reportType: "sales" } // Workflow input
);The schedule() function defines a timing configuration. Call activate() to bind it to a workflow - the workflow will then trigger automatically based on the schedule. The third argument is the input passed to the workflow on each run.
Each activate() call creates a unique schedule instance, identified by the workflow name, version, timing spec, input, and run options. Activating the same schedule with different inputs creates independent instances, each with their own overlap tracking.
The same schedule spec can be bound to different workflows:
const hourly = schedule({
type: "interval",
every: { hours: 1 },
});
// Schedule 2 different workflows to run hourly
await hourly.activate(aikiClient, inventorySyncV1);
await hourly.activate(aikiClient, pricingSyncV1);Schedule Types
Cron
Use cron expressions for complex timing patterns:
const dailyCleanup = schedule({
type: "cron",
expression: "0 0 * * *", // Midnight every day
});
const weeklyReport = schedule({
type: "cron",
expression: "0 9 * * 1", // 9 AM every Monday
timezone: "America/New_York", // Optional timezone (default: UTC)
});Interval
Use intervals for simple recurring patterns:
const hourlySync = schedule({
type: "interval",
every: { hours: 1 },
});
const frequentCheck = schedule({
type: "interval",
every: { minutes: 15 },
});The every field accepts a duration object with milliseconds, seconds, minutes, hours, and days.
Overlap Policy
When a schedule triggers but a previous run is still active, the overlap policy determines what happens:
const syncSchedule = schedule({
type: "interval",
every: { minutes: 5 },
overlapPolicy: "skip", // Skip if previous run is still active
});| Policy | Behavior |
|---|---|
"allow" | Start a new run regardless of active runs |
"skip" (default) | Skip this occurrence if a run is still active |
"cancel_previous" | Cancel the active run and start a new one |
The policy also decides what happens to the occurrences a schedule missed, whether because the server was down or because the schedule was paused. "allow" runs every missed occurrence, oldest first, working through a large backlog in batches rather than all at once. "skip" and "cancel_previous" run only the most recent one.
Overlap policies are evaluated per schedule instance, not globally. If you activate the same schedule for multiple tenants with different inputs, each tenant has independent overlap handling.
Run Options
A schedule fires runs of the workflow you hand to activate(), and those runs carry that workflow's options — whatever it declared at definition time, plus anything you set via with(). Configure the workflow, not the schedule:
const hourlySync = schedule({
type: "interval",
every: { hours: 1 },
});
await hourlySync.activate(
client,
inventorySyncV1
.with("retry", { type: "exponential", maxAttempts: 3, baseDelayMs: 1000 })
.with("pool", "foo-bar")
);Only retry, pool, and priority travel this way. reference or delay answers something about one particular run — which run it is, when execution begins — and a schedule fires a fresh run every tick, so passing a workflow that carries either will not compile. See Workflow Options.
Run options are part of a schedule's identity, so changing them is a different schedule — or, with a reference ID, a conflict.
Idempotent Activation
Calling activate() is idempotent. If a schedule already exists with the same parameters, the existing schedule is returned unchanged.
If you call activate() with a different input or timing configuration (such as a new cron expression or interval), that is a different schedule identity: you are activating a new schedule, not modifying the first. A schedule's definition is immutable — there is no in-place edit. To change the timing or input, activate the new definition (a new schedule) and deactivate the old one. A reference ID gives a schedule a stable identity for lookups.
Reference IDs
By default, schedule identity is derived from a hash of the workflow name, version, timing spec, input, and run options. You can provide an explicit reference ID instead:
const handle = await dailyReport
.with("reference.id", "tenant-acme-daily-report")
.activate(client, reportWorkflowV1, { tenantId: "acme" });Reference IDs are useful when you need a stable, predictable identifier for lookups or external integrations.
Conflict Policy
When activating a schedule with a reference ID that already identifies a schedule with a different definition, the conflict policy determines what happens:
const handle = await dailyReport
.with("reference", {
id: "my-schedule",
conflictPolicy: "error",
})
.activate(client, workflowV1, input);| Policy | Behavior |
|---|---|
"error" (default) | Throw a ScheduleConflictError if the reference ID already identifies a schedule with a different definition |
"return_existing" | Return the existing schedule unchanged |
The definition is immutable, so a reference ID that already points at a different definition is a conflict, not an update. With "error" the activation throws a ScheduleConflictError; with "return_existing" it returns the existing schedule as-is. Re-activating with the same definition is idempotent: it returns the existing schedule. If that schedule is paused, re-activating does not resume it; only resume() does. If the schedule was deactivated, re-activating brings it back.
For more on reference IDs in workflows and events, see the Reference IDs guide.
Managing Schedules
The handle returned from activate() lets you manage the schedule:
const handle = await mySchedule.activate(aikiClient, workflowV1);
await handle.pause(); // Stop triggering until resumed
await handle.resume(); // Resume a paused schedule
await handle.deactivate(); // Deactivate schedule| Property/Method | Description |
|---|---|
id | Unique identifier for this schedule |
pause() | Stop triggering until resumed. Rejected on a deactivated schedule |
resume() | Resume a paused schedule. Rejected on a deactivated schedule; activate() brings it back |
deactivate() | Deactivate schedule |
Multi-Tenant Schedules
For multi-tenant applications, activate the same schedule with different inputs for each tenant. Each activation creates an independent schedule instance:
const dailyReport = schedule({
type: "cron",
expression: "0 9 * * *",
overlapPolicy: "skip",
});
// Each tenant gets an independent schedule instance
await dailyReport.activate(client, reportWorkflowV1, { tenantId: "acme" });
await dailyReport.activate(client, reportWorkflowV1, { tenantId: "globex" });
// These are completely independent:
// - If Acme's report is still running, Globex's report starts normally
// - The "skip" policy only skips Acme's next run, not Globex's