Schedules
Run an agent on a cron cadence, either a fire-and-forget prompt or a handler that hands work off to a channel.
A schedule starts the agent on its own clock instead of waiting for an inbound message. Use one for daily digests, data syncs, cleanup sweeps, heartbeats, or anything that should fire on a cadence. Root-authored schedules are single files under agent/schedules/, and mounted extensions can contribute them from extension/schedules/. Declared subagents cannot have a schedules/ directory.
The name comes from the path under schedules/ (agent/schedules/billing/sweep.ts → "billing/sweep"), and nested directories are fine. An extension mount prefixes that name (extension/schedules/sweep.ts mounted as crm → "crm__sweep") without changing its cron expression.
defineSchedule
Every schedule provides a cron and exactly one of markdown or run:
interface ScheduleDefinition {
cron: string;
markdown?: string; // fire-and-forget prompt (task mode)
run?: (args: ScheduleHandlerArgs) => Promise<void> | void; // handler
}
interface ScheduleHandlerArgs {
to: ScheduleToFn; // select a channel target, then send
waitUntil: (task: Promise<unknown>) => void; // keep the cron task alive past return
appAuth: SessionAuthContext; // pre-built app principal
}defineSchedule is a type-level pass-through. The compiler is what enforces the one-of rule.
A TypeScript schedule with markdown is compile-only because eve stores the prompt in the manifest. A schedule with run remains a runtime entry so the handler can execute. See Authored module lifecycle.
cron is a standard 5-field string (minute hour day-of-month month day-of-week) with minute granularity. On Vercel, each schedule becomes a Vercel Cron Job, and Vercel evaluates the expression in UTC, so "0 9 * * 1-5" fires at 09:00 UTC on weekdays. eve dev never fires schedules on their cron cadence. A built app served with eve start does run production scheduled tasks. To trigger one while iterating in dev, use the dispatch route below.
Markdown form (fire-and-forget)
This is the minimal schedule. eve runs the agent on the prompt and throws away the output, though the agent can still call tools, write to backends, and log along the way. We call this task mode. A task-mode session runs to completion or fails, and cannot park to wait for a person or an OAuth sign-in.
import { defineSchedule } from "eve/schedules";
export default defineSchedule({
cron: "*/5 * * * *",
markdown: "Pull open Linear issues and POST a summary to the metrics endpoint.",
});You can write the same thing as a plain .md file: its frontmatter takes cron and nothing else, and the body is the prompt.
agent/schedules/cleanup.md:
---
cron: "0 0 * * 0"
---
Sweep stale workflow state.Handler form (run)
Use a handler when the schedule needs to deliver to a channel, branch on conditions, or compute its arguments at fire time. The handler is in full control. It has no channel of its own, so it selects a channel target with to(...) and sends through the returned handle.
import { defineSchedule } from "eve/schedules";
import slack from "../channels/slack";
export default defineSchedule({
cron: "* * * * *",
async run({ to, waitUntil, appAuth }) {
waitUntil(
to(slack, { channelId: "C0123ABC" }).send(
"Check for new critical alerts. Report only when there are any.",
{ auth: appAuth },
),
);
},
});The agent does not have to deliver a message on every run. When a prompt makes delivery conditional, as in the alert check above, eve tells the agent how to finish successfully without sending anything to the channel. Frequent polling schedules do not need a separate filter or delivery setting.
to(channel, target).send(message, { auth }): starts a session on another channel. It has the same contract as a route handler'sctx.to(...)handle.waitUntil(promise): extends the cron task's lifetime so the parked session and any in-flight fetches settle before the task ends. Wrap thesendcall in it.appAuth: the app principal ({ authenticator: "app", principalId: "eve:app", principalType: "runtime" }). Pass it asto(...).send(..., { auth: appAuth })for work the agent does on its own behalf.
The auth option controls which principal the session runs as. A handler may instead supply a user principal when scheduled work needs that user's grants; a session created by that dispatch still retains schedule provenance and conditional delivery behavior.
A handler-form session runs on the same durable runtime engine as any other session, so it can park (durably suspend), for instance when the channel handoff is waiting for a Slack reply. Only markdown task mode is barred from waiting.
Session continuity
Markdown schedules start a new session on every fire. Handler schedules do too, unless they send to an existing conversation, such as a Slack thread. Store data that must survive across fires outside session state.
Trigger a schedule while iterating
The dev server mounts a one-shot dispatch route that fires a schedule by name, out of band, exactly once. Since eve dev never runs schedules on their cron cadence, this is how you trigger one without waiting for the next production tick.
curl -X POST http://localhost:2000/eve/v1/dev/schedules/heartbeat
# -> { "scheduleId": "heartbeat", "sessionIds": ["..."] }:scheduleId is the path-derived schedule name (agent/schedules/heartbeat.ts → heartbeat; URL-encode the / in nested names). It runs the exact dispatch path the production cron handler uses and returns the started session ids as JSON, so you can subscribe to each one's stream at GET /eve/v1/session/:sessionId/stream. An unknown id comes back 404 with availableScheduleIds, listing the schedules the app actually defines.
The route is dev-only. Production builds never mount it, and it needs no auth since the dev server is local-only.
On Vercel
Hosted Vercel builds turn every defineSchedule(...) into a Vercel Cron Job, with each cron written as an entry in .vercel/output/config.json. This also applies when withEve embeds one or more agents in a Next.js deployment: Vercel assembles each generated eve service's schedules into the project Build Output config while preserving existing project cron entries. Named agents use their public /eve/agents/<name> route prefix automatically.
When running vercel build locally, use Vercel CLI 56.4.0 or later so generated-service cron entries are included in the project output.
Vercel evaluates cron expressions in UTC. Confirm discovery under Settings → Cron Jobs and watch execution history under Observability → Cron Jobs. Per-run logs land under Observability → Logs.
Self-deployed hosts
Production builds register schedules as Nitro scheduled tasks. On Vercel, Nitro's Vercel preset wires those task registrations into Vercel Cron for you. Outside Vercel, the standard eve build && eve start path serves Nitro's Node output and starts Nitro's schedule runner, so the tasks fire on their cron cadence while that process is running.
The gotcha is custom hosting. If you adapt the generated output to a process manager, container platform, or Nitro preset that only serves HTTP and does not start Nitro's scheduled task runner, the schedule definitions still compile, but they will not fire automatically. In that case, run eve through eve start, use a host that supports Nitro scheduled tasks, or trigger the same work from your own scheduler through an authenticated route, channel handoff, or application-specific job runner. The dev dispatch route above is only for eve dev; production builds do not mount it.
What to read next
- Channels: deliver schedule output to users.
- Sessions, runs & streaming: inspect a schedule run.
- Dynamic scheduling: manage schedule rows in your own store behind one dispatcher schedule.