Client integration
ayjnt's URL shape is different from the Agents SDK's default. That means one option on the client matters: basePath, not path.
The default SDK URL shape
The Cloudflare Agents client SDK (useAgent, AgentClient, agentFetch) constructs URLs
like this:
wss://your-worker.workers.dev/agents/<kebab-class-name>/<instance>
# e.g., for a class ChatAgent with instance name "room-42":
wss://your-worker.workers.dev/agents/chat-agent/room-42
The /agents/ prefix is hardcoded. The class name is
kebab-cased. The last segment is the instance.
The ayjnt URL shape
ayjnt doesn't use that shape. A ChatAgent in agents/chat/ lives at /chat/<instance>,
not /agents/chat-agent/<instance>. Which means
the default SDK URL doesn't match any route in our worker.
You'll get a 404.
The fix: basePath
The SDK has a basePath option that overrides the default
URL construction entirely:
import { useAgent } from "agents/react";
// WRONG — uses default /agents/chat-agent/<name> URL, gets 404
const agent = useAgent({ agent: "ChatAgent", name: "room-42" });
// RIGHT — basePath replaces the entire URL prefix
const agent = useAgent({
agent: "ChatAgent",
basePath: "chat/room-42", // → wss://host/chat/room-42
});
Same for agentFetch and AgentClient. The agent field is still required by the SDK typing, but
it's ignored when basePath is set — pass the class
name anyway so TypeScript stays happy.
There's also a path option. It doesn't do
what you want. path is appended to the
default URL (/agents/chat-agent/room-42/<your-path>),
not substituted for the prefix. basePath is the one
that replaces everything.
From the SDK's own docstring: “Full URL path — bypasses agent/name URL construction. When set, the client connects to this path directly. Server must handle routing manually.” That's exactly what ayjnt does on the server side.
A working client script
This is from examples/with-client/client.ts:
import { agentFetch } from "agents/client";
const host = process.env.HOST ?? "http://localhost:8787";
const roomId = "demo-room";
// POST a message
const post = await agentFetch(
{
agent: "ChatAgent", // required by SDK typing, ignored here
basePath: `chat/${roomId}`, // owns the URL
host,
},
{
method: "POST",
body: JSON.stringify({ text: "hello from the client" }),
},
);
console.log("POST:", post.status, await post.json());
// GET the current state
const get = await agentFetch({
agent: "ChatAgent",
basePath: `chat/${roomId}`,
host,
});
console.log("GET:", await get.json());From a React app
In the browser, use useAgent from agents/react. ayjnt already generates a typed wrapper
for every agent (see
Co-located UI
), so if your UI lives in app.tsx next to the agent,
you can import the typed hook directly:
// agents/chat/app.tsx
import { useAgent } from "@ayjnt/chat"; // generated hook — basePath already wired
export function Chat() {
const agent = useAgent(); // no args — basePath derived from URL
// ...
}
If your UI is in a separate app (Next.js, Vite) and you want to
point at the agent directly, use the raw SDK with basePath:
import { useAgent } from "agents/react";
export function Chat({ roomId }: { roomId: string }) {
const agent = useAgent<ChatState>({
agent: "ChatAgent",
basePath: `chat/${roomId}`,
});
// ...
}Why the server uses getAgentByName
A side note that matters if you're ever debugging identity issues:
When a client connects via the SDK, the server sends back a CF_AGENT_IDENTITY message containing { name: this.name, agent: kebab(ClassName) }. The
client uses this to know which instance it's talking to.
For this.name on the DO to be populated, the server
must call stub.setName(name) before the request reaches
the agent. ayjnt's generated dispatch calls getAgentByName(env.BINDING, instanceId), which does idFromName + get + setName
internally. If you were hand-rolling your own dispatch and did namespace.idFromName(id) + namespace.get(id)
+ stub.fetch(...) manually, you'd skip setName and every identity message would carry an
empty name. Symptoms: the client's onIdentity
callback fires with bad data, useAgent's agent.name is undefined.
ayjnt handles this correctly out of the box. You just need to know that if you ever build your own worker entry around the same pattern, getAgentByName is the right primitive.
CORS
If your client is on a different origin from the worker, you'll need CORS. There's no built-in CORS in ayjnt — write a middleware. See the CORS pattern in the Middleware guide .
WebSocket vs HTTP on the same URL
The ayjnt worker serves three different things off the same URL depending on the request shape:
-
GETwithUpgrade: websocket→ agent's WebSocket handler (this is whatuseAgentuses) -
GETwithAccept: text/html→ HTML shell (if the agent hasapp.tsx) -
Anything else → agent's
onRequest
So useAgent (WebSocket) and agentFetch(method: 'POST') (HTTP) can coexist
pointing at the same URL; they hit different code paths on the
server. The choice between them is a question of whether you need
realtime state sync (WebSocket) or fire-and-forget (HTTP).