Writing transforms

AssemblyScript compiled to Wasm in your browser — entry points, types and limits.

Transforms are written in AssemblyScript — a TypeScript-like language that compiles to WebAssembly. Compilation happens in your browser, so you find out about a type error while you are typing, not after a deploy. The resulting module runs inside a sandbox in your worker pod.

The three code surfaces#

Three different nodes accept code, and each has its own entry point. The editor scaffolds the right one for you; these are what it expects to find when it compiles.

Entry points
FieldTypeDescription
transform()Wasm Transform nodeTakes the node's wired inputs, returns an object carrying its outputs.
buildPayload()Webhook / WebSocket nodeTakes the payload inputs, returns the JSON string to send. It must be valid JSON.
evaluate()Trigger Condition nodeTakes the condition inputs, returns a bool. true fires the sink, false drops the event.

A transform#

transform.ts
// Inputs and outputs are named on the node; the editor keeps this
// signature in sync with your wiring.
export function transform(amount: u64, mint: string): SwapRow {
  const row = new SwapRow();
  row.mint = mint;
  // u64 exceeds what a JSON number can hold — carry it as text.
  row.amount = amount.toString();
  return row;
}

A payload builder#

payload.ts
export function buildPayload(amount: u64, owner: string): string {
  return `{"owner":"${owner}","amount":"${amount.toString()}"}`;
}

A condition#

condition.ts
export function evaluate(amount: u64): bool {
  // Only notify on transfers above 1 SOL.
  return amount > 1_000_000_000;
}

The editor#

Opening a node's code button brings up a Monaco editor with autocomplete for the platform types. On first open it scaffolds a function from your wiring. On subsequent opens it patches the existing signature to match how the node is wired now, preserving your function body — so adding an input does not cost you your work.

Compile from the editor. A successful compile uploads the module and satisfies the deploy-time requirement that at least one transform has been compiled.

Types across the boundary#

Values cross two boundaries: from the Rust types you declared on instruction and account nodes into AssemblyScript, and from AssemblyScript into your Postgres columns. Both mappings are applied for you, but knowing them explains the signatures you get.

Rust to AssemblyScript
FieldTypeDescription
u8, u16, u32, u64u8, u16, u32, u64Mapped directly.
u128, usizeu64Narrowed — AssemblyScript has no 128-bit integer. Values beyond u64 will not survive the trip.
Pubkey, String, charstringPublic keys arrive as their base58 text form.
boolboolMapped directly.
Vec<u8>, [u8; N]Uint8ArrayRaw bytes, length preserved.
Option<T>, COption<T>TUnwrapped to the inner type. Handle the absent case in your own code.
anything unrecognisedstringA safe fallback so an exotic type never blocks the canvas — but check the value is what you expect.

The reverse mapping governs which transform outputs may legally feed which column types, and incompatible wires are rejected at the canvas. The practical rules:

  • Large integers belong in text or NUMERIC. A u64 can exceed what BIGINT holds and what JSON can represent. Convert with .toString() and store it in NUMERIC or TEXT.
  • Public keys are strings. VARCHAR or TEXT, not UUID.
  • Byte arrays go to BYTEA, or hex-encode them into TEXT if you want them readable in a SQL client.
  • Structured output goes to JSONB. Build the JSON string yourself and let Postgres parse it.

Runtime limits#

Your compiled module runs inside a WebAssembly sandbox in the worker pod. It has no network access, no filesystem, and no ambient clock — it sees exactly the inputs you wired into it and returns exactly the outputs it declares. Memory and CPU are bounded, so a transform is not the place for unbounded loops or large allocations per event.

This is also why transforms are safe to iterate on: a broken transform can produce wrong rows, but it cannot reach anything outside its own module.