Dynamic Scheduling Patterns
Purpose and Scope
Dynamic scheduling is the pattern to use when users or tenants need to create, edit, pause, or delete future agent work at runtime. Authored eve schedules are static files discovered at build time, so they are a good fit for known cadences but not for user-managed reminders, tenant-specific recurring jobs, or ad hoc proactive tasks. The documented pattern keeps eve’s authored schedule model intact while moving the mutable schedule data into your application store. One static dispatcher schedule wakes frequently, claims due rows, and starts normal durable agent sessions for each claimed row.
Sources: docs/patterns/dynamic-scheduling.md
The important design choice is that eve does not become the database for user schedules. Your application owns rows, tenant policy, ownership, recurrence rules, and any UI used to manage them. eve supplies the durable runtime, the schedule handler entry point, and the channel handoff used to execute each due job. This keeps the pattern portable across PostgreSQL, a durable key-value store, or another storage service. The storage adapter can choose its own schema, but it must support an atomic lease so multiple dispatcher invocations do not run the same row twice.
Sources: docs/patterns/dynamic-scheduling.md
Relevant Source Files
- docs/patterns/dynamic-scheduling.md — First-party pattern documentation for composing one authored schedule, application-managed schedule rows, channel handoff, and CRUD tools into dynamic scheduling.
Core Primitives
A dynamic schedule implementation has four primitives. First, application-managed rows describe the tenant-owned work to run, including prompt text, channel target, owner identity, and retry or recurrence metadata. Second, tenant-scoped CRUD tools let the agent create, list, update, and delete those rows on behalf of the current user. Third, one authored eve schedule acts as a dispatcher and wakes every minute. Fourth, the schedule handler calls the channel receive function so each due row enters the same durable execution path as an inbound message.
Sources: docs/patterns/dynamic-scheduling.md
The dispatcher schedule uses a handler form rather than a fire-and-forget markdown prompt. In the documented example, the handler receives a cross-channel receive function and a wait-until helper. The wait-until call keeps the cron invocation alive while asynchronous claiming and channel handoff finish. The receive call starts a regular durable session, which means the proactive scheduled run can use the same agent instructions, tools, auth context, and channel behavior as a user-initiated conversation. That is the key reason the pattern composes with existing eve channel integrations rather than creating a parallel job runner.
Sources: docs/patterns/dynamic-scheduling.md
System-to-Code Mapping
The documented file layout separates authored eve files from application-owned helpers. The channel file configures the target channel, the schedule file is the single static dispatcher, the storage adapter hides database details, and the tools expose tenant-safe schedule management to the agent. The pattern intentionally avoids generating a new eve schedule file for every user request. Instead, every dynamic row is data, while the authored dispatcher remains stable and can be discovered during the normal eve build process.
Sources: docs/patterns/dynamic-scheduling.md
agent/
channels/slack.ts
lib/schedule-store.ts # your storage adapter
lib/tenant.ts
schedules/dynamic.ts
tools/create_schedule.ts
tools/delete_schedule.ts
tools/list_schedules.ts
tools/update_schedule.ts| Concern | Pattern responsibility |
|---|---|
| Authored schedule | Wake once per minute and dispatch due rows. |
| Schedule store | Claim due jobs atomically, complete successful jobs, and release failed jobs for retry. |
| Channel | Receive proactive work using a target such as a Slack channel identifier. |
| Tenant helper | Derive tenant and owner identity from the active session, not from model-supplied input. |
| CRUD tools | Let the agent manage rows for the current tenant while enforcing application ownership rules. |
Execution Flow
The normal flow begins when a user asks the agent to create or modify a schedule. A CRUD tool validates that the active session belongs to an authenticated tenant user, then writes or updates a row in the application store. One-time rows can use a null recurrence interval, while recurring rows can store an interval or another application-defined recurrence representation. The model can propose the requested task, but ownership and tenant identity come from the session context. This distinction prevents a prompt from assigning work to an arbitrary tenant or user.
Sources: docs/patterns/dynamic-scheduling.md
At runtime, the dispatcher wakes on the minute-level cadence and asks the storage adapter to claim a bounded batch of due rows. The example claims up to twenty-five jobs, records a lease lasting five minutes, and uses the current time when selecting due work. After claiming, it maps each job into a proactive channel receive call. The message includes an instruction to run the dynamic schedule, the tenant-owned task prompt, and the schedule identifier. Successful handoffs are completed in the store; failed handoffs are released with an error and a later retry time.
Sources: docs/patterns/dynamic-scheduling.md
Slack is used in the example because it supports a proactive target with a channel identifier, but the channel choice is not fundamental to the pattern. Any channel that implements receive can replace Slack if it can accept the target data needed to deliver a proactive session. That makes the dispatcher mostly channel-agnostic: the storage adapter supplies a channel target, the schedule handler passes that target to the chosen channel, and the agent runtime handles the work as an ordinary durable session once receive has accepted it.
Sources: docs/patterns/dynamic-scheduling.md
API and Configuration Details
The compact public API surface in this pattern is small but important. The dispatcher is authored with the schedules entry point and a cron expression. Its handler receives the primitives needed to hand work to a channel and keep the scheduled invocation alive. The channel is configured separately, as shown by the Slack example that uses Vercel Connect credentials and the Slack channel helper. The storage adapter is deliberately application-defined, so its names can vary, but the documented example exposes claim, complete, and release operations around a lease-oriented workflow.
Sources: docs/patterns/dynamic-scheduling.md
| Name | Role in the pattern |
|---|---|
| defineSchedule | Declares the static dispatcher schedule and its cron cadence. |
| cron | Uses a one-minute expression for frequent polling of application-managed rows. |
| run | Implements the dispatcher handler instead of a static markdown task. |
| waitUntil | Keeps asynchronous claiming and receive handoff alive after the handler returns. |
| receive | Starts the normal durable runtime through a channel. |
| claimDue | Application storage operation that leases due rows atomically. |
| complete | Application storage operation called after a successful handoff. |
| release | Application storage operation called after a failure, optionally with retry timing. |
export default defineSchedule({
cron: "* * * * *",
run({ receive, waitUntil }) {
waitUntil(dispatchDueRows(receive));
},
});Tenant Safety and Failure Handling
Tenant safety is central because dynamic scheduling lets an agent create future work. The documentation’s tenant helper reads the current session auth, checks that the principal is a user, extracts the tenant identifier from auth attributes, and returns the authenticated user identifier with the auth context. The model is not trusted to provide tenant or owner identity. Store operations should use those session-derived values when creating, listing, updating, or deleting rows, and the dispatcher should include the same tenant attributes when starting the proactive session.
Sources: docs/patterns/dynamic-scheduling.md
The atomic lease is the main reliability boundary. Without it, two dispatcher invocations could observe the same due row and start duplicate sessions. With a lease, a worker claims responsibility for a bounded time, then either completes the row or releases it for retry. The example uses a five-minute lease and schedules a retry five minutes after a failure. Applications can tune those numbers for their expected channel latency, job volume, and tolerance for duplicate notifications, but the store should preserve the claim, complete, and retry semantics.
Sources: docs/patterns/dynamic-scheduling.md
Next Steps
Use this pattern when schedules are product data rather than source files. Start by designing the schedule row and lease behavior in your application store, then add tenant-safe CRUD tools so the agent can manage only the current tenant’s rows. After that, add the single dispatcher schedule and connect it to the channel where proactive work should appear. For adjacent concepts, read the schedules reference for authored schedule semantics, the channel documentation for receive behavior, and multi-tenant authorization or approvals guidance when scheduled jobs can trigger sensitive tools.