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:

// 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);
        }
    }
}