Documentation menu
Agent capabilities

WebAssembly modules

Compile compute-heavy Rust, Zig, C, C++, or Go code to WebAssembly and call it from Ayjnt agents and workflows in workerd.

Put compiled .wasm artifacts in an optional root-level modules/ directory. Ayjnt discovers them recursively, generates stable imports, and packages the same precompiled modules for dev, run, compile, and deploy.

project tree
my-agent/
├── agents/
├── workflows/
├── modules/                  # optional, compiled artifacts
│   ├── math.wasm             # @ayjnt/modules/math
│   └── image/
│       └── resize.wasm       # @ayjnt/modules/image/resize
├── native/                   # optional source; organize it however you like
└── package.json
Ayjnt loads artifacts; it does not run language toolchains.

Compile source code before starting Ayjnt. Commit the resulting Wasm or run its compiler in your build pipeline. A source-file change alone does not rebuild the artifact.

Import and run a module

A file at modules/math.wasm is imported from @ayjnt/modules/math. Nested paths are preserved. The default export is a precompiled WebAssembly.Module, not bytes, so pass it directly to WebAssembly.instantiate().

agents/calculator/agent.ts
// agents/calculator/agent.ts
import { Agent } from "ayjnt";
import mathModule from "@ayjnt/modules/math";

type MathExports = {
  add(a: number, b: number): number;
};

// Instantiated once when this workerd isolate starts.
const instance = await WebAssembly.instantiate(mathModule);
const math = instance.exports as MathExports;

export default class CalculatorAgent extends Agent {
  override async onRequest(): Promise<Response> {
    return Response.json({ result: math.add(20, 22) });
  }
}

The same import works in a co-located or shared workflow:

agents/report/workflow.ts
// agents/report/workflow.ts
import {
  AgentWorkflow,
  type AgentWorkflowEvent,
  type AgentWorkflowStep,
} from "ayjnt/workflows";
import statsModule from "@ayjnt/modules/stats";

type StatsExports = {
  score(value: number): number;
};

const stats = (await WebAssembly.instantiate(statsModule))
  .exports as StatsExports;

type Params = { value: number };

export default class ReportWorkflow extends AgentWorkflow<Params> {
  override async run(
    event: Readonly<AgentWorkflowEvent<Params>>,
    _step: AgentWorkflowStep,
  ) {
    return { score: stats.score(event.payload.value) };
  }
}

Choose an instance lifecycle

Module-scope instantiation does the compilation and initialization once per workerd isolate. It is the best default for pure functions. An instance's exported memory and mutable globals are shared by calls in that isolate, so create a fresh instance when one operation must not observe another's Wasm state.

isolated.ts
import parserModule from "@ayjnt/modules/parser";

// Use a fresh instance when exported memory or globals must not be shared.
async function parseIsolated(input: Uint8Array) {
  const instance = await WebAssembly.instantiate(parserModule);
  // Copy input into instance.exports.memory, then call the parser export.
  return instance;
}

Module-scope state must never hold request-specific or user-specific data. For large inputs, copy a batch into exported linear memory and make one Wasm call; repeated tiny calls can spend more time crossing the JavaScript boundary than doing useful work.

Keep the first ABI small

JavaScript can call numeric Wasm parameters and results directly: i32, i64, f32, and f64. JavaScript represents i64 as bigint. Strings, arrays, structs, and errors need an explicit ABI using exported linear memory, offsets, lengths, and an allocator owned by one side. Start with scalar functions, then document memory ownership beside the module adapter.

Rust

Use Rust's freestanding wasm32-unknown-unknown target. A cdylib produces a Wasm module, and the C ABI keeps exported scalar signatures predictable.

native/math-rust/Cargo.toml
[package]
name = "ayjnt-math"
version = "0.1.0"
edition = "2024"

[lib]
crate-type = ["cdylib"]

