# 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

```js
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](#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](/docs/build/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](/docs/build/project-storage). |
| `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:

```js
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`.

```js
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

```sh
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.json
```

`test` 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.
