Creating custom Wasm action steps
Learn how to build, publish, and test your own Wasm action steps with Rust and Betty Blocks CLI
Custom Wasm action steps let you add your own logic to actions when the built-in steps aren't enough. You write the step in Rust, publish it with the Betty Blocks CLI, and use it on the action canvas like any other step.
A note on languages: WebAssembly components can in principle be written in several languages, such as Rust, JavaScript, or Go. Today, though, Rust is the only documented option for Betty Blocks: the CLI only generates Rust projects, and all official examples are written in Rust. This article uses Rust for that reason. If you want to try another language, check with your Betty Blocks contact first.
What are Wasm action steps?
A Wasm action step is a WebAssembly component that runs as one step in an action. Compared to ActionJS custom steps:
|
ActionJS custom steps |
Wasm action steps |
|
|
Language |
JavaScript ( |
Rust ( |
|
Interface |
|
|
|
Project setup |
|
|
Note: you can also upload a .wasm file with Add custom Wasm step in Actions, but that wizard ignores function.json. This article uses the CLI.
Getting started with custom Wasm action steps
You need basic knowledge of Rust and the command line. Install:
- Rust from rustup.rs, plus the Wasm target:
rustup target add wasm32-wasip2 - Betty Blocks CLI:
npm install -g @betty-blocks/cli - wkg, to download WIT dependencies
- just (optional), for build shortcuts
Working on Windows? All of these tools run on Windows, with a few differences:
- Run the commands in PowerShell. The commands in this article work there as written, but not in Cmd.
- Before installing Rust, install the Microsoft C++ Build Tools with the Desktop development with C++ workload. Rust can't build on Windows without them.
- Install wkg with
cargo install wkgonce Rust is set up. - The generated J
ustfileneeds a Unix shell. Either run just from Git Bash, or skipjustand run the build commands from step 6 yourself.
Before you build, check block-store-wasm-components and native-wasm-components. The step may already exist.
1. Initialize your application
bb functions init --type wasm [identifier]
The identifier is your app's subdomain, for example my-app in my-app.bettyblocks.com. The command creates a project folder with a sample step in functions/say-hello/1.0/.
Important: delete functions/say-hello/ (or run just build inside it). Without a compiled .wasm file, it makes publishing fail for the whole project.
2. Create a new function
cd [identifier]
bb functions new [name]
cd functions/[name]/1.0
This adds Cargo.toml, wit/world.wit, src/lib.rs, function.json, and a Justfile with placeholder content.
Configuring your custom Wasm action step
The example builds a step that returns a random hex string of a chosen length.
3. Define the interface in world.wit
wit/world.wit is the contract between your step and the platform. Written in WIT (WebAssembly Interface Type), it states which function your step exposes, what inputs that function takes, and what it returns. It contains no logic: the platform reads it to know your step's shape, and the Rust compiler uses it to check your code in step 4. In the example below, package names and versions the step, interface lists its function, and world declares what the step exports.
package betty-blocks:generate-random-hex@1.0.0;
interface generate-random-hex {
generate-random-hex: func(size: u32) -> string;
}
world main {
export generate-random-hex;
}
Keep the package version (@1.0.0) in line with the folder name (1.0/). A few rules:
- Inputs: use
string, u32, option<T>, list<T>, andrecord. u32is the only integer type confirmed to work. - Outputs: return a
stringoru32, arecordfor several outputs, orresult<T, string>if the step can fail. Don't useoption<T>in outputs: it builds and publishes, then fails at run time with "expected a string, received [object]". Tuples aren't supported. - Platform features: import the current versioned interfaces, such as
betty-blocks-types:upload-file/upload-file@3.0.0, and runwkg wit fetch. The olderbetty-blocks-utilities:*imports no longer work.
4. Implement lib.rs
Replace the whole scaffolded src/lib.rs with:
use crate::exports::betty_blocks::generate_random_hex::generate_random_hex::Guest;
use rand::RngExt;
wit_bindgen::generate!({ generate_all });
struct Component;
impl Guest for Component {
fn generate_random_hex(size: u32) -> String {
let mut rng = rand::rng();
(0..size)
.map(|_| format!("{:X}", rng.random_range(0..16)))
.collect()
}
}
export! {Component}
#[test]
fn returns_requested_length() {
assert_eq!(Component::generate_random_hex(32).len(), 32);
}
The function signature must match world.wit. The scaffolded Cargo.toml already includes wit-bindgen = "0.42.0". Add the crates you use below it, here rand = "0.10.1".
5. Configure function.json
function.json defines how the step looks in the IDE:
{
"label": "Random Hex",
"description": "Generates a string of random hexadecimal characters with a given size",
"category": "Misc",
"icon": { "name": "ActionsIcon", "color": "Orange" },
"options": [
{
"meta": { "type": "Number", "default": 8, "validations": { "required": true, "min": 1 } },
"name": "size",
"label": "Size",
"info": "The length of the result string"
},
{
"meta": { "type": "Output", "output": { "type": "Text" } },
"name": "result",
"label": "Result",
"info": "The generated hex string"
}
],
"yields": "NONE"
}
Common option types are Text, MultilineText, Number, Select, Value, Map, Model, Property, SchemaModel, and Output. For JSON inputs, use Value.
Important: keep description, labels, and info under 255 characters each. Longer text can cause "Failed to create action step" on the canvas.
Check by hand before publishing. bb functions validate only checks the JSON format and that a .wasm file exists. It doesn't compare function.json with world.wit, so wrong names pass without errors. Make sure that:
- Each input option's
namematches a WIT parameter (size) - Each output option's
namematches a record field, if the step returns a record - A step that returns a single value has one output option named
result
Tips:
- For secrets, use a plain Text option and bind it to one of the app's Configurations.
- For an
Objectoutput that later steps can read fields from, return a named type alias (type json-string = string;) in WIT, not a plainstring.
Building and publishing your function
6. Build and test the function locally
From functions/[name]/1.0, run:
wkg wit fetch
cargo build --release --target wasm32-wasip2
mv ./target/wasm32-wasip2/release/*.wasm .
cargo test
This downloads WIT dependencies, builds the component, moves the .wasm file next to function.json, and runs the test in lib.rs. With just, run just build and just test instead. wkg may show a deprecation warning, which you can ignore.
Important: publishing uploads the .wasm file as it is and doesn't rebuild it. Rebuild after every change.
7. Publish the function
From the project root, run:
bb functions publish
Select the functions to publish and press Enter, or add --all to publish all of them. The CLI asks you to log in if needed, and may ask for your app's UUID, which you can find in the app's URLs in the IDE.
8. Test the step in your application
Open an action, find your step in the Wasm steps palette (tagged Application specific), and drag it onto the canvas. Fill in real values, save, and run the action. The step is only available in the app you published it to.
Editing custom Wasm action steps
- Fixes that keep the same inputs and outputs: change the code, rebuild, and publish again.
- Changes to inputs or outputs: create a new version folder, for example 2.0/, with a matching package version (
@2.0.0). Leave1.0/in place to keep a version history, so you can compare changes between versions.
Troubleshooting
|
Problem |
Solution |
|
"Failed to create action step" on the canvas |
Shorten |
|
"Actions compiling…" never finishes |
First, check the imports: if the step imports |
|
"Expected a string, received [object]" at run time |
Remove |
|
An |
Return a named type alias in WIT instead of a plain |
|
|
The live schema is stricter than |
|
|
Delete |
|
|
Run |
|
|
Build with |
Key commands reference
| bb functions init --type wasm [identifier] | # create a Wasm project |
| bb functions new [name] | # add a step |
| wkg wit fetch | # download WIT dependencies |
| cargo build --release --target wasm32-wasip2 | # build the component |
| bb functions validate | # check JSON format and .wasm only |
| bb functions publish | # publish to your app |
By following these steps, you'll be able to create, configure, and publish custom Wasm action steps that extend your Betty Blocks application.