[profile.release]
opt-level = 3
lto = true
panic = "abort"
strip = true
native/math-rust/src/lib.rs
#[unsafe(no_mangle)]
pub extern "C" fn add(a: i32, b: i32) -> i32 {
    a + b
}
terminal
rustup target add wasm32-unknown-unknown
cargo build --manifest-path native/math-rust/Cargo.toml \
  --release --target wasm32-unknown-unknown
cp native/math-rust/target/wasm32-unknown-unknown/release/ayjnt_math.wasm \
  modules/math.wasm

Zig

Zig ships a freestanding WebAssembly target. Exported functions become members of instance.exports without a JavaScript glue file.

native/math.zig
export fn add(a: i32, b: i32) i32 {
    return a + b;
}
terminal
zig build-exe native/math.zig \
  -target wasm32-freestanding \
  -O ReleaseFast \
  -fno-entry \
  --export=add \
  -femit-bin=modules/math.wasm

C and C++

Compile freestanding code with Clang, omit a program entry point, and list every function that should be callable. C++ exports need extern "C" to avoid name mangling. Code requiring libc or libc++ needs an appropriate Wasm sysroot and is beyond this minimal ABI. Use an LLVM distribution with the WebAssembly target enabled; Apple's system Clang may not include that backend.

native/math.c
// native/math.c
int add(int a, int b) {
  return a + b;
}
native/math.cpp
// native/math.cpp
extern "C" int add(int a, int b) {
  return a + b;
}
terminal
# C
clang --target=wasm32 -O3 -nostdlib \
  -Wl,--no-entry -Wl,--export=add \
  -o modules/math.wasm native/math.c

# C++: use clang++ with the same linker flags
clang++ --target=wasm32 -O3 -nostdlib \
  -Wl,--no-entry -Wl,--export=add \
  -o modules/math.wasm native/math.cpp

Go

Go 1.24 or newer can build a WASI reactor and expose functions with //go:wasmexport. Unlike the freestanding examples above, the resulting module needs WASI imports and must run its _initialize export before use.

native/math-go/main.go
package main

//go:wasmexport add
func add(a, b int32) int32 {
    return a + b
}

func main() {}
terminal
GOOS=wasip1 GOARCH=wasm go build \
  -buildmode=c-shared \
  -o modules/math.wasm \
  ./native/math-go

Install a user-space WASI Preview 1 shim:

terminal
bun add @bjorn3/browser_wasi_shim
agents/calculator/go-math.ts
import {
  ConsoleStdout,
  File,
  OpenFile,
  WASI,
} from "@bjorn3/browser_wasi_shim";
import mathModule from "@ayjnt/modules/math";

const wasi = new WASI([], [], [
  new OpenFile(new File([])),
  ConsoleStdout.lineBuffered((line) => console.log(line)),
  ConsoleStdout.lineBuffered((line) => console.error(line)),
]);

type GoInstance = WebAssembly.Instance & {
  exports: WebAssembly.Exports & {
    memory: WebAssembly.Memory;
    _initialize(): void;
    add(a: number, b: number): number;
  };
};

const instance = new WebAssembly.Instance(mathModule, {
  wasi_snapshot_preview1: wasi.wasiImport,
}) as GoInstance;

// Go's c-shared Wasm output is a reactor. Initialize it once before use.
wasi.initialize(instance);
const { add } = instance.exports;
WASI support is experimental.

The Go example's user-space shim implements only part of WASI Preview 1. Prefer a freestanding module when the operation does not need system calls, and verify every required syscall in local workerd and Cloudflare before deploying.

Runtime constraints

  • Workers do not provide WebAssembly threads or Web Workers.
  • Runtime compilation from raw bytes and streaming instantiation are not available; import the precompiled module from modules/.
  • Language runtimes can make Wasm substantially larger. Prefer release builds and consider wasm-opt after measuring behavior.
  • Wasm shares the Worker's CPU and memory budget. It is an execution format, not an additional security or resource boundary inside the isolate.

References