agents 0.16.2 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/README.md +11 -8
  2. package/dist/{agent-tool-types-CTw3UJUP.d.ts → agent-tool-types-Cd1TZPfB.d.ts} +587 -137
  3. package/dist/agent-tool-types.d.ts +34 -18
  4. package/dist/agent-tool-types.js +20 -1
  5. package/dist/agent-tool-types.js.map +1 -0
  6. package/dist/agent-tools-BXlsuX0d.js +304 -0
  7. package/dist/agent-tools-BXlsuX0d.js.map +1 -0
  8. package/dist/agent-tools-_E8wxUIK.d.ts +119 -0
  9. package/dist/agent-tools.d.ts +24 -18
  10. package/dist/agent-tools.js.map +1 -1
  11. package/dist/ai-chat-agent.d.ts +1 -1
  12. package/dist/ai-chat-agent.js +1 -2
  13. package/dist/ai-chat-agent.js.map +1 -1
  14. package/dist/ai-chat-v5-migration.d.ts +1 -1
  15. package/dist/ai-chat-v5-migration.js +1 -2
  16. package/dist/ai-chat-v5-migration.js.map +1 -1
  17. package/dist/ai-react.d.ts +1 -1
  18. package/dist/ai-react.js +1 -2
  19. package/dist/ai-react.js.map +1 -1
  20. package/dist/ai-types.d.ts +1 -7
  21. package/dist/ai-types.js +2 -8
  22. package/dist/ai-types.js.map +1 -1
  23. package/dist/browser/ai.js +1 -1
  24. package/dist/browser/index.js +1 -1
  25. package/dist/chat/index.d.ts +1918 -72
  26. package/dist/chat/index.js +1730 -245
  27. package/dist/chat/index.js.map +1 -1
  28. package/dist/chat/react.d.ts +602 -0
  29. package/dist/chat/react.js +1506 -0
  30. package/dist/chat/react.js.map +1 -0
  31. package/dist/chat-sdk/index.d.ts +4 -4
  32. package/dist/{classPrivateFieldGet2-CZ7QjTXN.js → classPrivateFieldGet2-DZBYAB34.js} +5 -5
  33. package/dist/{classPrivateMethodInitSpec-D-0__zd9.js → classPrivateMethodInitSpec-qMjJ6sHQ.js} +2 -2
  34. package/dist/{client-BXJ9n2f7.js → client-BZ-B3NhC.js} +2 -2
  35. package/dist/client-BZ-B3NhC.js.map +1 -0
  36. package/dist/client-tools-aIBO0Fk7.d.ts +53 -0
  37. package/dist/client.d.ts +76 -57
  38. package/dist/client.js +33 -5
  39. package/dist/client.js.map +1 -1
  40. package/dist/{connector-CrKhowfD.js → connector-CdldGF3h.js} +5 -5
  41. package/dist/{connector-CrKhowfD.js.map → connector-CdldGF3h.js.map} +1 -1
  42. package/dist/email.js +1 -1
  43. package/dist/email.js.map +1 -1
  44. package/dist/{index-B7IbEeze.d.ts → index-CcbnKkNh.d.ts} +188 -14
  45. package/dist/index.d.ts +91 -71
  46. package/dist/index.js +562 -24
  47. package/dist/index.js.map +1 -1
  48. package/dist/mcp/client.d.ts +18 -14
  49. package/dist/mcp/client.js +1 -1
  50. package/dist/mcp/index.d.ts +30 -30
  51. package/dist/mcp/index.js +7 -7
  52. package/dist/mcp/index.js.map +1 -1
  53. package/dist/{agent-tools-3zLG7MgA.js → message-builder-BymO4N_D.js} +45 -126
  54. package/dist/message-builder-BymO4N_D.js.map +1 -0
  55. package/dist/observability/index.d.ts +1 -1
  56. package/dist/observability/index.js +4 -2
  57. package/dist/observability/index.js.map +1 -1
  58. package/dist/react.d.ts +123 -110
  59. package/dist/react.js +44 -11
  60. package/dist/react.js.map +1 -1
  61. package/dist/serializable.d.ts +1 -1
  62. package/dist/skills/compile.js +1 -1
  63. package/dist/skills/compile.js.map +1 -1
  64. package/dist/skills/index.js +5 -5
  65. package/dist/skills/index.js.map +1 -1
  66. package/dist/sub-routing.d.ts +6 -6
  67. package/dist/utils.js +1 -1
  68. package/dist/utils.js.map +1 -1
  69. package/dist/vite.js +5 -5
  70. package/dist/vite.js.map +1 -1
  71. package/dist/wire-types-nflOzNuU.js +240 -0
  72. package/dist/wire-types-nflOzNuU.js.map +1 -0
  73. package/dist/{workflow-types-SrZK_o9p.d.ts → workflow-types-Baz_PO5v.d.ts} +42 -22
  74. package/dist/workflow-types.d.ts +25 -21
  75. package/dist/workflow-types.js.map +1 -1
  76. package/dist/workflows.d.ts +22 -21
  77. package/dist/workflows.js +31 -7
  78. package/dist/workflows.js.map +1 -1
  79. package/docs/adding-to-existing-project.md +450 -0
  80. package/docs/agent-class.md +503 -0
  81. package/docs/agent-tools.md +552 -0
  82. package/docs/browse-the-web.md +430 -0
  83. package/docs/callable-methods.md +627 -0
  84. package/docs/chat-agents.md +1687 -0
  85. package/docs/chat-sdk.md +181 -0
  86. package/docs/client-sdk.md +520 -0
  87. package/docs/client-tools-continuation.md +177 -0
  88. package/docs/codemode.md +440 -0
  89. package/docs/configuration.md +775 -0
  90. package/docs/cross-domain-authentication.md +171 -0
  91. package/docs/durable-execution.md +537 -0
  92. package/docs/email.md +663 -0
  93. package/docs/get-current-agent.md +204 -0
  94. package/docs/getting-started.md +305 -0
  95. package/docs/http-websockets.md +668 -0
  96. package/docs/human-in-the-loop.md +661 -0
  97. package/docs/index.md +151 -0
  98. package/docs/long-running-agents.md +730 -0
  99. package/docs/mcp-client.md +620 -0
  100. package/docs/mcp-servers.md +526 -0
  101. package/docs/mcp-transports.md +308 -0
  102. package/docs/migration-to-ai-sdk-v5.md +96 -0
  103. package/docs/migration-to-ai-sdk-v6.md +163 -0
  104. package/docs/observability.md +261 -0
  105. package/docs/push-notifications.md +367 -0
  106. package/docs/queue.md +329 -0
  107. package/docs/readonly-connections.md +278 -0
  108. package/docs/resumable-streaming.md +127 -0
  109. package/docs/retries.md +444 -0
  110. package/docs/routing.md +749 -0
  111. package/docs/scheduling.md +898 -0
  112. package/docs/securing-mcp-servers.md +359 -0
  113. package/docs/server-driven-messages.md +477 -0
  114. package/docs/sessions.md +1024 -0
  115. package/docs/state.md +512 -0
  116. package/docs/sub-agents.md +389 -0
  117. package/docs/webhooks.md +604 -0
  118. package/docs/workflows.md +877 -0
  119. package/package.json +41 -14
  120. package/dist/agent-tools-3zLG7MgA.js.map +0 -1
  121. package/dist/agent-tools-DZhI5F6Q.d.ts +0 -14
  122. package/dist/client-BXJ9n2f7.js.map +0 -1
