Skip to main content
An agent is only reachable through an Edge Compute function. Before mountAgents, every project wrote the function’s front door by hand, and each copy differed a little.
  • It parsed the URL and checked for an upgrade.
  • It pulled the credential and encoded the name.
  • It picked the binding and called idFromName(...).fetch(req).
  • It answered health checks.
mountAgents (SDK ≥ 0.12.0) is that front door, already written. The user supplies the routing table and the policy, and the mount supplies the rest.
fetch: is not optional. mountAgents returns a handler, and a function entry must default-export an object carrying that handler on fetch. Writing export default mountAgents(map) produces a bundle the runtime refuses at load time — the deploy does not start, so there is no request to debug.
The map is the entire routing table. Nothing is discovered, and no name is derived from a class name: an agent is reachable once it is written there, and an unknown mount key is a 404 with no binding consulted. It is a function of env, so bindings resolve per request and nothing is cached between them. Each value is an actor binding, an ActorNamespace. An [[actors]] block in telnyx.toml declares it, as the Stateful Actors configuration reference shows.

One Address, Three Transports

Each mounted agent answers on <base>/<mount>/<name>:
  • <base> is the path prefix the mount owns, /agents unless the option sets another, base.
  • <mount> is a key of the map above.
  • <name> is the routing name of one instance — one agent, one persistent identity.
So the function above serves /agents/conversation/alice and /agents/billing/acct-1004. Each name reaches one actor instance, the same one on every request. The first request brings it up, as Lifecycle & Placement describes. The shape of the request selects the transport, not the path. The same address answers all three: This is why the AgentClient reference connects to wss://my-func.telnyxcompute.com/agents/conversation/alice — that URL is the default base, the conversation mount key, and the name alice. A browser talking to the function above needs no route of its own:
An RPC call from a page with no SDK at all is the same address with a method on the end:
The request body is the argument list, and the response is the same result / error frame a WebSocket caller would receive. Only methods decorated with @rpc() are callable.
SSE arrives in batches on deployed functions. A streamed response cannot cross the actor boundary, so each SSE response carries the frames ready when the agent answers. The EventSource reconnects about once a second and resumes from Last-Event-ID. The WebSocket and RPC paths are unaffected, and SSE streams live in local development.The WebSocket path is the live one for production. It carries the same frames under the same policy on the same address, so the change is new WebSocket(url) in place of new EventSource(url), or AgentClient, which reconnects and resumes on its own. The full detail is on the AgentHttpServer page.

Authorization Composes — It Does Not Replace

The mount adds no policy of its own. It routes, and it runs the one authorize callback it is given. That callback does not stand in for the agent’s own authorize(token, req): both run, and they do different jobs. Gate one, at the edge. authorize(req, route) runs after the address resolves and before anything touches an actor, on every transport — an upgrade, an SSE stream, and each RPC POST alike. It receives the request unmodified and the resolved route: the mount key, the decoded name, the actor id, the transport, and the method for an RPC. Answer with a Response to reject (nothing is forwarded, no actor wakes), with headers to admit and stamp, or with nothing to admit as-is. Gate two, inside the agent. The caller’s credential rides through untouched — ?token= or an Authorization header, exactly as sent. The agent’s authorize resolves this connection’s claims from it exactly as it would with no mount in front, and onAttach may still veto.
Identity at the mount, grants in the agent. Two rules make the stamped identity safe to trust:
  • A stamped header overwrites any header of the same name the caller sent, so a client cannot forge an identity the agent trusts.
  • Headers the mount does not stamp are forwarded as the caller sent them. The agent trusts only what the mount stamps.
A throw from the mount’s authorize is not an admission — it propagates and the request never reaches an actor. Reading the body is safe: the agent is handed an independent copy, so a signature verified here still arrives there intact. That copy is made only for a request that carries a body, and it does buffer while both copies are outstanding — on a large streamed upload, prefer deciding from the headers.

Names Customers Already Use

Actor ids are not free-form: only ASCII letters, digits, -, _, and . are addressable, and an id built from anything else belongs to an instance that never activates. The names an application routes on are not so confined — a phone number, an email address, a composite key, a name with accents or emoji. The mount decodes the name segment and re-encodes it with encodeAgentName, which maps any name onto the addressable set reversibly:
/agents/conversation/%2B15550100 therefore addresses the agent for +15550100, and no two names ever share an instance. Three things to keep in mind:
  • The mapping is part of an actor’s identity and is frozen. The id a name first resolves to is where that instance’s state and timers live. Do not re-derive the rule by hand — call encodeAgentName from any other front door that addresses the same instances, or that door points the name at a different, empty instance.
  • Case is significant. alice and Alice are two agents. Names meant to reach one agent are lowercased before routing.
  • Encode the raw name exactly once. Encoding an already-encoded name escapes its : signs and addresses something else.

How Long a Name May Be

The limit is on the actor id once URL-escaped — MAX_ESCAPED_ACTOR_ID_LENGTH, 225 characters — because the id is carried into the name of the file the instance’s persistent state is kept in, which is capped at 255 bytes. How many characters of name that allows depends on which characters they are, since an escape is longer than what it replaces: Every separate escaped run also spends 6 on the pair of : bracketing it, so a mixed name fits less than any single row suggests. A name whose id would exceed the ceiling is refused by the mount with 400 malformed_name before any actor wakes, and encodeAgentName throws rather than handing back an unusable id.
An over-long id does not fail on first use. The instance activates, serves requests, and accepts writes; the long name is only needed when it is loaded again after being evicted from memory, and from then on the instance cannot be opened and what it stored is unreachable. Nothing about the first, healthy run of it looks wrong — which is why the limit is checked up front. See Limits and escapedActorIdLength().

What the Mount Answers Itself

No actor is woken for any of them. Refusals from the mount carry a JSON body with an error of not_found, method_not_allowed, or malformed_name; answers from the agent — including its own errors — are relayed unchanged. Set health: false when the surrounding function owns its own health routes.

Composing with Other Routes

A mount does not have to own the whole function. fallback receives every request whose path is not under base, so the function’s own handler keeps its routes and the agent addresses never reach it:

Cross-origin Browsers

Absent cors, the mount sends no CORS headers and answers OPTIONS with 405, so a page from another origin cannot reach it. Opt in when a browser page served elsewhere must open an EventSource or fetch an RPC address:
It is a passthrough and nothing more: it does not authenticate, and it never substitutes for authorize. Mount names are ordinary path segments, not secrets — they are part of the address every legitimate client already knows.

Next