/AYjnt/
All examples
schedulingalarms

Agent task scheduling

An agent that schedules one-shot work with `this.schedule()`. Relative delays (seconds from now), absolute times (ISO dates), and persistent state for both pending and fired reminders.

What you'll learn
  • How `this.schedule()` uses Cloudflare DO alarms to survive restarts
  • Difference between relative (seconds), absolute (Date), and unix-time (number) whens
  • How to cancel pending schedules with `this.cancelSchedule(id)`
01 step

Start from the blank scaffold

Every ayjnt example starts here. `bunx ayjnt new` drops a one-agent project with a single `alive` agent that responds "I'm alive" to any request — enough to prove the pipeline works before you replace it with the real thing.

~/my-agent-app
my-app/ (blank scaffold)
agent.ts
package.json
tsconfig.json
.gitignore
README.md
02 step

Add a reminder agent

Replace the starter `alive` agent with one that schedules reminders. Every `ReminderAgent` instance holds its own pending + fired queue.

~/my-agent-app
03 step

agents/reminder/agent.ts

The `fire` method is the scheduled callback. `this.schedule(when, "fire", payload)` tells the SDK to invoke `this.fire(payload)` at `when`. Cloudflare persists the alarm on the DO so it survives worker restarts.

agents/reminder/agent.ts ts
import { Agent } from "agents";
import type { GeneratedEnv } from "@ayjnt/env";

type Reminder = { id: string; text: string; due: number; firedAt?: number };

type State = { pending: Reminder[]; fired: Reminder[] };

export default class ReminderAgent extends Agent<GeneratedEnv, State> {
  override initialState: State = { pending: [], fired: [] };

  /** Scheduled callback. The name must match the second arg to schedule(). */
  async fire(reminder: Reminder): Promise<void> {
    this.setState({
      pending: this.state.pending.filter((r) => r.id !== reminder.id),
      fired: [...this.state.fired, { ...reminder, firedAt: Date.now() }],
    });
  }

  override async onRequest(request: Request): Promise<Response> {
    if (request.method !== "POST") {
      return Response.json({ instance: this.name, ...this.state });
    }
    const body = (await request.json()) as { text: string; in?: number; at?: string };
    const due = body.at ? new Date(body.at) : new Date(Date.now() + (body.in ?? 0) * 1000);

    const reminder: Reminder = { id: crypto.randomUUID(), text: body.text, due: due.getTime() };
    await this.schedule(due, "fire", reminder);   // ← persists an alarm on the DO

    this.setState({ ...this.state, pending: [...this.state.pending, reminder] });
    return Response.json({ ok: true, scheduled: reminder });
  }
}
04 step

Schedule three reminders, watch them fire

POSTing three reminders with different timings shows each arrive on schedule. The alarm persists even across worker restarts — tear down dev, bring it back, and the 6-second reminder still fires when it's due.

~/my-agent-app
05 step

What it looks like

Poll every second to watch reminders migrate from `pending` to `fired`. Running the provided `bun run client` script automates this.

state over time result
t=1s  pending: 3  fired: —
t=2s  pending: 2  fired: say hi
t=3s  pending: 2  fired: say hi
t=4s  pending: 1  fired: say hi, drink water
t=5s  pending: 1  fired: say hi, drink water
t=6s  pending: 0  fired: say hi, drink water, stretch

           each reminder
           └── one persisted Cloudflare alarm
               ↓ fires even if the worker was evicted
               ↓ callback mutates state via setState
               ↓ state visible on next GET
06 step

Deploy

`ayjnt deploy` checks your git tree is clean + synced with origin, regenerates the wrangler config from scratch, then shells out to `wrangler deploy`. The committed migrations.json file is the source of truth for what's in production.

~/my-agent-app