Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/cloudflare/agents/llms.txt

Use this file to discover all available pages before exploring further.

Overview

The @callable decorator marks Agent methods as remotely callable from WebSocket clients. Clients can invoke these methods via RPC and receive typed responses.
import { Agent, callable } from "agents";

class MyAgent extends Agent<Env, State> {
  @callable()
  async greet(name: string) {
    return `Hello, ${name}!`;
  }
}

Usage

Basic Callable Method

class CounterAgent extends Agent<Env, { count: number }> {
  @callable()
  async increment() {
    this.setState({ count: this.state.count + 1 });
    return this.state.count;
  }

  @callable()
  async add(amount: number) {
    this.setState({ count: this.state.count + amount });
    return this.state.count;
  }
}

With Description

@callable({ description: "Increments the counter by 1" })
async increment() {
  this.setState({ count: this.state.count + 1 });
  return this.state.count;
}

Streaming Methods

Mark methods as streaming to send multiple responses:
@callable({ streaming: true })
async *generateText(stream: StreamingResponse, prompt: string) {
  for (let i = 0; i < 10; i++) {
    stream.send({ token: `word-${i}`, index: i });
    await new Promise(resolve => setTimeout(resolve, 100));
  }
  stream.end({ complete: true });
}

Decorator Options

metadata
CallableMetadata
description
string
Optional description of what the method does
streaming
boolean
default:"false"
Whether the method supports streaming responses

Calling from Clients

React (useAgent)

import { useAgent } from "agents/react";

function Counter() {
  const agent = useAgent({
    agent: "CounterAgent",
    name: "default"
  });

  // Call via call() method
  const handleIncrement = async () => {
    const newCount = await agent.call("increment");
    console.log("New count:", newCount);
  };

  // Or use the stub (typed)
  const handleAdd = async () => {
    const newCount = await agent.stub.add(5);
    console.log("New count:", newCount);
  };

  return (
    <button onClick={handleIncrement}>Increment</button>
  );
}

AgentClient

import { AgentClient } from "agents/client";

const client = new AgentClient({
  host: "localhost:1999",
  agent: "CounterAgent",
  name: "default"
});

// Call method
const result = await client.call("increment");
console.log("Result:", result);

// With arguments
const sum = await client.call("add", [5]);

Streaming Responses

Server Side

class ChatAgent extends Agent<Env, State> {
  @callable({ streaming: true })
  async chat(stream: StreamingResponse, message: string) {
    // Generate response in chunks
    const response = await generateStreamingResponse(message);

    for await (const chunk of response) {
      stream.send({ text: chunk });
    }

    stream.end({ done: true });
  }
}

Client Side

const client = new AgentClient({
  agent: "ChatAgent",
  name: "default"
});

await client.call("chat", ["Hello!"], {
  stream: {
    onChunk: (chunk) => {
      console.log("Chunk:", chunk.text);
    },
    onDone: (final) => {
      console.log("Stream complete:", final);
    },
    onError: (error) => {
      console.error("Stream error:", error);
    }
  }
});

React Streaming

function Chat() {
  const agent = useAgent({ agent: "ChatAgent", name: "default" });
  const [chunks, setChunks] = useState<string[]>([]);

  const handleSend = async () => {
    await agent.call("chat", ["Hello!"], {
      onChunk: (chunk) => {
        setChunks(prev => [...prev, chunk.text]);
      },
      onDone: () => {
        console.log("Complete!");
      }
    });
  };

  return (
    <div>
      <div>{chunks.join("")}</div>
      <button onClick={handleSend}>Send</button>
    </div>
  );
}

StreamingResponse API

send()

Send a chunk of data to the client.
chunk
unknown
required
The data to send
stream.send({ token: "hello", index: 0 });
Returns: boolean - false if stream is already closed (no-op), true if sent

end()

End the stream and send a final value.
finalValue
unknown
Final value to send before closing
stream.end({ complete: true, totalChunks: 10 });

error()

Send an error and close the stream.
errorMessage
string
required
Error message
stream.error("Failed to generate response");

isClosed

Check if the stream has been closed.
if (!stream.isClosed) {
  stream.send({ data: "more data" });
}

Type Safety

Callable methods are type-safe when using TypeScript:
type MyAgent = Agent<Env, State> & {
  greet(name: string): Promise<string>;
  add(a: number, b: number): Promise<number>;
};

const agent = useAgent<MyAgent>({
  agent: "MyAgent",
  name: "default"
});

// ✅ Type-safe
await agent.stub.greet("Alice");
await agent.stub.add(1, 2);

// ❌ Type error - wrong argument type
await agent.stub.greet(123);

Error Handling

Server Side

Throw errors in callable methods to send error responses:
@callable()
async divide(a: number, b: number) {
  if (b === 0) {
    throw new Error("Division by zero");
  }
  return a / b;
}

Client Side

Handle errors in the promise rejection:
try {
  await agent.call("divide", [10, 0]);
} catch (error) {
  console.error("RPC error:", error.message); // "Division by zero"
}

Timeout

Set a timeout for RPC calls:
const result = await client.call("slowMethod", [], {
  timeout: 5000 // 5 seconds
});

Security

Method Visibility

Only methods marked with @callable() can be invoked from clients. Private methods are not accessible:
class SecureAgent extends Agent<Env, State> {
  @callable()
  async publicMethod() {
    return this.privateMethod();
  }

  // ❌ Not callable from clients
  private privateMethod() {
    return "secret data";
  }
}

Validation

Validate arguments in callable methods:
@callable()
async updateUser(userId: string, name: string) {
  if (!userId || typeof userId !== "string") {
    throw new Error("Invalid userId");
  }
  if (name.length > 100) {
    throw new Error("Name too long");
  }
  // Safe to proceed
}

Authentication

Use connection state for authentication:
async onConnect(connection: Connection, ctx: ConnectionContext) {
  const token = new URL(ctx.request.url).searchParams.get("token");
  const userId = await verifyToken(token);
  connection.setState({ userId, authenticated: true });
}

@callable()
async sensitiveOperation() {
  const { connection } = getCurrentAgent();
  const state = connection?.state as { authenticated?: boolean };

  if (!state?.authenticated) {
    throw new Error("Unauthorized");
  }

  // Safe to proceed
}

Best Practices

Keep Methods Small

// ✅ Good - focused, single-responsibility
@callable()
async increment() {
  this.setState({ count: this.state.count + 1 });
  return this.state.count;
}

// ❌ Bad - doing too much
@callable()
async doEverything(data: unknown) {
  // Complex logic, multiple responsibilities
}

Use Descriptive Names

// ✅ Good
@callable()
async addItemToCart(itemId: string, quantity: number) {
  // ...
}

// ❌ Bad
@callable()
async doIt(id: string, n: number) {
  // ...
}

Return Serializable Data

Only return JSON-serializable data:
// ✅ Good
@callable()
async getUser() {
  return { id: "123", name: "Alice" };
}

// ❌ Bad - functions are not serializable
@callable()
async getBadData() {
  return { callback: () => {} };
}

Build docs developers (and LLMs) love