Skip to content

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.

realm.yml
name: ping
host: wasm
wasm/handlers.js
export async function ping(input, ctx) {
return "pong";
}
dist/manifest.json
{
"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.