Skip to content

MoonBit WASM Nodes

MoonBit compiles directly to WASM via its native --target wasm backend, producing compact Core Module binaries without any component-model tooling. The SDK is imported as the @sdk package alias, and all six required exports are declared in moon.pkg.json.

This template uses the Core Module runtime model — see Runtime Models for how this differs from the Component Model.

  • MoonBit toolchain (moon CLI)
  • The local SDK directory (../wasm-sdk-moonbit) placed next to the template
wasm-node-moonbit/
├── node.mbt # Node definition + run logic
├── moon.mod.json # Module manifest (SDK dependency)
├── moon.pkg.json # Package config (WASM exports)
├── flow-like.toml # Flow-Like package manifest
├── mise.toml # Task runner config
├── examples/
│ └── http_request.mbt # HTTP request example
└── README.md

The module manifest declares the SDK as a local path dependency. Once the SDK is published to mooncakes.io, replace the path with a version string.

moon.mod.json
{
"name": "example/custom-node-moonbit",
"version": "0.1.0",
"deps": {
"felix-schultz/flow-like-wasm-sdk": { "path": "../wasm-sdk-moonbit" }
}
}

The package config marks this as the main entrypoint, imports the SDK under the sdk alias, and lists all six required WASM exports plus the exported memory.

moon.pkg.json
{
"is-main": true,
"import": [
"moonbitlang/core/string",
{ "path": "felix-schultz/flow-like-wasm-sdk", "alias": "sdk" }
],
"link": {
"wasm": {
"exports": [
"get_node", "get_nodes", "run",
"alloc", "dealloc", "get_abi_version"
],
"export-memory-name": "memory"
}
}
}

get_definition() builds the node metadata, permissions, and pins using the @sdk helpers:

node.mbt
fn get_definition() -> @sdk.NodeDefinition {
let def = @sdk.NodeDefinition::new(
"my_custom_node_moonbit",
"My Custom Node (MoonBit)",
"A template WASM node built with MoonBit",
"Custom/WASM",
)
def.add_permission("streaming")
def.add_pin(@sdk.input_pin("exec", "Execute", "Trigger execution", @sdk.data_type_exec()))
def.add_pin(
@sdk.input_pin("input_text", "Input Text", "Text to process", @sdk.data_type_string())
.with_default("\"\""),
)
def.add_pin(
@sdk.input_pin("multiplier", "Multiplier", "Number of times to repeat", @sdk.data_type_i64())
.with_default("1"),
)
def.add_pin(@sdk.output_pin("exec_out", "Done", "Execution complete", @sdk.data_type_exec()))
def.add_pin(@sdk.output_pin("output_text", "Output Text", "Processed text", @sdk.data_type_string()))
def.add_pin(@sdk.output_pin("char_count", "Character Count", "Number of characters in output", @sdk.data_type_i64()))
def
}

handle_run receives a @sdk.Context, reads inputs, writes outputs, and returns an ExecutionResult:

node.mbt
fn handle_run(ctx : @sdk.Context) -> @sdk.ExecutionResult {
let input_text = ctx.get_string("input_text")
let multiplier = ctx.get_i64("multiplier", default=1)
let mut output_text = ""
for _i = 0; _i < multiplier; _i = _i + 1 {
output_text = output_text + input_text
}
let char_count = output_text.length()
ctx.stream_text("Generated " + char_count.to_string() + " characters")
ctx.set_output("output_text", @sdk.json_string(output_text))
ctx.set_output("char_count", char_count.to_string())
ctx.success()
}

Every Core Module node must export these six pub fn symbols. The SDK provides the underlying serialization and memory management:

node.mbt
pub fn get_node() -> Int64 {
@sdk.serialize_definition(get_definition())
}
pub fn get_nodes() -> Int64 {
let json = "[" + get_definition().to_json() + "]"
@sdk.serialize_to_mem(json)
}
pub fn run(ptr : Int, len : Int) -> Int64 {
let input = @sdk.parse_input(ptr, len)
let ctx = @sdk.Context::new(input)
let result = handle_run(ctx)
@sdk.serialize_result(result)
}
pub fn alloc(size : Int) -> Int { @sdk.alloc(size) }
pub fn dealloc(ptr : Int, size : Int) -> Unit { @sdk.dealloc(ptr, size) }
pub fn get_abi_version() -> Int { @sdk.abi_version }

Host FFI functions (logging, I/O) are imported from the env module via MoonBit’s extern "wasm" mechanism inside the SDK’s host.mbt.

MethodDescription
ctx.get_string(name, default="")Read a string input
ctx.get_i64(name, default=0)Read an integer input
ctx.get_f64(name, default=0.0)Read a float input
ctx.get_bool(name, default=false)Read a boolean input
ctx.set_output(name, value)Set an output pin value
ctx.success()Finish with success (activates exec_out)
ctx.fail(error)Finish with an error
ctx.debug(msg) / info / warn / errorLog at various levels
ctx.stream_text(text)Stream partial text output
ctx.stream_json(data)Stream JSON data
ctx.stream_progress(pct, msg)Stream a progress update
Terminal window
# One-time: verify the SDK is in place
mise run setup
# Build the Core Module
moon build --target wasm --release
mkdir -p build
cp _build/wasm/release/build/custom-node-moonbit.wasm build/node.wasm
# Or simply:
mise run build

Output: build/node.wasm

Terminal window
mise run test # run unit tests
mise run clean # remove _build/, build/, .mooncakes/