/AYjnt/
All examples
rpccallableuiwebsocket

Client-callable methods

Cloudflare's `@callable()` decorator + `agent.stub.method()` from the React UI. Three patterns share the word "callable" in this framework — this example uses all three on one agent so the differences are unmissable: CF's decorator for browser→agent WebSocket RPC, ayjnt's `getAgent<T>` for agent→agent DO RPC, and ayjnt's `/** @callable */` JSDoc for catalog metadata.

What you'll learn
  • How `@callable()` from "agents" exposes methods to the browser over WebSocket
  • How `agent.stub.method(args)` and `agent.call("method", [args])` differ
  • Why `@callable()` complements `setState({...})` rather than replacing it
  • How the three "callable" patterns layer on the same method without conflict
01 step

Start from the blank scaffold (with UI)

Same starter as the blank template but with React, react-dom and matching @types preinstalled so your agent.ts can have an app.tsx next to it. The default project is a Counter agent — we'll replace it with the example's agent in the next steps.

~/my-agent-app
my-app/ (--with-ui scaffold)
agent.ts
app.tsx
package.json
tsconfig.json
02 step

Decorate methods with @callable from "agents"

`@callable()` is a real TypeScript 5 decorator imported from the Cloudflare Agents SDK. At runtime the SDK registers each decorated method in its callable registry; bundlers transpile the decorator using the ES decorator helper. Bun + Bun.build handle this natively — no plugin, no `experimentalDecorators` flag.

agents/notes/agent.ts ts
import { Agent, callable } from "agents";
import type { GeneratedEnv } from "@ayjnt/env";

type Note = { id: string; text: string; createdAt: number };
type State = { notes: Note[] };

export default class NotesAgent extends Agent<GeneratedEnv, State> {
  override initialState: State = { notes: [] };

  /**
   * Add a note. The agent generates the id; only the server can.
   * @callable
   */
  @callable({ description: "Add a new note." })
  async addNote(text: string): Promise<Note> {
    const note = { id: crypto.randomUUID(), text, createdAt: Date.now() };
    this.setState({ notes: [...this.state.notes, note] });
    return note;
  }

  /**
   * Delete a note by id. Returns true if it existed.
   * @callable
   */
  @callable({ description: "Delete a note by id." })
  async deleteNote(id: string): Promise<boolean> {
    const before = this.state.notes.length;
    this.setState({ notes: this.state.notes.filter((n) => n.id !== id) });
    return this.state.notes.length < before;
  }
}
03 step

Call from the UI via agent.stub.<method>

The generated `useAgent()` hook is pre-bound to `NotesAgent` at codegen time, so `agent.stub` is a typed proxy over every `@callable()` method. Calling `agent.stub.addNote("hello")` sends a WebSocket frame, the agent dispatches to the decorated method, the return value is JSON-serialised back, and the Promise resolves — typed end-to-end. `setState` inside the method broadcasts the new state to every connected client, so a second tab sees the note immediately.

agents/notes/app.tsx tsx
import { useState } from "react";
import { useAgent } from "@ayjnt/notes";

export default function NotesApp() {
  const agent = useAgent();                    // no generic needed — typed
  const notes = agent.state?.notes ?? [];
  const [text, setText] = useState("");

  const submit = async (e: React.FormEvent) => {
    e.preventDefault();
    if (!text.trim()) return;
    const note = await agent.stub.addNote(text.trim());   // typed!
    console.log("created:", note.id, note.text);
    setText("");
  };

  return (
    <main>
      <form onSubmit={submit}>
        <input value={text} onChange={(e) => setText(e.target.value)} />
        <button type="submit">add</button>
      </form>
      <ul>
        {notes.map((n) => (
          <li key={n.id}>
            {n.text}{" "}
            <button onClick={() => agent.stub.deleteNote(n.id)}>×</button>
          </li>
        ))}
      </ul>
    </main>
  );
}
04 step

Three "callable" patterns on one agent

`@callable()` (CF's decorator) makes methods reachable from the browser. `/** @callable */` (ayjnt's JSDoc tag) advertises them in `/__ayjnt/catalog`. `getAgent<T>` calls them from another agent. The three are orthogonal — pick the audience(s) you want. The example uses all three on every method so each pattern is observable independently.

~/my-agent-app
05 step

Run it

`bun run dev` exposes the React UI at /notes and the catalog at /__ayjnt/catalog. Open the UI in two tabs — every `agent.stub.addNote` from one tab triggers a `setState` broadcast that the other tab sees live.

~/my-agent-app
06 step

Deploy

`ayjnt deploy` checks your git tree is clean + synced with origin, regenerates the wrangler config from scratch, then shells out to `wrangler deploy`. The committed migrations.json file is the source of truth for what's in production.

~/my-agent-app