Skip to content

Routines that call a Realm Function, and dispatch-scoped replies — forward-looking

A routine may target a Realm Function instead of embedding inline TypeScript. The same routine surface dispatches a manifest-declared function on either execution host:

agents/support.yml
name: support
job: Answer questions in the support channel
routines:
- name: discord-autoreply
description: Reply to questions in the support channel
match:
signalType: discord.message
spec:
kind: function
function:
namespace: discord
name: onMessage

Rules:

  • function resolves against the owning realm’s manifest. A missing target, or a target carrying schedule or onType, rejects the routine at load with a recorded problem.
  • The routine is declarative content: it loads (inactive) even when the manifest or executable surface is unavailable, and cannot dispatch in that state.
  • One execution per (signal, routine); two routines targeting the same function create two executions. Before fan-out, the host durably admits autonomous work by atomically inserting its deterministic executionId, derived from (adoptionId, trigger-registration generation, concrete trigger occurrence). The same occurrence therefore retains one id across crash recovery and worker attempts. An on-demand call receives an executionId at durable admission. Its request idempotency mapping is keyed by (worldId, contextId, principalId, target function, request idempotency key), so transport retries reach the same execution without deduplicating or exposing another principal’s work. The mapping stores a canonical request digest and rejects reuse of the same key with different arguments. A call without a request idempotency key is a new execution. An explicit operator replay creates a new executionId linked to the original in audit. A crash after signal deduplication but before queueing must leave a recoverable QUEUED or ABANDONED record, never only an in-memory gap. Multi-instance hosts put the lease epoch/fencing token in host-only dispatch context; every gateway, effect, and delivery handoff atomically verifies current RUNNING ownership, world epoch, principal authority, and adoption, refusing a partitioned stale worker. A host without those controls must declare itself single-instance. v1 is at-most-once after admission and does not retry guest execution.
  • Routines are inactive until their agent is adopted. An observing stage runs the function against a read-only gateway: mutating calls return a coded refusal (channels.reply returns NOT_PERMITTED). Enforcement is host-side; there is no flag the function is trusted to honor.
  • The dispatch input is trigger-discriminated: {trigger: "signal", signal: {id, typeName, subject, occurredAt, source, properties}} for a signal firing; {trigger: "cron", firedAt: <ISO-8601>} — no signal key — for a scheduled one. A routine may declare both match and schedule. At load the host validates the target and schema and rejects any trigger shape it can prove incompatible. Immediately before every concrete signal or cron dispatch, it validates the complete input against inputSchema; invalid input records a failed dispatch and guest code does not run. Transport, thread, connector, worldId, and principal details never appear in either variant.

A Realm Function dispatched by a channel signal may reply to the originating thread. The handler receives the trigger-discriminated event as its first argument and ctx as its second:

// wasm/handlers.js — invoked on discord.message by the support agent's routine
export async function onMessage(event, ctx) {
if (event.trigger !== "signal") return null; // no reply route off a cron firing
if (!event.signal.properties.content.includes("?")) return null;
const r = await ctx.reply({
text: "Looking into it.",
idempotencyKey: "autoreply-" + event.signal.id,
});
return { replied: r.status };
}

ctx.reply({ text, idempotencyKey }) is the dispatch-scoped reply — sugar over ctx.gateway.channels.reply, which takes no destination and no signal id: the reply can only reach the thread that triggered the current dispatch, and only while that route is live (connector-configured expiry). It returns { status }, where status is one of SENT (provider-acknowledged) | OUTCOME_UNKNOWN (handoff without acknowledgement) | NOT_REPLYABLE (no channel route: cron trigger, non-channel signal) | NOT_PERMITTED | EXPIRED | CONNECTOR_UNAVAILABLE | REJECTED. Multiple replies in one execution are serialized by host acceptance order and bounded by a host-configured per-routine and per-route reply budget; exhausting the budget returns REJECTED. The receipt key is (worldId, contextId, executionId, surface, operation, idempotencyKey), so the same author key in independent executions never collides. principalId, adoptionId, realmDigest, policy/grant revisions, and worldEpoch remain immutable audit or fencing fields on the execution and receipt, not key fields; changing one cannot make the same logical execution spend again. An omitted key is derived from the durably recorded execution-local acceptance sequence. The outbound envelope is {text} only. A connector’s own outbound messages never fire routines; the reply budget bounds loops involving other bots that self-echo suppression cannot identify.

Proactive sends to a channel with no triggering signal are a different authority and not part of this contract.