Build a capability to reuse a feature that participates in your agent's startup, request handling, or background work. The Lifecycle starts the capability and calls the hooks it implements.
This guide builds an UptimeMonitor capability. It checks a URL periodically, saves each result, and serves the history over HTTP. When a check fails, it notifies your agent through a callback.
The monitor uses three Lifecycle features:
- Tables store check results in the object's SQLite database.
- Jobs record when the next check should run. Lifecycle manages the alarm and calls the monitor's
onJob()hook when the check is due. - Request handling serves the check history at
/uptime. Other requests continue to the next capability or your agent'sonRequest().
A capability only implements the hooks it needs. For how capabilities share startup, storage, and the alarm, refer to Why capabilities.
There are two ways to write a capability:
- Extend
LifecycleCapability. The capability gets a stable ID and the standard Lifecycle services: storage, readiness, its own jobs, events, and a way to call user code in the host context. Use this for almost every capability. - Implement the
DurableObjectCapabilityinterface. The capability is a plain object with the hooks it needs. It gets no services and no ID, so it cannot push jobs or emit events. Pass anything it needs through its constructor.
A capability built on the interface is enough for simple middleware:
import { DurableObject } from "cloudflare:workers";
import { Lifecycle } from "agents/lifecycle";
class HealthCheck {
storage;
constructor(storage) {
this.storage = storage;
}
onStart() {
this.storage.sql.exec(
"CREATE TABLE IF NOT EXISTS health_checks (checked_at INTEGER NOT NULL)",
);
}
onRequest({ request }) {
if (!new URL(request.url).pathname.endsWith("/health")) return;
this.storage.sql.exec(
"INSERT INTO health_checks (checked_at) VALUES (?)",
Date.now(),
);
return new Response("ok");
}
}
export class MyAgent extends DurableObject {
health = new HealthCheck(this.ctx.storage);
lifecycle = Lifecycle.install(this).use(this.health);
onRequest() {
return new Response("application response");
}
}import { DurableObject } from "cloudflare:workers";
import {
Lifecycle,
type CapabilityRequestContext,
type DurableObjectCapability,
} from "agents/lifecycle";
class HealthCheck implements DurableObjectCapability {
readonly storage: DurableObjectStorage;
constructor(storage: DurableObjectStorage) {
this.storage = storage;
}
onStart(): void {
this.storage.sql.exec(
"CREATE TABLE IF NOT EXISTS health_checks (checked_at INTEGER NOT NULL)",
);
}
onRequest({ request }: CapabilityRequestContext): Response | undefined {
if (!new URL(request.url).pathname.endsWith("/health")) return;
this.storage.sql.exec(
"INSERT INTO health_checks (checked_at) VALUES (?)",
Date.now(),
);
return new Response("ok");
}
}
export class MyAgent extends DurableObject<Env> {
readonly health = new HealthCheck(this.ctx.storage);
readonly lifecycle = Lifecycle.install(this).use(this.health);
onRequest(): Response {
return new Response("application response");
}
}The rest of this guide extends LifecycleCapability.
Pass a stable, non-empty ID to super(). The Lifecycle uses the ID to route jobs and events back to the capability. Do not change it after you deploy, or pending jobs lose their owner:
import { LifecycleCapability } from "agents/lifecycle";
export class UptimeMonitor extends LifecycleCapability {
options;
constructor(options) {
super("uptime-monitor");
this.options = options;
}
}import { LifecycleCapability } from "agents/lifecycle";
type UptimeMonitorOptions = {
url: string;
intervalMs: number;
onDown?: (status: number) => void | Promise<void>;
};
export class UptimeMonitor extends LifecycleCapability {
readonly options: UptimeMonitorOptions;
constructor(options: UptimeMonitorOptions) {
super("uptime-monitor");
this.options = options;
}
}Once you install the capability, this.lifecycle gives it the Lifecycle services. The capability never receives the host object itself. Pass bindings and callbacks through its constructor.
this.lifecycle throws if the capability has not been installed with use().
The Lifecycle calls onStart() on every capability before the host's onStart(). Use it to create or migrate the capability's tables. Prefix table names so they do not collide with the host's tables or with other capabilities:
class UptimeMonitor extends LifecycleCapability {
// ...
onStart() {
this.lifecycle.storage.sql.exec(`
CREATE TABLE IF NOT EXISTS uptime_checks (
checked_at INTEGER NOT NULL,
status INTEGER NOT NULL
)
`);
}
}class UptimeMonitor extends LifecycleCapability {
// ...
override onStart(): void {
this.lifecycle.storage.sql.exec(`
CREATE TABLE IF NOT EXISTS uptime_checks (
checked_at INTEGER NOT NULL,
status INTEGER NOT NULL
)
`);
}
}Push a job to wake the capability later, and handle it in onJob(). Return { rescheduleAt } to run it again:
import { LifecycleCapability } from "agents/lifecycle";
class UptimeMonitor extends LifecycleCapability {
// ...
async startMonitoring() {
await this.lifecycle.ready();
await this.lifecycle.jobs.push({
id: "check",
fn: "check",
time: Date.now(),
});
}
async onJob({ job }) {
if (job.fn !== "check") return;
const response = await fetch(this.options.url, { method: "HEAD" });
this.lifecycle.storage.sql.exec(
"INSERT INTO uptime_checks (checked_at, status) VALUES (?, ?)",
Date.now(),
response.status,
);
this.lifecycle.events.emit("uptime:check", { status: response.status });
const { onDown } = this.options;
if (!response.ok && onDown) {
await this.lifecycle.runInHostContext(() => onDown(response.status));
}
return { rescheduleAt: Date.now() + this.options.intervalMs };
}
}import {
LifecycleCapability,
type LifecycleJobContext,
type LifecycleJobOutcome,
} from "agents/lifecycle";
class UptimeMonitor extends LifecycleCapability {
// ...
async startMonitoring(): Promise<void> {
await this.lifecycle.ready();
await this.lifecycle.jobs.push({
id: "check",
fn: "check",
time: Date.now(),
});
}
async onJob({ job }: LifecycleJobContext): Promise<LifecycleJobOutcome> {
if (job.fn !== "check") return;
const response = await fetch(this.options.url, { method: "HEAD" });
this.lifecycle.storage.sql.exec(
"INSERT INTO uptime_checks (checked_at, status) VALUES (?, ?)",
Date.now(),
response.status,
);
this.lifecycle.events.emit("uptime:check", { status: response.status });
const { onDown } = this.options;
if (!response.ok && onDown) {
await this.lifecycle.runInHostContext(() => onDown(response.status));
}
return { rescheduleAt: Date.now() + this.options.intervalMs };
}
}The job has a fixed ID, so calling startMonitoring() again replaces the pending check instead of adding a second one. Keep onJob() short. For work that takes longer, refer to Run long work.
startMonitoring() awaits this.lifecycle.ready() first. A public method on a capability can run before startup, because RPC calls do not start the object. Startup holds the object's input gate while onStart() runs. Work that begins outside startup, and that onStart() then awaits, cannot finish until startup times out after 30 seconds and the object resets. This shows up as an intermittent timeout, depending on whether an RPC call arrives first.
Await ready() at the top of any method that uses storage or jobs, or that starts work onStart() also starts. ready() resolves immediately inside startup, so onStart() can call the same method.
Capability hooks run outside the host's invocation context, so getCurrentAgent() returns undefined values inside them. Call user code through runInHostContext(), so that getCurrentAgent() returns the host inside it. Scheduler calls its callbacks the same way.
A capability that owns connections or requests can pass them as the second argument, runInHostContext(fn, { connection, request }), so the user code sees them through getCurrentAgent().
this.lifecycle.events.emit(type, payload) publishes a best-effort event under the capability's ID. On a plain Durable Object, events go to the agents:* diagnostics channels. The channel depends on the prefix of the event type. For example, schedule: events go to agents:schedule. A type with a prefix the SDK does not recognize, such as uptime:check, goes to agents:lifecycle.
Events emitted during startup are held until startup finishes. They are telemetry, not a durable outbox. If delivery is part of your business logic, store an outbox in the capability's own table.
Implement onRequest() to act as middleware in front of the host. Return a Response to handle the request, or undefined to pass it on:
import { LifecycleCapability } from "agents/lifecycle";
class UptimeMonitor extends LifecycleCapability {
// ...
onRequest({ request }) {
if (!new URL(request.url).pathname.endsWith("/uptime")) return;
const checks = this.lifecycle.storage.sql
.exec(
"SELECT checked_at, status FROM uptime_checks ORDER BY checked_at DESC LIMIT 20",
)
.toArray();
return Response.json(checks);
}
}import {
LifecycleCapability,
type CapabilityRequestContext,
} from "agents/lifecycle";
class UptimeMonitor extends LifecycleCapability {
// ...
onRequest({ request }: CapabilityRequestContext): Response | undefined {
if (!new URL(request.url).pathname.endsWith("/uptime")) return;
const checks = this.lifecycle.storage.sql
.exec(
"SELECT checked_at, status FROM uptime_checks ORDER BY checked_at DESC LIMIT 20",
)
.toArray();
return Response.json(checks);
}
}Match only the routes the capability owns. A capability that handles every request should set claims to "catch-all", so that the Lifecycle runs it after all other capabilities. For more information, refer to Capability order.
Install the capability like any other:
import { DurableObject } from "cloudflare:workers";
import { Lifecycle } from "agents/lifecycle";
export class SiteAgent extends DurableObject {
uptime = new UptimeMonitor({
url: "https://example.com/",
intervalMs: 5 * 60_000,
onDown: (status) => console.warn(`example.com returned ${status}`),
});
lifecycle = Lifecycle.install(this).use(this.uptime);
async onRequest() {
await this.uptime.startMonitoring();
return new Response("Monitoring started", { status: 202 });
}
}import { DurableObject } from "cloudflare:workers";
import { Lifecycle } from "agents/lifecycle";
export class SiteAgent extends DurableObject<Env> {
readonly uptime = new UptimeMonitor({
url: "https://example.com/",
intervalMs: 5 * 60_000,
onDown: (status) => console.warn(`example.com returned ${status}`),
});
readonly lifecycle = Lifecycle.install(this).use(this.uptime);
async onRequest(): Promise<Response> {
await this.uptime.startMonitoring();
return new Response("Monitoring started", { status: 202 });
}
}A request to /agents/site-agent/example/uptime returns the stored checks. Every other request reaches the host's onRequest().
Test a capability by installing it on a small Durable Object with a real Lifecycle, as in the SiteAgent example. Do not construct fake Lifecycle services. Run the tests with the Workers Vitest integration, so jobs, alarms, and storage behave as they do in production.
A capability can also implement these hooks. For their full signatures, refer to Lifecycle API:
| Hook | Purpose |
|---|---|
onWebSocketUpgrade(context) |
Claim a WebSocket upgrade by returning a Response |
onWebSocketMessage, onWebSocketClose, onWebSocketError |
Handle events for sockets the capability owns, and return true |
onJobError(context, error) |
Decide what happens after a job fails its last retry |
onMemoryLimit(context) |
Apply policy after an alarm ends in a memory-limit reset |
onRoute(context) |
Handle a message routed to this capability's ID |
dispose() |
Release live resources during explicit teardown |
The same capability works on the Agent class, which installs the same Lifecycle. Construct the capability as a field, and install it with this.lifecycle.use(this.uptime) in the Agent's constructor, after super().