@@ -0,0 +1,898 @@
1
+ # Scheduling
2
+
3
+ Schedule tasks to run in the future — whether that's seconds from now, at a specific date/time, or on a recurring cron schedule. Scheduled tasks survive agent restarts and are persisted to SQLite.
4
+
5
+ ## Overview
6
+
7
+ The scheduling system supports four modes:
8
+
9
+ | Mode | Syntax | Use Case |
10
+ | ------------- | ----------------------------------- | ------------------------- |
11
+ | **Delayed** | `this.schedule(60, ...)` | Run in 60 seconds |
12
+ | **Scheduled** | `this.schedule(new Date(...), ...)` | Run at specific time |
13
+ | **Cron** | `this.schedule("0 8 * * *", ...)` | Run on recurring schedule |
14
+ | **Interval** | `this.scheduleEvery(30, ...)` | Run every 30 seconds |
15
+
16
+ Under the hood, scheduling uses [Durable Object alarms](https://developers.cloudflare.com/durable-objects/api/alarms/) to wake the agent at the right time. Tasks are stored in a SQLite table and executed in order.
17
+
18
+ ## Quick Start
19
+
20
+ ```typescript
21
+ import { Agent } from "agents";
22
+
23
+ export class ReminderAgent extends Agent {
24
+ async onRequest(request: Request) {
25
+ const url = new URL(request.url);
26
+
27
+ // Schedule in 30 seconds
28
+ await this.schedule(30, "sendReminder", {
29
+ message: "Check your email"
30
+ });
31
+
32
+ // Schedule at specific time
33
+ await this.schedule(new Date("2025-02-01T09:00:00Z"), "sendReminder", {
34
+ message: "Monthly report due"
35
+ });
36
+
37
+ // Schedule recurring (every day at 8am)
38
+ await this.schedule("0 8 * * *", "dailyDigest", {
39
+ userId: url.searchParams.get("userId")
40
+ });
41
+
42
+ return new Response("Scheduled!");
43
+ }
44
+
45
+ async sendReminder(payload: { message: string }) {
46
+ console.log(`Reminder: ${payload.message}`);
47
+ // Send notification, email, etc.
48
+ }
49
+
50
+ async dailyDigest(payload: { userId: string }) {
51
+ console.log(`Sending daily digest to ${payload.userId}`);
52
+ // Generate and send digest
53
+ }
54
+ }
55
+ ```
56
+
57
+ ## Scheduling Modes
58
+
59
+ ### Delayed Execution
60
+
61
+ Pass a number to schedule a task to run after a delay in **seconds**:
62
+
63
+ ```typescript
64
+ // Run in 10 seconds
65
+ await this.schedule(10, "processTask", { taskId: "123" });
66
+
67
+ // Run in 5 minutes (300 seconds)
68
+ await this.schedule(300, "sendFollowUp", { email: "user@example.com" });
69
+
70
+ // Run in 1 hour
71
+ await this.schedule(3600, "checkStatus", { orderId: "abc" });
72
+ ```
73
+
74
+ **Use cases:**
75
+
76
+ - Debouncing rapid events
77
+ - Delayed notifications ("You left items in your cart")
78
+ - Retry with backoff
79
+ - Rate limiting
80
+
81
+ ### Scheduled Execution
82
+
83
+ Pass a `Date` object to schedule a task at a specific time:
84
+
85
+ ```typescript
86
+ // Run tomorrow at noon
87
+ const tomorrow = new Date();
88
+ tomorrow.setDate(tomorrow.getDate() + 1);
89
+ tomorrow.setHours(12, 0, 0, 0);
90
+ await this.schedule(tomorrow, "sendReminder", { message: "Meeting time!" });
91
+
92
+ // Run at a specific timestamp
93
+ await this.schedule(new Date("2025-06-15T14:30:00Z"), "triggerEvent", {
94
+ eventId: "conference-2025"
95
+ });
96
+
97
+ // Run in 2 hours using Date math
98
+ const twoHoursFromNow = new Date(Date.now() + 2 * 60 * 60 * 1000);
99
+ await this.schedule(twoHoursFromNow, "checkIn", {});
100
+ ```
101
+
102
+ **Use cases:**
103
+
104
+ - Appointment reminders
105
+ - Deadline notifications
106
+ - Scheduled content publishing
107
+ - Time-based triggers
108
+
109
+ ### Recurring (Cron)
110
+
111
+ Pass a cron expression string for recurring schedules:
112
+
113
+ ```typescript
114
+ // Every day at 8:00 AM
115
+ await this.schedule("0 8 * * *", "dailyReport", {});
116
+
117
+ // Every hour
118
+ await this.schedule("0 * * * *", "hourlyCheck", {});
119
+
120
+ // Every Monday at 9:00 AM
121
+ await this.schedule("0 9 * * 1", "weeklySync", {});
122
+
123
+ // Every 15 minutes
124
+ await this.schedule("*/15 * * * *", "pollForUpdates", {});
125
+
126
+ // First day of every month at midnight
127
+ await this.schedule("0 0 1 * *", "monthlyCleanup", {});
128
+ ```
129
+
130
+ **Cron syntax:** `minute hour day month weekday`
131
+
132
+ | Field | Values | Special Characters |
133
+ | ------------ | -------------- | ------------------ |
134
+ | Minute | 0-59 | `*` `,` `-` `/` |
135
+ | Hour | 0-23 | `*` `,` `-` `/` |
136
+ | Day of Month | 1-31 | `*` `,` `-` `/` |
137
+ | Month | 1-12 | `*` `,` `-` `/` |
138
+ | Day of Week | 0-6 (0=Sunday) | `*` `,` `-` `/` |
139
+
140
+ **Common patterns:**
141
+
142
+ ```typescript
143
+ "* * * * *"; // Every minute
144
+ "*/5 * * * *"; // Every 5 minutes
145
+ "0 * * * *"; // Every hour (on the hour)
146
+ "0 0 * * *"; // Every day at midnight
147
+ "0 8 * * 1-5"; // Weekdays at 8am
148
+ "0 0 * * 0"; // Every Sunday at midnight
149
+ "0 0 1 * *"; // First of every month
150
+ ```
151
+
152
+ **Use cases:**
153
+
154
+ - Daily/weekly reports
155
+ - Periodic cleanup jobs
156
+ - Polling external services
157
+ - Health checks
158
+ - Subscription renewals
159
+
160
+ ### Interval
161
+
162
+ Use `scheduleEvery()` to run a task at fixed intervals (in seconds). Unlike cron, intervals support sub-minute precision and arbitrary durations:
163
+
164
+ ```typescript
165
+ // Poll every 30 seconds
166
+ await this.scheduleEvery(30, "poll", { source: "api" });
167
+
168
+ // Health check every 45 seconds
169
+ await this.scheduleEvery(45, "healthCheck", {});
170
+
171
+ // Sync every 90 seconds (1.5 minutes - can't be expressed in cron)
172
+ await this.scheduleEvery(90, "syncData", { destination: "warehouse" });
173
+ ```
174
+
175
+ **Idempotency:**
176
+
177
+ `scheduleEvery()` is idempotent on the combination of callback name, interval, and payload — calling it multiple times with the same arguments does not create duplicate schedules. This makes it safe to call in `onStart()`, which runs on every Durable Object wake:
178
+
179
+ ```typescript
180
+ class MyAgent extends Agent {
181
+ async onStart() {
182
+ // Safe: only one schedule is created, no matter how many times the DO wakes
183
+ await this.scheduleEvery(30, "tick");
184
+ }
185
+
186
+ async tick() {
187
+ console.log("tick", new Date().toISOString());
188
+ }
189
+ }
190
+ ```
191
+
192
+ Calling `scheduleEvery()` with a different interval or payload creates a separate schedule, even for the same callback:
193
+
194
+ ```typescript
195
+ // First call creates one schedule
196
+ await this.scheduleEvery(30, "poll");
197
+
198
+ // Second call with a different interval creates a second schedule
199
+ await this.scheduleEvery(60, "poll");
200
+ // Two "poll" schedules exist: one every 30s and one every 60s
201
+
202
+ // Third call with the same arguments as the first is a no-op
203
+ await this.scheduleEvery(30, "poll");
204
+ // Still two schedules
205
+ ```
206
+
207
+ Different callbacks also get their own independent schedules:
208
+
209
+ ```typescript
210
+ // These create two separate schedules (different callbacks)
211
+ await this.scheduleEvery(30, "poll");
212
+ await this.scheduleEvery(30, "healthCheck");
213
+ ```
214
+
215
+ **Key differences from cron:**
216
+
217
+ | Feature | Cron | Interval |
218
+ | ------------------- | ------------------------------ | ---------------------- |
219
+ | Minimum granularity | 1 minute | 1 second |
220
+ | Arbitrary intervals | No (must fit cron pattern) | Yes |
221
+ | Fixed schedule | Yes (e.g., "every day at 8am") | No (relative to start) |
222
+ | Overlap prevention | No | Yes (built-in) |
223
+
224
+ **Overlap prevention:**
225
+
226
+ If a callback takes longer than the interval, the next execution is skipped (not queued). This prevents runaway resource usage:
227
+
228
+ ```typescript
229
+ class PollingAgent extends Agent {
230
+ async poll() {
231
+ // If this takes 45 seconds and interval is 30 seconds,
232
+ // the next poll is skipped (with a warning logged)
233
+ const data = await slowExternalApi();
234
+ await this.processData(data);
235
+ }
236
+ }
237
+
238
+ // Set up 30-second interval
239
+ await this.scheduleEvery(30, "poll", {});
240
+ ```
241
+
242
+ When a skip occurs, you'll see a warning in logs:
243
+
244
+ ```
245
+ Skipping interval schedule abc123: previous execution still running
246
+ ```
247
+
248
+ **Error resilience:**
249
+
250
+ If the callback throws an error, the interval continues — only that execution fails:
251
+
252
+ ```typescript
253
+ async syncData() {
254
+ // Even if this throws, the interval keeps running
255
+ const response = await fetch("https://api.example.com/data");
256
+ if (!response.ok) throw new Error("Sync failed");
257
+ // ...
258
+ }
259
+ ```
260
+
261
+ **Use cases:**
262
+
263
+ - Sub-minute polling (every 10, 30, 45 seconds)
264
+ - Intervals that don't map to cron (every 90 seconds, every 7 minutes)
265
+ - Rate-limited API polling with precise control
266
+ - Real-time data synchronization
267
+
268
+ ## Keeping the Agent Alive
269
+
270
+ Durable Objects are evicted after a period of inactivity (typically 70-140 seconds with no incoming requests, WebSocket messages, or alarms). During long-running operations — streaming LLM responses, waiting on external APIs, running multi-step computations — the agent can be evicted mid-flight.
271
+
272
+ `keepAlive()` prevents this by holding an alarm-backed heartbeat ref that keeps the agent active until you are done:
273
+
274
+ ```typescript
275
+ const dispose = await this.keepAlive();
276
+ try {
277
+ // Long-running work that must not be interrupted
278
+ const result = await longRunningComputation();
279
+ await sendResults(result);
280
+ } finally {
281
+ dispose();
282
+ }
283
+ ```
284
+
285
+ The returned disposer function cancels the heartbeat. Always call it when the work is done — otherwise the heartbeat continues indefinitely.
286
+
287
+ ### keepAliveWhile()
288
+
289
+ For scoped work, use `keepAliveWhile()` — it runs an async function and automatically cleans up the heartbeat when it completes (or throws):
290
+
291
+ ```typescript
292
+ const result = await this.keepAliveWhile(async () => {
293
+ const data = await longRunningComputation();
294
+ return data;
295
+ });
296
+ ```
297
+
298
+ This is the recommended approach since you cannot forget to dispose the heartbeat.
299
+
300
+ ### How it works
301
+
302
+ `keepAlive()` uses an in-memory reference count and the Durable Object alarm system directly. Each call increments the count; the disposer decrements it. While the count is above zero, `_scheduleNextAlarm()` ensures an alarm fires every 30 seconds, which resets the inactivity timer. No schedule rows are created and no observability events are emitted — the heartbeat is invisible to `listSchedules()` and the `agents:schedule` diagnostics channel.
303
+
304
+ The heartbeat does not conflict with your own schedules — the alarm system multiplexes all schedules and the keepAlive heartbeat through a single alarm slot.
305
+
306
+ Inside sub-agents, `keepAlive()` delegates that heartbeat ref to the top-level parent because facets do not have independent alarm slots. `keepAliveWhile()` works the same way because it calls `keepAlive()` and automatically disposes the delegated ref when the scoped work completes.
307
+
308
+ ### Multiple concurrent callers
309
+
310
+ Each `keepAlive()` call returns an independent disposer:
311
+
312
+ ```typescript
313
+ const dispose1 = await this.keepAlive();
314
+ const dispose2 = await this.keepAlive();
315
+
316
+ // Both heartbeats are active (ref count = 2)
317
+ dispose1(); // Decrements ref count to 1
318
+ // Agent is still alive via dispose2's ref
319
+
320
+ dispose2(); // Ref count reaches 0 — agent can go idle
321
+ ```
322
+
323
+ ### AIChatAgent
324
+
325
+ `AIChatAgent` automatically calls `keepAlive()` during streaming responses. You do not need to add it yourself when using `AIChatAgent` — every LLM stream is protected from idle eviction by default.
326
+
327
+ ### When to use keepAlive()
328
+
329
+ | Scenario | Use keepAlive()? |
330
+ | ------------------------------------------- | -------------------------------------- |
331
+ | Streaming LLM responses via `AIChatAgent` | No — already built in |
332
+ | Long-running computation in a custom Agent | Yes |
333
+ | Waiting on a slow external API call | Yes |
334
+ | Multi-step tool execution | Yes |
335
+ | Short request-response handlers | No — not needed |
336
+ | Background work via scheduling or workflows | No — alarms already keep the DO active |
337
+
338
+ ## Managing Schedules
339
+
340
+ ### Get a Schedule
341
+
342
+ Retrieve a scheduled task by its ID:
343
+
344
+ ```typescript
345
+ const schedule = await this.getScheduleById(scheduleId);
346
+
347
+ if (schedule) {
348
+ console.log(
349
+ `Task ${schedule.id} will run at ${new Date(schedule.time * 1000)}`
350
+ );
351
+ console.log(`Callback: ${schedule.callback}`);
352
+ console.log(`Type: ${schedule.type}`); // "scheduled" | "delayed" | "cron" | "interval"
353
+ } else {
354
+ console.log("Schedule not found");
355
+ }
356
+ ```
357
+
358
+ ### List Schedules
359
+
360
+ Query scheduled tasks with optional filters:
361
+
362
+ ```typescript
363
+ // Get all scheduled tasks
364
+ const allSchedules = await this.listSchedules();
365
+
366
+ // Get only cron jobs
367
+ const cronJobs = await this.listSchedules({ type: "cron" });
368
+
369
+ // Get tasks in the next hour
370
+ const upcoming = await this.listSchedules({
371
+ timeRange: {
372
+ start: new Date(),
373
+ end: new Date(Date.now() + 60 * 60 * 1000)
374
+ }
375
+ });
376
+
377
+ // Get a specific task by ID
378
+ const specific = await this.listSchedules({ id: "abc123" });
379
+
380
+ // Combine filters
381
+ const upcomingCronJobs = await this.listSchedules({
382
+ type: "cron",
383
+ timeRange: {
384
+ start: new Date(),
385
+ end: new Date(Date.now() + 24 * 60 * 60 * 1000)
386
+ }
387
+ });
388
+ ```
389
+
390
+ ### Cancel a Schedule
391
+
392
+ Remove a scheduled task before it executes:
393
+
394
+ ```typescript
395
+ const cancelled = await this.cancelSchedule(scheduleId);
396
+
397
+ if (cancelled) {
398
+ console.log("Schedule cancelled successfully");
399
+ } else {
400
+ console.log("Schedule not found (may have already executed)");
401
+ }
402
+ ```
403
+
404
+ `cancelSchedule(id)` only matches schedules owned by the agent it is called on. A top-level agent cannot cancel a sub-agent's schedules by id, and a sub-agent cannot reach a sibling's schedules. To clear every schedule under a sub-agent (and any of its descendants), call `parent.deleteSubAgent(Cls, name)` from the parent — that bulk-cancels the prefix and tears the sub-agent down.
405
+
406
+ **Example: Cancellable reminders**
407
+
408
+ ```typescript
409
+ class ReminderAgent extends Agent {
410
+ async setReminder(userId: string, message: string, delaySeconds: number) {
411
+ const schedule = await this.schedule(delaySeconds, "sendReminder", {
412
+ userId,
413
+ message
414
+ });
415
+
416
+ // Store the schedule ID so user can cancel later
417
+ this.sql`
418
+ INSERT INTO user_reminders (user_id, schedule_id, message)
419
+ VALUES (${userId}, ${schedule.id}, ${message})
420
+ `;
421
+
422
+ return schedule.id;
423
+ }
424
+
425
+ async cancelReminder(scheduleId: string) {
426
+ const cancelled = await this.cancelSchedule(scheduleId);
427
+
428
+ if (cancelled) {
429
+ this.sql`DELETE FROM user_reminders WHERE schedule_id = ${scheduleId}`;
430
+ }
431
+
432
+ return cancelled;
433
+ }
434
+
435
+ async sendReminder(payload: { userId: string; message: string }) {
436
+ // Send the reminder...
437
+
438
+ // Clean up the record
439
+ this.sql`DELETE FROM user_reminders WHERE user_id = ${payload.userId}`;
440
+ }
441
+ }
442
+ ```
443
+
444
+ ## The Schedule Object
445
+
446
+ When you create or retrieve a schedule, you get a `Schedule` object:
447
+
448
+ ```typescript
449
+ type Schedule<T = string> = {
450
+ id: string; // Unique identifier
451
+ callback: string; // Method name to call
452
+ payload: T; // Data passed to the callback
453
+ retry?: RetryOptions; // Retry options (if configured)
454
+ time: number; // Unix timestamp (seconds) of next execution
455
+ } & (
456
+ | { type: "scheduled" } // One-time at specific date
457
+ | { type: "delayed"; delayInSeconds: number } // One-time after delay
458
+ | { type: "cron"; cron: string } // Recurring (cron expression)
459
+ | { type: "interval"; intervalSeconds: number } // Recurring (fixed interval)
460
+ );
461
+ ```
462
+
463
+ **Example:**
464
+
465
+ ```typescript
466
+ const schedule = await this.schedule(
467
+ 60,
468
+ "myTask",
469
+ { foo: "bar" },
470
+ { retry: { maxAttempts: 5 } }
471
+ );
472
+
473
+ console.log(schedule);
474
+ // {
475
+ // id: "abc123xyz",
476
+ // callback: "myTask",
477
+ // payload: { foo: "bar" },
478
+ // retry: { maxAttempts: 5 },
479
+ // time: 1706745600,
480
+ // type: "delayed",
481
+ // delayInSeconds: 60
482
+ // }
483
+ ```
484
+
485
+ ## Patterns
486
+
487
+ ### Rescheduling from Callbacks
488
+
489
+ For dynamic recurring schedules, schedule the next run from within the callback:
490
+
491
+ ```typescript
492
+ class PollingAgent extends Agent {
493
+ async startPolling(intervalSeconds: number) {
494
+ await this.schedule(intervalSeconds, "poll", { interval: intervalSeconds });
495
+ }
496
+
497
+ async poll(payload: { interval: number }) {
498
+ try {
499
+ const data = await fetch("https://api.example.com/updates");
500
+ await this.processUpdates(await data.json());
501
+ } catch (error) {
502
+ console.error("Polling failed:", error);
503
+ }
504
+
505
+ // Schedule the next poll (regardless of success/failure)
506
+ await this.schedule(payload.interval, "poll", payload);
507
+ }
508
+
509
+ async stopPolling() {
510
+ // Cancel all polling schedules
511
+ const schedules = await this.listSchedules({ type: "delayed" });
512
+ for (const schedule of schedules) {
513
+ if (schedule.callback === "poll") {
514
+ await this.cancelSchedule(schedule.id);
515
+ }
516
+ }
517
+ }
518
+ }
519
+ ```
520
+
521
+ ### Retry on Failure
522
+
523
+ For immediate retries (within seconds), use the built-in retry option:
524
+
525
+ ```typescript
526
+ // Retry up to 5 times with exponential backoff
527
+ await this.schedule(
528
+ 60,
529
+ "processTask",
530
+ { taskId: "123" },
531
+ {
532
+ retry: { maxAttempts: 5 }
533
+ }
534
+ );
535
+ ```
536
+
537
+ For longer recovery windows (minutes or hours), combine `this.retry()` for immediate retries with scheduled retries for extended outages:
538
+
539
+ ```typescript
540
+ class RetryAgent extends Agent {
541
+ async attemptTask(payload: {
542
+ taskId: string;
543
+ attempt: number;
544
+ maxAttempts: number;
545
+ }) {
546
+ try {
547
+ // Immediate retries for transient failures
548
+ await this.retry(() => this.doWork(payload.taskId), {
549
+ maxAttempts: 3
550
+ });
551
+ console.log(
552
+ `Task ${payload.taskId} succeeded on attempt ${payload.attempt}`
553
+ );
554
+ } catch (error) {
555
+ if (payload.attempt >= payload.maxAttempts) {
556
+ console.error(
557
+ `Task ${payload.taskId} failed after ${payload.maxAttempts} attempts`
558
+ );
559
+ return;
560
+ }
561
+
562
+ // Schedule a retry in the future for longer outages
563
+ const delaySeconds = Math.pow(2, payload.attempt) * 60;
564
+
565
+ await this.schedule(delaySeconds, "attemptTask", {
566
+ ...payload,
567
+ attempt: payload.attempt + 1
568
+ });
569
+
570
+ console.log(`Scheduled retry in ${delaySeconds}s`);
571
+ }
572
+ }
573
+
574
+ async doWork(taskId: string) {
575
+ // Your actual work here
576
+ }
577
+ }
578
+ ```
579
+
580
+ See [Retries](./retries.md) for full documentation on retry options and patterns.
581
+
582
+ ### Self-Destructing Agents
583
+
584
+ You can safely call `this.destroy()` from within a scheduled callback:
585
+
586
+ ```typescript
587
+ class TemporaryAgent extends Agent {
588
+ async onStart() {
589
+ // Self-destruct in 24 hours
590
+ await this.schedule(24 * 60 * 60, "cleanup", {});
591
+ }
592
+
593
+ async cleanup() {
594
+ // Perform final cleanup
595
+ console.log("Agent lifetime expired, cleaning up...");
596
+
597
+ // This is safe to call from a scheduled callback
598
+ await this.destroy();
599
+ }
600
+ }
601
+ ```
602
+
603
+ ### Timezone-Aware Scheduling
604
+
605
+ JavaScript Dates are UTC by default. For timezone-aware scheduling:
606
+
607
+ ```typescript
608
+ class TimezoneAgent extends Agent {
609
+ async scheduleForTimezone(
610
+ hour: number,
611
+ minute: number,
612
+ timezone: string,
613
+ callback: keyof this
614
+ ) {
615
+ // Create a date in the target timezone
616
+ const now = new Date();
617
+ const formatter = new Intl.DateTimeFormat("en-US", {
618
+ timeZone: timezone,
619
+ year: "numeric",
620
+ month: "2-digit",
621
+ day: "2-digit",
622
+ hour: "2-digit",
623
+ minute: "2-digit",
624
+ hour12: false
625
+ });
626
+
627
+ // Parse and construct target time
628
+ const targetDate = new Date(
629
+ now.toLocaleString("en-US", { timeZone: timezone })
630
+ );
631
+ targetDate.setHours(hour, minute, 0, 0);
632
+
633
+ // If time already passed today, schedule for tomorrow
634
+ if (targetDate <= now) {
635
+ targetDate.setDate(targetDate.getDate() + 1);
636
+ }
637
+
638
+ return this.schedule(targetDate, callback, { timezone });
639
+ }
640
+ }
641
+ ```
642
+
643
+ ## AI-Assisted Scheduling
644
+
645
+ The SDK includes utilities for parsing natural language scheduling requests with AI.
646
+
647
+ ### getSchedulePrompt()
648
+
649
+ Returns a system prompt for parsing natural language into scheduling parameters:
650
+
651
+ ```typescript
652
+ import { getSchedulePrompt, scheduleSchema } from "agents";
653
+ import { generateObject } from "ai";
654
+ import { openai } from "@ai-sdk/openai";
655
+
656
+ class SmartScheduler extends Agent {
657
+ async parseScheduleRequest(userInput: string) {
658
+ const result = await generateObject({
659
+ model: openai("gpt-4o"),
660
+ system: getSchedulePrompt({ date: new Date() }),
661
+ prompt: userInput,
662
+ schema: scheduleSchema
663
+ });
664
+
665
+ return result.object;
666
+ }
667
+
668
+ async handleUserRequest(input: string) {
669
+ // Parse: "remind me to call mom tomorrow at 3pm"
670
+ const parsed = await this.parseScheduleRequest(input);
671
+
672
+ // parsed = {
673
+ // description: "call mom",
674
+ // when: {
675
+ // type: "scheduled",
676
+ // date: "2025-01-30T15:00:00Z"
677
+ // }
678
+ // }
679
+
680
+ if (parsed.when.type === "scheduled" && parsed.when.date) {
681
+ await this.schedule(new Date(parsed.when.date), "sendReminder", {
682
+ message: parsed.description
683
+ });
684
+ } else if (parsed.when.type === "delayed" && parsed.when.delayInSeconds) {
685
+ await this.schedule(parsed.when.delayInSeconds, "sendReminder", {
686
+ message: parsed.description
687
+ });
688
+ } else if (parsed.when.type === "cron" && parsed.when.cron) {
689
+ await this.schedule(parsed.when.cron, "sendReminder", {
690
+ message: parsed.description
691
+ });
692
+ }
693
+ }
694
+
695
+ async sendReminder(payload: { message: string }) {
696
+ console.log(`Reminder: ${payload.message}`);
697
+ }
698
+ }
699
+ ```
700
+
701
+ ### scheduleSchema
702
+
703
+ A Zod schema for validating parsed scheduling data:
704
+
705
+ ```typescript
706
+ import { scheduleSchema } from "agents";
707
+
708
+ // The schema uses a discriminated union on `when.type`:
709
+ // {
710
+ // description: string,
711
+ // when:
712
+ // | { type: "scheduled", date: string } // ISO 8601 date string
713
+ // | { type: "delayed", delayInSeconds: number }
714
+ // | { type: "cron", cron: string }
715
+ // | { type: "no-schedule" }
716
+ // }
717
+ ```
718
+
719
+ When using this schema with OpenAI models via the AI SDK, you must pass `providerOptions: { openai: { strictJsonSchema: false } }` to `generateObject`. This is because the schema uses a discriminated union which is not compatible with OpenAI's strict structured outputs mode.
720
+
721
+ ## Scheduling vs Queue vs Workflows
722
+
723
+ | Feature | Queue | Scheduling | Workflows |
724
+ | ------------------ | ------------------ | ----------------- | ------------------- |
725
+ | **When** | Immediately (FIFO) | Future time | Future time |
726
+ | **Execution** | Sequential | At scheduled time | Multi-step |
727
+ | **Retries** | Automatic | Automatic | Automatic |
728
+ | **Persistence** | SQLite | SQLite | Workflow engine |
729
+ | **Recurring** | No | Yes (cron) | No (use scheduling) |
730
+ | **Complex logic** | No | No | Yes |
731
+ | **Human approval** | No | No | Yes |
732
+
733
+ **Use Queue when:**
734
+
735
+ - You need background processing without blocking the response
736
+ - Tasks should run ASAP but don't need to block
737
+ - Order matters (FIFO)
738
+
739
+ **Use Scheduling when:**
740
+
741
+ - Tasks need to run at a specific time
742
+ - You need recurring jobs (cron)
743
+ - Delayed execution (debouncing, retries)
744
+
745
+ **Use Workflows when:**
746
+
747
+ - Multi-step processes with dependencies
748
+ - Automatic retries with backoff
749
+ - Human-in-the-loop approvals
750
+ - Long-running tasks (minutes to hours)
751
+
752
+ ## API Reference
753
+
754
+ ### schedule()
755
+
756
+ ```typescript
757
+ async schedule<T = string>(
758
+ when: Date | string | number,
759
+ callback: keyof this,
760
+ payload?: T,
761
+ options?: { retry?: RetryOptions; idempotent?: boolean }
762
+ ): Promise<Schedule<T>>
763
+ ```
764
+
765
+ Schedule a task for future execution.
766
+
767
+ **Parameters:**
768
+
769
+ - `when` - When to execute: `number` (seconds delay), `Date` (specific time), or `string` (cron expression)
770
+ - `callback` - Name of the method to call
771
+ - `payload` - Data to pass to the callback (must be JSON-serializable)
772
+ - `options.retry` - Optional retry configuration. See [Retries](./retries.md) for details.
773
+ - `options.idempotent` - Deduplicate by callback + payload. Defaults to `true` for cron schedules, `false` for delayed and Date-based schedules.
774
+
775
+ **Returns:** A `Schedule` object with the task details
776
+
777
+ **Idempotency:**
778
+
779
+ Cron schedules are idempotent by default — calling `schedule("0 * * * *", "tick")` multiple times with the same callback, cron expression, and payload returns the existing schedule instead of creating a duplicate. Set `idempotent: false` to override this.
780
+
781
+ For delayed and Date-based schedules, set `idempotent: true` to opt in to the same dedup behavior (matched on callback + payload). This is especially useful when calling `schedule()` in `onStart()` to avoid accumulating duplicate rows across Durable Object restarts:
782
+
783
+ ```typescript
784
+ class MyAgent extends Agent {
785
+ async onStart() {
786
+ // Without idempotent: true, this creates a new row on every DO restart
787
+ await this.schedule(3600, "hourlyCleanup", {}, { idempotent: true });
788
+ }
789
+ }
790
+ ```
791
+
792
+ ### scheduleEvery()
793
+
794
+ ```typescript
795
+ async scheduleEvery<T = string>(
796
+ intervalSeconds: number,
797
+ callback: keyof this,
798
+ payload?: T,
799
+ options?: { retry?: RetryOptions }
800
+ ): Promise<Schedule<T>>
801
+ ```
802
+
803
+ Schedule a task to run repeatedly at a fixed interval.
804
+
805
+ **Parameters:**
806
+
807
+ - `intervalSeconds` - Number of seconds between executions (must be > 0)
808
+ - `callback` - Name of the method to call
809
+ - `payload` - Data to pass to the callback (must be JSON-serializable)
810
+ - `options.retry` - Optional retry configuration. See [Retries](./retries.md) for details.
811
+
812
+ **Returns:** A `Schedule` object with `type: "interval"`
813
+
814
+ **Behavior:**
815
+
816
+ - **Idempotent on (callback, interval, payload)** — calling with the same callback, interval, and payload returns the existing schedule instead of creating a duplicate. A different interval or payload creates a new, independent schedule.
817
+ - First execution occurs after `intervalSeconds` (not immediately)
818
+ - If callback is still running when next execution is due, it's skipped (overlap prevention)
819
+ - If callback throws an error, the interval continues
820
+ - Cancel with `cancelSchedule(id)` to stop the entire interval
821
+
822
+ ### getScheduleById()
823
+
824
+ ```typescript
825
+ async getScheduleById(id: string): Promise<Schedule<unknown> | undefined>
826
+ ```
827
+
828
+ Get a scheduled task by ID. This method works in both top-level agents and sub-agents.
829
+
830
+ ### listSchedules()
831
+
832
+ ```typescript
833
+ async listSchedules(criteria?: {
834
+ id?: string;
835
+ type?: "scheduled" | "delayed" | "cron" | "interval";
836
+ timeRange?: { start?: Date; end?: Date };
837
+ }): Promise<Schedule<unknown>[]>
838
+ ```
839
+
840
+ Get scheduled tasks matching the criteria. This method works in both top-level agents and sub-agents.
841
+
842
+ ### getSchedule()
843
+
844
+ ```typescript
845
+ getSchedule<T = string>(id: string): Schedule<T> | undefined
846
+ ```
847
+
848
+ Deprecated. Get a scheduled task by ID synchronously. This method only works in top-level agents; use `await this.getScheduleById(id)` instead.
849
+
850
+ ### getSchedules()
851
+
852
+ ```typescript
853
+ getSchedules<T = string>(criteria?: {
854
+ id?: string;
855
+ type?: "scheduled" | "delayed" | "cron" | "interval";
856
+ timeRange?: { start?: Date; end?: Date };
857
+ }): Schedule<T>[]
858
+ ```
859
+
860
+ Deprecated. Get scheduled tasks matching the criteria synchronously. This method only works in top-level agents; use `await this.listSchedules(criteria)` instead.
861
+
862
+ ### cancelSchedule()
863
+
864
+ ```typescript
865
+ async cancelSchedule(id: string): Promise<boolean>
866
+ ```
867
+
868
+ Cancel a scheduled task. Returns `true` if cancelled, `false` if not found.
869
+
870
+ ### keepAlive()
871
+
872
+ ```typescript
873
+ async keepAlive(): Promise<() => void>
874
+ ```
875
+
876
+ Create an alarm-backed heartbeat that prevents the Durable Object from being evicted due to inactivity. Returns a disposer function that cancels the heartbeat when called. The disposer is idempotent — calling it multiple times is safe.
877
+
878
+ See [Keeping the Agent Alive](#keeping-the-agent-alive) for usage details.
879
+
880
+ ### keepAliveWhile()
881
+
882
+ ```typescript
883
+ async keepAliveWhile<T>(fn: () => Promise<T>): Promise<T>
884
+ ```
885
+
886
+ Run an async function while keeping the Durable Object alive. The heartbeat is automatically started before the function runs and stopped when it completes (whether it succeeds or throws). Returns the value returned by the function.
887
+
888
+ This is the recommended way to use keepAlive — it guarantees cleanup.
889
+
890
+ ## Limits
891
+
892
+ - **Maximum tasks:** Limited by SQLite storage (each task is a row). Practical limit is tens of thousands per agent.
893
+ - **Task size:** Each task (including payload) can be up to 2MB.
894
+ - **Minimum delay:** 0 seconds (runs on next alarm tick)
895
+ - **Cron precision:** Minute-level (not seconds)
896
+ - **Interval precision:** Second-level
897
+ - **Cron jobs:** After execution, automatically rescheduled for the next occurrence
898
+ - **Interval jobs:** After execution, rescheduled for `now + intervalSeconds`; skipped if still running