Module Blocks: JavaScript and WebAssembly
Write a Block as a modern ES module with npm libraries and WebAssembly, run sandboxed in the browser with progress, cancel, on-device media processing and the StillMade host API.
A module Block ("runtime": "module") is an ordinary ES module. It can use everything a modern browser Worker offers: async/await, timers, typed arrays, WebAssembly, OffscreenCanvas, createImageBitmap, WebCodecs, and any npm package you bundle into it, including C and C++ libraries compiled to WebAssembly (OpenCV, libvips builds, codecs, solvers). Use it when a recipe or the QuickJS javascript runtime is too small for the job.
Folder layout
my-block/
stillmade.block.json manifest with "runtime": "module"
module/ everything the Block runs
main.js the entry (bundle npm dependencies into it)
filters.wasm optional WebAssembly and data files
module.json optional: {"entry": "other.js"}
tests/fixtures.json [{name, input, expected}]Start one with stillmade-block create my-block module. Bundle npm dependencies into module/main.js with any ES module bundler (esbuild, rolldown, Vite). A module holds up to 64 files, 64 MiB each and 128 MiB in total.
The entry
export default async function run(input, stillmade) {
stillmade.progress(0.1, 'Loading');
const { instance } = await WebAssembly.instantiate(stillmade.asset('filters.wasm'));
const bytes = await stillmade.files.read(input.photo); // a saved image the Block received
// ... process ...
const saved = await stillmade.files.write(pngBlob, { mediaType: 'image/png', name: 'result.png' });
return { result: saved };
}Return an object whose keys are your declared outputs. Outputs are checked against the manifest's port types.
The stillmade host API
| Member | What it does |
|---|---|
input | The validated input values (the same object as the first argument). |
signal | An AbortSignal that fires when the person cancels. |
progress(value, label) | Reports progress from 0 to 1 with a short label. Each report extends the run limit. |
log(...values) | Adds a line to the run log (up to 500 lines). |
asset(path) | A copy of one of the module's own files, such as a .wasm build, as an ArrayBuffer. |
files.read(ref) | The bytes of a saved image, video, audio or asset the Block received as input, or that StillMade made in this run: files it wrote, transform results and generated media. Other files cannot be read, and transforms, hosted calls and actions accept only these references too. |
files.write(data, {mediaType, name}) | Saves PNG, JPEG, WebP, GIF, MP4, WebM, MP3, WAV, M4A or OGG, or a PDF, TXT, CSV, Markdown, JSON or ZIP file, to the runner's library and returns a reference you can return as an output. At most 64 files per run. |
media.transform(request) | On-device media work done by StillMade: decode audio to samples, extract or loudness-normalise audio, cut, join or re-encode video, and take thumbnails. Results are saved like files.write. See Media processing. |
actions.run(action) | Runs a declared portable action. media.generate asks the person to approve one StillMade credit maximum for the batch; stock.search runs directly. |
hosted.run(operation, request) | Calls a hosted operation the manifest lists in permissions.capabilities (for example text.generate, image.generate, audio.speech, web.research). Each call is quoted and approved like a capability Block and paid with StillMade credits. |
net.fetch(url, {method, headers, body}) | Calls one of the origins declared in permissions.network through StillMade, which adds the person's own key or OAuth token on its servers. Returns a standard Response. See Outside APIs. |
connections.call(appId, operationId, input) | Calls an operation of a published adapter declared in permissions.connections, with the person's own account. Returns the checked JSON result. |
storage.get/set/remove/list/info(...) | The Block's own cloud storage in the current project: JSON values up to 1 MiB under folder-like keys. Kept across runs, devices and the Block's versions. See Project storage, reads and assets. |
storage.putFile(key, data, {mediaType}) / storage.getFile(key) | Files up to 64 MiB in the same storage; getFile returns {key, mediaType, bytes, data} with data as an ArrayBuffer, or null. |
project.read(fields?) | A copy of the current project limited to the fields the manifest may read (project.read plus context.<field>.read), such as timeline, shots or assets. |
assets.create(file, {name}) / assets.appendVersion(asset, file) | Adds a saved image, video or audio file to the project's media as a new asset (asset.create), or as the newest version of an asset the Block received, read or created (asset.version.append). |
Hosted calls take the operation's request fields, for example:
const plan = await stillmade.hosted.run('text.generate', {
prompt: `Write a 20-second podcast intro about ${input.topic}.`,
system: 'Warm, short sentences.', maxTokens: 400,
schema: { type: 'object', required: ['script'], properties: { script: { type: 'string' } } },
});
const voice = await stillmade.hosted.run('audio.speech', { text: plan.script, settings: { voice: 'adam' } });image.describe, media.analyze and audio.transcribe take saved references (image/images, media, audio; transcription also takes an MP4 or WebM video and transcribes its sound); web.fetch takes url and instruction; web.research takes topic; music and sound effects take prompt and durationSec.
Media processing
stillmade.media.transform(request) asks StillMade to do heavy media work on the person's device: Web Audio for audio, and the same renderer as Final Render for video (WebCodecs in the browser, the native renderer on desktop). It never runs on StillMade's servers and costs no credits. A transform reads only files the Block received, wrote or generated in this run, and saves its result to the runner's library, so you can return it as an output or pass it to the next transform.
| Request | Result |
|---|---|
{operation: 'audio.decode', source, sampleRate?} | {sampleRate, duration, samples}: mono Float32Array PCM for analysis (default 16 kHz; 8000 to 48000). |
{operation: 'audio.extract', source, format?} | A saved WAV (16-bit, 48 kHz) or MP3 of the audio track. |
{operation: 'audio.normalize', source, targetLufs?, format?} | A saved WAV or MP3 at the target integrated loudness (default -16 LUFS, peaks at most -1 dBFS). |
{operation: 'video.cut', source, keep, settings?} | A saved MP4 with only the keep ranges ([{start, end}] seconds, ordered, up to 500). |
{operation: 'video.concat', sources, settings?} | A saved MP4 of 2 to 50 videos in order. |
{operation: 'video.resize', source, settings} | A saved MP4 re-encoded with new settings. |
{operation: 'image.thumbnail', source, at?, width?} | A saved PNG of one frame (default the first frame, 1280 px wide). |
source/sources are saved references ({kind, assetId, versionId, url}).
Render settings are `{fps: 24|30|60, resolution: 720|1080|2160, quality:
'low'|'medium'|'high'}`; the default is 30 fps, 1080, high. Video keeps the source's aspect ratio. Rendering follows the person's export plan, like any export.
Pure helpers ship as @stillmade/block-sdk/media for you to bundle: silentRanges(samples, sampleRate, {thresholdDb, minSilence}), keepRanges(silent, duration, {padding}), integratedLoudness(channels, sampleRate) (ITU-R BS.1770), loudnessGain, encodeWav and decodeWav.
import { silentRanges, keepRanges } from '@stillmade/block-sdk/media';
export default async function run(input, stillmade) {
const audio = await stillmade.media.transform({ operation: 'audio.decode', source: input.video });
const keep = keepRanges(silentRanges(audio.samples, audio.sampleRate), audio.duration);
const trimmed = await stillmade.media.transform({ operation: 'video.cut', source: input.video, keep });
return { trimmed };
}For anything else, decode and encode in the Block itself: the Worker has WebCodecs (VideoDecoder, VideoEncoder, AudioDecoder), OffscreenCanvas and createImageBitmap, and you can bundle mp4-muxer, mp4box or a WebAssembly codec. Images arrive as saved references: read the bytes with files.read and decode them with createImageBitmap, at full resolution.
Sandbox and limits
- The module runs in one dedicated Worker inside a response-sandboxed frame
with an opaque origin. It has no network (fetch, WebSocket and remote imports fail), no storage of the app, no page or DOM access, and cannot read another Block's files.
- Every file is fetched from StillMade's store and checked against its SHA-256
before anything runs.
- A run may take 2 minutes; each progress report allows 60 more seconds. Time
StillMade spends on a host call (rendering a video, waiting for the person to approve credits) does not count. Every run stops after 30 minutes in total. When a limit passes, or the person cancels, the frame is removed and the Worker stops immediately, even inside an endless loop.
- Generating code at run time (
new Function, used by Emscripten bindings) is
allowed inside the sandbox.
Module Blocks run in StillMade in the person's browser, only while StillMade is open. They cannot run on a schedule, from an event or on new media: StillMade has no server sandbox for them. A workflow's background run stops before a module Step and the person runs it in the project. Marketplace listings show module Blocks as Browser only. For work that must run unattended, use a JavaScript Block.
Test, pack and import
stillmade-block test my-block # runs fixtures in a local Node worker
stillmade-block preview my-block input.json
stillmade-block pack my-block # writes <id>-<version>.stillmade-module.jsontest and preview run your code on your machine in a disposable Node worker with the same host API and run limits (not a security sandbox; it is your own code). files.read, files.write, media.transform, actions and hosted calls need StillMade, or a host you pass to createNodeModuleExecutor in your own tests.
Import the .stillmade-module.json in StillMade (Create, then Import). StillMade verifies every file digest, uploads each file to its content-addressed store, and saves the Block. Fixtures then run in your browser's module sandbox.
Uploaded module files count against your cloud storage. A digest alone never grants access: a person can read a module file only when they uploaded those exact bytes or can open a saved Block that names it (yours, an installed or community version, or one shared with their team).
See examples/module-opencv/ for a Block that runs OpenCV on a 12-megapixel photo, examples/module-silence-trimmer/ for one that cuts the pauses out of a talking video, and examples/module-audio-extract/ for one that saves a video's audio as WAV.