Callable methods
Last updated Jun 3, 2026
Callable methods let clients invoke agent methods over WebSocket using RPC (Remote Procedure Call). Mark methods with @callable() to expose them to external clients like browsers, mobile apps, or other services.
Overview
import { Agent, callable } from "agents";
export class MyAgent extends Agent {
@callable()
async greet(name) {
return `Hello, ${name}!`;
}
}
import { Agent, callable } from "agents";
export class MyAgent extends Agent {
@callable()
async greet(name: string): Promise<string> {
return `Hello, ${name}!`;
}
}
// Client
const result = await agent.stub.greet("World");
console.log(result); // "Hello, World!"
// Client
const result = await agent.stub.greet("World");
console.log(result); // "Hello, World!"
How it works
sequenceDiagram
participant Client
participant Agent
Client->>Agent: agent.stub.greet("World")
Note right of Agent: Check @callable<br/>Execute method
Agent-->>Client: "Hello, World!"
When to use @callable()
| Scenario | Use |
|---|---|
| Browser/mobile calling agent | @callable() |
| External service calling agent | @callable() |
| Worker calling agent (same codebase) | Durable Object RPC (no decorator needed) |
| Agent calling another agent | Durable Object RPC via getAgentByName() |
The @callable() decorator is specifically for WebSocket-based RPC from external clients. When calling from within the same Worker or another agent, use standard Durable Object RPC directly.
Basic usage
Defining callable methods
Add the @callable() decorator to any method you want to expose:
import { Agent, callable } from "agents";
export class CounterAgent extends Agent {
initialState = { count: 0, items: [] };
@callable()
increment() {
this.setState({ ...this.state, count: this.state.count + 1 });
return this.state.count;
}
@callable()
decrement() {
this.setState({ ...this.state, count: this.state.count - 1 });
return this.state.count;
}
@callable()
async addItem(item) {
this.setState({ ...this.state, items: [...this.state.items, item] });
return this.state.items;
}
@callable()
getStats() {
return {
count: this.state.count,
itemCount: this.state.items.length,
};
}
}
import { Agent, callable } from "agents";
export type CounterState = {
count: number;
items: string[];
};
export class CounterAgent extends Agent<Env, CounterState> {
initialState: CounterState = { count: 0, items: [] };
@callable()
increment(): number {
this.setState({ ...this.state, count: this.state.count + 1 });
return this.state.count;
}
@callable()
decrement(): number {
this.setState({ ...this.state, count: this.state.count - 1 });
return this.state.count;
}
@callable()
async addItem(item: string): Promise<string[]> {
this.setState({ ...this.state, items: [...this.state.items, item] });
return this.state.items;
}
@callable()
getStats(): { count: number; itemCount: number } {
return {
count: this.state.count,
itemCount: this.state.items.length,
};
}
}
Calling from the client
There are two ways to call methods from the client:
Using agent.stub (recommended):
// Clean, typed syntax
const count = await agent.stub.increment();
const items = await agent.stub.addItem("new item");
const stats = await agent.stub.getStats();
// Clean, typed syntax
const count = await agent.stub.increment();
const items = await agent.stub.addItem("new item");
const stats = await agent.stub.getStats();
Using agent.call():
// Explicit method name as string
const count = await agent.call("increment");
const items = await agent.call("addItem", ["new item"]);
const stats = await agent.call("getStats");
// Explicit method name as string
const count = await agent.call("increment");
const items = await agent.call("addItem", ["new item"]);
const stats = await agent.call("getStats");
The stub proxy provides better ergonomics and TypeScript support.
Method signatures
Serializable types
Arguments and return values must be JSON-serializable:
// Valid - primitives and plain objects
class MyAgent extends Agent {
@callable()
processData(input) {
return { result: true };
}
}
// Valid - arrays
class MyAgent extends Agent {
@callable()
processItems(items) {
return items.map((item) => item.length);
}
}
// Invalid - non-serializable types
// Functions, Dates, Maps, Sets, etc. cannot be serialized
// Valid - primitives and plain objects
class MyAgent extends Agent {
@callable()
processData(input: { name: string; count: number }): { result: boolean } {
return { result: true };
}
}
// Valid - arrays
class MyAgent extends Agent {
@callable()
processItems(items: string[]): number[] {
return items.map((item) => item.length);
}
}
// Invalid - non-serializable types
// Functions, Dates, Maps, Sets, etc. cannot be serialized
Async methods
Both sync and async methods work:
// Sync method
class MyAgent extends Agent {
@callable()
add(a, b) {
return a + b;
}
}
// Async method
class MyAgent extends Agent {
@callable()
async fetchUser(id) {
const user = await this.sql`SELECT * FROM users WHERE id = ${id}`;
return user[0];
}
}
// Sync method
class MyAgent extends Agent {
@callable()
add(a: number, b: number): number {
return a + b;
}
}
// Async method
class MyAgent extends Agent {
@callable()
async fetchUser(id: string): Promise<User> {
const user = await this.sql`SELECT * FROM users WHERE id = ${id}`;
return user[0];
}
}
Void methods
Methods that do not return a value:
class MyAgent extends Agent {
@callable()
async logEvent(event) {
await this.sql`INSERT INTO events (name) VALUES (${event})`;
}
}
class MyAgent extends Agent {
@callable()
async logEvent(event: string): Promise<void> {
await this.sql`INSERT INTO events (name) VALUES (${event})`;
}
}
On the client, these still return a Promise that resolves when the method completes:
await agent.stub.logEvent("user-clicked");
// Resolves when the server confirms execution
await agent.stub.logEvent("user-clicked");
// Resolves when the server confirms execution
Streaming responses
For methods that produce data over time (like AI text generation), use streaming:
Defining a streaming method
import { Agent, callable } from "agents";
export class AIAgent extends Agent {
@callable({ streaming: true })
async generateText(stream, prompt) {
// First parameter is always StreamingResponse for streaming methods
for await (const chunk of this.llm.stream(prompt)) {
stream.send(chunk); // Send each chunk to the client
}
stream.end(); // Signal completion
}
@callable({ streaming: true })
async streamNumbers(stream, count) {
for (let i = 0; i < count; i++) {
stream.send(i);
await new Promise((resolve) => setTimeout(resolve, 100));
}
stream.end(count); // Optional final value
}
}
import { Agent, callable, type StreamingResponse } from "agents";
export class AIAgent extends Agent {
@callable({ streaming: true })
async generateText(stream: StreamingResponse, prompt: string) {
// First parameter is always StreamingResponse for streaming methods
for await (const chunk of this.llm.stream(prompt)) {
stream.send(chunk); // Send each chunk to the client
}
stream.end(); // Signal completion
}
@callable({ streaming: true })
async streamNumbers(stream: StreamingResponse, count: number) {
for (let i = 0; i < count; i++) {
stream.send(i);
await new Promise((resolve) => setTimeout(resolve, 100));
}
stream.end(count); // Optional final value
}
}
Consuming streams on the client
// Preferred format (supports timeout and other options)
await agent.call("generateText", [prompt], {
stream: {
onChunk: (chunk) => {
// Called for each chunk
appendToOutput(chunk);
},
onDone: (finalValue) => {
// Called when stream ends
console.log("Stream complete", finalValue);
},
onError: (error) => {
// Called if an error occurs
console.error("Stream error:", error);
},
},
});
// Legacy format (still supported for backward compatibility)
await agent.call("generateText", [prompt], {
onChunk: (chunk) => appendToOutput(chunk),
onDone: (finalValue) => console.log("Done", finalValue),
onError: (error) => console.error("Error:", error),
});
// Preferred format (supports timeout and other options)
await agent.call("generateText", [prompt], {
stream: {
onChunk: (chunk) => {
// Called for each chunk
appendToOutput(chunk);
},
onDone: (finalValue) => {
// Called when stream ends
console.log("Stream complete", finalValue);
},
onError: (error) => {
// Called if an error occurs
console.error("Stream error:", error);
},
},
});
// Legacy format (still supported for backward compatibility)
await agent.call("generateText", [prompt], {
onChunk: (chunk) => appendToOutput(chunk),
onDone: (finalValue) => console.log("Done", finalValue),
onError: (error) => console.error("Error:", error),
});
StreamingResponse API
| Method | Description |
|---|---|
send(chunk) |
Send a chunk to the client |
end(finalChunk?) |
End the stream, optionally with a final value |
error(message) |
Send an error to the client and close the stream |
class MyAgent extends Agent {
@callable({ streaming: true })
async processWithProgress(stream, items) {
for (let i = 0; i < items.length; i++) {
await this.process(items[i]);
stream.send({ progress: (i + 1) / items.length, item: items[i] });
}
stream.end({ completed: true, total: items.length });
}
}
class MyAgent extends Agent {
@callable({ streaming: true })
async processWithProgress(stream: StreamingResponse, items: string[]) {
for (let i = 0; i < items.length; i++) {
await this.process(items[i]);
stream.send({ progress: (i + 1) / items.length, item: items[i] });
}
stream.end({ completed: true, total: items.length });
}
}
TypeScript integration
Typed client calls
Pass your agent class as a type parameter for full type safety:
import { useAgent } from "agents/react";
function App() {
const agent = useAgent({
agent: "MyAgent",
name: "default",
});
async function handleGreet() {
// TypeScript knows the method signature
const result = await agent.stub.greet("World");
// ^? string
}
// TypeScript catches errors
// await agent.stub.greet(123); // Error: Argument of type 'number' is not assignable
// await agent.stub.nonExistent(); // Error: Property 'nonExistent' does not exist
}
import { useAgent } from "agents/react";
import type { MyAgent } from "./server";
function App() {
const agent = useAgent<MyAgent>({
agent: "MyAgent",
name: "default",
});
async function handleGreet() {
// TypeScript knows the method signature
const result = await agent.stub.greet("World");
// ^? string
}
// TypeScript catches errors
// await agent.stub.greet(123); // Error: Argument of type 'number' is not assignable
// await agent.stub.nonExistent(); // Error: Property 'nonExistent' does not exist
}
Excluding non-callable methods
If you have methods that are not decorated with @callable(), you can exclude them from the type:
class MyAgent extends Agent {
@callable()
publicMethod() {
return "public";
}
// Not callable from clients
internalMethod() {
// internal logic
}
}
// Exclude internal methods from the client type
const agent = useAgent({
agent: "MyAgent",
});
agent.stub.publicMethod(); // Works
// agent.stub.internalMethod(); // TypeScript error
class MyAgent extends Agent {
@callable()
publicMethod(): string {
return "public";
}
// Not callable from clients
internalMethod(): void {
// internal logic
}
}
// Exclude internal methods from the client type
const agent = useAgent<Omit<MyAgent, "internalMethod">>({
agent: "MyAgent",
});
agent.stub.publicMethod(); // Works
// agent.stub.internalMethod(); // TypeScript error
Error handling
Throwing errors in callable methods
Errors thrown in callable methods are propagated to the client:
class MyAgent extends Agent {
@callable()
async riskyOperation(data) {
if (!isValid(data)) {
throw new Error("Invalid data format");
}
try {
await this.processData(data);
} catch (e) {
throw new Error("Processing failed: " + e.message);
}
}
}
class MyAgent extends Agent {
@callable()
async riskyOperation(data: unknown): Promise<void> {
if (!isValid(data)) {
throw new Error("Invalid data format");
}
try {
await this.processData(data);
} catch (e) {
throw new Error("Processing failed: " + e.message);
}
}
}
