You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Allow sub-agents to use alarm-backed APIs by delegating the physical Durable Object alarm to the top-level parent while executing logical work inside the owning sub-agent. This enables `schedule()`, `scheduleEvery()`, `cancelSchedule()`, `getScheduleById()`, `listSchedules()`, `keepAlive()`, `keepAliveWhile()`, `runFiber()`, and Think chat recovery inside sub-agents.
6
+
7
+
Sub-agent schedules are scoped to the calling child, so sibling sub-agents cannot cancel each other's schedules by id. The deprecated synchronous `getSchedule()` and `getSchedules()` APIs now throw inside sub-agents; use the async alternatives instead. Destroying a sub-agent now delegates cleanup through the parent so parent-owned schedules and descendant fiber recovery leases are removed consistently.
Copy file name to clipboardExpand all lines: docs/agent-class.md
+2-2Lines changed: 2 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -293,7 +293,7 @@ Tasks are stored in the `cf_agents_queues` SQL table and are automatically flush
293
293
294
294
### `this.schedule` and friends
295
295
296
-
Agents support scheduled execution of methods by wrapping the Durable Object's `alarm()`. The available methods are `this.schedule`, `this.getSchedule`, `this.getSchedules`, `this.cancelSchedule`. Schedules can be one-time, delayed, or recurring (using cron expressions).
296
+
Agents support scheduled execution of methods by wrapping the Durable Object's `alarm()`. The available methods are `this.schedule`, `this.getScheduleById`, `this.listSchedules`, `this.cancelSchedule`, and the deprecated synchronous `this.getSchedule` / `this.getSchedules`. Schedules can be one-time, delayed, or recurring (using cron expressions).
297
297
298
298
Since DOs only allow one alarm at a time, the `Agent` class works around this by managing multiple schedules in SQL and using a single alarm.
299
299
@@ -450,7 +450,7 @@ class MyAgent extends Agent {
450
450
451
451
### `this.keepAlive`
452
452
453
-
`this.keepAlive()` prevents the Durable Object from being evicted due to inactivity by creating a 30-second heartbeat schedule. Returns a disposer function to stop the heartbeat. For scoped work, use `this.keepAliveWhile(fn)` which automatically cleans up when the function completes. See [Keeping the Agent Alive](./scheduling.md#keeping-the-agent-alive) for full documentation.
453
+
`this.keepAlive()` prevents the Durable Object from being evicted due to inactivity by holding an alarm-backed heartbeat ref. Returns a disposer function to stop the heartbeat. For scoped work, use `this.keepAliveWhile(fn)` which automatically cleans up when the function completes. See [Keeping the Agent Alive](./scheduling.md#keeping-the-agent-alive) for full documentation.
Copy file name to clipboardExpand all lines: docs/durable-execution.md
+9-1Lines changed: 9 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -81,7 +81,7 @@ try {
81
81
82
82
While any `keepAlive` ref is held, an alarm fires every 30 seconds that resets the inactivity timer. When all disposers are called, alarms stop and the DO can go idle naturally.
83
83
84
-
The heartbeat is invisible to `getSchedules()` — no schedule rows are created. It does not conflict with your own schedules; the alarm system multiplexes all schedules and the keepAlive heartbeat through a single alarm slot.
84
+
The heartbeat is invisible to `listSchedules()` — no schedule rows are created. It does not conflict with your own schedules; the alarm system multiplexes all schedules and the keepAlive heartbeat through a single alarm slot.
85
85
86
86
### Configurable interval
87
87
@@ -169,6 +169,14 @@ runFiber("work", fn)
169
169
170
170
Both recovery paths call the same hook. The alarm path is critical for background agents that have no incoming client connections — the persisted alarm wakes the agent on its own.
171
171
172
+
#### Sub-agents
173
+
174
+
Fibers also work inside sub-agents. The fiber row and snapshots are stored in the sub-agent's own SQLite database, and `onFiberRecovered()` runs with the sub-agent as `this`.
175
+
176
+
Sub-agents do not have independent alarm slots, so the top-level parent owns the physical heartbeat. When a sub-agent starts a fiber, the parent stores a small root-side index entry for that facet and fiber ID. Root alarm housekeeping uses that index to route recovery checks back into the owning sub-agent, even if the child has no client connection or incoming RPC.
177
+
178
+
This keeps recovery local to the child while preserving the single physical alarm slot owned by the parent. A recovered continuation can use `schedule()` from inside the facet; the parent owns the physical alarm and routes the callback back to the child.
Copy file name to clipboardExpand all lines: docs/long-running-agents.md
+4-2Lines changed: 4 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -514,7 +514,9 @@ export class ProjectManager extends Agent<ProjectState> {
514
514
}
515
515
```
516
516
517
-
Sub-agents have their own state and lifecycle, but not full parity with top-level Durable Objects yet. In particular, they do **not** have their own alarms today — `schedule()` / `scheduleEvery()` are not supported on facets yet (support is coming soon). Put scheduled work on the parent and let it dispatch into children by RPC. The parent also does not need to stay awake while the child handles request-scoped work; once the child is woken it can complete the current turn independently.
517
+
Sub-agents have their own state and lifecycle. They can schedule their own logical callbacks and run durable fibers; the top-level parent owns the physical alarm and routes scheduled work back into the child. Recovery rows live in the child's SQLite database, so `onFiberRecovered()` and Think `chatRecovery` run with the child as `this`.
518
+
519
+
Sub-agents still do not have independent physical alarm slots. The root parent keeps a small index of active child fibers, and its alarm routes recovery checks back into idle children. The parent does not need to stay awake while the child handles request-scoped work; once the child is woken it can complete the current turn independently.
518
520
519
521
For a full user-facing guide to the routing primitive (`subAgent`, `onBeforeSubAgent`, `useAgent({ sub })`, `parentAgent`, `hasSubAgent`, `listSubAgents`), see [Sub-agents](./sub-agents.md).
520
522
@@ -622,7 +624,7 @@ A long-running agent eventually completes its purpose. The project ships, the in
Copy file name to clipboardExpand all lines: docs/scheduling.md
+36-12Lines changed: 36 additions & 12 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -269,7 +269,7 @@ async syncData() {
269
269
270
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
271
272
-
`keepAlive()` prevents this by creating a 30-second heartbeat schedule that keeps the agent active until you are done:
272
+
`keepAlive()` prevents this by holding an alarm-backed heartbeat ref that keeps the agent active until you are done:
273
273
274
274
```typescript
275
275
const dispose =awaitthis.keepAlive();
@@ -299,10 +299,12 @@ This is the recommended approach since you cannot forget to dispose the heartbea
299
299
300
300
### How it works
301
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 `getSchedules()` and the `agents:schedule` diagnostics channel.
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
303
304
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
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
+
306
308
### Multiple concurrent callers
307
309
308
310
Each `keepAlive()` call returns an independent disposer:
@@ -340,7 +342,7 @@ dispose2(); // Ref count reaches 0 — agent can go idle
const specific =this.getSchedules({ id: "abc123" });
378
+
const specific =awaitthis.listSchedules({ id: "abc123" });
377
379
378
380
// Combine filters
379
-
const upcomingCronJobs =this.getSchedules({
381
+
const upcomingCronJobs =awaitthis.listSchedules({
380
382
type: "cron",
381
383
timeRange: {
382
384
start: newDate(),
@@ -399,6 +401,8 @@ if (cancelled) {
399
401
}
400
402
```
401
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
+
402
406
**Example: Cancellable reminders**
403
407
404
408
```typescript
@@ -504,7 +508,7 @@ class PollingAgent extends Agent {
Get scheduled tasks matching the criteria. This method is synchronous.
860
+
Deprecated. Get scheduled tasks matching the criteria synchronously. This method only works in top-level agents; use `awaitthis.listSchedules(criteria)` instead.
837
861
838
862
### cancelSchedule()
839
863
@@ -849,7 +873,7 @@ Cancel a scheduled task. Returns `true` if cancelled, `false` if not found.
849
873
asynckeepAlive():Promise<() =>void>
850
874
```
851
875
852
-
Create a 30-second heartbeat schedule 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.
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.
853
877
854
878
See [Keeping the Agent Alive](#keeping-the-agent-alive) for usage details.
0 commit comments