Skip to content
  • There are no suggestions because the search field is empty.

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 (index.js)

Rust (src/lib.rs)

Interface

function.json

wit/world.wit, plus function.json for the IDE

Project setup

bb functions init [identifier] (type js is the default)

bb functions init --type wasm [identifier]

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 wkg once Rust is set up.
  • The generated Justfile needs a Unix shell. Either run just from Git Bash, or skip just and 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>, and record. u32 is the only integer type confirmed to work.
  • Outputs: return a string or u32, a record for several outputs, or result<T, string> if the step can fail. Don't use option<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 run wkg wit fetch. The older betty-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 name matches a WIT parameter (size)
  • Each output option's name matches 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 Object output that later steps can read fields from, return a named type alias (type json-string = string;) in WIT, not a plain string.

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). Leave 1.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 function.json text fields to under 255 characters

"Actions compiling…" never finishes

First, check the imports: if the step imports betty-blocks-utilities:*, switch to the current betty-blocks-types:*@[version] imports and rebuild. This hang can have other causes too, and not all of them are known yet. If the imports are up to date, hover over the error in the Action Builder header to see the full message, then contact Betty Blocks support with that message and your step's world.wit and function.json files.

"Expected a string, received [object]" at run time

Remove option<T> from the step's outputs in WIT

An Object output shows as raw JSON text

Return a named type alias in WIT instead of a plain string

400 error at publish, although validate passes

The live schema is stricter than validate. Check the meta of the option the error path names, for example #/options/2/meta

Missing .wasm file in …/say-hello/1.0

Delete functions/say-hello/ or build it

package '…' not found during the build

Run wkg wit fetch first

Undefined symbols on cargo build

Build with --target wasm32-wasip2

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.