branch:
ScheduleDemo.tsx
11934 bytesRaw
import { useAgent } from "agents/react";
import type { Schedule } from "agents";
import { useState, useEffect, useCallback } from "react";
import { Button, Input, Surface, Text } from "@cloudflare/kumo";
import { DemoWrapper } from "../../layout";
import {
LogPanel,
ConnectionStatus,
CodeExplanation,
type CodeSection
} from "../../components";
import { useLogs, useUserId, useToast } from "../../hooks";
import type { ScheduleAgent, ScheduleAgentState } from "./schedule-agent";
const codeSections: CodeSection[] = [
{
title: "Schedule tasks from within an agent",
description:
"Use this.schedule() to fire a callback after a delay. The schedule is durable — if the Worker hibernates and wakes up, pending schedules still execute.",
code: `import { Agent, callable } from "agents";
class ScheduleAgent extends Agent<Env> {
@callable()
async scheduleTask(delaySeconds: number, message: string) {
const schedule = await this.schedule(
delaySeconds,
"onScheduledTask",
{ message }
);
return schedule.id;
}
async onScheduledTask(payload: { message: string }) {
console.log("Executed:", payload.message);
this.broadcast(JSON.stringify({
type: "schedule_executed",
payload,
}));
}
}`
},
{
title: "Recurring intervals",
description:
"Pass a callback name and payload — the agent will keep invoking it at the given interval. Use this.cancelSchedule(id) to stop it.",
code: ` @callable()
async scheduleRecurring(intervalSeconds: number, label: string) {
const schedule = await this.schedule(
intervalSeconds,
"onRecurringTask",
{ label, recurring: true }
);
return schedule.id;
}
@callable()
async cancelTask(id: string) {
return await this.cancelSchedule(id);
}
@callable()
listSchedules() {
return this.getSchedules();
}`
},
{
title: "React to schedule events on the client",
description:
"The agent broadcasts messages when schedules fire. Listen for them with the onMessage callback in useAgent.",
code: `const agent = useAgent({
agent: "schedule-agent",
name: "my-instance",
onMessage: (event) => {
const data = JSON.parse(event.data);
if (data.type === "schedule_executed") {
// a one-time schedule just fired
} else if (data.type === "recurring_executed") {
// an interval tick
}
},
});
// schedule a task from the client
const id = await agent.call("scheduleTask", [10, "hello"]);
// cancel it later
await agent.call("cancelTask", [id]);`
}
];
export function ScheduleDemo() {
const userId = useUserId();
const { logs, addLog, clearLogs } = useLogs();
const { toast } = useToast();
const [schedules, setSchedules] = useState<Schedule[]>([]);
const [delaySeconds, setDelaySeconds] = useState("5");
const [message, setMessage] = useState("Hello from schedule!");
const [intervalSeconds, setIntervalSeconds] = useState("10");
const [intervalLabel, setIntervalLabel] = useState("Recurring ping");
const agent = useAgent<ScheduleAgent, ScheduleAgentState>({
agent: "schedule-agent",
name: `schedule-demo-${userId}`,
onOpen: () => {
addLog("info", "connected");
refreshSchedules();
},
onClose: () => addLog("info", "disconnected"),
onError: () => addLog("error", "error", "Connection error"),
onMessage: (message: MessageEvent) => {
try {
const data = JSON.parse(message.data as string);
if (data.type === "schedule_executed") {
addLog("in", "schedule_executed", data.payload);
toast("Schedule fired!", "success");
refreshSchedules();
} else if (data.type === "recurring_executed") {
addLog("in", "recurring_executed", data.payload);
}
} catch {
// Not JSON or not our message type
}
}
});
const refreshSchedules = useCallback(async () => {
try {
const result = await agent.call("listSchedules");
setSchedules(result);
} catch {
// Ignore errors during refresh
}
}, [agent]);
useEffect(() => {
if (agent.readyState === WebSocket.OPEN) {
refreshSchedules();
}
}, [agent.readyState, refreshSchedules]);
const handleScheduleTask = async () => {
addLog("out", "scheduleTask", {
delaySeconds: Number(delaySeconds),
message
});
try {
const id = await agent.call("scheduleTask", [
Number(delaySeconds),
message
]);
addLog("in", "scheduled", { id });
toast("Task scheduled — fires in " + delaySeconds + "s", "info");
refreshSchedules();
} catch (e) {
addLog("error", "error", e instanceof Error ? e.message : String(e));
}
};
const handleScheduleRecurring = async () => {
addLog("out", "scheduleRecurring", {
intervalSeconds: Number(intervalSeconds),
label: intervalLabel
});
try {
const id = await agent.call("scheduleRecurring", [
Number(intervalSeconds),
intervalLabel
]);
addLog("in", "scheduled", { id });
toast("Recurring task started — every " + intervalSeconds + "s", "info");
refreshSchedules();
} catch (e) {
addLog("error", "error", e instanceof Error ? e.message : String(e));
}
};
const handleCancel = async (id: string) => {
addLog("out", "cancelTask", { id });
try {
const result = await agent.call("cancelTask", [id]);
addLog("in", "cancelled", { id, success: result });
refreshSchedules();
} catch (e) {
addLog("error", "error", e instanceof Error ? e.message : String(e));
}
};
const formatTime = (timestamp: number) => {
const date = new Date(timestamp * 1000);
return date.toLocaleString();
};
return (
<DemoWrapper
title="Scheduling"
description={
<>
Agents can schedule work for the future using{" "}
<code className="text-xs bg-kumo-fill px-1 py-0.5 rounded">
this.schedule()
</code>
. Schedule a one-time task with a delay in seconds, or set up a
recurring interval that fires repeatedly. Schedules are durable — they
persist across Worker restarts and hibernation, so a task scheduled
for an hour from now will still fire even if the Durable Object sleeps
in between. Try scheduling a 5-second task and watch the event log.
</>
}
statusIndicator={
<ConnectionStatus
status={
agent.readyState === WebSocket.OPEN ? "connected" : "connecting"
}
/>
}
>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Controls */}
<div className="space-y-6">
{/* One-time Task */}
<Surface className="p-4 rounded-lg ring ring-kumo-line">
<div className="mb-4">
<Text variant="heading3">One-time Task</Text>
</div>
<p className="text-sm text-kumo-subtle mb-3">
Schedule a task to run after a delay
</p>
<div className="space-y-2">
<div className="flex gap-2">
<Input
aria-label="Delay in seconds"
type="number"
value={delaySeconds}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setDelaySeconds(e.target.value)
}
className="w-20"
min={1}
/>
<span className="text-sm text-kumo-subtle self-center">
seconds
</span>
</div>
<Input
aria-label="Task message"
type="text"
value={message}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setMessage(e.target.value)
}
className="w-full"
placeholder="Message"
/>
<Button
variant="primary"
onClick={handleScheduleTask}
className="w-full"
>
Schedule Task
</Button>
</div>
</Surface>
{/* Recurring Task */}
<Surface className="p-4 rounded-lg ring ring-kumo-line">
<div className="mb-4">
<Text variant="heading3">Recurring Task</Text>
</div>
<p className="text-sm text-kumo-subtle mb-3">
Schedule a task to repeat at an interval
</p>
<div className="space-y-2">
<div className="flex gap-2">
<Input
aria-label="Interval in seconds"
type="number"
value={intervalSeconds}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setIntervalSeconds(e.target.value)
}
className="w-20"
min={5}
/>
<span className="text-sm text-kumo-subtle self-center">
second interval
</span>
</div>
<Input
aria-label="Recurring task label"
type="text"
value={intervalLabel}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setIntervalLabel(e.target.value)
}
className="w-full"
placeholder="Label"
/>
<Button
variant="primary"
onClick={handleScheduleRecurring}
className="w-full"
>
Schedule Recurring
</Button>
</div>
</Surface>
{/* Active Schedules */}
<Surface className="p-4 rounded-lg ring ring-kumo-line">
<div className="flex items-center justify-between mb-4">
<Text variant="heading3">
Active Schedules ({schedules.length})
</Text>
<Button variant="ghost" size="xs" onClick={refreshSchedules}>
Refresh
</Button>
</div>
{schedules.length === 0 ? (
<p className="text-sm text-kumo-inactive">No active schedules</p>
) : (
<div className="space-y-2">
{schedules.map((schedule) => (
<div
key={schedule.id}
className="flex items-center justify-between py-2 px-3 bg-kumo-elevated rounded text-sm"
>
<div>
<div className="font-medium text-kumo-default">
{schedule.callback}
</div>
<div className="text-xs text-kumo-subtle">
{schedule.type === "interval"
? `Every ${schedule.intervalSeconds}s`
: schedule.time
? `At ${formatTime(schedule.time)}`
: schedule.type}
</div>
</div>
<Button
variant="ghost"
size="xs"
onClick={() => handleCancel(schedule.id)}
className="text-kumo-danger"
>
Cancel
</Button>
</div>
))}
</div>
)}
</Surface>
</div>
{/* Logs */}
<div className="space-y-6">
<LogPanel logs={logs} onClear={clearLogs} maxHeight="400px" />
</div>
</div>
<CodeExplanation sections={codeSections} />
</DemoWrapper>
);
}