Authoring a wasm realm
A realm written with defineRealm gets realm.yml and dist/manifest.json generated from one
realm.ts, and synth copies the handler entry it names (wasm/handlers.ts by default) beside
them; see TypeScript realms. By hand, a realm needs at least the three
files below: the realm, the handlers and the manifest that registers them.
name: pinghost: wasmexport async function ping(input, ctx) { return "pong";}{ "version": 1, "entries": [ { "namespace": "ping", "name": "ping", "description": "Answers pong.", "inputSchema": { "type": "object", "properties": {} }, "outputSchema": { "type": "string" } } ]}A handler file contributes named handler functions, and the manifest binds each declared function to one by its name (the namespace is manifest-side and never encoded in the code). A handler is async (input, ctx): it returns its result value directly — the value described by the manifest’s outputSchema — or throws to fail. The host wraps the return into the { result } / { error } wire envelope; authors never write the envelope. A return value must be JSON-serializable; undefined serializes to a null result. Three forms are supported:
// Named export — one handler per Realm Function (the default). input-first, ctx second.export async function ping(input, ctx) { return "pong";}
// Default-export object — group a file's functions.export default { async ping(input, ctx) { return "pong"; },};
// Register by name — for a file that cannot use export syntax (e.g. generated).defineHandler("ping", async (input, ctx) => "pong");The name a form contributes is the export identifier, the property key, or the defineHandler string. Resolution is deterministic: for each manifest function the host looks up its name among the contributed functions; a name contributed more than once, or by more than one form, is a load-time problem — a name is defined exactly once. Discovery is the manifest’s; the module supplies implementations. The host reads the manifest for what functions exist — it never executes guest code to discover functions — then evaluates the module to resolve each entry’s implementation (collecting the exports and any defineHandler registrations) and binds them. A function with no manifest entry is unreachable; a manifest function with no contributed function is a recorded load problem and is unavailable for calls or triggers; a realm with no manifest registers no functions at all.
input is the dispatch input, described by the manifest’s inputSchema: the call arguments for an on-demand call, or the trigger-discriminated event for a signal- or cron-bound dispatch (see routines that call a Realm Function). An on-demand call passes the arguments verbatim, with no trigger key; a function that a routine also targets distinguishes the cases by the presence of trigger. ctx carries:
| Member | Contract |
|---|---|
ctx.gateway.<namespace>.<method>(args) |
Calls back into the host’s gateway surface — the same namespaces and schemas a TS handler or LLM-generated script sees. The host binds the world, context, access-policy revision, execution, and principal. On-demand calls run as the authenticated caller; autonomous calls run as the adoption’s pinned principal. The guest never holds or presents a credential or scope key. |
ctx.log(message) |
Guest logging, surfaced in the host’s logs against the dispatch. |
ctx.reply({ text, idempotencyKey }) |
Dispatch-scoped reply to the triggering channel thread (routines that call a Realm Function). Returns { status } (one of the reply results listed there). Always present; on a dispatch with no live route it returns { status: "NOT_REPLYABLE" } rather than being absent. |
Statelessness applies to every form. Every dispatch runs in a fresh instance, so nothing in the file survives between dispatches — a module-level variable and a value closed over by a default-export object both reset each dispatch. Durable state lives in the graph, through the gateway. wasm/handlers.js is compiled as a module; defineHandler is a host-provided global available both in that module and in a file with no export syntax — reach for it when generating a file that cannot use export syntax.
A realistic Realm Function — an activity digest over signals the realm’s own event source ingests:
// wasm/handlers.js — manifest namespace "chatops", name "digest" binds to export `digest`export async function digest(input, ctx) { const limit = Number(input && input.limit) > 0 ? Number(input.limit) : 5; const messages = await ctx.gateway.signals.recent({ type: "chatops.message", hours: 24 * 7, limit: 200, }); const byChannel = {}; for (const m of messages) { const channel = (m.properties || {}).channel_name || "unknown"; byChannel[channel] = (byChannel[channel] || 0) + 1; } ctx.log("digest over " + messages.length + " message(s)"); // Return the value directly — the host wraps it into the wire envelope. // Throw to fail; the host turns the thrown error into { error }. return { totalMessages: messages.length, channels: Object.entries(byChannel) .map(([name, n]) => ({ name, messages: n })) .sort((a, b) => b.messages - a.messages), recent: messages.slice(0, limit).map((m) => ({ subject: m.subject, at: m.occurredAt, })), };}What the guest has, and what it does not:
| Available | Absent |
|---|---|
ctx.gateway.*, ctx.log, plain JavaScript, JSON |
Node (require, process, fs), npm packages |
The function’s input, described by the manifest’s inputSchema |
Network sockets, filesystem, environment variables |
| Any credential or scope key. World and principal are bound host-side; there is no key to read, leak, replay, or substitute. |
The absences are the point: a wasm function is pure logic over gateway calls. A handler that needs an npm library, streaming I/O, or the filesystem belongs on the docker host. The engine is an embedded modern-ECMAScript interpreter, not Node: the language and its built-ins (JSON, Promise, Math) are there; host-shaped globals (require, timers, fetch) are not.

