Callable methods·续
Client-side error handling
try {
const result = await agent.stub.riskyOperation(data);
} catch (error) {
// Error thrown by the agent method
console.error("RPC failed:", error.message);
}
try {
const result = await agent.stub.riskyOperation(data);
} catch (error) {
// Error thrown by the agent method
console.error("RPC failed:", error.message);
}
Streaming error handling
For streaming methods, use the onError callback:
await agent.call("streamData", [input], {
stream: {
onChunk: (chunk) => handleChunk(chunk),
onError: (errorMessage) => {
console.error("Stream error:", errorMessage);
showErrorUI(errorMessage);
},
onDone: (result) => handleComplete(result),
},
});
await agent.call("streamData", [input], {
stream: {
onChunk: (chunk) => handleChunk(chunk),
onError: (errorMessage) => {
console.error("Stream error:", errorMessage);
showErrorUI(errorMessage);
},
onDone: (result) => handleComplete(result),
},
});
Server-side, you can use stream.error() to gracefully send an error mid-stream:
class MyAgent extends Agent {
@callable({ streaming: true })
async processItems(stream, items) {
for (const item of items) {
try {
const result = await this.process(item);
stream.send(result);
} catch (e) {
stream.error(`Failed to process ${item}: ${e.message}`);
return; // Stream is now closed
}
}
stream.end();
}
}
class MyAgent extends Agent {
@callable({ streaming: true })
async processItems(stream: StreamingResponse, items: string[]) {
for (const item of items) {
try {
const result = await this.process(item);
stream.send(result);
} catch (e) {
stream.error(`Failed to process ${item}: ${e.message}`);
return; // Stream is now closed
}
}
stream.end();
}
}
Connection errors
If the WebSocket connection closes while RPC calls are pending, they automatically reject with a "Connection closed" error:
try {
const result = await agent.call("longRunningMethod", []);
} catch (error) {
if (error.message === "Connection closed") {
// Handle disconnection
console.log("Lost connection to agent");
}
}
try {
const result = await agent.call("longRunningMethod", []);
} catch (error) {
if (error.message === "Connection closed") {
// Handle disconnection
console.log("Lost connection to agent");
}
}
Retrying after reconnection
The client automatically reconnects after disconnection. To retry a failed call after reconnection, await agent.ready before retrying:
async function callWithRetry(agent, method, args = []) {
try {
return await agent.call(method, args);
} catch (error) {
if (error.message === "Connection closed") {
await agent.ready; // Wait for reconnection
return await agent.call(method, args); // Retry once
}
throw error;
}
}
// Usage
const result = await callWithRetry(agent, "processData", [data]);
async function callWithRetry<T>(
agent: AgentClient,
method: string,
args: unknown[] = [],
): Promise<T> {
try {
return await agent.call(method, args);
} catch (error) {
if (error.message === "Connection closed") {
await agent.ready; // Wait for reconnection
return await agent.call(method, args); // Retry once
}
throw error;
}
}
// Usage
const result = await callWithRetry(agent, "processData", [data]);
Note
Only retry idempotent operations. If the server received the request but the connection dropped before the response arrived, retrying could cause duplicate execution.
When NOT to use @callable
Worker-to-Agent calls
When calling an agent from the same Worker (for example, in your fetch handler), use Durable Object RPC directly:
import { getAgentByName } from "agents";
export default {
async fetch(request, env) {
// Get the agent stub
const agent = await getAgentByName(env.MyAgent, "instance-name");
// Call methods directly - no @callable needed
const result = await agent.processData(data);
return Response.json(result);
},
};
import { getAgentByName } from "agents";
export default {
async fetch(request: Request, env: Env) {
// Get the agent stub
const agent = await getAgentByName(env.MyAgent, "instance-name");
// Call methods directly - no @callable needed
const result = await agent.processData(data);
return Response.json(result);
},
} satisfies ExportedHandler<Env>;
Agent-to-Agent calls
When one agent needs to call another:
class OrchestratorAgent extends Agent {
async delegateWork(taskId) {
// Get another agent
const worker = await getAgentByName(this.env.WorkerAgent, taskId);
// Call its methods directly
const result = await worker.doWork();
return result;
}
}
class OrchestratorAgent extends Agent {
async delegateWork(taskId: string) {
// Get another agent
const worker = await getAgentByName(this.env.WorkerAgent, taskId);
// Call its methods directly
const result = await worker.doWork();
return result;
}
}
Why the distinction?
| RPC Type | Transport | Use Case |
|---|---|---|
@callable |
WebSocket | External clients (browsers, apps) |
| Durable Object RPC | Internal | Worker to Agent, Agent to Agent |
Durable Object RPC is more efficient for internal calls since it does not go through WebSocket serialization. The @callable decorator adds the necessary WebSocket RPC handling for external clients.
API reference
@callable(metadata?) decorator
Marks a method as callable from external clients.
import { callable } from "agents";
class MyAgent extends Agent {
@callable()
method() {}
@callable({ streaming: true })
streamingMethod(stream) {}
@callable({ description: "Fetches user data" })
getUser(id) {}
}
import { callable } from "agents";
class MyAgent extends Agent {
@callable()
method(): void {}
@callable({ streaming: true })
streamingMethod(stream: StreamingResponse): void {}
@callable({ description: "Fetches user data" })
getUser(id: string): User {}
}
CallableMetadata type
type CallableMetadata = {
/** Optional description of what the method does */
description?: string;
/** Whether the method supports streaming responses */
streaming?: boolean;
};
StreamingResponse class
Used in streaming callable methods to send data to the client.
import {} from "agents";
class MyAgent extends Agent {
@callable({ streaming: true })
async streamData(stream, input) {
stream.send("chunk 1");
stream.send("chunk 2");
stream.end("final");
}
}
import { type StreamingResponse } from "agents";
class MyAgent extends Agent {
@callable({ streaming: true })
async streamData(stream: StreamingResponse, input: string) {
stream.send("chunk 1");
stream.send("chunk 2");
stream.end("final");
}
}
| Method | Signature | Description |
|---|---|---|
send |
(chunk: unknown) => void |
Send a chunk to the client |
end |
(finalChunk?: unknown) => void |
End the stream |
error |
(message: string) => void |
Send an error and close the stream |
Client methods
| Method | Signature | Description |
|---|---|---|
agent.call |
(method, args?, options?) => Promise |
Call a method by name |
agent.stub |
Proxy |
Typed method calls |
// Using call()
await agent.call("methodName", [arg1, arg2]);
await agent.call("streamMethod", [arg], {
stream: { onChunk, onDone, onError },
});
// With timeout (rejects if call does not complete in time)
await agent.call("slowMethod", [], { timeout: 5000 });
// Using stub
await agent.stub.methodName(arg1, arg2);
// Using call()
await agent.call("methodName", [arg1, arg2]);
await agent.call("streamMethod", [arg], {
stream: { onChunk, onDone, onError },
});
// With timeout (rejects if call does not complete in time)
await agent.call("slowMethod", [], { timeout: 5000 });
// Using stub
await agent.stub.methodName(arg1, arg2);
CallOptions type
type CallOptions = {
/** Timeout in milliseconds. Rejects if call does not complete in time. */
timeout?: number;
/** Streaming options */
stream?: {
onChunk?: (chunk: unknown) => void;
onDone?: (finalChunk: unknown) => void;
onError?: (error: string) => void;
};
};
Note
The legacy format { onChunk, onDone, onError } (without nesting under stream) is still supported. The client automatically detects which format you are using.
getCallableMethods() method
Returns a map of all callable methods on the agent with their metadata. Useful for introspection and automatic documentation.
const methods = agent.getCallableMethods();
// Map<string, CallableMetadata>
for (const [name, meta] of methods) {
console.log(`${name}: ${meta.description || "(no description)"}`);
if (meta.streaming) console.log(" (streaming)");
}
const methods = agent.getCallableMethods();
// Map<string, CallableMetadata>
for (const [name, meta] of methods) {
console.log(`${name}: ${meta.description || "(no description)"}`);
if (meta.streaming) console.log(" (streaming)");
}
Troubleshooting
SyntaxError: Invalid or unexpected token
If your dev server fails with SyntaxError: Invalid or unexpected token when using @callable(), you need two things:
1. Add the agents/vite plugin — Vite 8 uses Oxc for transpilation, which does not yet support TC39 decorators. The plugin adds the required transform:
vite.config.tsts
import agents from "agents/vite";
export default defineConfig({
plugins: [agents(), react(), cloudflare()],
});
2. Extend agents/tsconfig — this sets "target": "ES2021" and all other recommended compiler options:
tsconfig.jsonjson
{
"extends": "agents/tsconfig"
}
If you cannot extend the shared config, set "target": "ES2021" manually in your tsconfig.json.
Caution
Do not set "experimentalDecorators": true in your tsconfig.json. The Agents SDK uses TC39 standard decorators ↗︎, not TypeScript legacy decorators. Enabling experimentalDecorators applies an incompatible transform that silently breaks @callable() at runtime.
Next steps
Agents API
Complete API reference for the Agents SDK.
WebSockets
Real-time bidirectional communication with clients.
State management
Sync state between agents and clients.
Was this helpful?
YesNo
On this page
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/agents/runtime/lifecycle/callable-methods/#page","headline":"Callable methods","description":"Expose Agent methods to external clients over WebSocket RPC using the @callable() decorator.","url":"https://developers.cloudflare.com/agents/runtime/lifecycle/callable-methods/","inLanguage":"en","image":"https://developers.cloudflare.com/agents/runtime/lifecycle/callable-methods/og.png?v=30f5b86891924c7a","dateModified":"2026-06-03","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
description: Access the current Agent context from external utility functions using getCurrentAgent() in the Agents SDK. title: getCurrentAgent() image: https://developers.cloudflare.com/agents/runtime/lifecycle/get-current-agent/og.png?v=326b34596aad5414
Documentation Index
Fetch the complete documentation index at: https://developers.cloudflare.com/agents/llms.txt
Use this file to discover all available pages before exploring further.
Docs