# StillMade developer documentation
URL: https://www.stillmade.shop/docs
## Build for StillMade
A Block is a reusable production capability. Place it in a Project Type and it becomes a Step, with its own configuration and pinned version. StillMade handles the production workspace, typed connections, and persistent project context.
## Build an original Block
Give your coding agent [the SDK authoring contract](/docs/tools/llm) and your idea. Build on the SDK contracts from the first iteration, then select the finished source folder in StillMade. The implementation and interface import unchanged; no adaptation phase is needed. See [original SDK folders](/docs/build/original-blocks).
## Adapt an existing repository
This public SDK contains the contracts, examples and tooling needed to adapt a GitHub repository outside StillMade. Coding agents can inspect the repository, retain licensing and return one installable .stillmade-block file. [Portable packages and repository adaptation](/docs/tools/repository-adaptation).
## Choose what to build
- [Build your first Block](/docs/build/quickstart): create, test, preview, and package a working capability.
- [Block builder contract](/docs/build/block-builder-contract): the rules every Block follows, however it is made, and how readiness is judged.
- [Tutorial: a React Block in 30 minutes](/docs/build/tutorial): build, preview with hot reload, test offline, import and publish.
- [What you can build](/docs/gallery): templates and complete examples across video, images, music, data, documents, integrations, education and e-commerce.
- [Preview with hot reload](/docs/build/local-preview): start from a template for any runtime, preview the interface in StillMade's own view host, and test offline with stand-ins for every hosted operation.
- [Generate images](/docs/build/hosted-images): create a reviewed prompt-to-image Block with quality and payment chosen in StillMade.
- [Generate speech from a script](/docs/build/hosted-speech): declare typed audio, suggest a catalog voice, review cost, and listen before accepting.
- [Frame views](/docs/build/frame-views): interfaces written with React or another framework, SVG, canvas and drag, with theming and shared editing.
- [Outside APIs](/docs/build/outside-apis): call any API with the person's own key or OAuth sign-in, through declared origins and published OpenAPI adapters; keys never reach Block code.
- [Project storage, reads and assets](/docs/build/project-storage): keep values and files of up to 64 MiB per project in the Block's cloud storage, read the project, add media assets and propose edits people review and undo.
- [Triggers and unattended runs](/docs/build/triggers): run on new media, a schedule, a webhook, another project's completion or a provider event, once the owner turns it on; hosted Blocks stay inside an approved budget.
- [Data types](/docs/build/data-types): files (PDF, text, CSV, JSON, ZIP), documents, tables, links, dates and colors, with their controls, result layouts and conversions.
- [Module Blocks](/docs/build/modules): modern JavaScript with npm libraries and WebAssembly (OpenCV and other C/C++ builds), sandboxed in the browser, with on-device media processing (decode, cut, join, extract and normalise audio and video).
- [Analyze media](/docs/build/media-analysis): ask about a saved video (optionally one time range) or up to four images and get timestamped evidence.
- [Read the web](/docs/build/web-reading): read a public page or research a topic with cited sources, paid with StillMade credits.
- [Generate music and sound effects](/docs/build/hosted-music): create music or a sound effect with the app's generators, paid with StillMade credits.
- [Transcribe stored audio](/docs/build/transcription): return reviewed text and word timings through the host-owned BYOK boundary.
- [Add speech to the Editor](/docs/build/audio-to-editor): place an accepted project recording on the audio lane at the playhead.
- [Build a Project Type](/docs/project-types/build): assemble an ordered workflow from compatible Blocks.
- [Build with your own AI coding tool](/docs/tools/llm): take the complete authoring guide to Claude, Codex, Gemini, or another coding assistant.
## SDK and documentation
The **SDK** is the code, validators, isolated runtime, TypeScript declarations, examples, and command-line tools you download and run. This **documentation** explains their contracts, workflows, and integration boundaries. Both are versioned for SDK 0.1.0.
[Download the SDK](/docs/sdk) or explore the [API reference](/docs/reference/execution).
## Development lifecycle
1. Describe the capability and declare its typed inputs and outputs.
2. Implement a recipe, standalone JavaScript function body, or supported declarative host request.
3. Test realistic synthetic examples, including edge cases.
4. Preview the interface with `stillmade-block dev` (hot reload, stand-ins for hosted calls), package the tested source, and import it into StillMade.
5. Place the Block into a Project Type and rehearse its connections.
6. Release an immutable version when you are ready to share.
## Production model
Normal users work through ordered Steps. They add a capability at a position in the workflow; compatible primary ports connect automatically. Secondary values can come from scoped project context. A technical graph is not required to build a Project Type.
Read [Blocks, Steps, and Project Types](/docs/build/concepts) before designing a package.
---
# Build an original SDK folder
URL: https://www.stillmade.shop/docs/build/original-blocks
Extract `/block-sdk/sdk-docs.zip` into a development directory, then run:
```sh
node packages/block-cli/cli.js create my-block javascript
node packages/block-cli/cli.js validate my-block
node packages/block-cli/cli.js test my-block
node packages/block-cli/cli.js pack my-block my-block.stillmade-block --original
```
`create` supplies an editable starter. Replace its identity, task behavior, controls
and fixtures with your Block's requirements, retaining applicable SDK license
notices. The directory layout is:
```text
my-block/
stillmade.block.json
src/run.js
src/view.html optional custom interface
src/view.css optional styles
src/view.js optional interface behavior
tests/fixtures.json
```
`src/run.js` is an isolated function body operating on `input`; it returns declared
outputs. The custom interface uses the supplied `StillMade` bridge. A photo editor
can read typed image pixels, apply its edits in `run.js`, show before/after images
through `StillMade.previewImage`, and call `StillMade.run` from its controls.
Author those files directly under the SDK contract; external framework packages,
Node services and a separate standalone website are not this runtime's source.
No build/install command executes during import.
Select **my-block** with Choose folder to import that source unchanged. Development
tools may live beside it; select the Block directory, not an SDK checkout containing
several example builds. Use at least two fixtures (normal and edge cases) for the
canonical archive, and use `--original` only for your own original implementation
without reused repository/dependency code. Direct folder import and archive import
retain the same runtime and interface; packaging adds explicit retained-source
evidence. Security, source-rights confirmation and hosted execution approvals still
use the ordinary review path. They do not adapt the Block.
This is the implemented SDK, not the full future runtime catalog. It supports
data-only recipes, isolated JavaScript, reviewed hosted text and speech generation, and a reviewed ComfyUI core-image adapter,
typed inputs/outputs, schema-driven controls,
optional sandboxed HTML interfaces,
behavior fixtures, package validation, standalone execution, and app import.
Python source, external modules, native dependencies, custom ComfyUI nodes and
per-frame editor renderers are not executable through this SDK version. ComfyUI
requires an explicitly selected account connection and a real remote review;
recipe and JavaScript examples continue to run offline.
---
# Build your first Block
URL: https://www.stillmade.shop/docs/build/quickstart
## Prerequisites
Download and extract the [SDK 0.1.0](/block-sdk/stillmade-sdk-0.1.0.zip). Install Node.js 22 or later. Run commands from the extracted SDK folder. The runtime and its dependencies are bundled; the following example needs no account, API key, npm install, or network request.
## Create a Block
```sh
node packages/block-cli/cli.js create ./my-block javascript
```
The scaffold creates these editable files:
```text
my-block/
stillmade.block.json
src/run.js
tests/fixtures.json
```
## Understand the source
The manifest declares the contract. The source is a synchronous function body receiving validated inputs through `input`:
```js
const text = input.text.trim();
return { text, words: text ? text.split(/\s+/).length : 0 };
```
The exact input/output names come from the [manifest](/docs/reference/manifest). There is no server to configure and no hidden credential to supply.
## Validate and test
```sh
node packages/block-cli/cli.js validate ./my-block
node packages/block-cli/cli.js test ./my-block
node packages/block-cli/cli.js preview ./my-block
```
Validation checks package structure. Tests execute every fixture through the isolated runtime and compare the entire returned output with its expected value. Preview executes the first fixture and prints its outputs. A failed test must be corrected before packaging.
## Import the finished folder
Open [Create import](/create/import), choose **Choose folder**, and select `my-block`. Its manifest, implementation, fixtures and optional `src/view.html`, `src/view.css`, `src/view.js` interface load unchanged. Review checks and the actual preview, then confirm import after the required source-rights and device review. The SDK contracts are the foundation from the first development step; there is no source adaptation phase.
## Package an alternate delivery file
For an original implementation, retain complete license notices and at least two fixtures (normal and edge cases), then run:
```sh
node packages/block-cli/cli.js pack ./my-block ./my-block.stillmade-block --original
```
Select that file with **Choose package** for the same source and interface. Packaging adds retained source evidence and reruns applicable checks; it does not rewrite the implementation. Output files are never silently overwritten. Use repository evidence instead of `--original` when reusing third-party code.
## Photo-editor folder example
The extracted SDK contains `examples/original-photo-editor/`, including its actual custom interface, pixel processing and fixtures. Select that folder directly or try [its packaged original](/block-sdk/examples/original-photo-editor.stillmade-block). This bounded example adjusts brightness/color and rotates pixels with transparency. See [the supported original-authoring contract](/docs/build/original-blocks).
## Use and iterate
Add the Block to a Project Type, configure its inputs, and run it. Keep source changes in checkpoints; publish a new release version when the behavior changes. See [versioning and remixing](/docs/distribute/versioning).
---
# Block builder contract
URL: https://www.stillmade.shop/docs/build/block-builder-contract
Contract `stillmade.block-builder@1.0.0`. Humans, coding agents, imported source, GitHub repositories and generated code all adapt to the Block platform; anything that becomes a StillMade Block obeys this contract.
## Source of truth
Treat the current SDK (`stillmade.block.json` manifest, `packages/block-sdk` contracts, validators, examples and published docs) as the source of truth. Before writing code, read the manifest contract, at least two working first-party Blocks closest to the request, the runtime you will use, the typed port and resolver contracts, and the shared-interface contract when you need a custom view. Do not invent runtimes, operations, permissions, ports, host services or bridge calls. If the documentation and the validator disagree, the validator wins and the mismatch should be reported.
## Reuse first
Before building, check what already exists (`find_existing_blocks` over MCP, or the Marketplace and your library) by the inputs, results and operations the goal needs. Use an installed or reviewed Block that already does it; remix a close one so its license, notices and creator credit stay attached; chain existing Blocks and build only the missing step. Never republish another creator's source without remix lineage: public copies are refused. Build a new Block when nothing fits or the person asks for their own version, and say which similar Blocks you considered.
## Define the capability first
Translate the request into an explicit capability contract before implementation: what the Block does; each input with its type, meaning and whether it is required (prefer optional settings with safe defaults over extra required questions); each output with its type and meaning; persistent state; project context it reads; permissions; UI; long-running behavior; expected failures; cancellation; determinism; and whether it needs hosted models, media, desktop tools or shared editing. Keep the public interface as small and general as possible and keep implementation details out of it.
## Blocks compose
A Block is not a mini-app. Its outputs become other Blocks' inputs and its inputs come from other Blocks, project context or the user. Use the typed port system: shared StillMade types (`image`, `video`, `audio`, `asset`, `script`, `shot[]`, ...), a `semantic` meaning on every port, `required` and a short `description`. Mark exactly one primary input and one primary output so Project Type Steps connect automatically, and use `kind: "task"`: a `workspace` Block owns a project document and is never connected automatically. Inputs marked `context` read the project and cannot receive a connection from another Step. Output reusable StillMade values: media references (`{kind,assetId,versionId,url}`) and structured production values with `schemaVersion: 1`, preserving provenance, versions and relationships downstream Blocks need. Never make ordinary users wire JSON, copy IDs, understand internal paths or translate one Block's output into another: `createUniversalResolver()` (the same engine as the Project Type composer) resolves inputs from compatible upstream outputs, permitted project context and, for optional non-context inputs, `StillMade.requestFromUser`.
## Use platform primitives
Prefer, in order: an SDK capability; a documented host capability (the hosted operations are text.generate, audio.speech, audio.transcribe, audio.music, audio.sfx, image.generate, video.generate, image.describe, media.analyze, web.fetch, web.research, timeline.propose, workspace.propose, connection.execute; connected third-party apps go through `connection.execute`); an approved dependency the SDK already bundles; small Block-local code. Do not duplicate what StillMade provides. A Block never contains its own asset system, file picker, authentication, project-context system, input resolver, Block connection system, credit system, secrets store, permission system, progress system, multiplayer system, persistence system, model-provider abstraction or execution protocol.
## No hidden workarounds
Never make a Block look functional through hard-coded local paths, developer-machine dependencies, undocumented environment variables, admin or database access, hidden core modifications, direct database writes, fake or mocked production results, hand-prepared test assets that hide incompatibilities, credentials in code, or assumptions about one user's machine. The finished Block must work through the same interfaces available to any installed Block; `permissions.network`, `permissions.filesystem` and `permissions.secrets` must be empty arrays.
## Interface
If the capability needs UI, use the Block UI SDK: generated `manifest.ui` controls first; a sandboxed custom view (`view.html`/`view.css`/`view.js`) only when the core interaction genuinely needs one. Expose the capability, not the implementation: meaningful controls, sensible defaults, preview, direct manipulation, progress, clear errors and visible outputs. Do not show raw JSON, internal IDs, filesystem details, provider terminology or developer settings unless the Block is itself a developer tool. Follow the host appearance (Light, Dark and White) and the mobile layout contract: one interface for every screen size that fits a 320 CSS-pixel phone without horizontal scrolling.
## Shared editing
Projects are multiplayer. A custom view subscribes through `StillMade.onShared(callback)` and publishes every editable field: give static controls a unique `id` and `data-stillmade-share="field"` (the host owns their synchronization), use `StillMade.bindShared(field, elementId)` for controls created dynamically, and `StillMade.updateShared(values, before, options)` for structured values; private variables and DOM values are not shared. Mark a truly device-only control (playback, hover, device selection) with `data-stillmade-local`. Observe shared outputs as well as state, honor `canEdit` in every mutating callback (read-only participants cannot write), never run, click or call a paid integration in response to a remote update, and preserve conflicting drafts. Shared background work uses `sharedWork` with the `work.shared` permission.
## External services
Reach external services only through StillMade's hosted operations, the connections service (`connection.execute`), or, in a module Block, `stillmade.net.fetch` to origins declared in `permissions.network` (with the key each needs) and `stillmade.connections.call` to adapters or catalog apps in `permissions.connections`; never put keys in code, inputs or headers: StillMade adds each person's own key on its servers. The host owns the model and provider choice, BYOK keys, StillMade credits, spending consent, authorization, token refresh, rate limits, retries and provider errors; a Block never receives a secret and never names a provider unless it must. Treat provider unavailability, timeout, cancellation and partial failure as typed outcomes the Block reports, not crashes. Generated images, video, voices, music and sound effects come from hosted operations or the `media.generate` action, paid with the user's StillMade credits at StillMade's prices; for structured model output declare a `json` output with `capability.schema` instead of parsing prose. When a Block needs npm libraries, WebAssembly or heavy compute, use `runtime: "module"` and reach hosted operations through `stillmade.hosted.run` with the operations listed in `permissions.capabilities`. Decode, cut, join, re-encode, extract or normalise audio and video with `stillmade.media.transform` on the device; never ask for a server render. For a rich interactive interface (React or another framework, SVG, canvas, drag), use a frame view: `src/view.config.json` with `{"runtime":"frame"}` and `src/view.js` bundled as one IIFE, colored with StillMade's theme variables. For files and structured data use the `file`, `document`, `table`, `url`, `date` and `color` types and their generated controls and presentations, not JSON text.
## Dependencies
A Block must declare everything it requires and must not assume anything is installed. Portable packages carry no package dependencies (`portable.dependencies` is empty): inline small, license-compatible code and record its license and notice in `manifest.license` and `manifest.provenance`. Desktop-only tools are reached solely through declared desktop permissions and the host desktop bridge. If a dependency cannot be delivered through the Block runtime, report the incompatibility instead of assuming the user's machine has it.
## Security
Treat Block code, external data, files, repositories, model output and network responses as untrusted. Request the minimum permissions and capabilities. Do not weaken the sandbox (QuickJS has no fetch, DOM, timers, modules or Node APIs). Validate and bound every value crossing a boundary. Never expose secrets through logs, outputs, project context, UI, error messages or serialized state.
## Execution lifecycle
A successful run means more than a returned value. Validate inputs before work; return only declared output ports with values matching their types; run long work as a cooperative job (`job.pending` with monotonic progress) instead of blocking; honor cancellation and the host timeout; keep persistent state in `manifest.state` (module Blocks: `stillmade.storage`) with migration routes so a reopened project and a repeated run behave the same; never leave orphaned jobs, temporary values or corrupted project state after cancellation or failure.
## Useful failures
Errors must say what failed, why it likely failed, whether a retry makes sense and what the user can do, so the native project chat and controller can troubleshoot. In JavaScript, throw an `Error` with that message: the host reports it as a `JAVASCRIPT` run failure carrying your message (guest code cannot set its own error code). Reject invalid or oversized input up front with a message naming the input and the limit. Never reduce a failure to "Something went wrong".
## Adapt to the platform
Do not modify StillMade core because a request does not fit immediately. First implement the capability entirely through documented SDK and platform primitives. If that is impossible, classify why: A Block implementation problem (fix it), B missing SDK exposure of an existing host capability, C missing generic platform primitive, D security or permission restriction, E runtime or language incompatibility, F fundamentally incompatible capability. For B or C, name the smallest general primitive that would serve this Block and a meaningful class of future Blocks (shaped like `files.watch()` or `media.transform()`, never `supportThisSpecificBlock()`). Return `{"unsupported":"exact reason","gapClass":"B|C|D|E|F","primitive":"..."}` instead of a package that pretends to work.
## What StillMade verifies
StillMade runs the readiness check on every Block it admits: manifest and contract validation, the source security scan, every fixture in the sandbox, wrong-type and over-limit inputs, declared output types, cancellation and timeout cleanup, state reopen and repeated runs, composition with a real upstream and downstream catalog Block through the resolver, a Project Type preview, the shared-interface contract with two participants editing a custom view in the real preview runtime (their edits must converge and a read-only participant must not write), and phone and desktop layout. Design for all of them and include normal and edge-case fixtures (1 to 50; stateful Blocks also declare `expectedState`). Test materially different inputs within the declared capability, and document limitations instead of overclaiming generality. Two of these checks need StillMade itself: two-participant shared editing and the phone and desktop UI profile run in StillMade's preview runtime when the Block is imported or released, so `ready` in the downloaded SDK reports them as not verified rather than passed.
## Project Types
A Project Type is an ordered production workflow. Each Step's primary output must be a compatible input for the next Step, secondary values come from permitted project context, and every member Block obeys this contract. Install Blocks into a realistic Project Type: project chat must understand each Step, inputs must resolve intuitively, outputs and execution state must be visible, failures must be diagnosable, and the user must be able to continue through the workflow.
## Readiness report
Finish with a Block Readiness Report: capability, contract (inputs, outputs, permissions, dependencies), platform APIs used, tests actually performed and their results, the composition test (upstream, Block, downstream), known limitations, unsupported requirements and any platform gap with its smallest general primitive. Choose exactly one status: PRODUCTION READY (every applicable check passed), READY WITH DOCUMENTED LIMITATIONS (works reliably within stated boundaries), BLOCKED BY PLATFORM CAPABILITY (needs a missing generic primitive) or FAILED. Never label a Block production ready to satisfy a request, and never claim a check ran when it did not.
## Definition of done
A Block is done only when every applicable item holds. Passing a compile, one unit test, a rendered UI or one happy-path sample is not done.
- DOD-01 Installs through the normal Block mechanism
- DOD-02 Manifest and contracts validate
- DOD-03 Runs through the normal runtime
- DOD-04 Required permissions are declared
- DOD-05 Required dependencies are reproducible
- DOD-06 Real inputs work
- DOD-07 Invalid inputs fail correctly
- DOD-08 Outputs conform to their declared types
- DOD-09 Outputs are usable by compatible downstream Blocks
- DOD-10 Universal resolver integration works
- DOD-11 Applicable project context works
- DOD-12 Applicable persistence works
- DOD-13 Applicable multiplayer behavior works
- DOD-14 Cancellation and failure cleanup work
- DOD-15 UI works in relevant states and screen sizes
- DOD-16 No hidden developer setup is required
- DOD-17 Security boundaries remain intact
- DOD-18 Relevant integration tests pass
- DOD-19 Works after application restart or project reopen
- DOD-20 A realistic Project Type execution succeeds when applicable
## Platform gap classes
- A: Block implementation problem
- B: Missing SDK exposure of an existing host capability
- C: Missing generic platform primitive
- D: Security or permission restriction
- E: Runtime or language incompatibility
- F: Fundamentally incompatible capability
---
# Blocks, Steps, and Project Types
URL: https://www.stillmade.shop/docs/build/concepts
## Block
A reusable production workspace or capability. The built-in catalog consists of full screens: Script Writer, Voiceover, Shots, Blueprint, Canvas, Editor, Image Panels, White Board Motion, and Assets. Chat is the protected starting workspace. Internal canvas nodes and individual editor controls are implementation details inside their workspace, not separate built-in Blocks.
External processing Blocks, such as Thicken Line Art or lip sync, add a capability to the workflow. A Block declares input and output ports, permissions, controls, and an implementation runtime.
## Step
An instance of a Block inside a Project Type. Each Step has its own instance ID, pinned Block ID and version, configuration, enabled state, and optional condition. Reusing a Block in two positions creates two independent Steps.
## Project Type
An ordered production workflow. Its persisted `stages` array stores Steps for compatibility with existing projects. Users can add, remove, duplicate, configure, disable, or reorder Steps. The protected starting chat belongs to the host and cannot be replaced or remixed.
## Typed connections
An image-producing Block can connect to an image-consuming Block. Primary ports make insertion predictable; semantic roles distinguish a source image from a character reference. Secondary inputs can bind to project context.
See [types and compatibility](/docs/reference/types), [context](/docs/build/context), and [Project Type construction](/docs/project-types/build).
## Complete workspace Blocks
Built-in Blocks are the existing complete production screens. A Guided Project Type follows Chat → Voiceover → Shots → Blueprint → Canvas → Editor; export belongs to Editor. The reusable Script Writer is the full Narrative Compiler; it and the legacy Script adapter do not replace Chat. Canvas remains the home of persistent visual references and character consistency.
The Block boundary follows the complete user-placeable Step. Package and remix the complete workspace, including its related tools. For example, Motion Graphics remains inside the Editor; Image Panels, Blueprint, and the Whiteboard Step are each one Block. The White Board Motion Project Type contains protected Chat followed by Voiceover, Whiteboard, and Editor: four complete Blocks, with no extra Project-Type Block. Historical component IDs may remain resolvable for saved projects without becoming discoverable Blocks.
A source snapshot or host alias alone is not portable. A portable workspace owns its interface and executes through an installable SDK runtime. The first-party sm.blueprint@2.0.0, sm.board-editor@2.0.0, sm.editor@2.0.0, and sm.pipeline@2.0.0 packages in the downloadable SDK demonstrate complete sandboxed workspaces using typed documents and the public before/after workspace proposal contract. Whiteboard retains all boards, placed media, timing, transitions, motion settings, and custom fields as one remixable Block. Editor retains Motion Graphics and every other editing tool inside the Editor Block. Canvas retains its native content and layout while the Project Type builder owns Step placement and wiring. Package-supplied React bundles are not exposed; use [custom sandboxed HTML interfaces](/docs/build/custom-interface).
---
# Script Writer workspace
URL: https://www.stillmade.shop/docs/build/script-writer
Script Writer (`sm.script@1.1.0`) reuses the full Narrative Compiler: reference
transcripts, writing and optional research, review, inline edits, revision chat,
saved scripts, learned voices, and version history. Open its listing in Marketplace
or the Block library, then choose **Use in studio → Open workspace**. It is no
longer an account-menu utility. Starting Chat remains separate and protected.
The manifest and adapter are in `packages/block-platform/script-writer.js`.
They use the same SDK `validateValues` and `resolveContextInputs` contract as other
Blocks. `client/src/components/ScriptWriterBlock.jsx` mounts the existing UI.
- Primary `input`: optional `brief` document (schemaVersion 1) with production
planning context. The app supplies the project title and conversation. A Step
can prefill the topic with `config.input: {schemaVersion: 1, topic: "Your topic"}`.
- Secondary `script`: optional `script` document from `context.script.read`,
containing `{schemaVersion: 1, text: "Current narration"}`.
- Primary `output`: `script`, semantic role `narration_script`, containing
`{schemaVersion: 1, text: "Accepted narration"}`. Empty scripts, non-text values,
and text over 200,000 characters are rejected.
**Use this script** validates the output and updates the shared project narration.
The recommended workflow is **Script Writer → Voiceover**; its compatible typed
connection is implemented through the shared narration document, without graph
nodes or wires. Opening the Block does not generate a script or charge credits.
Existing writing and research actions retain their displayed prices and server
billing checks. A viewer cannot write; accepting a stale draft cannot overwrite a
teammate's changed narration.
The version-addressed source download includes the current wrapper, Narrative
Compiler, local source dependencies, and this SDK. This is a trusted built-in host workspace, not
an arbitrary React package admitted by the external sandbox. External imports
still support recipe and isolated JavaScript runtimes; changing an imported
manifest to `runtime: host` cannot load privileged workspace code.
---
# Send a script to Voiceover
URL: https://www.stillmade.shop/docs/build/script-handoff
A recipe, JavaScript, or hosted text-generation Block can supply narration to a
Voiceover Step through an explicit Project Type connection. Add both Steps,
then use **Advanced connections and legacy workflows → Connect steps** to connect
the Block's declared `script` output to Voiceover's `input`. Placing the Steps
next to one another alone does not create this document handoff. Production users
then work through the normal Step screens; no Canvas node or wire is required.
The [Script draft example](/block-sdk/examples/script-draft.stillmade.json) declares
`outputs: {"script":{"type":"script","primary":true}}`. Embed that exact package
in the Project Type's `packages` array, with these Steps and connection:
```json
{
"stages": [
{"id":"draft","blockId":"example.script-draft","version":"1.0.0","label":"Draft narration"},
{"id":"narration","blockId":"sm.voiceover","version":"1.1.0","label":"Voiceover"}
],
"connections": [
{"from":{"stage":"draft","port":"script"},"to":{"stage":"narration","port":"input"}}
]
}
```
This is the relevant portion of a Project Type, not a complete package. The
selected output must contain `{"schemaVersion":1,"text":"Your narration"}` with
non-empty text of at most 200,000 characters. The hosted text runtime has its
smaller 32,768-character output limit. A plain `text` port is not automatically
converted to `script`; authors must declare and return the structured type.
Run the source Block, then continue to Voiceover. Review **Use script from [Step]**
and choose **Apply script**, or **Keep current** to leave the existing narration.
Applying rechecks the exact source placement, pinned version, completed result,
current narration and edit access. Stale, skipped or unverifiable results are not
substituted; older saved outputs may need a fresh run. A new library release does
not replace the pinned Block. Multiple results are not concatenated or selected
arbitrarily for narration.
Applying preserves existing audio takes and history, clears stale word timings
and transcript text, and marks the narration changed. Shared narration uses the
normal collaboration and conflict controls. Voice generation and timing analysis
remain separate explicit actions; opening Voiceover or applying a script does not
start a paid audio request. Protected Chat and the built-in Script Writer's
Narrative Compiler remain unchanged.
---
# Custom Block interfaces
URL: https://www.stillmade.shop/docs/build/custom-interface
A package can include an optional **`view`** beside `manifest`, `code`/`recipe`,
and `tests`. This is the Block's real interface in Preview: a user edits controls,
presses its action, and sees the code's result. Packages without `view` use
StillMade's generated controls and output viewer. `manifest.ui` remains the
fallback controls schema; it is not the custom interface source.
`view` optionally declares `appearance: "original"` or `"host"` and contains
`html` (required, up to 64 KiB), `css` (optional, up to 32 KiB),
and `javascript` (optional, up to 64 KiB). These are UTF-8 byte limits. For folder
packages, use separate `src/view.html`, `src/view.css`, and `src/view.js` files
(the last two are optional). Optional theme-specific CSS uses
`src/view.themed.css`. The older `src/view.json` object format is also supported,
but never combine it with separate interface files. CLI pack, folder import,
and ZIP import reconstruct the same `view` object and run the same validators.
Use **Choose folder** on the import page to select an edited SDK package directory.
`src/run.js` remains the production function; `src/view.js` controls the interface. A standard view is a reviewed subset of standalone
controls: no framework, URL, npm module or build step runs inside it. For React
or another framework, SVG, canvas or WebGL, drag, timers and loops, use a
[frame view](/docs/build/frame-views) instead.
### Interface bridge
The host provides `StillMade` inside the sandbox:
| API | Behavior |
| --- | --- |
| `StillMade.input` | Copy of the current preview input bindings after initialization |
| `StillMade.onInput(callback)` | Receives initial input and later host input changes; returns an unsubscribe function |
| `StillMade.connectDocument(options)` | Connects a declared JSON content store to shared edits; returns status, flush and recovery methods |
| `await StillMade.previewImage(value)` | Resolves a currently authorized image to a bounded, display-only PNG copy; preserves the original run input |
| `await StillMade.run(values)` | Validates the named input bindings, runs the package in the isolated computation worker, validates outputs, and resolves to the output bindings object |
`admitPackage` and local admission include a **Shared interface contract** source
check. The lightest path for an ordinary form control is one HTML annotation:
``. The host turns
that into the same checked, permission-aware binding as `bindShared`; the Block
does not implement networking, cursors, batching, conflict transport or retries.
A `connectDocument` content-store connection supplies the platform-owned
receive/publication paths for an existing structured store. Otherwise an
`onShared` subscription is required, and detected editable controls must call
`bindShared` or `updateShared`. This is a minimum static prerequisite,
not full collaboration verification. Its `runtimeVerified` remains false even
when it passes. Calls may be unreachable or fail to cover all controls; actual
two-participant acceptance remains required. Runtime safety checks for already
pinned immutable packages are separate and do not rewrite those releases.
The general `scanPackage` keeps compatibility by default; passing
`{collaboration:true}` requests this admission prerequisite. Creation and import
services use `scanAdmissionPackage` for the same mandatory source check,
including hosted Blocks. This is not yet an enforced two-user runtime acceptance
guarantee for every import route.
Project workspaces also expose `StillMade.onShared(callback)` and
`StillMade.getShared()`. The snapshot contains `state` (shared interface JSON),
`outputs` (the latest saved results, including another collaborator's generation),
`memory` (declared Block runtime memory), and `canEdit`. Use
`StillMade.updateShared({field: value})` to queue a shared interface edit. It
checks edit permission and per-field preconditions; the host sync status reports
durable acknowledgement. In saved Step workspaces, the promise resolves only
after an acknowledged shared save, so `run` waits for pending edits to reach
storage. Builder previews remain local. The `{queued:true}` result does not
itself expose a server receipt or establish durability in every host.
The unchanged original photo-editor folder has local behavioral acceptance:
independent owner/editor/viewer browsers share edits and saved image outputs,
reopen them, wait for a delayed save, preserve independent concurrent fields and
reject stale field preconditions. Actual isolated producer/editor/consumer
execution verifies its input/output handoff. Fixture identities and storage
transport are used; authors still need to exercise their own complete journey.
Saved declared outputs can be previewed with the current package pin and exact
host media receipt; a shared URL alone does not grant image access.
The host's existing guarded Step/Home exits also await pending direct
`updateShared` writes, including dependent promise continuations in the current
task. Rejected fields remain unconfirmed until an acknowledged replacement for
those fields; unrelated successful edits do not clear them. This internal
barrier is not exposed to Block code, does not cover every navigation route,
and does not wait for future provider results or arbitrary private callbacks.
The state is limited to 384 KiB of UTF-8 JSON per placement. Shared edit requests,
including before-values, are capped at 800,000 bytes; in-flight and queued requests
together are capped at 2 MiB. The project still has its existing 1 MB operation
envelope and 4 MB live-document limits. These limits are cumulative, not an
unlimited quota per Block. Use named fields and commit text
drafts on blur or a short trailing debounce; do not send an entire document on
every pointer movement. Observe `onShared` to render remote changes and results.
Conflicting edits must be reconciled with the latest snapshot, not silently
overwritten. Builder previews simulate this state locally, without network sharing.
For a structured object, opt into checked nested merging with
`StillMade.updateShared({plan: editedPlan}, {plan: {exists: true, value: basePlan}}, {merge: true})`.
`basePlan` must be the exact snapshot used to construct `editedPlan`, not a newer
snapshot read just before sending. Unchanged nested fields are retained from the
current shared state. Arrays of objects with distinct string `id` fields merge
by identity; independent edits and additions can coexist. Overlapping edits,
deleting an item someone changed, or incompatible reorderings reject the entire
patch. Scalars, non-keyed arrays and normal calls without `merge` stay strict.
This is not character-level text merging. Structured merges still obey the
384 KiB state limit and 2,000-operation limit per structured field. Keep edits
bounded and retain a rejected local draft for explicit resolution. Initializing
an absent field is a strict write; do not assume concurrent initializations merge.
For a structured editor, `StillMade.scheduleSharedEdit('plan', callback)` replaces
the pending callback for that name, running after 150 ms without another call or
at 500 ms maximum wait. Capture the original edit base once, keep the local draft,
and serialize/publish in the callback. Do not recapture a newer remote base for
an old edit. There are at most 32 pending names; closing the interface cancels
them unless the host first completes an explicit flush. Step-header navigation
now requests that flush and waits for the bound controls and scheduled callbacks.
Return the write promise from a scheduled callback so it can be awaited. This is
not a server save acknowledgement or a durable queue. Keep a pending-edit
indicator and block Review until your writes settle; surface errors and retain
the draft. Check permission again before publishing. Do not schedule paid runs
or replay user actions from remote updates.
For a large object document, `StillMade.diffDocument(base, draft)` returns checked
field/identity operations and `StillMade.applyDocument(base, operations)` returns
a new document or throws on a stale precondition. Both are pure local helpers:
they do not save anything, mutate the supplied object, grant permissions, or run
a Block. They use the same operation implementation as project collaboration.
Each object document is limited to 4 MiB of UTF-8 JSON; operations are limited to
384 KiB and 2,000 entries. Reorder operations carry IDs instead of copies of the
entire collection, retaining concurrently added items and supporting replay.
Unchanged executable source, runtime memory and nested interface state do not
appear in a field edit. Actual insertion/deletion/replacement values still count
toward all limits. Paths and collection identities remain validated.
`StillMade.indexById(items)` returns a detached `Map` for a JSON collection with
distinct nonempty string IDs (at most 10,000 items and 4 MiB). Changing the map or
its values cannot mutate the input. Use this bounded helper for identity lookup
instead of repeatedly scanning large collections in instrumented view callbacks.
These helpers let a workspace keep a bounded edit overlay rather than copying a
large source document into `state`. Publishing, overlay merging, original edit
bases, conflict recovery and `onShared` rendering are still the Block's job.
Never rebase an old edit by substituting newer before-values, and never treat a
successful local apply as a server acknowledgement. Call document helpers in a
scheduled edit, not on every pointer event. The helpers alone do not make an
existing local-only interface collaborative.
### Connect an existing content store once
`StillMade.connectDocument` owns shared subscriptions, checked edit overlays,
150 ms quiet / 500 ms maximum batching, in-flight rebasing and conflict status.
If two participants initialize an empty shared field together, a rejected
initializer waits up to five seconds for the actual winning snapshot before
rebasing its original edit intent. It does not invent a base or blindly retry;
conflicts, permission loss, disposal and missing-snapshot timeouts preserve the
local draft and require recovery.
Keep the Block's existing synchronous JSON store and renderer:
```js
const shared = StillMade.connectDocument({
key: 'boardDraft',
source: initialBoard,
collections: [
{path: ['strip'], key: 'panelId'},
{path: ['strip', '*', 'elements'], key: 'id'},
],
read: () => draft,
replace: value => { draft = value; draw(); },
});
// Add this to the existing content-change callback, not every individual control:
const changed = () => shared.changed();
// Optional: subscribe: notify => existingStore.subscribe(notify)
// replaces explicit changed() calls for stores with a change subscription.
StillMade.onInput(input => shared.replaceSource(input.board));
shared.onStatus(status => { save.disabled = !status.canEdit || !!status.error; });
```
`replace` must synchronously replace content and redraw without saving, running
a Block or dispatching other side effects. Local selection, playback, preview
URLs, credentials and device state do not belong in the shared document. Source
refreshes use `replaceSource`; a user reset/undo is a content change and must
notify `changed`. Inputs, raw-text drafts and delayed result handlers that bypass
the store still require adaptation. Private variables are never discovered or
automatically synchronized.
Collection declarations address original document fields. `*` traverses an
explicitly declared parent collection. Declared IDs must be distinct nonempty
strings; missing IDs do not fall back to array indexes. `panelId`, `shotId`, `id`
and other safe declared keys preserve their original saved shapes. Renaming an
identity is a checked removal/insertion. The adapter shares small overlays, not
copies of unchanged documents. Existing 4 MiB document, 384 KiB shared-state and
2,000-edit limits still apply; there are at most eight connected documents in a
view, each using a different shared field.
For legacy data whose accepted contract permits missing, duplicate or otherwise
unusable IDs, opt in explicitly with
`{path:['scenes'],key:'shotId',fallback:'atomic'}`. Use that same declaration for
every participant; never select a different codec based on the first loaded
document. The codec uses a canonical tagged representation: usable identities
still support small item-level edits, while legacy collections remain exact
checked atomic values. It never invents IDs or silently normalizes saved data.
Atomic collection changes can conflict or exceed the shared-state limit on large
documents; they are not a substitute for author-defined stable identities.
Changes between source representations keep the ordinary explicit
source-conflict/reset rules. Strict declarations retain their original encoding.
`getStatus()` and `onStatus(callback)` expose `phase`, `canEdit`, `dirty`,
`pending`, `sourceConflict` and `error`; `onStatus` returns an unsubscribe function. The host also
shows pending changes or the current adapter error. `await shared.flush()` waits
for local transport acknowledgement, **not a durable server receipt**. The
existing guarded workspace exits flush registered document connections before
the shared-write barrier. Before a run that depends on the final draft, await
`shared.flush()` and then read the current content. Incoming shared results do
not replay a run; use `onShared` to display results/progress separately.
Conflicting drafts stay local and visible. `await shared.retry()` retries against
the preserved edit baseline. `await shared.discard()` acknowledges an exact
no-op for this shared field before discarding its local draft, clearing that
field's rejected-write ledger. A racing teammate edit, a changed local draft,
pending writes or readonly/closed access refuse recovery rather than silently
erase content or claim a successful exit. Other failed fields still block exit.
`dispose()` refuses dirty, pending or failed connections; flush/discard first.
This adapter is not character-level text merging and does not establish runtime
coverage for arbitrary imported interfaces. No existing immutable Block release
is rewritten merely by exposing this API.
If a project input changes so an existing shared overlay no longer applies,
`replaceSource` retains the last valid source and remembers the incoming source
without publishing it. Source errors appear through adapter status, not a second
generic `onInput` exception that would outlive recovery. Ordinary retry/discard refuse to silently use that stale
source. Present a deliberate review/confirmation action before calling
`await shared.resetSharedSource({discardSharedEdits:true})`: **this clears the
shared draft for everyone**, not just the caller. The reset uses an exact current
field precondition and adopts the remembered source only after acknowledgement.
If a teammate acknowledges a compatible reset, the controller can adopt its
remembered incoming source without needing another input event. It rebases only
unconfirmed local intent, never reinstates the obsolete shared draft. A local
edit that conflicts with the reset remains visible and requires explicit discard;
ordinary retry cannot republish the stale shared content.
Readonly access, pending writes, racing teammate edits, newer local edits or a
newer incoming source prevent that adoption and preserve the local draft for
fresh review. An acknowledged reset may already have cleared the old shared
overlay when a subsequent local/source change is detected; the adapter reports
that failure and requires a new explicit confirmation. Reset never runs a Block.
For an ordinary static editable control, prefer
`data-stillmade-share="title"` on the element and give it a unique `id`. No
authored JavaScript adapter is needed. For a dynamically created or replaced
control, call `StillMade.bindShared('title','title-input')` once after its element
exists. Mark a genuinely device-only playback/filter control with
`data-stillmade-local`; admission rejects an unexplained static control when the
interface does not use a structured `updateShared` or `connectDocument` adapter.
Both shared forms use the same host-owned binding and share a text, numeric,
checkbox, or select value with a 150 ms trailing debounce, a 500 ms maximum
batch wait during continuous typing, and a blur flush. Unchanged/duplicate native
events do not publish or restart the delay. An edit already in flight must settle
before the next write; network and persistence latency are separate. It
observes remote changes, respects read-only access, and returns an unsubscribe
function. It never dispatches synthetic input/click events or runs a Block in
response to a teammate's edit. Derive non-DOM state and render saved outputs
through `onShared`; private variables are not synchronized by binding a control.
Do not bind credentials, file pickers, transient selection, or generation buttons.
If a bound control is replaced dynamically, unsubscribe before binding its replacement.
Text composition (IME) stays local until it ends; the debounce and maximum-wait
timer do not publish unfinished preedit. Incoming shared values/defaults cannot
overwrite the composing control. Its final edit retains the original field
precondition, so a same-field teammate edit still conflicts instead of being
silently overwritten. Workspace exit and `StillMade.run` reject while composition
is unfinished. If editing access changes during composition, the final draft
stays local even if access returns; duplicate native input events cannot submit
it. A genuinely changed later value may retry against the original checked base.
This does not provide character-level text merging or a general save-before-run
guarantee.
Use `StillMade.setSharedDefaults({title: initialTitle})` when input-derived
defaults change. This updates defaults only for existing bound fields; it does
not publish an edit, override a saved shared value, or replace a dirty/in-flight
draft. Do not assign to a bound control's value during remote rendering. Read
its current value for an explicit run so a preserved draft is not replaced by
an unrelated teammate's update. Keep slider/input elements mounted while a
gesture is active; update labels without rebuilding those controls.
Conflicting drafts stay visible with a validation error; do not automatically
overwrite the teammate's version. Dispose and rebind after the user chooses to
discard a conflicting draft. Builder previews use local-only shared state
(`localOnly: true`); this is not evidence of network collaboration.
`updateShared` queues up to 32 waiting updates and serializes their per-field
preconditions. A conflict rejects dependent queued updates instead of silently
rebasing them. An optional second argument supplies explicit field preconditions
captured when a draft began: `{title: {exists: true, value: 'original'}}`.
An acknowledgement timeout fails closed until the Block is reopened. This
protects against blindly retrying an edit whose acceptance is unknown.
Cursor forwarding is installed by the host in all admitted sandbox views and
does not require package code. Arbitrary private JavaScript variables and DOM
mutations are **not** shared state. When adapting a repository, move persistent
interface values onto this contract; importing a repository alone cannot make
its private state collaborative. The transport supports authenticated pushed
invalidations when configured, with polling fallback; concurrent
edits to the same text field can conflict; this is not character-level CRDT editing.
Always use `onInput` to initialize controls; the interface may load before its
initial values arrive. Catch run errors and display them in the interface. Disable
the run control while a request is pending. One run can be active; the host limits
the view to 30 requests per minute, 4 MiB and 500,000 JSON nodes per message, and
15 seconds for local execution. Host-confirmed ComfyUI, text and speech requests
allow up to 6 minutes; image generation allows up to 15 minutes for confirmation
and its bounded fixture/sample sequence. These are interface wait budgets, not
permission to retry or extend provider deadlines. Raw RGBA messages must fit both
message budgets (roughly 124,000 pixels after reserving nodes for other inputs);
use smaller preview images.
The existing computation limits (including the JavaScript 500 ms guest deadline)
still apply. Reset discards the interface and aborts its pending computation.
Closing Preview disposes the frame and aborts outstanding work.
For a port named `prompt`, initialize its control from the host input callback:
```js
const inputField = document.getElementById('prompt');
StillMade.onInput(input => { inputField.value = input.prompt || ''; });
```
Inside the async run button handler, call
`await StillMade.run({prompt: inputField.value})` and display its output.
Use `inputField` or `promptInput` for local variables; `prompt` is also a browser
API name and cannot be a local identifier. Its data-property exception applies
only to the single, unshadowed and unreassigned parameter of a direct inline
`StillMade.onInput` callback; declare that parameter name only once in the
interface source. Use ordinary `.prompt` access there, rather than
computed/destructured access or a property on an unrelated object. Object literal
`{prompt: value}` keys are supported. Browser dialogs remain unavailable.
Use the DOM for interaction and rendering: `document.getElementById`,
`querySelector`, `addEventListener`, `textContent`, `value`, `checked`, `disabled`,
`hidden`, and canvas are supported. Declare elements in `html`; show/hide existing
elements or draw into a declared canvas. Dynamic element creation, HTML insertion,
reflection, computed dynamic property lookups, module imports, browser navigation,
account APIs, parent/window access, and storage are rejected. Property access such
as `result.text` and `image.data[0]` is supported; use array methods for iteration.
Synchronous loops, function declarations, directly recursive helpers, timers, and
microtask queues are rejected. Every callback/helper also checks a private task
budget (10,000 calls or 50 ms between checks), including indirect recursion. This
guard cannot interrupt a single native browser operation, so it is not a
substitute for the computation worker. Use small native array operations such as `map`/`forEach` for
rendering; heavy work belongs in the isolated Block runtime.
Use `.textContent` for user text. Tags are ordinary controls, text, layout, media,
and canvas; no script, iframe, SVG, link, form, or document metadata tags. Markup
must use complete tags without HTML comments, inline event handlers, or navigation
attributes. JavaScript belongs in `view.javascript`, not the HTML string.
The frame has an opaque origin and `sandbox="allow-scripts"`, without same-origin,
popups, forms, downloads, or top navigation permissions. CSP denies connections,
workers, frames, external scripts, and resources. Image and media sources must be
local `data:` or `blob:` URLs. Styles cannot import resources. No token, account,
project mutation API, filesystem, or parent DOM handle is passed into the frame.
Only copied inputs, explicit run results, and authorized image display copies cross the bridge. Static checks
are an additional admission gate; they do not replace this browser boundary or
the separate computation sandbox. Keep the UI responsive: move computation into
`code` or `recipe`; DOM JavaScript is not the CPU-metered computation runtime.
A file input can read a user-selected image with `FileReader.readAsDataURL`, load
it into an existing ` `, and draw it into a declared ``. Use
`getImageData` to pass `{width,height,data:Array.from(pixels.data)}` to an image
input, staying within the SDK image and message budgets. Nothing is uploaded by
the view. Use the host's file controls when you do not need a custom picker.
The host supplies StillMade's shared Light, Dark and White theme variables and basic controls.
For optional StillMade appearance, inherit its typography or use
`var(--font-body)` for controls and `var(--font-display)` with weight 200 and
italic style for display headings. Use shared spacing, radius and color tokens
such as `var(--bg)`, `var(--fg)`, `var(--fg-muted)`, `var(--border-tok)`, and
`var(--r-3)`. Original appearance may keep its own typography and colors within
the existing sandbox; importing external fonts or resources remains unavailable.
Use a clear input, primary action, and visible result. Preview runs
synthetic samples or the user's explicitly chosen input; it does not commit to a
production project. SDK fixture tests prove code behavior and input/output
compatibility. They do not claim to test browser interaction: review the actual
custom interface in Preview before **Confirm Import**.
### Appearance compatibility
StillMade has three modes: **Light** (cream), **Dark** (ink), and **White**
(neutral white surfaces with black text). Generated controls inherit the app mode.
A custom interface may keep its original design by explicitly setting
`view.appearance: "original"`. The surrounding StillMade app keeps its own theme.
Omitting this field, or setting it to `"host"`, preserves the existing requirement
to provide theme-compatible colors. Existing package sources and installed pins
are never rewritten to change appearance.
A minimal original-style view is:
```json
{
"appearance": "original",
"html": " ",
"css": "body{background:#14213d;color:#fff}",
"javascript": "StillMade.onShared(shared=>{document.getElementById('result').textContent=shared.outputs.text||'';});"
}
```
Keep this object in `src/view.json` when using the explicit appearance field;
do not combine it with split `src/view.html`/CSS/JavaScript files. Folder, archive
and GitHub SDK-folder imports retain the field, as do source editing and export.
The stylesheets have separate purposes:
- `view.css` (up to 32 KiB) retains the original design. Original-style packages
use it inside projects as well as in Original design previews.
- `view.themedCss` (optional, up to 32 KiB) is a **complete replacement stylesheet**
for authors who also want StillMade appearance. Include the same layout and
responsive rules using host tokens. Preview offers this mode only when the
complete view passes the host-appearance check.
For example, original CSS can use `body { background:#14213d; color:#fff; }`.
Its optional themed stylesheet uses `body { background:var(--bg); color:var(--fg); }`.
Move fixed inline colors into the stylesheets when offering both appearances.
Never change image/video/canvas pixels to match the interface palette.
For StillMade appearance, use `var(--bg)`, `var(--bg-elev-1)`, `var(--bg-elev-2)`,
`var(--fg)`, `var(--fg-muted)`, `var(--border-tok)`, `var(--signal)`, and
`var(--shadow-1)`. The SDK exports `APPEARANCE_TOKENS`. Inherit host typography
or use `var(--font-body)` and `var(--font-display)`. Do not redefine host tokens
or use raw palette tokens such as `--cream-100` in theme-aware declarations.
Original appearance does not require these color or typography conventions.
**The appearance declaration does not widen the interface runtime.** Existing
HTML/JavaScript syntax restrictions, CSS restrictions on escapes, filters and
color blending, resource-loading bans, size limits, shared-state requirements,
permissions, CSP and typed input/output validation still apply. Use CSS classes
for interface changes; dynamic element-style mutation remains unavailable.
Static original colors are allowed only with the explicit original mode;
any optional themed stylesheet still needs valid semantic colors.
`scanPackage`, `admitPackage`, and CLI validation check the declared appearance
alongside all other admission gates. A styling pass does not prove working UI,
accessibility or full repository compatibility. Review the actual interface on
its declared devices and, when supplied, each themed mode. This option preserves
styling within the existing restricted view format; it does not load framework
bundles, arbitrary React applications, remote fonts or external scripts.
### Display an authorized image
Use **`await StillMade.previewImage(value)`** for an image selected in
StillMade, received from a connected Block, or present in explicitly granted
project context. The host also supports its registered sample pixels, local
uploads, and verified run outputs. A well-formed reference alone does not grant
access: a changed URL, version, role, unrelated asset, or expired preview session
is rejected. Catch and display the error; do not fall back to fetching the URL.
The result is exactly `{url, width, height}`. Its URL is a PNG data URL
that can be assigned to a declared ` `. It is a display copy, at most
512 pixels on either edge, with aspect ratio preserved. It does not generate,
upload, save, select, or replace an asset. Continue to pass the **original**
image value to `StillMade.run`; processing uses the original input within
the runtime's own limits. Do not use the display URL as a processing input.
Image display allows one pending request, up to 60 requests per minute, and a
15-second deadline. It is independent of the pending computation request, so an
input can be displayed while its Block runs. Both directions retain the 4 MiB
and 500,000-node message limits. Input changes, source/session reset, or closing
the workspace cancel unfinished display requests. Use a local revision counter
in asynchronous UI code so a late result cannot repaint an earlier selection.
StillMade's host image controls remain available with a custom interface.
A project lets the user select a compatible authorized image or upload one;
connected/context bindings remain read-only. A standalone preview keeps uploaded
files local to that preview. Host uploads accept PNG, JPEG, WebP or GIF up to
12 MB and one megapixel; larger uploads are rejected with a request to resize
a copy. Existing authorized project references can be displayed up to 16
megapixels, but the selected runtime still applies its processing limit. No
automatic processing resize occurs. The host validates permissions and exact media
identity before resolving bytes; no account token, native file handle, or remote
fetch capability enters the iframe.
This `view` example assumes one image input and one image output, both
named `image`. It shows the actual selected input and processed result
while retaining their original typed values:
```json
{
"html": "Selected image Run block
",
"javascript": "const source=document.getElementById('source'), result=document.getElementById('result'), button=document.getElementById('run'), status=document.getElementById('status');\nlet current={}, revision=0;\nStillMade.onInput(input=>{\n current=input;\n const started=++revision;\n source.hidden=true;\n result.hidden=true;\n if(!input.image)return;\n StillMade.previewImage(input.image).then(preview=>{\n if(started!==revision)return;\n source.src=preview.url;\n source.width=preview.width;\n source.height=preview.height;\n source.hidden=false;\n status.textContent='';\n }).catch(error=>{if(started===revision)status.textContent=error.message});\n});\nbutton.addEventListener('click',async()=>{\n const started=revision;\n button.disabled=true;\n try{\n const output=await StillMade.run(current);\n if(started!==revision)return;\n const preview=await StillMade.previewImage(output.image);\n if(started!==revision)return;\n result.src=preview.url;\n result.width=preview.width;\n result.height=preview.height;\n result.hidden=false;\n status.textContent='Finished';\n }catch(error){if(started===revision)status.textContent=error.message}\n finally{button.disabled=false}\n});"
}
```
### Complete custom interface example
Download [custom-interface.stillmade.json](/block-sdk/examples/custom-interface.stillmade.json)
for the complete tested text-cleanup package. Its `view` is:
```json
{
"html": "Clean text Your text Clean text
Result Your result appears here. ",
"css": "output { display:block; white-space:pre-wrap; padding:16px; border:1px solid var(--border-tok); border-radius:var(--r-3); }",
"javascript": "const source=document.getElementById('source'), button=document.getElementById('run'), result=document.getElementById('result'); StillMade.onInput(input=>{source.value=input.text||''}); button.addEventListener('click',async()=>{button.disabled=true;try{const output=await StillMade.run({text:source.value});result.textContent=output.text}catch(error){result.textContent=error.message}finally{button.disabled=false}});"
}
```
### Dynamic lists and controls
Use `StillMade.render(containerId, nodes)` to replace the children of an existing
container with structured controls. Use `StillMade.onAction(callback)` for their
click, input, and change events; it returns an unsubscribe function. This supports
variable-length lists without raw HTML injection or direct DOM creation.
For example, with `
`:
```js
StillMade.onInput(input => {
StillMade.render('choices', (input.shots || []).map(shot => ({
tag: 'button', text: shot.name || shot.id, value: shot.id, action: 'select'
})));
});
StillMade.onAction(event => {
document.getElementById('selection').textContent = event.value;
});
```
Each node has `tag`, optional `text` or `children`, and optional `id`, `className`,
`label` (accessible name), `action`, `value`, `type`, `checked`, `disabled`,
`placeholder`, `min`, `max`, or `step`. Unknown fields are rejected. Supported tags
are div, section, article, header, footer, p, span, strong, em, small, h1–h4,
label, button, input, textarea, select, option, ul, ol, li, table, thead, tbody,
tr, th, td, details, summary, output, and progress. Input types are text, number,
range, checkbox, radio, and color. Use a div, section, article, main, aside, ul,
ol, or tbody as the existing root. Style nodes with classes in your view CSS.
An action belongs to a button, input, textarea, or select. Its callback receives
`{action, value, checked, event}`. Buttons emit click; checkbox, radio, and select
emit change; other fields emit input. Stable IDs preserve matching controls,
focus and selection during reconciliation. Keep edited values in your declared
content store, not only in the DOM. Text inputs emit one committed action at the
end of IME composition, not intermediate preedit or duplicate final input events.
Sibling redraws preserve active composition. If the same control's declared value
changes remotely during composition, the renderer retains local text and blocks
dispatch/exit rather than applying it against a silently advanced model base.
Copy the retained text before reopening the Block to review the shared version;
there is no automatic merge or retry. Disabling a composing control also retains
its draft and blocks run/exit after access returns. Removed or rebound controls
cannot commit their old composition to a different action; removed-control draft
recovery is not provided. Call `StillMade.run` explicitly
to execute production code. Rendering
does not mutate project context or bypass execution permissions.
Limits: 1,000 nodes per render, 12 child levels, 256 KiB of serialized descriptors,
5,000 total frame elements, 16 action listeners, 10,000 characters per text/value,
120 per ID/action and 240 per class/name/placeholder. IDs must be unique. Invalid
renders leave the previous controls intact. Text is always literal; scripts,
inline handlers, links, file inputs, network attributes, and raw HTML are rejected.
### Audio and video playback
`await StillMade.previewMedia(reference)` opens an authorized `audio` or `video`
reference as a temporary playback copy and returns `{kind, url, mimeType, bytes}`.
Assign its URL to an existing `` or `` element.
The host requires an exact reference from this Block’s bound input, an explicitly
selected project asset, permitted project context, or a host-recorded output.
A URL alone is insufficient. Preview has host file pickers for audio/video inputs;
project Steps can select existing project assets or receive connected media.
For `
`:
```js
const player = document.getElementById('player');
const status = document.getElementById('status');
let revision = 0, activeUrl = '';
StillMade.onInput(input => {
const started = ++revision;
player.pause(); player.src = '';
if (activeUrl) StillMade.releaseMedia(activeUrl);
activeUrl = '';
if (!input.audio) { status.textContent = 'Choose audio above.'; return; }
status.textContent = 'Opening audio…';
StillMade.previewMedia(input.audio).then(preview => {
if (started !== revision) { StillMade.releaseMedia(preview.url); return; }
activeUrl = preview.url;
player.src = preview.url;
status.textContent = 'Ready to play.';
}).catch(error => {
if (started === revision) status.textContent = error.message;
});
});
```
`StillMade.releaseMedia(url)` revokes a playback copy. Release old copies before
loading another: at most two can remain active. Input changes and closing/resetting
the interface revoke all copies and cancel pending reads automatically. Requests
share the image-preview queue and its limit of 60 requests per minute; only one
image/audio/video read can run at a time. Keep the original reference for production
runs; temporary URLs must never be saved as project assets or returned as outputs.
`await StillMade.useOutput(outputs)` selects complete saved media references as the Block outputs without dispatching a generation run. This is available in a saved project workspace only. Every output must match the manifest, and every media identity must come from the current authorized Block input or the current trusted desktop recording session. The host rejects changed URLs, unknown assets, and non-media outputs.
Files must be at most 24 MB and load within 15 seconds. Supported containers include
MP4, WebM, Ogg, QuickTime, MP3, AAC, WAV, and FLAC; actual playback depends on browser
codec support. Remote assets need CORS support and an appropriate media content type.
The host omits credentials and rejects redirects. Network access remains disabled
inside the iframe; larger media should use the existing native Editor workspace.
---
# Hosted text generation
URL: https://www.stillmade.shop/docs/build/hosted-text
Use `runtime: "capability"` to implement a portable Block that asks StillMade to
generate text. The package declares the prompt, instructions, output contract,
and fixtures. StillMade owns account authentication, model choice, provider
keys, cost confirmation, and execution. This is a separate declarative runtime;
`fetch`, `ctx.capabilities`, and arbitrary host calls remain unavailable inside
JavaScript Blocks.
### Create and package
```sh
node packages/block-cli/cli.js create ./script-draft capability
node packages/block-cli/cli.js validate ./script-draft
node packages/block-cli/cli.js pack ./script-draft script-draft.stillmade.json
```
The folder contains `stillmade.block.json`, `src/capability.json`, and
`tests/fixtures.json`. A packed JSON contains `{manifest, capability, tests}`
and may include a sandboxed `view`. No provider key, endpoint, model, billing
selection, or receipt belongs in the package. Those fields are rejected.
Download [the complete script-draft example](/block-sdk/examples/script-draft.stillmade.json).
Its manifest uses `entry: "src/capability.json"`, one primary `prompt` input of
type `text`, one primary `script` output of type `script`, and:
```json
{"project":[],"capabilities":["text.generate"],"network":[],"filesystem":[],"secrets":[]}
```
Its capability declaration is:
```json
{
"schemaVersion": 1,
"operation": "text.generate",
"prompt": {"$input": "prompt"},
"system": "Write a concise narration script. Return only the script text.",
"maxTokens": 1200,
"output": "script"
}
```
Declare exactly one prompt input (`text`, `script`, or `brief`) and exactly one
output (`text`, `script`, or `json`). Structured prompt inputs contain
`{"schemaVersion":1,"text":"..."}`. A generated script returns that same
structured shape; a text output returns a string. Typed ports, primary markers,
semantic roles, and scoped project context retain their SDK meaning. A context
prompt needs its declared read permission and fresh authorized project snapshot.
Do not infer scene or character structure from an unstructured generated script.
Up to three other read-only inputs (for example the saved project script) can
also reach the model: list them in the descriptor as `"context": ["script"]`.
StillMade appends their current values after the prompt as delimited data the
model is told never to treat as instructions, and the review covers that exact
text. An input that is not listed (one a custom view only displays) is never
sent.
#### Conversations
To continue a conversation, declare one optional `json` input for the earlier
turns and bind it as `history`:
```json
{"schemaVersion":1,"operation":"text.generate","prompt":{"$input":"message"},
"history":{"$input":"history"},"system":"You are a friendly script coach.",
"maxTokens":800,"output":"reply"}
```
The history value is up to 20 turns of `{"role":"user"|"assistant","content":"..."}`
(each at most 8,000 characters, 24,000 in total); StillMade sends them between
the system instructions and the prompt, and the review covers the exact turns.
Other fields and other roles are rejected. A missing history starts a fresh
conversation. A custom view or a JavaScript Step can keep the turns and pass
them back on the next run.
#### Structured replies (JSON schema)
For data instead of prose, declare a `json` output and add `schema`, the JSON
Schema the reply must match:
```json
{"schemaVersion":1,"operation":"text.generate","prompt":{"$input":"topic"},
"system":"You plan short podcast intros.","maxTokens":800,"output":"plan",
"schema":{"type":"object","additionalProperties":false,"required":["title","script"],
"properties":{"title":{"type":"string","maxLength":80},
"script":{"type":"string","maxLength":1200},
"beats":{"type":"array","items":{"type":"string"},"minItems":3,"maxItems":6}}}}
```
StillMade tells the model to reply with JSON matching the schema, parses the
reply (one surrounding code fence is tolerated) and checks it against the
schema before any Step receives it. A reply that does not match fails the run
with the first mismatch, for example `$.beats: needs at least 3 items`. The
output port then carries the parsed value, so a JavaScript Block downstream
reads `input.plan.title` directly.
The supported subset is `type` (one or a list of `object`, `array`, `string`,
`number`, `integer`, `boolean`, `null`), `description`, `title`, `enum`,
`properties`, `required`, `additionalProperties`, `items`, `minItems`,
`maxItems`, `minLength`, `maxLength`, `pattern`, `format` (`date`,
`date-time`, `email`, `uri`), `minimum` and `maximum`; up to 8 levels deep and
8,192 characters. The system instructions plus the schema instruction must fit
in 8,000 characters. Fixture expectations for a json output are
`{"kind":"json"}` with optional `includes` literal phrases.
Limits: package 256 KiB; prompt 32,000 characters; system instructions 8,000
characters; generated text 32,768 characters; `maxTokens` integer 1–4096.
The operation is one text generation call. Image, audio, video, tool calling,
streaming, multi-call agent loops, and arbitrary API requests are not supported
by this declaration.
### Fixtures and real review
Generated text varies, so fixtures use bounded expectations instead of an exact
`expected` string:
```json
[{
"name": "A short opening narration",
"input": {"prompt":"Write two narration sentences about a quiet forest at sunrise."},
"expectations": {"script":{"kind":"script","minLength":20,"maxLength":6000}}
}]
```
Include 1–3 fixtures. Every output has its matching `kind`, positive `minLength`,
and `maxLength` no larger than the output limit. Optional `includes` contains at
most eight required literal strings of at most 256 characters each; regular
expressions are not supported. Expectations check the declared interface and
bounded behavior, not factual correctness. Review the actual generated result.
Offline `validate` and `pack` check source only. `pack` reports `tests: 0`,
`reviewRequired: true`, and `liveVerified: false`. Offline `test` and `preview`
fail with `RUNTIME_UNAVAILABLE`; they do not invent a generation result.
Import the JSON or ZIP into StillMade. Test and preview loads available models
and payment methods. **Review cost** obtains a non-billable quote. Only the
separate **Run tests and sample** action submits one call per fixture and one
additional sample call. The host displays the complete call count and credit
cost first. BYOK uses your encrypted account OpenRouter key; provider usage is
billed by that provider and does not consume StillMade generation credits.
A missing BYOK key never silently switches to credits.
The server binds the report to your account, exact source digest, inputs,
resolved model/payment selection, quote, and execution policy. Every fixture,
the separate sample, typed outputs, and expectations must pass. Inspect the
sample and source rights, then **Confirm Import** to install. A client-supplied
`passed` flag or an LLM-written report cannot authorize import or release.
AI authoring generates and statically validates a draft; it stops for this
hosted review and does not silently run the drafted capability.
### Run in a project and recover
Add the reviewed Block to a Project Type to make a Step. Its controls and
optional custom interface use the same host execution dialog. Guest UI cannot
choose a model, reveal keys, or confirm a quote. Existing matching review
results can be reused for project execution, but the current project input
always gets its own quote and explicit run confirmation. An expired import
review can be reused for a project only after the server revalidates its source,
selection, policy, and current project edit access.
Each confirmed request has an account-scoped UUID and durable execution record.
The host advances one invocation at a time, retains results, and makes duplicate
requests idempotent. An unknown provider outcome is not automatically submitted
again. If the creation response is lost, the host finds the original request by
its UUID without generating. Continuing an unfinished recovered review requires
another explicit Continue generation action; it advances the already confirmed
plan, not a new request. Cancellation cannot guarantee that a provider stops already submitted
work; completed results are retained, while a cancelled or stale project run
cannot be automatically accepted into the project. Failed StillMade-credit
charges use the existing idempotent refund ledger. BYOK provider charges follow
the provider account and cannot be refunded by StillMade.
A Project Type can be imported after every embedded hosted package receives its
own current account review; see [review hosted Project Types](/docs/project-types/hosted-review).
This is package admission, not proof that the connected flow ran. Connected
automatic runs and unattended future-media jobs stop at the hosted capability
boundary; they do not grant advance permission for paid calls.
### Publish a verified listing sample
After releasing the Block publicly, choose **Publish verified sample** in its
listing editor. Confirm the public display of that sample's input and output.
StillMade reuses the exact retained sample from the release review; it does not
call the provider or charge again. The source, account ownership, current runtime
policy, quarantine state, complete fixture evidence, and sample contract are
checked again. An expired import receipt or a removed API key does not invalidate
historical output. Project-run receipts cannot publish a private project sample
through this action. The stored preview records the original model and generation
time, so it is not represented as a new execution.
### Host integration API
```js
import {runPackageAsync, prepareCapabilityInvocation, capabilityOutputs}
from './packages/block-sdk/index.js';
const result = await runPackageAsync(pkg, input, {
capability: async (reviewedPackage, values, {signal}) => {
const request = prepareCapabilityInvocation(reviewedPackage, values);
// Trusted host: authenticate, review cost and approval, execute once,
// retain the result, then return typed outputs. Never implement this in
// package code or treat this SDK callback as proof of import review.
const text = await approvedHostTextRun(request, {signal});
return {outputs: capabilityOutputs(reviewedPackage, text)};
}
});
```
`approvedHostTextRun` is a host integration placeholder, not an SDK function.
The SDK validates declaration and output types; StillMade's server supplies the
account receipt and billing guarantees. Optional returned metadata is bounded
to 8 KiB and is not an authorization receipt. Without a trusted executor,
`runPackageAsync` fails closed. `validateCapability`,
`prepareCapabilityInvocation`, `capabilityOutputs`,
`validateCapabilityExpectations`, and `testCapabilityOutputs` are the portable
validation helpers. They never access a provider.
StillMade's authenticated `/api/block-capabilities` routes provide options,
quotes, review creation/lookup, run creation/status/poll/cancel, and read-only
`GET /requests/:requestId` recovery. Status GETs never execute calls or reconcile
refunds; POST polling/cancellation handles those mutations. They use StillMade's
credit ledger and encrypted BYOK vault or platform model key; Block authors
configure nothing.
---
# Hosted image generation
URL: https://www.stillmade.shop/docs/build/hosted-images
Use `runtime: "capability"` with `operation: "image.generate"` for one visual
prompt (and optional reference images) to one generated PNG. Every image model
the app's own Canvas and Image Panels use is available: Nano Banana 2, 2 Lite and
Pro, GPT Image 2 and 1.5, Kling Image V3 and Seedream 4.5, with their aspect
ratios, resolutions and qualities (see
[generation models](/docs/reference/generation-models)). Users pay with their
StillMade credits at StillMade's own price, or with their OpenAI key for the
direct GPT Image 2 option. The host shows model, settings and the exact price
before any provider call. Imported packages never receive API keys or choose an
endpoint or a price.
### Complete package
The downloaded SDK includes `examples/generate-image.stillmade.json` and
`packages/block-sdk/image-generation-example.js` (export `imageGenerate`).
```json
{
"manifest": {
"schemaVersion": 1,
"sdkVersion": "0.1.0",
"id": "example.generate-image",
"version": "1.0.0",
"name": "Generate an image",
"description": "Generate a square PNG from a short visual prompt using an approved host request.",
"kind": "task",
"runtime": "capability",
"entry": "src/capability.json",
"license": "MIT",
"inputs": {
"prompt": {
"type": "text",
"primary": true
}
},
"outputs": {
"image": {
"type": "image",
"primary": true
}
},
"permissions": {
"project": [],
"capabilities": [
"image.generate"
],
"network": [],
"filesystem": [],
"secrets": []
},
"ui": [
{
"control": "text",
"port": "prompt"
}
]
},
"capability": {
"schemaVersion": 1,
"operation": "image.generate",
"prompt": {
"$input": "prompt"
},
"output": "image"
},
"tests": [
{
"name": "A simple forest illustration",
"input": {
"prompt": "A simple illustration of a quiet forest with clear dark outlines and soft green colors."
},
"expectations": {
"image": {
"kind": "image",
"format": "png",
"width": 1024,
"height": 1024,
"minBytes": 67,
"maxBytes": 12000000
}
}
}
]
}
```
### Inputs and controls
Declare exactly one primary input of type `text`, `script`, or `brief`, optionally
one `image` or `image[]` input of reference images, and one primary output of
type `image`. Script and brief values contain `schemaVersion:1`
and `text`; text ports receive a string. The resolved prompt must contain
1–32,000 characters and cannot be blank. Existing declared project context read
scopes can supply that input through the normal host resolver.
The descriptor contains `schemaVersion`, `operation`, `prompt` and `output`, and
optionally:
- `settings`: the Block's default `{model, aspectRatio, resolution, quality}`.
Each value must be one the model accepts; axes a model has no knob for are
`"default"`.
- `references`: `{"$input":"refs"}`, binding the declared `image`/`image[]`
input. Up to 8 saved images; the model must edit from references (the Nano
Banana family and GPT Image 2).
- `negativePrompt` (up to 2,000 characters) and `seed` (0–2147483647).
Declare exactly `capabilities:["image.generate"]`; network, filesystem and
secrets permissions stay empty. Payment, credentials and price are never
descriptor fields.
```json
"capability": {
"schemaVersion": 1, "operation": "image.generate",
"prompt": {"$input": "prompt"}, "references": {"$input": "refs"},
"settings": {"model": "nano-banana-pro", "aspectRatio": "16:9", "resolution": "2K"},
"negativePrompt": "text, watermark", "output": "image"
}
```
In StillMade, the run dialog preselects the Block's default model and settings;
the user can change them before running and sees the exact credit price. Every
review fixture and the separate sample is included in the displayed total. The
direct OpenAI option (fixed 1024 × 1024, your own key) takes text prompts only.
Reference images must be saved in the user's own library; anything else is
refused before any credit is spent.
### Output and tests
The output is an exact reference to a retained PNG:
```js
{
kind: "image", assetId, versionId, url,
mimeType: "image/png", width, height, bytes
}
```
The host bounds the provider response, decodes the actual PNG, JPEG or WebP,
stores one sRGB PNG under an immutable hash and measures its `width`, `height`
(up to 8192 a side and 40 megapixels) and `bytes` (up to 64 MiB); package code
cannot assert those measurements on its own. `assetId` identifies the execution's
image, `versionId` is its saved SHA-256, and `url` is a StillMade media URL.
Each fixture requires `expectations` for the declared output: `kind:"image"`,
`format:"png"`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`, `minBytes` and
`maxBytes` (ordered integers within the limits). Older fixtures that name the
fixed `width:1024, height:1024` square remain valid.
These checks verify a usable image contract. They cannot prove the picture
matches the meaning of the prompt; inspect the actual image before acceptance.
### Review, preview and project use
Upload the package to run schema, dependency, permissions and static checks.
Offline CLI validation and packaging can pass those checks but report that live
review is required; offline test/preview cannot call the provider or produce a
placeholder. In StillMade, review the cost, confirm generation, inspect every
fixture and sample, finish the review, then **Confirm Import**.
A project run has its own current prompt and quote. **Use this image** accepts
its reviewed output. Compatible image inputs can consume that result through
the host's existing image resolver; sandboxed pixel processing receives actual
decoded pixels rather than a guessed URL. Accepted current project results also
appear in the Editor's visual media inventory. Earlier placed images keep their
original source when the generating Block runs again.
A custom interface can use the existing `StillMade.run` flow. Host image
previews remain outside the isolated iframe, and guest code cannot supply
execution receipts or access the provider. No `generateImage`, provider-key or
arbitrary fetch API is added to the guest interface.
### Host integration and recovery
`prepareCapabilityInvocation` returns `{operation:"image.generate",prompt}` plus
`references`, `negativePrompt` and `seed` when declared. A credit selection is
`{payment:"credits",provider:"stillmade",model,aspectRatio,resolution,quality}`;
the direct key option stays `{provider:"openai",model:"gpt-image-2",size:"1024x1024",quality,payment}`.
`capabilityOutputs(package, imageReference)` maps the generated reference to the
named output. `validateGeneratedImageReference` and
`defaultCapabilityExpectations` enforce the declared shape and bounds.
A trusted executor may return `{outputs, metadata, mediaReceipts}`. The receipt
map is keyed by the output port; each receipt contains its `schemaVersion` (3 for catalog images), all
canonical image fields, plus `ownerId`, `runId`, `index`, `storageKey` and
`sha256`. SDK validation checks shape and equality to the output. The
authenticated host additionally binds owner/run/index and reads the saved bytes
again during review acceptance and completed-result recovery. The receipt is
host evidence, not a credential or a guest-authored permission grant.
Execution uses one exact provider request per invocation, without automatic
provider, payment or model fallback. An uncertain submission is not retried
automatically. Cancelling stops further work and acceptance; it cannot promise
that a remote provider stopped or that already-generated output was free.
Received output that fails storage or contract checks remains a charged request;
a definite provider rejection follows the existing refund path.
Automated checks use local image fixtures and mocked providers. They do not
claim a successful live provider image test. Each account must complete its
actual hosted review before importing a generated-image Block.
### Connected production runs with hosted Blocks
Inside a project, **Run connected steps** passes the actual previous result to
each next Block. Hosted text, speech and image Blocks use StillMade's existing
model and payment dialog for each call. Confirm its cost before execution; a
Block cannot choose approval or payment. Waiting for this choice does not consume
the local sandbox timeout. Cancelling stops downstream work and preserves already
completed results. Use **Use completed results** to adopt them into the project.
Saved hosted results are rechecked against the signed-in account's server run,
including the exact project, source, input, selection, quote and retained media.
This verification does not generate again. Standalone import reviews are not
production outputs. Unattended hosted automation is not enabled by this control.
---
# Hosted speech generation
URL: https://www.stillmade.shop/docs/build/hosted-speech
Use `runtime: "capability"` and `operation: "audio.speech"` to turn up to 3,800
characters of supplied text into one speech recording. This operation speaks
the supplied text; it does not draft a script or run another generation first.
The package contains no provider, payment method, endpoint, credentials, or
execution receipts. It may suggest a voice and speed; StillMade selects and
confirms the final choice outside the package and its optional sandboxed
interface.
### Package and declaration
Download [the complete Narrate a script package](/block-sdk/examples/narrate-script.stillmade.json).
The offline SDK also includes `packages/block-sdk/speech-example.js`, exporting
`audioSpeech`. Copy the example, choose your own namespace, and edit its fixtures:
```sh
node packages/block-cli/cli.js validate examples/narrate-script.stillmade.json
node packages/block-cli/cli.js pack examples/narrate-script.stillmade.json narrate-script.stillmade.json
```
The package has `manifest`, `capability`, `tests`, and optional `view`. Its
manifest declares exactly one primary input of type `text` or `script`, exactly
one primary output of type `audio`, and:
```json
{"project":[],"capabilities":["audio.speech"],"network":[],"filesystem":[],"secrets":[]}
```
The capability object has these four fields; the input and output names
must match its declared ports:
```json
{"schemaVersion":1,"operation":"audio.speech","text":{"$input":"script"},"output":"audio"}
```
It may add `settings` with a suggested StillMade catalog voice and speed. The
dialog preselects them when that voice is available, and the person running the
Block can still change both:
```json
{"schemaVersion":1,"operation":"audio.speech","text":{"$input":"script"},"output":"audio","settings":{"voice":"asteria","speed":1.1}}
```
`voice` is a catalog voice id (lowercase letters, digits, `-` or `_`) and
`speed` is 0.25 to 4. See [Generation models](/docs/reference/generation-models)
for the voice list.
A text input is a string. A script input contains
`{"schemaVersion":1,"text":"Words to speak."}`. Only its `text` is forwarded;
extra structured script fields cannot select a voice or add provider settings.
Text must be nonblank and at most 3,800 characters. Oversized text is rejected,
not silently truncated. A default is permitted; scoped script context still
requires `context.script.read`. Declare generic primary ports unless a matching
semantic role is needed by a particular downstream contract.
### Audio output and fixture expectations
StillMade catalog voices return the provider's measured MP3 or WAV, stored
byte for byte (schema 3 receipts, any sample rate, mono or stereo). The OpenAI
TTS-1 path normalizes and stores 16-bit PCM WAV at 24,000 Hz, mono. Either way
the output is a reference with exactly these fields, using host-created
identities and a saved HTTPS or host media URL:
```json
{
"kind":"audio","assetId":"speech-run-one","versionId":"v1",
"url":"/api/media/owner/speech-one.wav","mimeType":"audio/wav",
"duration":1,"bytes":48044,"sampleRate":24000,"channels":1
}
```
This reference is illustrative; it is not a generated file or execution receipt.
`duration` is measured from the actual PCM sample count; `bytes` includes the
canonical WAV header. Audio must be nonempty, at most 64 MiB (67,108,864 bytes),
and at most 1,400 seconds. The SDK validates finite bounds and the exact shape;
metadata alone cannot establish that bytes exist, are WAV, or belong to a user.
The host performs byte checks and stores a separate bound media receipt.
Include 1–3 fixtures using expectations, never an exact output or a fabricated
audio reference. Each expectation requires exactly `kind`, `format`,
`minDuration`, `maxDuration`, `minBytes`, and `maxBytes`:
```json
{
"name":"Read a short welcome",
"input":{"script":{"schemaVersion":1,"text":"Welcome to the quiet forest."}},
"expectations":{"audio":{"kind":"audio","format":"wav","minDuration":0.1,"maxDuration":30,"minBytes":46,"maxBytes":1500000}}
}
```
Duration bounds are finite seconds with `0 <= minDuration <= maxDuration <= 1400`
and a positive maximum. Byte bounds are integers with
`46 <= minBytes <= maxBytes <= 67108864`. Bounds are inclusive. These checks do
not transcribe the output, assess pronunciation, or verify a speaker's identity.
### Review, listen, and accept
Importing source and requesting **Review cost** do not generate audio. The host
dialog offers **StillMade voices**, the same catalog the app's Voiceover uses
(Deepgram, ElevenLabs, OpenAI and Kokoro voices configured on the server), paid
with StillMade credits at the Voiceover price for the text's length. It also
offers OpenAI TTS-1 with credits or your own key, and a speed from 0.25× to 4×.
Keys remain in the host's encrypted account vault. BYOK uses the account's
OpenAI key; missing keys never switch to credits.
Changing text, source, model, voice, speed, or price requires a new quote.
Review the displayed input, voice, speed, cost for every fixture and the separate
sample, and the total before choosing **Run tests and sample**. Each approved
invocation produces its own recording and receipt. Listen to the AI-generated
audio before choosing **Finish review**, then **Confirm Import** to install the
Block. A project run has its own **Run in project** confirmation and **Use this
audio** acceptance. The shared ledger binds source, inputs, account, project,
voice, speed, payment, quote, and retained outputs. Uncertain submissions are
not automatically repeated; recovering status does not itself generate audio.
The result is a typed audio output and a playable Block result. After accepting
a confirmed project speech run, open **Media** in the Editor and find **Audio
from Blocks** to listen and explicitly **Add at playhead**. See [Add speech to the Editor](/docs/build/audio-to-editor).
Generic audio outputs can also connect to compatible SDK audio inputs.
Adding audio to the Editor does not create or select a native Voiceover take,
replace its master recording or narration, or create word timings or a
transcript. Voiceover's primary input remains `script`; the Editor action does
not introduce an audio-to-Voiceover connection. A sandboxed view may request
`StillMade.run`, which opens the host approval flow; there is no guest
`previewAudio`, timeline mutation, or provider API.
### Validation status and host integration
No live provider speech test has been completed for this implementation.
Schema, fixture-contract, and host-boundary tests use synthetic inputs and
explicit mocks. They do not claim successful provider speech generation. Every
installation still requires the actual host fixtures, sample, playback review,
and import confirmation for the selected account and settings.
Offline `validate` and `pack` work without a key. `pack` reports `tests: 0`,
`reviewRequired: true`, and `liveVerified: false`. Offline `test` and `preview`
return `RUNTIME_UNAVAILABLE`; they do not fetch speech or return placeholders.
`prepareCapabilityInvocation(pkg, input)` returns exactly
`{operation:"audio.speech",text}`. The trusted host executes the separately
approved request and calls `capabilityOutputs(pkg, canonicalAudioReference)`.
`capabilityOperation(manifest)` distinguishes text, speech and image contracts;
`defaultCapabilityExpectations(manifest)` supplies their bounded default checks.
The existing `validateCapabilityExpectations`, `testCapabilityOutputs`, and
`validateCapabilityResult` dispatch through the declared operation.
A trusted speech executor may return `{outputs, metadata, mediaReceipts}`.
`mediaReceipts` is keyed by the exact audio output name. Its receipt has exactly
`schemaVersion:1`, `kind:"audio"`, `ownerId`, `runId`, `index`, `storageKey`,
`sha256`, and the canonical reference's `assetId`, `versionId`, `url`,
`mimeType`, `duration`, `bytes`, `sampleRate`, and `channels`. The SDK validates
shape, index 0–3, hash syntax, and equality to the output, and preserves a copy.
The authenticated host client must additionally verify account/run identity;
package code and custom interfaces cannot submit receipts as execution evidence.
Optional metadata remains bounded to 8 KiB and is not proof of generation.
---
# Frame views: any interface code
URL: https://www.stillmade.shop/docs/build/frame-views
A frame view (`"runtime": "frame"` in the view) is an interface written with
ordinary web code: React, Svelte, Vue or Preact bundles, SVG, canvas and
WebGL, pointer capture and drag, timers, loops, recursion, classes and Web
Workers. Use it when the reviewed subset of standard views is too small for
the interface you want.
### Folder layout
```
my-block/
stillmade.block.json
src/run.js or another runtime (recipe, module, capability)
src/view.html markup, for example
src/view.css styles; use StillMade's theme variables
src/view.js ONE classic script: bundle with --format=iife
src/view.config.json {"runtime": "frame"}
tests/fixtures.json
```
`src/view.config.json` may also set `"appearance"` and `"files"` (assets, see
below). Bundle npm dependencies into `src/view.js` as an IIFE, for example:
```sh
esbuild src/view.jsx --bundle --format=iife --minify --jsx=automatic \
--define:process.env.NODE_ENV='"production"' --outfile=src/view.js
```
Limits: markup and each stylesheet up to 256 KiB, `view.js` up to 4 MiB,
and up to 32 asset files with 8 MiB in total.
### The `StillMade` object
Frame views get the same `StillMade` object as standard views: `input` and
`onInput`, `run`, `useOutput`, `getShared`, `onShared`, `updateShared`,
`bindShared`, `connectDocument`, `previewImage`, `previewMedia`, `render`
and the rest of the interface bridge. In addition:
| Member | What it does |
| --- | --- |
| `StillMade.asset(path)` | A `blob:` URL for one of the view's asset files, for ` `, `new FontFace()` or ``. |
| `StillMade.assetBytes(path)` | A copy of an asset file's bytes as an `ArrayBuffer`. |
Shared state works as in every view: keep each independently edited value in
its own field (for example `point0`, `point1`) so two people editing
different values never conflict.
### Theming
The theme variables from StillMade's design system are always defined:
`var(--bg)`, `var(--bg-elev-1)`, `var(--fg)`, `var(--fg-muted)`,
`var(--accent)`, `var(--border-tok)`, `var(--divider)`, `var(--r-3)` and the
rest. Colors written with them follow Light, Dark and White automatically.
Frame views are not required to use them; the appearance check reports
hard-coded colors as advice instead of refusing the view.
### Assets
Images, fonts, JSON, audio and video the view needs ship as files named by
SHA-256, like module files:
```json
{"runtime": "frame", "files": {"schemaVersion": 1, "files": {
"fonts/inter.woff2": {"sha256": "…", "bytes": 48256, "mediaType": "font/woff2"}
}}}
```
`stillmade-block pack` fills in the digests and sizes. StillMade checks every
file against its SHA-256 before the view starts.
### What the frame does not allow
The view runs in an opaque-origin frame with its own policy:
- no network (`fetch`, WebSocket and remote images, fonts or scripts fail);
- no code from strings (`eval`, `new Function`, inline event attributes) and
no extra script elements;
- no navigation, popups, downloads, forms that submit, or dialogs;
- no WebAssembly on the page (run heavy computation in the Block's runtime,
for example a module Block, and call it with `StillMade.run`);
- no storage or access to StillMade's page.
Workers created from `blob:` URLs are allowed and run off the page's thread.
### Run limits
StillMade compiles the view so every function and loop checks a time budget.
One task may run for 250 ms; past that it is stopped with an error, and a view
that keeps the page busy most of the time, stops answering, or navigates its
frame is closed with a message. Move long computation into a Worker or the Block's runtime.
---
# Outside APIs: declared origins, OAuth and published adapters
URL: https://www.stillmade.shop/docs/build/outside-apis
Module Blocks can call any outside API with the account of the person running
them. Three pieces work together:
- **Declared origins.** `permissions.network` lists the exact https origins the
Block calls (up to 8), each with the key it needs. Code calls
`stillmade.net.fetch(url, {method, headers, body})` and gets a standard
`Response`.
- **OAuth.** A key can be an OAuth sign-in through a reviewed StillMade OAuth
app: `{scheme: "oauth2", app: "notion", label: "Notion workspace"}`.
- **Published adapters.** `permissions.connections` lists approved OpenAPI
adapters by app ID (`app:openapi:notion`). Code calls
`stillmade.connections.call(appId, operationId, input)` and gets the checked
JSON result.
- **Catalog apps.** `permissions.connections` can also list apps from
StillMade's connections catalog (Composio) by app ID, such as `app:notion`,
`app:gmail` or `app:google-sheets`. Code calls
`stillmade.connections.call(appId, actionName, input)` with the service's
action name, and the action runs with the account the person connected. See
[catalog apps](#catalog-apps).
```json
"permissions": {
"project": [],
"connections": ["app:openapi:notion"],
"network": [
"https://api.example.com",
{"origin": "https://api.airtable.com", "auth": {"scheme": "bearer", "label": "Airtable personal access token", "help": "https://airtable.com/create/tokens"}}
],
"filesystem": [],
"secrets": []
}
```
```js
const page = await stillmade.connections.call('app:openapi:notion', 'retrievePage', { path_page_id: input.pageId });
const response = await stillmade.net.fetch(`https://api.airtable.com/v0/${input.baseId}/Pages`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ records: [{ fields: { Name: 'New row' } }] }),
});
if (!response.ok) throw new Error(`Airtable answered ${response.status}`);
```
### Catalog apps
```js
const page = await stillmade.connections.call('app:notion', 'NOTION_CREATE_NOTION_PAGE', {
parent_id: input.parentPage,
title: input.title,
});
```
- The first time a person runs the Block, StillMade asks them to connect their
account for the app (they sign in to the app in a new window) and to allow this
Block to use it. Then, once per session for each action, it shows what the
action does (reads, makes changes, sends, publishes or deletes, or that
StillMade has not reviewed it) and its price.
- The action runs on StillMade's servers with that account. The Block gets the
checked result; the account and its tokens never reach it. People remove a
Block's access in its network access panel.
- Credits are charged to the person running the Block: the action's reviewed
price, or 1 credit per action when StillMade has not reviewed it. If the app
clearly fails, the credits are returned.
If it may have carried out the action (for example a timeout), the credits
are kept and the person is told to check the app before running it again.
- Actions reviewed as spending money on the person's account, or priced by
usage, run only as reviewed workflow Steps. Actions that look like moving
money (payments, charges, transfers, orders, refunds) and payment apps such
as Stripe or PayPal need a reviewed price first. For any other action
StillMade has not reviewed, the approval tells the person it may change,
send or pay for things in their account.
- Offline, answer calls in `tests/mocks.json` under
`connections["app:notion/NOTION_CREATE_NOTION_PAGE"]`.
### Keys belong to the people running the Block
Key schemes are `bearer` (the `Authorization` header), `header` (a named header,
for example `X-Api-Key`), `query` (a named URL parameter) and `oauth2`. The first
time a person runs the Block, StillMade asks for their key in its own dialog, or
opens the service's sign-in in a new window. The key is stored encrypted for that
person and that Block only (another Block declaring the same service gets
nothing), added to requests on StillMade's servers, and never reaches the
Block's code. People can replace or remove keys from the Block's Network access
panel. Listing pages and previews show the declared services before anyone runs
the Block.
### What the host enforces
- Requests go only to declared origins of a saved Block the person can open, over
https on the standard port, to public internet addresses. Each connection is
pinned to the address that was checked, and redirects are followed (up to 3)
only to declared origins; a key is never sent across a redirect.
- Block code cannot set the key's header or parameter, host-owned headers
(`Host`, `Cookie`, `Origin`, `Content-Length` and similar) or a body on GET.
- Limits: 4 MiB per request, 8 MiB per response (compressed responses are
measured after decompression), 30 seconds per request including redirects, and
120 requests a minute per person and Block.
- A response that contains the key (raw, URL-encoded or base64) is withheld.
### OAuth apps
A creator submits the provider's authorization and token endpoints, scopes,
client ID and secret with `POST /api/blocks/network/oauth-apps`. StillMade
reviews the app before any Block can use it. Sign-in uses the authorization code
flow with PKCE; StillMade keeps the tokens and refreshes them.
### Published OpenAPI adapters
Import an API's OpenAPI document as an API connection, then submit it with
`POST /api/blocks/network/adapters`: `{connectionId, slug, name, auth, headers}`,
where `auth` is the key it needs and `headers` are constant headers the API
requires (such as a version header). The definition is copied when submitted.
After StillMade approves it, the adapter has a public app ID, appears in
`GET /api/blocks/network/adapters`, and any module Block can declare it. An
approved adapter's origin also counts as reviewed for API connection read tests.
`examples/module-notion-to-airtable` reads a Notion page through a published
adapter (with the person's Notion sign-in) and adds an Airtable row through a
declared origin (with their own token).
---
# Project storage, reads and assets
URL: https://www.stillmade.shop/docs/build/project-storage
Module Blocks keep their own data in the project, read the project, add media
assets and propose edits that people review.
### Cloud storage
Every Block has the `storage` declaration (new Blocks get it automatically).
`scope` chooses how it is shared:
```json
{"storage": {"schemaVersion": 1, "scope": "project", "desktop": "device-unmetered", "cloud": "metered"}}
```
- `"workspace"` keeps one store per project Step that places the Block.
- `"project"` keeps one store for every placement of the Block in the project
(for example a brand kit, a glossary or a voice profile).
```js
const kit = await stillmade.storage.get('brand/kit.json'); // undefined when not set
await stillmade.storage.set('brand/kit.json', {name: 'Harbor Films', color: '#1F6FEB'});
await stillmade.storage.putFile('brand/logo.png', bytes, {mediaType: 'image/png'});
const logo = await stillmade.storage.getFile('brand/logo.png'); // {data: ArrayBuffer, mediaType, bytes} or null
const {items, next} = await stillmade.storage.list('brand/');
await stillmade.storage.remove('brand/old.png');
```
| Limit | Value |
| --- | --- |
| Key | Letters, digits, `. _ - /`, up to 200 characters, no `..` or leading `/` |
| Value | JSON, up to 1 MiB |
| File | Up to 64 MiB |
| Per Block per project | 256 MiB and 2,000 keys |
Storage belongs to the Block's saved entity and manifest ID, so its new
versions keep it and other Blocks (including another creator's Block with the
same ID) never see it. Inside a project the bytes live in the project owner's
StillMade cloud storage and count toward it; people who can open the project
read the Block's storage and people who can edit it change it. Outside a
project (for example in the Block preview) the storage is the person's own.
Storage is removed with the project. Block code never receives a storage link
or key: StillMade moves the bytes. `stillmade-block test` and `run` use an
in-memory store with the same rules.
On the desktop app, `StillMade.desktop('storage.*')` from a custom interface
keeps device files without a quota.
SDK helpers (from `@stillmade/block-sdk`): `CLOUD_STORAGE_LIMITS` holds the
limits above; `storageKey`, `storagePrefix`, `storageValueText`,
`storageMediaType` and `storageFileBytes` apply the same checks the host does;
`BLOCK_STORAGE_SCOPES` lists `workspace` and `project`. For tests,
`createMemoryStorage()` is an in-memory store with the same rules and
`storageHostHandlers(store)` turns a module's `storage.*` calls into calls on
it (the local runner uses both). `projectReadSnapshot(manifest, project, fields)`
computes what `project.read` returns from a project snapshot.
### Reading the project
Declare `project.read` plus `context..read` for each field:
```js
const {timeline, shots, metadata} = await stillmade.project.read(['timeline', 'shots']);
```
`project.read()` with no fields returns the metadata and every field the Block
may read. Media in the result can be used with `files.read`, transforms and
`assets.appendVersion`. Context inputs (`{"type": "timeline", "context": "timeline"}`)
still work and fill the value before the run starts.
### Adding project assets
```js
const saved = await stillmade.files.write(png, {mediaType: 'image/png', name: 'Brand card.png'});
const card = await stillmade.assets.create(saved, {name: 'Brand card'}); // asset.create
await stillmade.assets.appendVersion(card, await stillmade.files.write(next, {mediaType: 'image/png'})); // asset.version.append
```
Assets are images, videos and audio; they appear in the project's media and
the Editor's media bin. Other files (PDF, CSV and so on) become project files
when the Block returns them as outputs. Adding an asset needs edit access to
the project.
### Proposing edits
Edits to the project are proposals the person reviews: a `timeline-edit`
output (permissions `context.timeline.read` and `timeline.propose`) appears in
the Editor's media bin as "Timeline edits from Blocks", where the person
applies it as one edit and can undo it; a `workspace-edit` output
(`context..read` and `workspace..propose`) is reviewed in the
Step. Build the `before` values from `project.read`.
`examples/module-brand-kit` keeps a brand kit (name, color, logo and a brand
pack) in project storage, adds a brand card to the project's media and
proposes a title caption for the timeline.
---
# Triggers and unattended runs
URL: https://www.stillmade.shop/docs/build/triggers
A Block can declare events it runs on by itself. Declaring a trigger runs
nothing: when the Block is placed in a project, the project owner reviews each
trigger and turns it on for that Step, with the same reviews as the automation
panels. Results wait for the owner to review before they are used.
```json
{"triggers": [
{"event": "media.saved", "input": "video", "kinds": ["video"], "label": "When a new video is saved"},
{"event": "schedule", "everySeconds": 86400},
{"event": "webhook", "eventKind": "order.created"}
]}
```
| Event | Fields | Runs when |
| --- | --- | --- |
| `media.saved` | `input` (the primary image, video, audio or asset input), optional `kinds` | New media is saved to the project (checked about every minute). Existing media is not processed. |
| `schedule` | `everySeconds` (60 to 31,536,000) | On the interval, starting a minute after the owner turns it on. Missed runs are skipped. |
| `webhook` | optional `eventKind` | Another service sends `POST /api/workflow-trigger-events/` with the key the owner receives once (`Authorization: Bearer `) and a JSON event `{id, kind, occurredAt, data}` up to 64 KB. Repeated event IDs run once. |
| `project.completed` | none | Another project the owner chooses is marked complete. |
| `provider` | `appId`, `eventKind` | A connected app reports the event; the owner connects the account. |
Up to 4 triggers per Block. A webhook, schedule or provider Block receives the
event through an input with `"semantic": "workflow_event"` and
`"sources": ["project"]`.
Triggers work for recipe, JavaScript and hosted (`runtime: "capability"`)
Blocks. Module Blocks run only in the person's browser, so they cannot declare
triggers or run unattended. Sandboxed code has 25 seconds per run.
### Hosted Blocks run inside an approved budget
A hosted Block (generation, analysis, speech, transcription, web reading) runs
unattended only after the owner approves it for that Step:
the model and payment, credits per run, total credits, number of runs and how
many days the approval lasts. The approval reuses the owner's earlier review of
the Block with that model, so run the Step once yourself first. Each automatic
run:
- claims its credits on the approval before anything is sent, so the total is
never exceeded, and stops and waits for the owner if the price rises above the
per-run ceiling or the approval is used up, revoked or expired;
- runs as one job with one workflow budget, and is never paid twice: a resumed
or retried job polls the same hosted request;
- can take longer than the sandbox's 25 seconds. A long call (for example video
generation) puts the job in a timed wait and StillMade polls it until it
finishes. Cancelling the job cancels the hosted call.
Timeline and workspace proposals never run unattended; they always wait for a
person.
`autoCaptions` and `orderNote` in `@stillmade/block-sdk` are complete examples:
transcription of each new video into timed captions (`media.saved`), and a
JavaScript Block that turns a shop's webhook into a production note. The SDK
helpers `BLOCK_TRIGGER_EVENTS`, `BLOCK_TRIGGER_LIMITS`, `UNATTENDED_RUNTIMES`,
`triggerProblems`, `blockTriggers`, `describeTrigger` and `triggerReview`
(what the owner reviews for each trigger) check and describe declarations.
---
# Data types: files, documents, tables, links, dates and colors
URL: https://www.stillmade.shop/docs/build/data-types
Ports can carry general files and structured data, not only media and text.
Each type has a checked value shape, a generated control and a result layout,
so a Block that declares them needs no custom view.
| Type | Value | Generated control | Default result layout |
| --- | --- | --- | --- |
| `file` | `{kind: "file", assetId, versionId, mimeType, url?, name?, bytes?}`; `mimeType` is `application/pdf`, `text/plain`, `text/csv`, `text/markdown`, `application/json` or `application/zip` | `file` (upload) | `download` |
| `document` | `{schemaVersion: 1, format: "markdown" \| "plain", text, title?}`, up to 1,000,000 characters | `richtext` | `document` |
| `table` | `{columns: [name], rows: [{name: text \| number \| boolean \| null}]}`, up to 500 columns and 100,000 rows | `grid` | `table` (with CSV download) |
| `url` | An absolute `http` or `https` address without credentials | `url` | a link |
| `date` | ISO 8601: `2026-10-09` or `2026-10-09T14:30` (seconds and an offset optional) | `date` (`time: true` for date and time) | a formatted date |
| `color` | `#rrggbb` or `#rrggbbaa` | `color` | a swatch |
Lists of these types (`file[]`, `url[]`, `date[]`, `color[]`) work like other
lists; the `list` control edits `text[]`, `number[]`, `integer[]`, `url[]`,
`date[]` and `color[]` item by item.
Output presentations in `manifest.ui` choose a layout for an output:
`{control: "table", port}`, `{control: "document", port}` (a `document` or
Markdown `text`), `{control: "chart", port, chart: "bar" | "line", x, y}` (a
`table` with optional column names, or a `number[]`), `{control: "code", port,
language}` (`text`, `json` or `object`), `{control: "player", port}` (`audio` or
`video`) and `{control: "download", port}` (`file`, `file[]` or `file-bundle`).
Each control is checked against its port's type when the manifest is
validated. Layouts show Block data as text: links in a document are displayed,
not followed, and downloads go through the host's export policy.
### Files
People upload files to a `file` input from the Block's controls. The host
checks the extension, the declared type, the size (50 MB) and the bytes (a PDF
or ZIP signature, valid UTF-8 text, parseable JSON) before the Block receives a
saved reference. Module Blocks read the bytes with
`stillmade.files.read(input.script)` and save new files with
`stillmade.files.write(bytes, {mediaType: "text/csv", name: "shots.csv"})`,
which returns a `file` value. Files a person uploaded to a Block, or a Block
saved, are indexed in project context as `files` (`file[]`, permission
`context.files.read`), apart from media so media lists stay media.
### Helpers
`@stillmade/block-sdk/data` (also exported from the main entry) has the checks
and builders: `isFileValue`, `isDocumentValue`, `isTableValue`, `isUrlValue`,
`isDateValue`, `isColorValue`, `createTable(columns, rows)`,
`createDocument(text, {format, title})`, `tableToCsv(table)`,
`fileFormatFor(name)`, `checkFileSignature(bytes, mimeType)`, and the
`DATA_VALUE_LIMITS`, `FILE_FORMATS`, `FILE_MEDIA_TYPES` and `DOCUMENT_FORMATS`
tables. `IMPORT_FORMATS` lists every upload the host stores (media and files),
and `inspectMediaImportFile(file, {kinds: ["file"]})` checks a chosen file.
### Conversions
Connections convert between these types with host-owned conversions, version 1:
| Conversion | From | To |
| --- | --- | --- |
| `text-to-url`, `url-to-text` | `text` | `url` (checked), and back |
| `text-to-table`, `table-to-text` | CSV `text` | `table`, and back to CSV |
| `object-to-table`, `table-to-object` | an `object` shaped as a table (for example from `csv-to-table`) | `table`, and back |
| `text-to-document`, `document-to-text` | `text` | a plain `document`, and back |
| `shots-to-shot-plan` | `shot[]` | the Shots workspace's version 1 `shot-plan` |
A shot list reaches the Shots workspace when the output declares the
workspace's meaning: `{"type": "shot[]", "semantic": "production_shot_plan"}`.
### Example: PDF to shot list
`examples/module-pdf-shot-list` is a module Block that takes an uploaded PDF,
reads its text with pdf.js (bundled, its worker code imported directly so
nothing is fetched), plans the shots with one `text.generate` call that has a
JSON schema, and returns a `table` and a `shot[]` the Shots workspace accepts.
---
# Module Blocks: JavaScript and WebAssembly
URL: https://www.stillmade.shop/docs/build/modules
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..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 -.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.
---
# Hosted media analysis
URL: https://www.stillmade.shop/docs/build/media-analysis
`media.analyze` asks StillMade's media vision a question about saved media and
returns structured evidence. Declare one `video`, `image` or `image[]` (one to
four images) input, one `text` or `json` output and
`permissions.capabilities: ["media.analyze"]`. A video Block may bind two
optional `number` inputs as a time range to look at closely (at most 12 seconds):
```json
{"schemaVersion":1,"operation":"media.analyze","media":{"$input":"clip"},
"question":"Is the product logo clearly visible, and when?",
"range":{"start":{"$input":"start"},"end":{"$input":"end"}},"output":"report"}
```
StillMade reads the media from the runner's own library (never from another
account or an arbitrary URL), samples a video into a timestamped contact sheet
or uses the still image, and asks the vision model the Block's question (1 to
2,000 characters). A `json` output receives
`{"schemaVersion":1,"items":[{"kind","summary","timeline","observations","detectedText","issues","confidence","range"?}]}`
with timestamps in seconds; each list holds up to 20 entries. A `text` output
receives the summaries (numbered when there are several images).
Each analyzed image or video is one priced call, paid with StillMade credits;
the quote lists the price for every fixture and the sample. Media that cannot
be read (not saved in the library, too large, or undecodable) is refunded. The
offline SDK includes `packages/block-sdk/media-analysis-example.js`, exporting
`mediaAnalysis`.
---
# Hosted web reading
URL: https://www.stillmade.shop/docs/build/web-reading
Two operations let a Block use the web without network permissions, cookies or
keys. StillMade does the reading; the person running the Block pays with
StillMade credits at the app's research prices, shown in the quote first.
**`web.fetch`** reads one public page and answers the Block's instruction from
it with StillMade's fast model. Declare one `text` input holding the URL, one
`text` output and `permissions.capabilities: ["web.fetch"]`:
```json
{"schemaVersion":1,"operation":"web.fetch","url":{"$input":"url"},"instruction":"Summarize the page in five bullet points.","output":"summary"}
```
The URL must be a public `http` or `https` address of at most 2,048
characters. StillMade refuses private, local and credentialed addresses,
follows at most three redirects, reads HTML or plain text up to 5 MB, removes
scripts and markup, and treats the page as untrusted data, never as
instructions. A page that cannot be read is refunded. The instruction is 1 to
4,000 characters.
**`web.research`** searches the web for a topic and returns a cited summary.
Declare one `text` or `brief` topic input (1 to 500 characters) and one `text`
or `json` output with `permissions.capabilities: ["web.research"]`:
```json
{"schemaVersion":1,"operation":"web.research","topic":{"$input":"topic"},"output":"findings"}
```
A `json` output receives `{"schemaVersion":1,"summary":"...","sources":[{"url":"https://...","title":"..."}]}`
(up to 12 sources); a `text` output receives the summary followed by a
numbered source list. Fixture expectations are the usual text bounds, or
`{"kind":"json"}` with optional `includes`.
The offline SDK includes `packages/block-sdk/web-example.js`, exporting
`webFetch` and `webResearch`. Pair either with `text.generate` (as a `context`
input) to write from what was read.
---
# Hosted music and sound effects
URL: https://www.stillmade.shop/docs/build/hosted-music
Use `runtime: "capability"` with `operation: "audio.music"` or
`operation: "audio.sfx"` to create music or a sound effect with the app's own
generators: ACE-Step music and StillMade sound effects, the same ones the
Editor uses. The person running the Block pays with StillMade credits at the
app's prices (music by the minute, one flat price per sound effect). Block code
never sees keys, endpoints or payment.
The manifest declares one `text`, `script` or `brief` input, one `audio`
output, and `permissions.capabilities: ["audio.music"]` (or `["audio.sfx"]`):
```json
{"schemaVersion":1,"operation":"audio.music","prompt":{"$input":"brief"},"output":"music","settings":{"durationSec":30}}
```
`settings.durationSec` is optional and only suggests a length. Music runs 5 to
240 whole seconds; sound effects run 0.5 to 22 seconds in tenths. The person
running the Block can change the length in the dialog, and the quote shows the
price for that length before anything is generated.
The output is a measured MP3 or WAV reference:
```json
{"kind":"audio","assetId":"audio:run:0","versionId":"","url":"/api/media/owner/capaud3-....mp3","mimeType":"audio/mpeg","duration":30.02,"bytes":481254,"sampleRate":44100,"channels":2}
```
Fixture expectations use `format` `any`, `mp3` or `wav` with duration and byte
bounds (up to 1,800 seconds and 128 MiB):
```json
{"music":{"kind":"audio","format":"any","minDuration":1,"maxDuration":241,"minBytes":45,"maxBytes":134217728}}
```
The offline SDK includes `packages/block-sdk/audio-generation-example.js`,
exporting `audioMusic` and `audioSfx`. Review, listening and acceptance follow
the same flow as [hosted speech](/docs/build/hosted-speech#review-listen-and-accept): **Review cost**
never generates, each fixture and the sample is quoted and charged once, and a
provider refusal before generation is refunded.
---
# Add speech to the Editor
URL: https://www.stillmade.shop/docs/build/audio-to-editor
Use **Audio from Blocks** to place an accepted speech recording on the Editor's
audio lane. This action is available in the desktop and mobile Editor. It uses
the recording already generated by a project Step; adding it does not generate
speech again.
1. Run the speech Block in a saved project. Review its text, voice, speed, and
cost, confirm generation, listen, and choose **Use this audio**.
2. In the Editor, open **Media** and find **Audio from Blocks**. This section
appears in the desktop Media Bin and the mobile Media sheet. Listen to the
accepted result you want to use.
3. Move the playhead to the desired start time and choose **Add at playhead**.
The Editor adds an audio item with the recording's measured duration.
Adding an item requires project edit access. The list offers current, accepted
project speech results. Import-review fixtures, sample previews, unfinished
runs, and outdated results are not sources for new items. If the Block's input
or result changes, complete and accept its current project run before adding
that result.
An inserted item retains the recording you chose. Rerunning or removing its
source Block does not remove the item or replace its audio. To use a later
recording, accept that result and add it explicitly; existing timeline items
remain available for normal editing.
This action adds Editor audio. It leaves the native Voiceover master recording,
takes, transcript, word timings, and shared narration unchanged. Voiceover's
primary typed input remains `script`. The `audio.speech` package contract is
unchanged: declare one `audio` output, and let the host handle playback and
explicit insertion. Package code and custom interfaces do not receive a new
timeline-writing API.
The [speech guide](/docs/build/hosted-speech) describes source validation and the
required provider review. Playback and insertion checks do not establish that
a live provider speech test has passed.
---
# ComfyUI Blocks
URL: https://www.stillmade.shop/docs/build/comfyui
The reviewed adapter now supports core text-to-image and image-to-image generation. Use `CheckpointLoaderSimple`, `CLIPTextEncode`, `EmptyLatentImage`, `KSampler`, `VAEEncode` and `VAEDecode` with existing `PreviewImage` output. One installed safetensors checkpoint basename, one sampling pass, batch 1 and at most 40 steps are admitted. Generation dimensions are 64–1024 in multiples of 8, subject to the existing four-megapixel aggregate working budget (1024-square image-to-image can exceed it). The connection must report that checkpoint; filenames do not prove file hashes, licenses or GPU readiness. No weights are downloaded. `comfyModelRequirements(workflow)` returns the required model names. Text and numeric controls use typed scalar bindings. The import wizard preserves real sample inputs and prepares padded image-to-image fixture copies; project inputs are never silently resized. See `examples/comfy-text-to-image.stillmade.json` and replace the checkpoint placeholder before review.
A ComfyUI Block is a portable declaration of a reviewed image capability. Users
see its controls and resulting image, not its internal node wiring. Start with
[the complete example](/block-sdk/examples/comfy-invert.stillmade.json).
### Import an existing ComfyUI workflow
Choose **Import ComfyUI workflow** in the import tools, or choose its JSON file
with **Import package**. Export the **API format** from ComfyUI first; its
[API documentation](https://docs.comfy.org/development/cloud/overview) describes
the numbered node map. Visual exports containing `nodes` and `links` cannot be
converted without the exact frontend/node definitions and receive export guidance.
The importer accepts the prompt map itself or `{ "prompt": { … } }`. A ZIP may
contain one `workflow.json`, `workflow_api.json`, or `*.comfy.json` file.
The mapping screen asks for the Block name, description, source license, input
and result names, and the main image input/output. Multiple image inputs or
outputs require an explicit main choice. Optional semantic roles distinguish
source images from character references. Resize controls can stay fixed or become
user-editable inputs. Every LoadImage and PreviewImage must be mapped; unknown
nodes, dependencies, credentials, cycles, and disconnected processing are rejected.
This adapter accepts the reviewed image and diffusion classes listed below, not arbitrary custom-node workflows.
Choose a sample for every image input. The browser creates a copy no larger than
128 pixels per side, with a combined fixture budget, and includes those pixels
in the downloaded source. Originals are unchanged. The sample stays local until
you explicitly run the backend review. Optional expected dimensions add fixture
assertions; leaving them blank still requires a valid decoded single image.
Choose sample images you may distribute with this Block.
**Continue to tests and preview** creates an SDK package, not an installation or
a successful test. The existing account connection, real fixtures, separate
sample execution, preview, rights review and **Confirm Import** follow. Importing
never republishes third-party code. The original API JSON is retained in
`manifest.provenance.comfyuiImport.source`; execution uses normalized placeholders
instead of its saved image filenames. Original `_meta.title` remains attribution,
not executable behavior. Review this retained source before sharing a release.
The downloadable SDK exposes the same static conversion API:
```js
import {inspectComfyImport, buildComfyImportPackage}
from './packages/block-sdk/index.js';
import {comfyImageInvert} from './packages/block-sdk/comfyui-example.js';
// Replace with a supported API export. No backend is contacted by either call.
const raw = structuredClone(comfyImageInvert.comfyui.prompt);
raw['1'].inputs.image = 'my-example.png';
const inspection = inspectComfyImport(raw);
console.log(inspection.inputCandidates, inspection.outputCandidates);
const pkg = buildComfyImportPackage(raw, {
manifest: {...comfyImageInvert.manifest, id: 'my.invert', name: 'My invert'},
inputs: comfyImageInvert.comfyui.inputs,
outputs: comfyImageInvert.comfyui.outputs,
tests: comfyImageInvert.tests,
});
// Save pkg as JSON, then import it for real backend review and confirmation.
```
`inspectComfyImport(raw)` returns `format`, inert `source`, normalized `prompt`,
`nodes`, `inputCandidates`, `outputCandidates`, `warnings`, and
`execution: "not-run"`. It does not choose names, roles, primary ports or fixtures.
`buildComfyImportPackage(raw, {manifest, inputs, outputs, tests})` requires those
explicit choices, validates their contracts and selected resize values, and
returns a package without execution receipts. Source is limited to 64 KiB and
the complete package to the usual bounded JSON/import limits.
### Package and runtime contract
Set `manifest.runtime` to `comfyui`, `entry` to `src/comfyui.json`, and declare
`permissions: {project: [], capabilities: ["comfyui.execute"]}`. The top-level
package contains `manifest`, `comfyui`, `tests`, and an optional `view`. It cannot
contain `code` or `recipe`. Put the workflow in `comfyui` or, in an editable folder,
`src/comfyui.json`. Endpoints, tokens, connection IDs, file paths, dependencies,
and Python never belong in the package. Input/output types and semantic roles
use the same StillMade contract as other Blocks.
The workflow has four fields: `schemaVersion: 1`, `prompt`, `inputs`, and
`outputs`. `prompt` is an API-format map of numbered nodes with `class_type` and
`inputs`. Supported core classes are `LoadImage`, `ImageInvert`, `ImageScale`, `PreviewImage`, `CheckpointLoaderSimple`, `CLIPTextEncode`, `EmptyLatentImage`, `KSampler`, `VAEEncode` and `VAEDecode`. Every node must contribute to a declared output. A `LoadImage`
uses an empty filename placeholder and a binding such as
`image: {node: "1", input: "image", encoding: "uploaded-image"}`. StillMade uploads
verified input pixels and supplies the generated basename. Resize controls use
`encoding: "scalar"`. An output selects one image from a declared PreviewImage:
`image: {node: "3", collection: "images", index: 0}`.
Each fixture has `name`, `input`, and `expectations`. For example:
```json
{"name":"One pixel","input":{"image":{"width":1,"height":1,"data":[20,80,200,255]}},"expectations":{"image":{"kind":"image","count":1,"width":1,"height":1,"maxBytes":4096}}}
```
Every output requires `kind: "image"` and `count: 1`. Optional width, height,
maximum encoded bytes, and lowercase `sha256` check the host-decoded and
re-encoded PNG, not a claimed backend descriptor. Use deterministic hash fixtures
when exact image behavior matters. Bounds: 16 nodes, 8 inputs/outputs, one
megapixel per single-frame image, and four megapixels total working images.
Small inline fixtures fit the four-megabyte import limit. Large media inputs
should use existing account-owned media references.
In Settings → ComfyUI connections, save the public HTTPS endpoint and optional
bearer token. Tokens use the same stable account encryption key as BYOK with a
separate encryption purpose. They are never returned to the browser or guest UI.
StillMade rejects private network endpoints, redirects and arbitrary requests.
The backend is an explicitly selected service you operate or trust. Checking
its core-node schema is compatibility evidence, not attestation of its Python
implementation. Desktop loopback and arbitrary custom nodes are not available through this adapter. Reviewed core diffusion nodes may use the backend’s GPU after explicit approval.
Import the JSON or ZIP, choose the connection, and click **Run tests and sample**.
StillMade scans the package, records durable jobs, runs every fixture and a
separate sample, decodes outputs, and displays the before/after result. Only then
can you confirm rights and **Confirm Import**. Confirmation revalidates the
account/source/connection-bound receipt without silently submitting another job.
Changing source, connection revision, backend contract, or an expired receipt
requires another review. Saving a connection, opening a page, and custom iframe
JavaScript cannot confirm remote execution. Preview runs never install or
publish a Block. Cancellation targets only owned jobs; unsupported remote
cancellation can leave work running on the selected backend. The SDK rejects a
cancelled invocation immediately even if its trusted host callback is still
waiting. Source and input contracts are captured before dispatch; editing the
caller’s package during execution cannot change output validation.
In a saved project, the host checks edit access and finds a completed review for
this exact package and connection. A current backend compatibility check runs
without resubmitting fixtures. A successful import review can be reused for
project execution after its import-confirmation expiry; changed source,
connection revision, backend schema, or security status invalidates that reuse.
The host asks for **Run in project** before submitting the actual project input.
If review is required, **Run tests and sample** and its preview come first, then
a separate **Run in project** action. Custom interfaces cannot skip these actions.
Durable production jobs are associated with the saved project. Local demo and
Marketplace previews remain samples and cannot claim project access.
Offline `validate` and `pack` check and package source. For ComfyUI, `pack` returns
`reviewRequired: true` and zero executed tests. `test` and `preview` return
`RUNTIME_UNAVAILABLE` without a trusted host executor. A host integration may
pass `options.comfyui(package, input, {signal})` to `runPackageAsync` or
`testPackage`, returning `{outputs, metadata}` where every metadata entry has
verified width, height, bytes, `mimeType: "image/png"`, `frames: 1`, and optionally
SHA-256. This callback is trusted host code; it is never loaded from a package.
The server supports reviewed ComfyUI packages inside Project Types through the
account-bound envelope below. The import wizard opens each included ComfyUI Block
in its connection review panel. Unattended ComfyUI automation remains unavailable.
Existing recipe/JavaScript Project Types and exact-output fixture checks are unchanged.
### ComfyUI packages in Project Types: host integration
The offline CLI can package ComfyUI-only or mixed hosted/local Project Types:
```sh
node packages/block-cli/cli.js validate ./workflow.json
node packages/block-cli/cli.js pack ./workflow.json ./workflow.stillmade.json
```
This performs static/schema checks on every embedded package and preserves exact
source. It never runs a provider, connects to ComfyUI, or executes local fixtures.
The result explicitly reports `tests: 0`, `reviewRequired: true`,
`liveVerified: false`, and `workflowComplete: false`. Import the result into
StillMade for authenticated backend review, local tests, sample execution, and
user confirmation. Unsupported ComfyUI nodes still fail the static package scan.
The authenticated review/import/release/install APIs accept `typeComfyuiReviews`
alongside `typeCapabilityReviews`. It is request metadata, never portable source:
```json
{
"typeComfyuiReviews": {
"": {
"receiptId": "",
"connectionId": "",
"connectionRevision": 1
}
}
}
```
Provide exactly one entry for every embedded ComfyUI package, including unused
or disabled packages. The server authenticates every fixture and sample against
the selected backend contract, then checks the ledger again at the save boundary.
Missing, expired, foreign, cancelled, or changed-connection evidence is rejected.
Saved imports, release reports, and installation reports retain the fixture and
sample media. Another account must provide its own reviews. No prompt is
submitted by receipt verification.
A trusted SDK host can supply `verifyComfyPackage` in the third argument to
`verifyHostedProjectTypePackages(type, verifyCapabilityPackage, options)`.
The callback must return the server-verified Block report, including `receiptId`,
`connectionId`, and `connectionRevision`. The resulting opaque handle supports
`testProjectType` and `previewProjectType`; JSON claims cannot substitute for it.
Offline rehearsal defers reviewed ComfyUI execution and its dependent outputs.
Interactive rehearsal requires `verifyHostedResult` before downstream execution;
retained run metadata uses `comfyuiRun`. A passing package admission report still
does not prove that the complete connected workflow ran.
In the import or builder review, choose **Review Block** beside each ComfyUI
package, select your connection, and run its tests/sample (or reuse a fresh
matching review). Finish the package checks, inspect the result, and confirm
import. Connected workflow preview separately confirms each ComfyUI execution
and refetches its exact run evidence before handing images downstream. These
interactive controls do not enable unattended remote execution.
### Connected ComfyUI execution in a project
**Run connected steps** can continue through ComfyUI Blocks when you approve each
run on your account connection. It reuses the existing reviewed-connection dialog;
missing fixtures require a separate review before the project input runs. Image
references reach the broker intact, so the server can check project access and
bind the exact input. Local image processors decode accepted images as needed.
The host uses `planSegment(..., {interactive: true})` and supplies an authenticated
`verifyHostedResult` callback to `runSegment`. ComfyUI results retain `comfyuiRun`
and `mediaReceipts`; hosted model results retain `capabilityRun`. These callbacks
are trusted StillMade integration code, never imported Block code. A resumed run
or **Use results** re-fetches and verifies each remote result against the current
project, source, inputs, and connection metadata before accepting it. Waiting for
consent or a remote backend does not consume the local computation budget.
Cancellation or failed verification stops downstream work and retains earlier
completed results for review. The default non-interactive planner still stops at
ComfyUI or hosted model Blocks; future-media automation cannot submit those jobs.
---
# JavaScript Blocks
URL: https://www.stillmade.shop/docs/build/javascript
Computation code may use `StillMade.emit(name, value)` instead of returning an
output object. Names must be declared outputs; emit each once, and do not combine
emission with a return value or cooperative pending result. Emissions remain
inside the sandbox until final host validation and normal result acceptance.
`StillMade.resolveRequirement(inputName)` reads a declared input from the fixed
host-validated snapshot and returns `{resolved:true, requirement, value}` or
`{resolved:false, requirement}` for an absent optional input. Undeclared names
throw; this method does not search sources or call Chat.
`StillMade.getProjectContext()` returns an isolated snapshot of declared
context-bound inputs, keyed by their `context` field names. An optional declared
field argument reads one value; undeclared fields throw and absent optional
values return `undefined`. It does not enumerate the whole project or bypass
host permissions, and it does not fetch newer values during a run.
`StillMade.validate(value, schema)` returns a boolean for supported JSON/schema
validation only. It does not establish semantic correctness, media validity,
permissions, or provenance. Final host output validation remains mandatory.
These computation helpers are not available to `view.javascript`. Isolated
computation code also has `StillMade.requestFromUser` and `StillMade.requestFromChat`
(see `UNIVERSAL_RESOLVER.md`) and `StillMade.patchProject` (reviewable workspace-edit
proposals).
See `JAVASCRIPT_RESULTS.md` for state, continuation and permission boundaries.
Create an editable JavaScript block with:
```sh
node packages/block-cli/cli.js create ./my-js-block javascript
node packages/block-cli/cli.js test ./my-js-block
node packages/block-cli/cli.js pack ./my-js-block ./my-js-block.stillmade.json
```
Set `manifest.runtime` to `javascript`. The source file is `src/run.js`, packed
as `{manifest,code,tests}`. Write a synchronous function body: `input` contains
validated named inputs; return an object whose keys match declared outputs.
For example, with a text input and text/words outputs:
```js
const text = input.text.trim();
return { text, words: text ? text.split(/\s+/).length : 0 };
```
Use `await runPackageAsync(package, input)` from the SDK to dispatch either runtime.
The older synchronous `runPackage` remains recipe-only. In browsers, invoke the
async API inside a disposable worker; the app does this automatically. On Node,
the SDK creates and terminates a separate worker for every JavaScript execution.
The downloadable SDK includes its pinned QuickJS WASM runtime and dependency
licenses, so these examples run without installing packages or accessing a network.
The guest has standard QuickJS JavaScript built-ins. It has no DOM, browser storage,
fetch, Node globals, timers, module loader, filesystem, secrets, or host callbacks.
An asset reference is data, not file access. npm packages and imports are unsupported.
Promises are not awaited; return synchronously. Values cross through JSON and
are validated against output ports. Avoid Date/randomness in behavior fixtures.
Limits per run: 256 KiB source, 4 MiB serialized input and output, 500 ms guest
execution, 16 MiB guest heap, 512 KiB stack, and a fixed 32 MiB WASM memory.
Node adds a five-second worker watchdog and at most four concurrent executions;
app workers have a 15-second outer watchdog. Each package fixture suite has a ten-second deadline (callers may shorten it);
all embedded packages share a fifteen-second project-type deadline. Timeouts, allocation failures, invalid outputs and syntax errors
fail the run; no partial output is committed. Each run receives a fresh VM.
---
# Recipe Blocks
URL: https://www.stillmade.shop/docs/build/recipes
```json
{
"schemaVersion":1,
"steps":[{"id":"clean","op":"text.trim","args":{"text":{"$input":"text"}}}],
"outputs":{"text":{"$step":"clean"}}
}
```
Steps run in listed order. References may only read supplied inputs or a completed
prior step using own-property paths. There is no eval, module import, loop or shell.
Maximum 64 steps; unknown operations, unresolved references, bad outputs, and
undeclared fields fail. Cancellation is checked between operations. The web runner
executes in a disposable worker with a 15-second timeout; the trust boundary is
the data interpreter, not a claim that arbitrary code is safe in a Web Worker.
`runPackage(package,input,{signal,onProgress})` returns `{outputs}`. It does not
mutate the input or commit changes to a project. `BlockError` includes `code` and
`message`; display the failure, never install a build that fails required tests.
## Complete example
```json
{
"manifest": {
"schemaVersion": 1,
"license": "MIT",
"provenance": {
"notice": "MIT License\n\nCopyright (c) 2026 StillMade SDK contributors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and \nassociated documentation files (the \"Software\"), to deal in the Software without restriction, including \nwithout limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell \ncopies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the \nfollowing conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial \nportions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT \nLIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO \nEVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER \nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE \nUSE OR OTHER DEALINGS IN THE SOFTWARE.\n"
},
"sdkVersion": "0.1.0",
"id": "example.clean-text",
"version": "1.0.0",
"name": "Clean text",
"description": "Trims extra space from text and returns a clean copy for the next step.",
"kind": "task",
"runtime": "recipe",
"inputs": {
"text": {
"type": "text",
"semantic": "plain_text",
"required": true,
"description": "Text whose surrounding whitespace should be removed."
}
},
"outputs": {
"text": {
"type": "text",
"semantic": "plain_text",
"description": "The same text with surrounding whitespace removed."
}
},
"permissions": {
"project": [],
"network": [],
"filesystem": [],
"secrets": []
},
"ui": [
{
"control": "text",
"port": "text"
}
]
},
"recipe": {
"schemaVersion": 1,
"steps": [
{
"id": "clean",
"op": "text.trim",
"args": {
"text": {
"$input": "text"
}
}
}
],
"outputs": {
"text": {
"$step": "clean"
}
}
},
"tests": [
{
"name": "Trims surrounding whitespace",
"input": {
"text": " StillMade\n"
},
"expected": {
"text": "StillMade"
}
}
]
}
```
See the [operation reference](/docs/reference/operations) for the complete allowlist.
---
# Images and media references
URL: https://www.stillmade.shop/docs/build/media
Portable references are `{assetId,versionId,kind}`; a reference is not an absolute
file path. Current pure image operations require a decoded fixture/preview image:
`{width,height,data:[R,G,B,A,...]}` with 0–255 integer channels, up to 1 megapixel.
Large production media must use a host media adapter; this SDK does not pretend
to transcode an arbitrary asset reference. Line thickening uses a square dilation
kernel and thresholded luminance; it returns a new RGBA image.
Connected Project Type previews include a small, temporary media bridge. If an
image-producing Block returns pixels and the next input accepts an asset,
StillMade encodes a real PNG data URL in memory and supplies a preview media
reference. If a later image input receives that same exact generated reference,
the bridge supplies copied pixels to image-processing code. Original pixel
outputs remain visible as pixel previews; direct image-to-image connections do
not need this conversion.
This behavior is shared by the browser's connected preview, CLI `preview`, and
Project Type admission tests. It does not upload media, fetch external URLs, or
create project assets. Only references generated by that preview session can be
hydrated; changing their kind, asset/version identity, URL or semantic role fails.
Optional dimensions and labels may be omitted or changed; the private pixel copy
remains authoritative. Other references remain
inert references for the caller's supported host runner. Encoding and private
cache costs share the existing 4 MB sample budget and preview deadline, so use
small fixtures. Production execution uses its normal authorized media storage
and decoding path instead of these temporary references.
## Saved media collections
Use an `asset[]`, `audio[]` or `video[]` input to collect original media with
standard controls. Inside a project, these controls upload, select, order and
remove files without custom host code. Project uploads are saved before the
input is published, with exact per-file receipts bound to the Block version.
Compatible direct remixes retain uploaded media. Builder previews use temporary
local files; upload in a project for durable use. Project controls admit up to
64 list entries and 50 MiB per original file, subject to the shared format table.
The sandbox receives typed references, never upload credentials or local paths.
[Import media 2.0.0](/block-sdk/examples/import-media.stillmade-block) is a complete
portable example. Its owned JavaScript preserves metadata and selection order,
optionally filters media kinds or removes exact duplicate references, and returns
an `asset[]` output for subsequent Blocks. It does not transcode media or claim
rights over user files. Its license covers the original Block implementation.
Empty selections return an empty list. The native legacy upload node remains
available to old placements; new SDK placements use the standard sandbox.
Validate and test the downloaded archive with the same public CLI:
```sh
node packages/block-cli/cli.js validate import-media.stillmade-block
node packages/block-cli/cli.js test import-media.stillmade-block
node packages/block-cli/cli.js remix import-media.stillmade-block my-import.stillmade-block
node packages/block-cli/cli.js test my-import.stillmade-block
```
### Mandatory host-controlled exports
Blocks return typed results or `file-bundle` outputs for StillMade to deliver.
Do not create download links, invoke save-file pickers, open export destinations,
or implement a separate export/paywall/referral bypass. The upload admission
report requires **Host-controlled exports** (`host-export-1`), alongside existing
sandbox and interface checks. A passing static check is not a delivery receipt.
StillMade owns the current export policy for all creators; it may add a referral
step, checkout or other requirement without changing your Block. It can also
require the StillMade watermark on exports from accounts without a paid plan;
StillMade draws it while rendering videos and delivering images, so do not add
your own. Block pricing and export access are separate. Never claim payment or referral completion from
Block code. Native integrations must declare the host export service and route
delivery through it; an extracted source folder does not satisfy this boundary.
## Image-processing example
```json
{
"manifest": {
"schemaVersion": 1,
"license": "MIT",
"provenance": {
"notice": "MIT License\n\nCopyright (c) 2026 StillMade SDK contributors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and \nassociated documentation files (the \"Software\"), to deal in the Software without restriction, including \nwithout limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell \ncopies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the \nfollowing conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial \nportions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT \nLIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO \nEVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER \nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE \nUSE OR OTHER DEALINGS IN THE SOFTWARE.\n"
},
"sdkVersion": "0.1.0",
"id": "example.thicken-lines",
"version": "1.0.1",
"name": "Thicken line art",
"description": "Expands dark outlines in an image while preserving surrounding colors, creating a new image for the next production step.",
"kind": "task",
"runtime": "recipe",
"inputs": {
"image": {
"type": "image",
"primary": true,
"semantic": "source_image",
"required": true,
"description": "Line-art image whose dark outlines should be expanded."
},
"thickness": {
"type": "integer",
"default": 2,
"min": 1,
"max": 8,
"semantic": "line_thickness",
"required": false,
"description": "Number of pixels by which to expand dark outlines."
},
"threshold": {
"type": "number",
"default": 100,
"min": 0,
"max": 255,
"semantic": "darkness_threshold",
"required": false,
"description": "Brightness threshold used to identify dark outline pixels."
}
},
"outputs": {
"image": {
"type": "image",
"primary": true,
"semantic": "processed_image",
"description": "A new image with expanded dark outlines."
}
},
"permissions": {
"project": [],
"network": [],
"filesystem": [],
"secrets": []
},
"ui": [
{
"control": "asset",
"port": "image"
},
{
"control": "slider",
"port": "thickness"
},
{
"control": "slider",
"port": "threshold"
}
]
},
"recipe": {
"schemaVersion": 1,
"steps": [
{
"id": "outlines",
"op": "image.thicken-lines",
"args": {
"image": {
"$input": "image"
},
"thickness": {
"$input": "thickness"
},
"threshold": {
"$input": "threshold"
}
}
}
],
"outputs": {
"image": {
"$step": "outlines"
}
}
},
"tests": [
{
"name": "One dark point expands to a 3×3 square",
"input": {
"image": {
"width": 5,
"height": 5,
"data": [
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
0,
0,
0,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255
]
},
"thickness": 1
},
"expected": {
"image": {
"width": 5,
"height": 5,
"data": [
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
0,
0,
0,
255,
0,
0,
0,
255,
0,
0,
0,
255,
255,
255,
255,
255,
255,
255,
255,
255,
0,
0,
0,
255,
0,
0,
0,
255,
0,
0,
0,
255,
255,
255,
255,
255,
255,
255,
255,
255,
0,
0,
0,
255,
0,
0,
0,
255,
0,
0,
0,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255,
255
]
}
}
}
]
}
```
## Host boundary
The host resolves a selected image into RGBA before execution and stores accepted output as a new media version. A guest receives no filesystem path or network access. Large files require an explicit host-side conversion or resized copy.
See [batch execution](/docs/project-types/batches) for whole-scene and project-wide processing.
---
# Remember data between runs
URL: https://www.stillmade.shop/docs/build/state
## When to use memory
Use saved memory for small counters or preferences that belong to one placement. Use [project context](/docs/build/context) for scripts, scenes, shots, assets, and production history. Never put provider keys or media bytes into Block memory.
## Try the complete example
[Download Numbered takes](/block-sdk/examples/numbered-takes.stillmade.json). Import it, review its tests and preview, then confirm the import. Add it to a Project Type and run it twice: the labels advance from Take 1 to Take 2. A second placement has its own counter.
```json
{
"manifest": {
"schemaVersion": 1,
"sdkVersion": "0.1.0",
"id": "example.numbered-takes",
"version": "1.0.0",
"name": "Numbered takes",
"description": "Label successive creative takes with an independent counter for each production placement.",
"kind": "task",
"runtime": "javascript",
"license": "MIT",
"provenance": {
"notice": "MIT License\n\nCopyright (c) 2026 StillMade SDK contributors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and \nassociated documentation files (the \"Software\"), to deal in the Software without restriction, including \nwithout limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell \ncopies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the \nfollowing conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial \nportions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT \nLIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO \nEVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER \nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE \nUSE OR OTHER DEALINGS IN THE SOFTWARE.\n"
},
"inputs": {
"text": {
"type": "text",
"primary": true,
"semantic": "draft_text",
"required": true,
"description": "Creative take text to label with the next number."
}
},
"outputs": {
"text": {
"type": "text",
"primary": true,
"semantic": "numbered_take",
"description": "The supplied take prefixed with its saved sequence number."
}
},
"permissions": {
"project": [
"state.step"
]
},
"state": {
"scope": "step",
"version": 1,
"initial": {
"count": 0
}
},
"ui": [
{
"port": "text",
"control": "text"
}
]
},
"code": "state.count += 1; return {text: \"Take \" + state.count + \": \" + input.text.trim()};",
"tests": [
{
"name": "First take",
"input": {
"text": " Opening scene "
},
"expected": {
"text": "Take 1: Opening scene"
},
"expectedState": {
"version": 1,
"value": {
"count": 1
}
}
},
{
"name": "Continues saved numbering",
"input": {
"text": "Alternate ending"
},
"state": {
"version": 1,
"value": {
"count": 4
}
},
"expected": {
"text": "Take 5: Alternate ending"
},
"expectedState": {
"version": 1,
"value": {
"count": 5
}
}
}
]
}
```
## Run from the command line
Create a stateful starter, run its fixtures, and save two separate memory checkpoints:
```sh
node packages/block-cli/cli.js create ./takes stateful
node packages/block-cli/cli.js test ./takes
node packages/block-cli/cli.js preview ./takes --save-state take-1.json
node packages/block-cli/cli.js preview ./takes --state take-1.json --save-state take-2.json
```
Without --state, each preview starts fresh. --save-state writes a new file and refuses to overwrite an existing checkpoint. These files contain your Block memory, are pinned to the exact package digest, and should stay private if their contents are private. The CLI never writes state unless you request it.
## Run with the offline SDK
Extract the SDK and save this script alongside its packages folder. The host chooses when to save returned state. Here both runs use a disposable sandbox while the caller holds the accepted state.
```javascript
import {readFile} from 'node:fs/promises';
import {initialBlockState,runPackageAsync} from './packages/block-sdk/index.js';
const pkg=JSON.parse(await readFile('./examples/numbered-takes.stillmade.json','utf8'));
let memory=initialBlockState(pkg.manifest);
const first=await runPackageAsync(pkg,{text:'Opening'},{state:memory});
memory=first.state; // accept only after your host validates current permissions and inputs
const second=await runPackageAsync(pkg,{text:'Alternate opening'},{state:memory});
console.log(first.outputs.text,second.outputs.text);
```
## State contract and limits
Stateful JavaScript Blocks can opt into a small JSON state object. Production
placements save accepted state with their outputs in the existing project document.
Each placement has independent memory pinned to the exact package digest. Connected
runs retain proposed memory until the user accepts their results; changed memory
invalidates an older result. A failed or cancelled run does not advance memory.
Block previews keep temporary memory across runs; Reset or closing the preview
discards it. Import samples and each Project Type rehearsal placement start fresh.
Fixtures never become project memory. External SDK hosts must supply state explicitly
or execution fails with `STATE_REQUIRED`.
Changing a placement to another build starts fresh memory and archives the previous
placement through workflow history; restoring that exact placement restores its
previous memory. Duplicating a workflow placement starts independent fresh memory.
Stateful single-Block batches process the selection in order using provisional
memory. Choosing a batch result accepts memory through that result; later
previews cannot overwrite it. Changes to inputs, selected source versions, or
starting memory require a new batch. Connected multi-Block batches also carry
independent provisional memory for every placement in selection order. Use completed
results validates the original source selection and each memory transition before
committing the completed prefix together. A failed later transform preserves earlier
results for review without advancing saved memory automatically. Single-Block background automation supports saved memory when shared Canvas editing is enabled. Each completed job commits its outputs and memory in one transaction; a changed workspace or memory causes a retry. Failed, paused or stale workers do not advance memory. Selecting a completed result does not advance memory again. Connected background workflows pin all starting states and commit every placement’s memory together when the complete workflow succeeds. Failed prefixes retain their results without advancing shared memory; resuming requires the same starting memory. Hosted APIs and ComfyUI still use their explicit execution flow. State is bounded JSON, not a media-storage API. The production workspace’s
Block memory controls let users reset a placement and undo that reset until
the next accepted run. Reset keeps previous outputs visible but marks them stale.
Declare this alongside the usual manifest fields:
```json
{
"runtime": "javascript",
"permissions": {"project": ["state.step"]},
"state": {"scope": "step", "version": 1, "initial": {"count": 0}}
}
```
Your JavaScript body receives `input` and, only for stateful Blocks, a mutable
`state` object. Update its properties and return your ordinary output ports:
```js
state.count += input.amount;
return {count: state.count};
```
Do not reassign the `state` parameter. Keep it plain JSON. Each invocation uses a
fresh sandbox and a copy of the supplied state. No storage, network, native API,
or another Block's memory is exposed. State is limited to 64 KiB and a positive
schema version. Only JavaScript currently supports this contract.
A trusted host calls:
```js
const previous = initialBlockState(pkg.manifest); // first run only
const result = await runPackageAsync(pkg, {amount: 2}, {state: previous, signal});
// result.outputs is the normal output map.
// result.state is {version: 1, value: {count: 2}}.
```
The SDK never writes persistence itself. The host must store state under the
account, project, and Step instance; recheck the source, previous state, and write
permission; and save the returned state atomically with accepted results. A failed
or cancelled run must not overwrite prior state. Never share one state object
between placements, projects, accounts, or preview sessions. A mismatched state
version fails with `STATE_VERSION`; a host must not silently reinterpret it.
Every stateful fixture requires `expectedState` in addition to ordinary expected
outputs. Optional `state` supplies a saved starting value; omission uses a fresh
copy of `manifest.state.initial` for that fixture:
```json
{
"name": "Continue a saved count",
"input": {"amount": 3},
"state": {"version": 1, "value": {"count": 4}},
"expected": {"count": 7},
"expectedState": {"version": 1, "value": {"count": 7}}
}
```
`initialBlockState`, `validateBlockState`, and `STATE_LIMIT_BYTES` are exported by
the SDK. Both browser workers and Node's disposable QuickJS workers carry this
contract. Fixture success verifies state transformation, not host persistence.
## Troubleshooting
- STATE_REQUIRED: your external host did not supply an initial or accepted state envelope.
- STATE_VERSION: the saved version does not match the manifest; restore the previous build or explicitly reset memory. There is no automatic state migration.
- STATE_LIMIT: keep the JSON state value within 64 KiB.
- STATE_BEHAVIOR: output assertions passed but the fixture's expected state did not match.
- STATE_CHANGED: another edit or accepted run changed the memory; run again against current state.
## Test the actual behavior
Each fixture needs expectedState, even when the expected memory is unchanged. Include a fresh-state case and a continuation case. Fixture state is synthetic and never installed as project memory. Run details are available in the production workspace and preview without copying your content into diagnostic downloads.
---
# Propose timeline edits
URL: https://www.stillmade.shop/docs/build/timeline-edits
### Interactive timeline mixer
[Download Timeline mixer](/block-sdk/examples/timeline-mixer.stillmade.json), or
choose **Timeline mixer template** in the Block builder. Its complete interface
lists video and audio clips with individual level sliders, live percentages,
reset, empty-state guidance, and a Review mix action. It uses `StillMade.render`
and `StillMade.onAction`; the isolated production function returns a typed edit
proposal. Existing Editor permissions, conflict checks, review, and undo still
apply. No account APIs or provider keys are exposed to its interface.
Import the package, run its fixture, and change a slider in Preview. To use real
clips, add it to a Project Type after Editor and open its Step in a project with
a timeline. Review the proposed levels before applying them. It edits up to 100
clips per invocation; the interface does not play or render the timeline.
Download its source and SDK from the builder to modify the HTML, CSS, interface
JavaScript, production function, and fixtures independently, then reimport.
A Block can return a `timeline-edit` output to propose changes to existing Editor
clips. Declare `permissions.project: ["context.timeline.read", "timeline.propose"]`
and an input `{type:"timeline",context:"timeline",primary:true}`. The output is
`{schemaVersion:1,title,timelineId,commands}`. Copy `timeline.activeTimelineId` into
`timelineId`. Each command is `{collection,operation,before,values}`: collection is
`clips` or `audioItems`, and `before` is the exact unchanged target object from the
input snapshot. Use at most 100 commands, with one command per target.
| Operation | Values | Behavior |
| --- | --- | --- |
| move | `{start: seconds}` | Move an existing item; no silent ripple |
| trim | `{trimStart: sourceSeconds, duration: timelineSeconds}` | Trim within known source duration; variable-speed, frozen and looping clips defer to Editor |
| split | `{at: timelineSeconds, newId: "unique-clip-id"}` | Split into two source-aligned clips, at least 0.05 seconds from either edge |
| volume | `{volume: 0..200}` | Set the existing Editor volume percentage |
| mute | `{muted: boolean}` | Mute or unmute an existing item |
| remove | `{}` | Remove the timeline instance, preserving source media |
| grade | `{lut, color}` | Grade clips only; retain original media and timing |
Run the Block in a project, accept its result, and open the Editor's media panel.
Timeline edits from Blocks displays a visual before/after strip and each target's values and applies
all commands together using ordinary Editor undo history. Desktop and mobile use
the same proposal contract. A changed target, different timeline version, locked
track, source overrun, or new clip overlap rejects the proposal. The Block cannot
set arbitrary fields, insert external URLs, run code inside playback, or silently
write the timeline. Re-run it against current context after a conflict.
[Download Quieter clips](/block-sdk/examples/quieter-clips.stillmade.json) for a
complete isolated JavaScript example with a realistic timeline fixture. The
proposal type is a sink for Editor review; it is not interchangeable with a
`timeline` snapshot.
### Caption proposals
Use `collection:"captions"` with these commands:
| Operation | Before | Values |
| --- | --- | --- |
| caption.add | null | `{id,trackId,text,start,end}` |
| caption.update | Exact existing caption object | `{text,start,end}` |
| remove | Exact existing caption object | `{}` |
Caption times are seconds with end greater than start; text must be nonempty and
at most 5,000 characters. Adding uses an unused item ID and an unlocked existing
caption track. The standard `captions` track is created if absent. New captions
use StillMade's existing caption defaults; updating text or timing preserves the
caption's styling. Reapplying an insertion rejects its duplicate ID. The host
compares existing captions against their captured snapshots before update/removal.
[Download Caption from text](/block-sdk/examples/caption-from-text.stillmade.json).
This complete Block takes text plus a scoped timeline, chooses an unused caption
ID, and proposes insertion without modifying the project. Its output connects to
the Editor's proposal review. Add or run it after opening a timeline, then review
its result in the Editor's media panel.
## Timed captions and caption styling
[Download Captions 2.0.0](/block-sdk/examples/captions.stillmade-block). This
standalone, remixable Block converts a transcript into editable timeline captions.
It owns phrase grouping, word timing and caption appearance, runs in the JavaScript
sandbox, and returns ordinary `timeline-edit` proposals for review and Apply.
It does not transcribe audio or spend execution credits.
The optional context binding `{ "type": "transcript", "context": "transcript",
"required": false }` requires `context.transcript.read`. At whole-project scope
it supplies a copied `{schemaVersion:1, words:[{text,start,end}], text, audioUrl?}`
from the saved voiceover when word timings exist. It is omitted when absent and
for scene, shot or asset selections. URL metadata grants no media access.
Explicitly connected transcript inputs remain separate from this fallback.
Captions uses a connected transcript first, then entered text, then the project's
saved transcript. Text without word timings uses estimated timing, identified in
the proposal title. Start offsets word times into the timeline. Duration zero
keeps transcript timing; a positive duration clips the result to that interval.
A proposal supports at most 100 lines and rejects longer results without silently
truncating them. Existing captions stay intact and locked tracks reject Apply.
`caption.add` retains its required `{id,trackId,text,start,end}` values and also
accepts these optional fields. Unknown fields and values outside these limits
are rejected by the same SDK validator and host apply path:
| Field | Accepted values |
| --- | --- |
| `words` | Up to 1,000 `{text,start,end}` entries, ordered by start, contained in the caption interval; nonempty text up to 5,000 characters, end at or after start |
| `font` | `Hanken Grotesk` |
| `size` | 8–200 |
| `color`, `highlightColor` | Three- or six-digit hex color |
| `background` | `none` or three- or six-digit hex color |
| `backgroundOpacity`, `backgroundPadding`, `backgroundRadius` | 0–100 |
| `position` | `top`, `middle`, or `bottom` followed by `left`, `center`, or `right`, separated by one space |
| `animation` | `none` or `fade` |
The host fills omitted appearance values with its existing defaults, preserves
provided styling and clones word timings before saving through ordinary edit
history. Caption updates retain their existing `{text,start,end}` contract.
Package source, MIT notices, lineage and three fixtures are retained in the
canonical archive; validate, test and remix it using the public CLI.
---
# Production context templates
URL: https://www.stillmade.shop/docs/build/production-context-templates
## Asset details
[Download the Asset → Metadata Block](/block-sdk/examples/asset-metadata.stillmade.json). It returns only the supplied asset identity and available dimensions. It does not open its media URL or infer missing values.
## Shots in this scene
[Download Scene → Shots](/block-sdk/examples/scene-shots.stillmade.json). Its primary scene input connects from the workflow; its secondary shot list comes from scoped project context through context.shots.read. It selects exact scene IDs and preserves project order.
## Shot readiness
[Download Shot readiness](/block-sdk/examples/shot-readiness.stillmade.json). It reports shot IDs that lack descriptions or dialogue for shots explicitly marked as having dialogue. This is a production-input check, not an assessment of image quality or factual accuracy.
Each example includes synchronous sandboxed code, a typed manifest and sample fixtures. Import the package, run its admission checks and preview it before installation. No example makes a paid model call.
---
# Read project context
URL: https://www.stillmade.shop/docs/build/context
A Step can bind an input to an explicit project context field:
```json
{
"inputs": {
"shots": {"type":"shot[]", "context":"shots"}
},
"permissions": {"project":["context.shots.read"]}
}
```
Supported fields and exact types:
| Context field | Input type | Contents |
| --- | --- | --- |
| `script` | `script` | `{schemaVersion:1,text}`; selected-shot narration for a narrow selection |
| `scenes` | `scene[]` | Existing scene IDs, descriptions, and scoped `shotIds` |
| `shots` | `shot[]` | Existing shot/panel IDs, scene ownership, descriptions, dialogue and timing |
| `characters` | `character[]` | Canvas-selected visual references with blueprint descriptions |
| `locations` | `location[]` | Canvas-selected location references with blueprint descriptions |
| `styles` | `style[]` | Saved Canvas, blueprint, panel and board styles and references |
| `assets` | `asset[]` | Current media with `assetId`, `versionId`, `kind`, `url`, source and explicit ownership |
| `generations` | `metadata[]` | Retained generated-media records with `asset`, `active`, `status`, and saved generation metadata |
| `versions` | `metadata[]` | Retained media versions and project-level shot-plan/timeline version metadata |
| `timeline` | `timeline` | Current editor document; filtered collections in a scene/shot/asset selection |
| `metadata` | `metadata` | Project ID/name and host-selected `scope` |
Every binding requires its own `context..read` permission. These are exact
permissions: `context.assets.read` does not authorize `context.versions.read`.
The Step workspace, preview, and future-media worker derive this index from the
project's current source documents. No duplicate production state is saved.
Canvas remains authoritative for its existing asset IDs, approved/current version,
and selected character/location/style reference galleries. Legacy reference
images remain available without reselecting deliberately deselected images.
Panels and White Board Motion supply their existing scenes, panels, collage
media, transitions and voiceover. The editor supplies media-bin items, clips,
audio, saved score takes and retained timeline media. An editor item explicitly
linked by Canvas node/version or panel ID reuses that source identity; a stale
editor copy cannot promote a different Canvas version. URL-only panel histories
use deterministic version identifiers that survive history trimming. Different
assets sharing a URL remain distinct. Legacy global `assets`/`library` caches are
excluded because they can contain other projects' data.
Media history entries contain `{schemaVersion:1, id, assetId, versionId, asset,
source, active}` and only saved metadata such as timestamp, model or duration.
Generation entries additionally have `status:"completed"`. Missing model/provider
metadata is omitted, never guessed from a model currently selected in a control.
Document history entries contain `documentType`, `versionId`, `active` and a saved
label/name; they have no `asset`. These metadata entries do not contain old script
bodies. A URL is a reference, not permission for guest code to fetch it.
The host narrows context to the Step's selected scene, shots, or assets before
binding inputs. Only explicit scene/shot ownership and source associations count;
unowned media is excluded from narrow selections. Scene documents contain only
selected `shotIds`, script contains only selected narration, and a narrow timeline
omits unrelated collections, transcript and timeline histories. Unrelated
character/location references are excluded. `metadata.scope` describes the
selection. Guest inputs cannot widen it. Project-wide execution receives the
project-wide snapshot only for the exact requested fields.
`resolveContextInputs(manifest, explicitInputs, projectSnapshot)` copies only
requested fields into input bindings. Context bindings take precedence over manual
values. Guests have no live project handle, write-through access, credentials or
ambient filesystem/network access. Fixture inputs must include concrete context
values; offline execution does not invent a project. Context delivery is implemented
in the Step workspace, preview and automatic-processing worker, not the advanced
Canvas block runner.
For example, a history-aware Block can declare:
```json
{
"inputs": {"history": {"type": "metadata[]", "context": "versions"}},
"permissions": {"project": ["context.versions.read"]}
}
```
Its code reads `input.history` and checks for `record.asset` when processing media
records. It must not assume every version describes a file.
## Complete shot-plan context
Use `{ "type": "shot-plan", "context": "shotPlan", "required": false }`
with `permissions.project: ["context.shotPlan.read"]` to receive the complete
saved shot-plan document. This preserves scene grouping, narration, timing and
other saved planning fields instead of rebuilding a document from flattened
`shots`. Array-based saved plans are normalized to `{schemaVersion: 1, shots: [...]}`;
object plans retain their fields with `schemaVersion: 1`.
This binding is available only for whole-project context. It is omitted for
scene, shot and asset selections, and when no plan exists. The host copies the
value; editing it does not save changes. Use the existing scoped `shots` binding
for selected-shot processing. Media URLs inside documents grant no media-read
permission; downloading still requires selected typed asset references and
`asset.read`. Exporters can also read the saved score from `timeline.audioScore`
under their existing `context.timeline.read` grant.
## Standalone timeline export
[Download Export timeline 2.0.0](/block-sdk/examples/export-timeline.stillmade-block).
This independently versioned JavaScript Block owns serialization and uses generated
SDK controls plus the shared Download result UI. Add it to a project with a timeline,
choose XML or XML + media, run it, then download. Its timeline, media, Blueprint,
shot-plan and project-name inputs bind automatically to authorized project context.
The canonical archive includes source, typed ports, UI schema, permissions,
MIT notices, original-source evidence, lineage and three executable fixtures.
Validate, test and remix it with the public CLI. No dependencies require manual
installation. The exact package is also available through the first-party catalog.
XML retains media links. ZIP bundles selected timeline media and fails if a
requested transfer is unavailable. Auxiliary project documents are retained as
metadata; their unrelated external media is not a full project backup. Device-local
media and bundled sounds depend on supported host transfers. Normal sandbox and
download limits apply. This is the existing StillMade XML serializer; compatibility
with every external editing application has not been established.
## Timed captions and caption styling
[Download Captions 2.0.0](/block-sdk/examples/captions.stillmade-block). This
standalone, remixable Block converts a transcript into editable timeline captions.
It owns phrase grouping, word timing and caption appearance, runs in the JavaScript
sandbox, and returns ordinary `timeline-edit` proposals for review and Apply.
It does not transcribe audio or spend execution credits.
The optional context binding `{ "type": "transcript", "context": "transcript",
"required": false }` requires `context.transcript.read`. At whole-project scope
it supplies a copied `{schemaVersion:1, words:[{text,start,end}], text, audioUrl?}`
from the saved voiceover when word timings exist. It is omitted when absent and
for scene, shot or asset selections. URL metadata grants no media access.
Explicitly connected transcript inputs remain separate from this fallback.
Captions uses a connected transcript first, then entered text, then the project's
saved transcript. Text without word timings uses estimated timing, identified in
the proposal title. Start offsets word times into the timeline. Duration zero
keeps transcript timing; a positive duration clips the result to that interval.
A proposal supports at most 100 lines and rejects longer results without silently
truncating them. Existing captions stay intact and locked tracks reject Apply.
`caption.add` retains its required `{id,trackId,text,start,end}` values and also
accepts these optional fields. Unknown fields and values outside these limits
are rejected by the same SDK validator and host apply path:
| Field | Accepted values |
| --- | --- |
| `words` | Up to 1,000 `{text,start,end}` entries, ordered by start, contained in the caption interval; nonempty text up to 5,000 characters, end at or after start |
| `font` | `Hanken Grotesk` |
| `size` | 8–200 |
| `color`, `highlightColor` | Three- or six-digit hex color |
| `background` | `none` or three- or six-digit hex color |
| `backgroundOpacity`, `backgroundPadding`, `backgroundRadius` | 0–100 |
| `position` | `top`, `middle`, or `bottom` followed by `left`, `center`, or `right`, separated by one space |
| `animation` | `none` or `fade` |
The host fills omitted appearance values with its existing defaults, preserves
provided styling and clones word timings before saving through ordinary edit
history. Caption updates retain their existing `{text,start,end}` contract.
Package source, MIT notices, lineage and three fixtures are retained in the
canonical archive; validate, test and remix it using the public CLI.
---
# Tutorial: a React Block in 30 minutes
URL: https://www.stillmade.shop/docs/build/tutorial
You need Node.js 22 or newer and npm. Every step runs on your machine without
an account until you import the Block.
1. **Create the Block (3 minutes).** The SDK is one npm package on StillMade's
site; nothing else to download.
```sh
npx --yes --package=https://stillmade.shop/block-sdk/stillmade-sdk-0.1.0.tgz stillmade-block create title-cards view-react
cd title-cards
npm install --save-dev https://stillmade.shop/block-sdk/stillmade-sdk-0.1.0.tgz # the stillmade-block command
npm install # React and esbuild
```
2. **Look around (1 minute).** The folder has the manifest
(`stillmade.block.json`), the logic (`module/main.js`, a module Block), the
interface (`src/view.jsx`, `src/view.css`, `src/view.html`, and
`src/view.config.json`, which makes it a frame view), fixtures and
TypeScript types.
3. **Preview it (3 minutes).** `npm run build`, then `npm run dev`, and open
`http://127.0.0.1:4800`. Type two titles and choose **Make cards**: the cards
appear, and the console on the right shows the run.
4. **Change it (8 minutes).** Add a prefix for every card.
- In `stillmade.block.json`, add an input:
`"prefix": {"type": "text", "required": false, "default": "", "semantic": "card_prefix", "description": "Text before each title."}`.
- In `module/main.js`, use it: `title: (input.prefix + title).slice(0, 120)`.
- In `src/view.jsx`, add a text field for the prefix (its own `useState`) and
send it: `StillMade.run({lines, prefix})`.
- Run `npx stillmade-block types .` so your editor knows the new input.
Each save rebuilds and reloads the preview.
5. **Use a hosted operation and test it offline (5 minutes).** Ask a model for a
tagline.
- In `stillmade.block.json`, add `"capabilities": ["text.generate"]` to
`permissions` and an output:
`"tagline": {"type": "text", "semantic": "tagline", "description": "A short tagline."}`.
- In `module/main.js`:
`const tagline = await stillmade.hosted.run('text.generate', {prompt: 'Write a six-word tagline for: ' + titles.join(', '), maxTokens: 40});`
and return `tagline` with the cards.
- Record a reply in `tests/mocks.json`:
`{"hosted": {"text.generate": "Three scenes, one long road"}}`, and add
`"tagline": "Three scenes, one long road"` to each fixture's expected outputs.
- `npm test` passes offline and lists the stand-in that answered; the preview
uses it too. In StillMade, each call is priced and approved first.
6. **Package and import (5 minutes).** Change the `id` in the manifest to your
own, then `npm run pack`, which writes
`-1.0.0.stillmade-module.json`. In StillMade, open **Create**, choose
**Import**, select the file, review the checks and the preview, and confirm.
7. **Publish (5 minutes).** Open the Block in Create, choose **Publish**, pick
who can use it, confirm you have the rights to its content and publish. See
[publishing](/docs/distribute/publishing).
---
# Cookbook
URL: https://www.stillmade.shop/docs/build/cookbook
Short recipes for common work. Each links to a complete example or template.
**Read an uploaded CSV in a module Block.** Declare a `file` input, then
`const text = new TextDecoder().decode(await stillmade.files.read(input.csv));`.
Return a `table` value (`{columns, rows}`) to get a table with a CSV download.
Example: `examples/module-csv-summary`.
**Ask a model for structured JSON.** List `text.generate` in
`permissions.capabilities` and call
`await stillmade.hosted.run('text.generate', {prompt, schema})`; the reply is
checked against the schema. Example: `examples/module-action-items`.
**Generate an image and add it to the project.** Call
`await stillmade.hosted.run('image.generate', {prompt})`, then
`await stillmade.assets.create(image, {name: 'Poster'})` with `asset.create` in
`permissions.project`. Template: `module-hosted`.
**Remember settings between runs.** Declare `storage` with `cloud: "metered"`,
then `await stillmade.storage.get('settings')` and
`await stillmade.storage.set('settings', value)`. Template: `storage`.
**Call an outside API with the person's own key.** Declare the origin and key
in `permissions.network`, call `await stillmade.net.fetch(url)`, and record
responses under `network` in `tests/mocks.json` for offline tests. Template:
`outside-api`.
**Show progress and stop when cancelled.** Call
`stillmade.progress(0.5, 'Halfway')` as work advances and check
`stillmade.signal.aborted` in long loops.
**Analyse audio on the device.** `await stillmade.media.transform({operation:
'audio.decode', source: input.audio, sampleRate: 16000})` returns mono samples.
Example: `examples/module-tempo`.
**Run the Block from a React interface.** Initialize from
`StillMade.onInput(setInput)`, call `await StillMade.run(values)` from a button
and show the outputs. Template: `view-react`.
**Let collaborators edit together.** Keep each independently edited value in
its own shared field: `StillMade.updateShared({point3: 0.8})`, and listen with
`StillMade.onShared`. Example: `examples/view-react-chart`.
**Show an image output.** `const preview = await StillMade.previewImage(output.image)`
gives a display copy for an ` `; keep passing the original value to `run`.
**Run on every new video.** Declare
`"triggers": [{"event": "media.saved", "input": "video", "kinds": ["video"]}]`;
the project owner turns it on and approves a credit budget. Template:
`trigger-media`.
**Test hosted calls offline.** Add `tests/mocks.json` (an empty object uses the
recorded stand-ins) or pass `--mock`. See [local testing](/docs/build/testing).
**Type your code.** Run `stillmade-block types .` and use `BlockRun`,
`BlockInputs` and the typed `StillMade` global. See TypeScript above.
---
# What you can build
URL: https://www.stillmade.shop/docs/gallery
Start from a template (`stillmade-block create my-block `) or a
complete example in the SDK download's `examples/` folder.
| Area | Start from | What it does |
| --- | --- | --- |
| Video | `examples/module-silence-trimmer` | Cuts the pauses out of a recording on the device |
| Video | `trigger-media` | Captions each new upload automatically, inside an approved budget |
| Video | `timeline-proposal`, `video-generate` | Proposes Editor edits people review; generates clips |
| Images | `examples/module-opencv`, `image-generate`, `describe-image` | Computer vision with OpenCV; generation; description |
| Images | `examples/module-brand-kit` | Keeps a brand kit in project storage and makes brand cards |
| Audio and music | `examples/module-tempo`, `music`, `sfx`, `speech`, `transcribe` | Tempo detection; music, sound effects, voices and transcripts |
| Audio and music | `examples/module-audio-extract` | Extracts and normalises a video's audio |
| Data and spreadsheets | `examples/module-csv-summary` | Summarizes every column of a CSV file |
| Data and spreadsheets | `examples/view-react-chart` | A shared, draggable chart in React |
| Documents | `examples/module-pdf-shot-list`, `examples/module-action-items` | PDF to shot list; meeting notes to action items |
| Integrations | `examples/module-notion-to-airtable`, `outside-api`, `connection` | Outside APIs with each person's own keys and sign-ins |
| Integrations | `trigger-webhook` | Runs when another service sends an event |
| Education | `examples/javascript-cloze-quiz` | Fill-in-the-blank quizzes with an answer key |
| E-commerce | `examples/javascript-invoice` | Invoices from order lines, with tax and totals |
| Research | `web-fetch`, `web-research` | Answers from a page; research with sources |
| Anything else | `module`, `module-wasm` | Modern JavaScript, npm packages and WebAssembly |
If something you want to build is missing a capability,
[request a primitive](/create?request=primitive).
---
# TypeScript
URL: https://www.stillmade.shop/docs/build/typescript
`stillmade-block types ` writes `stillmade-env.d.ts` beside the
manifest (`create` writes it too, with a `jsconfig.json`, and running `types`
again replaces it after you change ports). It is self-contained and declares:
- `BlockInputs` and `BlockOutputs`, one property per port, typed by port type:
`text`, `url`, `date` and `color` are strings, `number` and `integer` are
numbers, media ports are saved references, `file`, `document`, `table` and
`transcript` have their value shapes, a `json` or `object` port with a compact
schema (`{"points": "number[]"}`) gets that shape, and lists are arrays.
Optional inputs (`required: false` or a default) are optional properties.
- The global `StillMade` in your interface, typed from the
[view bridge reference](/docs/reference/view-bridge):
`StillMade.run(values: BlockInputs): Promise`,
`StillMade.onInput(callback)` and the rest.
- The global `input` in `src/run.js` (JavaScript Blocks).
- `StillMadeModule` and `BlockRun` for a module entry:
```ts
import type {BlockRun} from '../stillmade-env';
const run: BlockRun = async (input, stillmade) => {
stillmade.progress(0.5, 'Working');
return {summary: input.text.toUpperCase()};
};
export default run;
```
Bundle TypeScript with your bundler as usual (esbuild handles `.ts` and
`.tsx`); StillMade runs the bundled JavaScript. The SDK's own declarations are
in `packages/block-sdk/*.d.ts`; `blockTypeDeclarations(manifest)`,
`portValueType(port)`, `schemaType(schema)` and `PORT_VALUE_TYPES` generate the
file, and `VIEW_BRIDGE` (with `VIEW_BRIDGE_MEMBERS` and
`viewBridgeReference()`) is the list of interface bridge members it types.
---
# Creator Program
URL: https://www.stillmade.shop/docs/creators
Everything needed to build for StillMade is free and works offline: the SDK,
the CLI with its templates, preview and stand-ins, these docs and the
examples. An account is needed only to import, publish and run with real
hosted operations.
- **Request a primitive.** When a Block cannot do something you need, tell us
from **Create → Creator tools → Request a primitive**, or
[open the form](/create?request=primitive). Name the missing capability and
what you are building. Requests are ranked together with the gaps StillMade
AI records when it cannot build a Block, and the most requested primitives
are built first.
- **Publish.** Community releases are free to install, use and remix (see
[pricing and access](/docs/distribute/selling)); curated releases go through
[marketplace review](/docs/distribute/publishing).
- **Track your work.** The Creator dashboard in Create → Creator tools shows how
your Blocks and Project Types are used.
- **Build what StillMade builds.** [StillMade Blocks and the SDK](/docs/reference/parity)
lists every service StillMade's own Blocks use and the public SDK features that
do the same, with any remaining gap.
---
# SDK versions and deprecation
URL: https://www.stillmade.shop/docs/distribute/sdk-versions
What is versioned:
- **The SDK**, named by `sdkVersion` in each manifest (now `0.1.0`), with its
packages, CLI and documentation.
- **Contracts inside a package**, each with its own `schemaVersion`: the
manifest, capability descriptors, module descriptors, portable packages and
conversions.
- **Each Block release**, with its own semantic version. A published release is
immutable: StillMade keeps its exact source and the contract it was built for.
What stays true:
- A published release keeps working. Installed pins never move to a newer
version on their own, and saved projects reopen with the pinned version.
- New capabilities are additive: new optional manifest fields, port types,
hosted operations, host API methods and templates do not change how existing
packages behave.
- A change that would alter existing behavior ships under a new name or a new
`schemaVersion`, and the old one keeps working for releases that use it.
Before 1.0 (`0.x`), the SDK may still change between minor versions; every
change is listed in the [changelog](/docs/changelog), and published releases
keep working regardless. From 1.0 the SDK follows semantic versioning: a major
version only for removals, and a deprecated API keeps working for at least 12
months after its replacement ships. Deprecations are marked in these docs, in
`stillmade-block validate` warnings and in the changelog, and removing an API
never changes a release that was already published with it.
**Licence.** The SDK (its packages, CLI, guides and examples) is licensed under
the [PolyForm Shield License 1.0.0](/block-sdk/LICENSE.txt). You may use and change
it, and build, publish and sell Blocks with it; you may not use it to provide a
product that competes with StillMade. Your Blocks are yours: each Block carries
the licence its creator names in its manifest, and StillMade's own Blocks use
MIT.
---
# Interface bridge reference
URL: https://www.stillmade.shop/docs/reference/view-bridge
Standard and frame views receive the same `StillMade` object; members marked Frame views exist only there. `In` and `Out` are the Block's input and output values (see [TypeScript](/docs/build/typescript)). The view host checks every request itself: a read-only viewer cannot run the Block or save outputs from any view.
### Inputs and runs
| Member | Where | What it does |
| --- | --- | --- |
| `readonly input: Partial` | Every view | A copy of the current input values. Read it after `onInput` first fires. |
| `onInput(callback: (input: Partial) => void): () => void` | Every view | Receives the input when the view starts and each time the host changes it. Returns a function that stops listening. Initialize controls here. |
| `run(values: In): Promise` | Every view | Checks the named input values, runs the Block (its recipe, JavaScript, module or hosted operation, after any approval the host asks for) and resolves to the checked outputs. One run at a time, 30 a minute. |
| `useOutput(outputs: Partial): Promise>` | A saved project Step | Chooses media the view already holds (input media or a desktop recording) as the Block outputs without running. Only complete references the host issued are accepted. |
| `readonly schemaFields: Readonly>` | Every view | Helpers that build form fields from the input ports and their schemas, honoring inputs the host locked. |
### Shared state
| Member | Where | What it does |
| --- | --- | --- |
| `getShared(): {state: Record; outputs: Record; memory: unknown; canEdit: boolean}` | Every view | A copy of the view state shared by everyone in the project, the last outputs, the Block memory and whether this person may edit. |
| `onShared(callback: (shared: ReturnType) => void): () => void` | Every view | Receives the shared state now and after every change by anyone. Returns a function that stops listening. |
| `updateShared(values: Record, preconditions?: Record, options?: Record): Promise` | Every view | Changes shared fields. Keep each independently edited value in its own field so two people never conflict; a field someone else changed first is refused. |
| `bindShared(key: string, id: string): () => void` | Every view | Binds a form element (by id) to one shared field: it shows other people's edits and saves this person's edits, keeping drafts while they type. |
| `setSharedDefaults(values: Record): void` | Every view | Values bound controls show until someone saves the field. |
| `scheduleSharedEdit(key: string, callback: () => unknown \| Promise): void` | Every view | Runs a named shared edit after a short pause (at most 500 ms after the first request), so rapid edits to one field (sliders, drags) become one save. |
| `connectDocument(options: Record): Readonly>` | Every view | Connects a declared JSON content store to shared edits once; returns its status, flush and recovery methods. |
| `indexById(items: Array<{id: string; [key: string]: unknown}>): Map` | Every view | Indexes list items by their stable id. |
| `diffDocument(before: Record, after: Record): Array>` | Every view | The minimal list of edits between two versions of a document. |
| `applyDocument(document: Record, patches: Array>): Record` | Every view | Applies edits from diffDocument to a document. |
### Media
| Member | Where | What it does |
| --- | --- | --- |
| `previewImage(value: StillMadeImage): Promise<{url: string; width: number; height: number}>` | Every view | A display-only PNG copy (at most 512 pixels a side) of an image the view may show. Keep passing the original value to run. |
| `previewMedia(value: StillMadeVideo \| StillMadeAudio): Promise<{kind: "video" \| "audio"; url: string}>` | Every view | A playable blob: URL for audio or video the view may play. Call releaseMedia when done. |
| `releaseMedia(url: string): void` | Every view | Frees a URL previewMedia returned. |
| `asset(path: string): string` | Frame views | A blob: URL for one of the view's own asset files, for , new FontFace() or . |
| `assetBytes(path: string): ArrayBuffer` | Frame views | A copy of one of the view's own asset files. |
### Generated interface
| Member | Where | What it does |
| --- | --- | --- |
| `render(targetId: string, tree: Array>): void` | Every view | Renders a tree of StillMade controls into an element, for standard views that build their interface from data. |
| `onAction(callback: (event: {action: string; value: unknown; checked: boolean; event: string}) => void \| Promise): () => void` | Every view | Receives actions from controls render drew. |
### Connected work
| Member | Where | What it does |
| --- | --- | --- |
| `connectedApp(operation: "tasks" \| "preview-task" \| "apply-task", args?: Record): Promise>` | A saved project Step | Lists, previews and applies tasks of the Block's connected-app contract. |
| `sharedWork(operation: string, args?: Record): Promise>` | A saved project Step | Reads and contributes to shared work items (reviews, contributions) in the project. |
| `onSharedWorkRefresh(callback: () => void): () => void` | A saved project Step | Called when shared work changes elsewhere; read it again. |
| `previewSharedWorkMedia(contributionId: string): Promise<{url: string; kind: "image" \| "video" \| "audio"}>` | A saved project Step | A display copy of media attached to a shared work contribution. |
| `releaseSharedWorkMedia(url: string): void` | A saved project Step | Frees a URL previewSharedWorkMedia returned. |
| `desktop(operation: string, args?: Record): Promise>` | StillMade Desktop | Runs a declared desktop permission (screen or camera capture, clipboard, native tools) after the person grants it. |
---
# StillMade Blocks and the SDK
URL: https://www.stillmade.shop/docs/reference/parity
StillMade's own Blocks run on host services. Each one is listed here with the public SDK features that give a creator Block the same capability, so nothing StillMade builds for itself stays out of reach without a stated reason. A test fails when one of StillMade's Blocks starts using a service that is not on this list.
| What StillMade's Blocks do | How a creator Block does it | Notes |
| --- | --- | --- |
| Animated scene assets: generated props and backgrounds, scene video slicing, sprite sheets. | hosted `image.generate`, hosted `video.generate`, action `media.generate`, `stillmade.media.transform`, `stillmade.files.write`, `stillmade.assets.create`, permission `context.shots.read`, permission `workspace.animation.propose` | Same capability |
| Browses other projects' scripts the person can open. | `stillmade.project.read` | Gap: Blocks read only the current project; reading another project the person can open is first-party only. |
| Canvas generation: images, videos, variations, stock and recording the asset. | hosted `image.generate`, hosted `video.generate`, action `media.generate`, action `stock.search`, `stillmade.assets.create` | Same capability |
| Canvas media: stock search, uploads and generation with prices. | hosted `image.generate`, hosted `video.generate`, action `media.generate`, action `stock.search`, `stillmade.files.write`, control `file` | Same capability |
| Canvas node placement for shipped workflow nodes. | permission `context.canvas.read`, permission `workspace.canvas.propose` | Same capability |
| Canvas workspace chrome (low-balance ribbon, default model). | `IMAGE_GENERATION_MODELS` from the SDK | The credit balance ribbon is account UI; creator Blocks see prices in StillMade's own approval dialog before any paid run. |
| Collaborative script editing. | `StillMade.getShared`, `StillMade.updateShared`, `StillMade.connectDocument` | Same capability |
| Composes the Canvas workspace from its native parts. | permission `workspace.canvas.propose` | StillMade React components and hooks only render inside the app; creator interfaces are frame views (any framework) or standard views with generated controls, styled with the same theme variables. |
| Connects a YouTube channel. | `stillmade.connections.call`, `stillmade.net.fetch` | Same capability |
| Creates a visual style with generated samples. | hosted `image.generate`, permission `context.styles.read` | Same capability |
| Credits or the person's own key for image, video, chat, voice, music and effects. | hosted `text.generate`, hosted `image.generate`, hosted `video.generate`, hosted `audio.speech`, hosted `audio.music`, hosted `audio.sfx` | Same capability |
| Crops panel images. | `stillmade.files.read`, `stillmade.files.write`, `stillmade.media.transform` | Same capability |
| Describes images and media for prompts and context. | hosted `image.describe`, hosted `media.analyze` | Same capability |
| Downloads the media a controller selected, under the export policy. | control `download`, port type `file-bundle`, action `final.render` | Same capability |
| Editor asset generation with a model, prompt, aspect, quality and reference images. | hosted `image.generate`, hosted `video.generate`, action `media.generate` | Same capability |
| Edits panel settings with local runs and confirmations. | `StillMade.run`, `StillMade.updateShared` | Same capability |
| Embeds the score, vibe and rough-cut panels in the Editor. | port type `timeline-edit`, permission `timeline.propose`, permission `context.timeline.read` | Same capability |
| Final renders of the timeline on the device, under the export policy. | action `final.render`, `stillmade.media.transform` | Same capability |
| Finds, loads and tags pipeline assets. | permission `context.assets.read`, `stillmade.project.read`, `stillmade.assets.appendVersion` | Gap: Clearing a generation watermark is tied to the account's plan and is not offered to Blocks. |
| Generates panel images with model, quality and references. | hosted `image.generate`, hosted `video.generate`, action `media.generate` | Same capability |
| Lists and applies saved visual styles. | permission `context.styles.read`, `stillmade.storage.get` | Same capability |
| Live collaborative timeline editing with permitted updates. | `StillMade.getShared`, `StillMade.updateShared`, `StillMade.connectDocument`, permission `workspace.editor.propose` | Same capability |
| Model lists and price labels for generation settings. | `IMAGE_GENERATION_MODELS` from the SDK, `VIDEO_GENERATION_MODELS` from the SDK, `AUDIO_GENERATION` from the SDK | Same capability |
| Model, resolution, quality and voice options for pipeline settings. | `IMAGE_GENERATION_MODELS` from the SDK, `VIDEO_GENERATION_MODELS` from the SDK, `AUDIO_GENERATION` from the SDK | Same capability |
| Notices, StillMade AI's presenter and upgrade prompts. | Not needed | Notices and plan upgrade prompts are account UI; a Block cannot raise them, so it cannot imitate billing or account messages. |
| Opens and closes StillMade AI's presentation of a Step. | Not needed | This is StillMade AI's own presentation of Steps; creator Blocks are presented the same way automatically and need no API. |
| Phone or desktop layout detection in the Editor. | `StillMade.render` | Device layout in a creator interface is ordinary responsive CSS inside the frame; StillMade checks it at 320 to 1280 pixels before import. |
| Places StillMade AI's generated elements on a board or canvas. | permission `workspace.board.propose`, permission `workspace.canvas.propose` | Same capability |
| Plans a panel strip from the script with a model. | hosted `text.generate`, permission `context.script.read`, permission `workspace.panels.propose` | Same capability |
| Provisions StillMade AI's paid generation for a presentation. | action `presenter.generate`, action `media.generate` | Same capability |
| Remembers a person's default image model and quality. | `stillmade.storage.get`, `stillmade.storage.set` | Same capability |
| Rewrites panel prompts with a model. | hosted `text.generate` | Same capability |
| Rough cut: frames, local footage, analysis, refinement, transcription and voiceover. | `stillmade.media.transform`, hosted `media.analyze`, hosted `audio.transcribe`, hosted `audio.speech`, hosted `text.generate`, `stillmade.files.write`, permission `timeline.propose` | Gap: Reading footage straight from the person's disk (without uploading it) is a desktop-only first-party path. |
| Runs a draft recipe or JavaScript Block locally while it is edited. | `StillMade.run` | Same capability |
| Runs Editor operations: renders, previews and timeline edits. | `stillmade.media.transform`, action `final.render`, permission `timeline.propose` | Same capability |
| Runs the production pipeline: assembly, generation, budgets, references and deliveries. | hosted `image.generate`, hosted `video.generate`, action `media.generate`, hosted `text.generate`, `stillmade.files.write`, permission `workspace.canvas.propose`, trigger `media.saved` | Same capability |
| Runs the shot planning workspace in its sandbox. | `StillMade.run`, permission `workspace.shotPlan.propose` | Same capability |
| Score and sound effect lanes: generated music and effects placed in the Editor. | hosted `audio.music`, hosted `audio.sfx`, action `media.generate`, action `stock.search`, permission `timeline.propose` | Same capability |
| Script services: drafts, models and project context. | hosted `text.generate`, permission `context.script.read`, `stillmade.project.read` | Same capability |
| Shot planning services: shots, versions and planning with a model. | hosted `text.generate`, permission `context.shots.read`, permission `workspace.shotPlan.propose`, port type `shot-plan` | Same capability |
| Shows a panel image large with its narration and text. | `StillMade.previewImage` | Same capability |
| Shows the script in its workspace layout. | port type `script`, control `document` | StillMade React components and hooks only render inside the app; creator interfaces are frame views (any framework) or standard views with generated controls, styled with the same theme variables. |
| StillMade AI presents a workspace and adds board elements. | action `presenter.generate`, permission `workspace.board.propose` | Same capability |
| StillMade dialogs and notices for Image Panels. | `StillMade.render` | StillMade's own dialogs stay host-owned so a Block cannot imitate account, payment or permission prompts; creator interfaces show their own confirmations inside their frame. |
| Story bible editing with collaboration and the bible graph hash. | `StillMade.getShared`, `StillMade.updateShared`, `StillMade.connectDocument`, permission `context.blueprint.read`, permission `workspace.blueprint.propose`, hosted `text.generate` | Same capability |
| Story bible workspace UI (selects, step header, bottom sheet, help). | `StillMade.render`, `StillMade.schemaFields` | StillMade React components and hooks only render inside the app; creator interfaces are frame views (any framework) or standard views with generated controls, styled with the same theme variables. |
| Text, story segments and voiceover inside pipeline nodes. | hosted `text.generate`, hosted `audio.speech` | Same capability |
| The Canvas preview shown for a Step. | `StillMade.render`, permission `context.canvas.read` | StillMade React components and hooks only render inside the app; creator interfaces are frame views (any framework) or standard views with generated controls, styled with the same theme variables. |
| The Editor media library: project assets and uploads. | permission `context.assets.read`, `stillmade.project.read`, `stillmade.assets.create`, `stillmade.assets.appendVersion` | Same capability |
| The Editor preview shown for a Step. | `StillMade.previewMedia`, permission `context.timeline.read` | StillMade React components and hooks only render inside the app; creator interfaces are frame views (any framework) or standard views with generated controls, styled with the same theme variables. |
| The host side of the shot planning sandbox. | `StillMade.run`, permission `workspace.shotPlan.propose` | Same capability |
| The voiceover preview shown for a Step. | `StillMade.previewMedia` | StillMade React components and hooks only render inside the app; creator interfaces are frame views (any framework) or standard views with generated controls, styled with the same theme variables. |
| Uploads a stored export to YouTube. | action `youtube.upload` | Same capability |
| Uploads and checks media files, thumbnails and downloads. | control `file`, `stillmade.files.write`, `stillmade.assets.create` | Same capability |
| Vectorizes and upscales panel images. | `stillmade.asset`, `stillmade.files.read`, `stillmade.files.write` | Gap: Upscaling with a hosted model is not a hosted operation yet; module Blocks can only upscale with a model they ship in WebAssembly. |
| Video assets workspace: model limits, durations, voices, prices and batch generation. | hosted `image.generate`, hosted `video.generate`, action `media.generate`, hosted `audio.speech`, `VIDEO_GENERATION_MODELS` from the SDK | Same capability |
| Voiceover services: voices, takes and transcripts. | hosted `audio.speech`, hosted `audio.transcribe`, permission `context.transcript.read` | Same capability |
| Voiceover workspace UI with live collaboration. | `StillMade.getShared`, `StillMade.updateShared`, `StillMade.connectDocument`, `StillMade.previewMedia` | StillMade React components and hooks only render inside the app; creator interfaces are frame views (any framework) or standard views with generated controls, styled with the same theme variables. |
| White Board Motion: live collaborative board edits, elements and assets. | `StillMade.getShared`, `StillMade.updateShared`, `StillMade.connectDocument`, permission `context.board.read`, permission `workspace.board.propose`, permission `context.assets.read` | Same capability |
| Who may change Image Panels right now. | `StillMade.getShared` | Same capability |
Missing something? [Request a primitive](/create?request=primitive).
---
# Limits
URL: https://www.stillmade.shop/docs/reference/limits
Every number below is read from the SDK constant named under its heading, the same value the runtime and StillMade enforce. Exceeding one fails with the error code in the [error reference](/docs/reference/errors).
## JavaScript Blocks (src/run.js)
`JAVASCRIPT_LIMITS`
| Limit | Value |
| --- | --- |
| Source size | 256 KiB |
| Input and output JSON | 4 MiB |
| CPU time per run | 500 ms |
| Memory | 16 MiB |
| Sandbox memory reserve | 32 MiB |
## Module Blocks
`MODULE_LIMITS`
| Limit | Value |
| --- | --- |
| Files in module/ | 64 |
| One file | 64 MiB |
| All files | 128 MiB |
| File path length | 160 |
| Run time without progress (host calls do not count) | 2 min |
| Longest run in real time | 30 min |
| Extra time after each progress report | 1 min |
| One message to or from the host | 16 MiB |
| Log lines kept per run | 500 |
## Standard views
`VIEW_LIMITS`
| Limit | Value |
| --- | --- |
| Markup | 64 KiB |
| Styles | 32 KiB |
| Script | 64 KiB |
| One message to the host | 4 MiB |
| StillMade.run calls per minute | 30 |
| Image previews per minute | 60 |
| One image preview | 15 s |
| Preview image side (pixels) | 512 |
## Frame views
`FRAME_VIEW_LIMITS`
| Limit | Value |
| --- | --- |
| Markup | 256 KiB |
| Each stylesheet | 256 KiB |
| Script bundle (view.js) | 4 MiB |
| Asset files | 32 |
| All asset files | 8 MiB |
| One task before it is stopped | 250 ms |
| A blocking native operation before the view is closed | 1 s |
## Shared view state
`SHARED_VIEW_LIMITS`
| Limit | Value |
| --- | --- |
| All shared fields | 384 KiB |
| One updateShared request | 800,000 bytes |
| Unsent edits | 2 MiB |
## Hosted operations
`CAPABILITY_LIMITS`
| Limit | Value |
| --- | --- |
| Prompt characters | 32,000 |
| Project context sent with a prompt | 24,000 bytes |
| System instruction characters | 8,000 |
| Text output characters | 32,768 |
| Tokens a reply may use | 4,096 |
| Fixtures in a capability Block | 3 |
| Literal phrases one fixture checks | 8 |
| One literal phrase | 256 |
| Capability package | 256 KiB |
| Host metadata | 8 KiB |
## Image generation
`IMAGE_CAPABILITY_LIMITS`
| Limit | Value |
| --- | --- |
| Prompt characters | 32,000 |
| Legacy fixed-size image | 12,000,000 bytes |
| Legacy fixed width | 1,024 |
| Legacy fixed height | 1,024 |
| Longest side (pixels) | 8,192 |
| Pixels | 40,000,000 |
| Image file | 64 MiB |
## Video generation
`VIDEO_CAPABILITY_LIMITS`
| Limit | Value |
| --- | --- |
| Prompt characters | 32,000 |
| Video file | 256 MiB |
| Seconds | 60 |
| Width (pixels) | 3,840 |
| Height (pixels) | 3,840 |
| Pixels per frame | 8,294,400 |
## Speech
`SPEECH_CAPABILITY_LIMITS`
| Limit | Value |
| --- | --- |
| Characters per call | 3,800 |
| Seconds | 1,400 |
| Audio file | 64 MiB |
| Canonical WAV sample rate | 24,000 |
| Canonical WAV channels | 1 |
## Music, sound effects and transcription
`AUDIO_CAPABILITY_LIMITS`
| Limit | Value |
| --- | --- |
| Audio file | 128 MiB |
| Seconds | 1,800 |
| Music seconds (min, max, default) | min 5; max 240; default 30 |
| Sound effect seconds (min, max, default) | min 0.5; max 22; default 4 |
## Conversation history (text.generate)
`TEXT_HISTORY_LIMITS`
| Limit | Value |
| --- | --- |
| Turns | 20 |
| Characters per turn | 8,000 |
| Characters in all turns | 24,000 |
## Web reading
`WEB_CAPABILITY_LIMITS`
| Limit | Value |
| --- | --- |
| URL length | 2,048 |
| Instruction characters | 4,000 |
| Research topic characters | 500 |
| Sources returned | 12 |
| Answer characters | 32,768 |
## Media analysis
`MEDIA_ANALYZE_LIMITS`
| Limit | Value |
| --- | --- |
| Images per call | 4 |
| Question characters | 2,000 |
| Seconds of one closer-inspected video range | 12 |
## Image description
`IMAGE_DESCRIBE_LIMITS`
| Limit | Value |
| --- | --- |
| Images per call | 4 |
## Generation from code (media.generate)
`MEDIA_GENERATE_LIMITS`
| Limit | Value |
| --- | --- |
| Items per batch | 50 |
| Prompt characters | 4,000 |
## Generation settings
`GENERATION_LIMITS`
| Limit | Value |
| --- | --- |
| Reference images | 8 |
| Negative prompt characters | 2,000 |
| Largest seed | 2,147,483,647 |
## Outside APIs (net.fetch)
`NETWORK_LIMITS`
| Limit | Value |
| --- | --- |
| Declared origins | 8 |
| Request body | 4 MiB |
| Response body after decompression | 8 MiB |
| One request | 30 s |
| Redirects followed | 3 |
| Request headers | 32 |
| All request headers | 16 KiB |
| URL length | 8,192 |
| Requests per minute (person and Block) | 120 |
| Requests per minute to one origin (everyone) | 3,000 |
| Saved key length | 4,096 |
## Cloud storage
`CLOUD_STORAGE_LIMITS`
| Limit | Value |
| --- | --- |
| Key length | 200 |
| One JSON value | 1 MiB |
| One file | 64 MiB |
| Everything one Block keeps in a project | 256 MiB |
| Keys per Block per project | 2,000 |
| Items per list page | 500 |
| Media type length | 100 |
## Triggers
`BLOCK_TRIGGER_LIMITS`
| Limit | Value |
| --- | --- |
| Triggers per Block | 4 |
| Label characters | 120 |
| Shortest schedule interval (seconds) | 60 |
| Longest schedule interval (seconds) | 31,536,000 |
## Media transforms
`MEDIA_TRANSFORM_LIMITS`
| Limit | Value |
| --- | --- |
| Ranges kept by one cut | 500 |
| Videos joined at once | 50 |
| Media length (seconds) | 21,600 |
| Decoded samples | 201,326,592 |
| Decode sample rates | 8,000, 16,000, 22,050, 24,000, 32,000, 44,100, 48,000 |
| Thumbnail width (pixels) | 4,096 |
## Files, documents and tables
`DATA_VALUE_LIMITS`
| Limit | Value |
| --- | --- |
| File name length | 255 |
| Document characters | 1,000,000 |
| Document title characters | 300 |
| Table columns | 500 |
| Table rows | 100,000 |
| Table cells | 500,000 |
| Column name length | 160 |
| Characters in one cell | 100,000 |
| Link length | 8,192 |
## File bundles
`FILE_BUNDLE_LIMITS`
| Limit | Value |
| --- | --- |
| Files | 256 |
| Text in all files | 3,000,000 bytes |
| Path length | 180 |
## JSON schemas (text.generate)
`JSON_SCHEMA_LIMITS`
| Limit | Value |
| --- | --- |
| Schema size | 8 KiB |
| Nesting depth | 8 |
| Properties per object | 64 |
| Values per enum | 64 |
| Characters in a schema string | 4,000 |
---
# Preview with hot reload
URL: https://www.stillmade.shop/docs/build/local-preview
`stillmade-block dev ` opens a local preview at `http://127.0.0.1:4800`
(`--port` changes it). The page is StillMade's own view host: the same sandbox
page, bridge, response policy, theme, run limits and guard that run your
interface in the app, so what works here works there.
- Your interface (standard or frame view) runs with the first fixture's input.
Edit the input as JSON beside it, pick another fixture, switch Light, Dark and
White, and try phone, tablet and full widths.
- `StillMade.run` runs the Block on your machine with the SDK runtime. Hosted
operations, files, network, connections and storage are answered by
[stand-ins](/docs/build/testing), and stand-in media (images, audio, video)
previews in the view. Shared view state (`updateShared`) is kept in the page.
- Saving any file the package reads reloads the preview, keeping the input and
shared state. With `--build "npm run build"`, saving any other source file
(such as `src/view.jsx`) runs the build first, then reloads.
- The console lists every run, the module's `stillmade.log` lines, each
stand-in that answered, reloads, build output and errors from the view.
- The server listens on this machine only, refuses other host names and other
sites, and never contacts StillMade or a provider. Paid runs, sign-ins and
keys happen only in StillMade, after you import the Block.
### Live link to your account
`stillmade-block dev --live` also prints a link to StillMade's Import
page. Open it while signed in and choose **Connect**: each time you save, the
packed Block (what `pack` would write) opens in StillMade's usual import review
with its checks and preview, and confirming installs that version in your
account, as a new revision of the same build. Use it in a project to run it with
real hosted operations, files and connections; every paid step shows its price
and asks first, as for any Block.
- Nothing is installed or published without your confirmation in the review.
- Only the StillMade site can read the link, and only with the random key in it,
which changes each time `dev` starts. `--site` sets another StillMade address
(https). Stop the link with **Disconnect** or by stopping `dev`.
- The browser may ask once whether StillMade may reach a local address.
## Templates
`stillmade-block create ` writes a complete folder that validates and tests offline:
| Template | What it starts |
| --- | --- |
| `recipe` (default), `image`, `stateful` | Built-in steps with no code; memory between runs |
| `javascript` | A few lines of sandboxed JavaScript |
| `module`, `module-wasm` | Modern JavaScript modules in a Worker, with WebAssembly |
| `module-hosted` | A module calling hosted text and image generation as tools |
| `view-react`, `view-html` | A frame view in React (bundled with esbuild) or plain web code |
| `text`, `image-generate`, `video-generate`, `speech`, `music`, `sfx`, `transcribe`, `describe-image`, `analyze-media`, `web-fetch`, `web-research`, `timeline-proposal` | One hosted operation each, on the person's credits |
| `outside-api`, `connection` | An outside API with the person's own key; a published adapter with their sign-in |
| `storage` | Values kept across runs in project storage |
| `trigger-media`, `trigger-webhook`, `trigger-schedule` | Blocks that run on their own once the project owner turns them on |
---
# Test and preview a Block
URL: https://www.stillmade.shop/docs/build/testing
## Fixtures are part of the package
Include 1–50 fixtures. Each names an input object and the complete expected output object. Defaults and optional ports still follow the declared contract.
```json
[
{
"name": "Counts words",
"input": {
"text": " Hello StillMade "
},
"expected": {
"text": "Hello StillMade",
"words": 2
}
},
{
"name": "Empty text",
"input": {
"text": " "
},
"expected": {
"text": "",
"words": 0
}
}
]
```
## Run the fixture suite
```sh
node packages/block-cli/cli.js test ./my-block
```
A test report includes `passed`, `digest`, `sdkVersion`, and individual results. Fixtures execute the real package runtime. Matching JSON output is required; approximate image similarity is not used.
## What to cover
- Ordinary input with a useful, observable result.
- Empty text or minimum-size images, when the contract allows them.
- Boundary values for controls.
- Behavior that preserves unrelated data, colors, or alpha.
- Values that the next Block will actually consume.
## Preview with your own sample
Save a JSON input object as `sample.json` and run:
```sh
node packages/block-cli/cli.js preview ./my-block ./sample.json
```
Use synthetic input in distributed packages. Private project material belongs in local samples, not published fixtures.
## Test offline with stand-ins
StillMade hosts part of what a Block does: hosted operations, saved files,
outside APIs and connections, cloud storage, project reads and assets. On your
own machine, `stillmade-block test`, `run` and `dev` answer these calls with
stand-ins:
- `--mock` turns them on. A folder with `tests/mocks.json` uses them every time,
including for `pack`, which runs the fixtures first.
- Every hosted operation has a recorded stand-in with the exact shape StillMade
returns, checked by the same validators the app uses. Text replies read
`Stand-in reply to: `; images are 1024-pixel PNGs (or the requested
aspect ratio); speech, music and sound effects are short WAV tones; video is a
one-second MP4; transcripts, research, media analysis, timeline proposals and
JSON replies (sampled from the Block's schema) are small fixed results. Files
a stand-in or `files.write` makes can be read back with `files.read`.
- `tests/mocks.json` records your own answers. Every section is optional, and a
list answers successive calls in order (the last one repeats):
```json
{
"hosted": {"text.generate": ["First reply", "Second reply"]},
"actions": {"stock.search": {"results": []}},
"network": [{"method": "GET", "url": "https://api.github.com/repos/octocat/hello-world/issues*", "status": 200, "json": []}],
"connections": {"app:openapi:notion/retrievePage": {"properties": {}}},
"files": {"clip-1": "fixtures/clip.wav"},
"storage": {"settings": {"runs": 3}},
"project": {"schemaVersion": 1, "metadata": {"name": "Launch"}},
"transforms": {"video.cut": {"kind": "video", "assetId": "cut-1", "versionId": "v1", "url": "/api/media/stillmade-mock/cut.mp4", "mimeType": "video/mp4", "width": 320, "height": 180, "duration": 1, "bytes": 2048}}
}
```
| Section | Answers | Value |
| --- | --- | --- |
| `hosted` | `stillmade.hosted.run` and capability Blocks, by operation | The output value the call returns |
| `actions` | `stillmade.actions.run`, by operation | The action result (`{value, failures}` for `media.generate`) |
| `network` | `stillmade.net.fetch` | Recorded responses: method, url (a trailing `*` matches the rest), status, headers and one of `json`, `text` or `base64` |
| `connections` | `stillmade.connections.call`, by `appId/operation` | The JSON result |
| `files` | `files.read`, by asset ID or URL | A path inside the Block folder |
| `storage` | `stillmade.storage` | Values stored before the run |
| `project` | `stillmade.project.read` | A project snapshot |
| `transforms` | `stillmade.media.transform`, by operation | The transform result (`audio.decode` on a WAV file decodes it for real) |
For a capability Block, fixture checks on the model's words (`includes`,
lengths, word counts) are listed as not checked rather than run against a
stand-in; StillMade runs them with a real model when you import the Block.
Media checks (sizes, durations, formats) still run. A run with stand-ins is
never a live verification: reports say `liveVerified: false` and list every
stand-in that answered. Nothing contacts a provider or spends credits.
The same stand-ins are available to your own tests from
`@stillmade/block-sdk/mock-host`: `createMockHost({mocks, readLocalFile,
manifest})` returns `host` (pass it to `createNodeModuleExecutor({files,
host})`), `capability` (pass it to `runPackageAsync` or `testPackage` for a
capability Block), `calls` (what answered each call) and `file(url)` (the bytes
of a stand-in or written file). `standInFixtures(pkg)` returns a capability
Block's fixtures with the checks on the model's words removed and lists them;
`validateMocks(value)` checks a `tests/mocks.json` value;
`sampleJsonSchema(schema)` makes a value that satisfies a JSON schema; and
`mockPng`, `mockWav` and `mockMp4` make the stand-in media. `MOCKS_FILE` is
`tests/mocks.json` and `MOCK_MEDIA_PREFIX` is the URL prefix of stand-in media.
## Admission and workflow tests
The app also scans source and permissions, repeats contract tests in its sandbox, runs a sample, validates outputs, and requires review before installation. After insertion, [rehearse the connected workflow](/docs/project-types/testing); passing one Block’s fixtures alone does not establish workflow compatibility.
---
# Production completion
URL: https://www.stillmade.shop/docs/project-types/completion
An optional `outcome` field declares the finished result required by this exact Project Type version:
```json
{"outcome":{"type":"export","dedupe":"material_project_state","minimumDurationSec":3}}
```
`type` is `export`, `delivery`, or `project_outcome`. `dedupe`, when present, must be `material_project_state`; that is also its default. `minimumDurationSec` is optional, finite, between 0 and 86,400 seconds. A positive minimum requires host-measured media duration. Omit it for text or editable project artifacts.
Configure this in **Project defaults & starting questions → Production completion**. Omitting `outcome` preserves the existing host completion behavior. These settings never launch exports, create revenue or mark a client action as successful. Version the Project Type when changing its production rule.
Only a trusted completion adapter can supply `materialStateHash`, measured duration, success, retained-source lineage and the actual outcome kind. The accounting qualifier reads the rule from the exact released Project Type; browser-provided contributors and success flags are not accepted. Matching material states deduplicate under the payer-period policy. Verified editable-project deliveries can satisfy `project_outcome`; a local MP4 upload alone does not prove an executed media export.
---
# Hosted video generation
URL: https://www.stillmade.shop/docs/build/video-generation
Use `runtime: "capability"` and `permissions.capabilities: ["video.generate"]`. Declare one text, script or brief prompt input, up to two `image` inputs for the first and last frame, and one video output. Every video model the app's own Blocks use is available — Seedance 2.0 (and Fast, Mini), Kling 3.0 Turbo, Wan 2.6 Flash, Veo 3.1 Lite, Vidu Q3 Turbo and Wan 2.7 — with their durations, resolutions, aspect ratios, image-to-video and first-and-last-frame modes (see [generation models](/docs/reference/generation-models)). Users pay with StillMade credits at StillMade's per-second price. The capability file is:
```json
{"schemaVersion":1,"operation":"video.generate","prompt":{"$input":"prompt"},
"firstFrame":{"$input":"start"},"lastFrame":{"$input":"end"},
"settings":{"model":"veo-3.1-lite","duration":6,"resolution":"720p","aspectRatio":"16:9","generateAudio":true},
"negativePrompt":"blur","seed":7,"output":"video"}
```
`settings`, `firstFrame`, `lastFrame`, `negativePrompt` and `seed` are optional; each setting must be one the model accepts (Veo locks 1080p to 8 seconds; Wan 2.7 needs a first frame). Never include provider endpoints, keys, prices or payment. The run dialog preselects the Block's defaults and the user may change them before seeing the exact price. The invocation is `{operation:"video.generate",prompt}` plus `firstFrame`, `lastFrame`, `negativePrompt` and `seed` when declared; frames must be the user's own saved images. Public trials use their separately reviewed acquisition policy and never account BYOK.
The canonical output has `kind:"video"`, bounded `assetId`/`versionId`, a stored HTTPS or host media `url`, `mimeType:"video/mp4"`, measured `width`, `height`, `duration` in seconds and `bytes`. The host keeps the provider's MP4 byte-for-byte after measuring its container. SDK limits are 60 seconds, 256 MiB, at most 3,840 pixels on either axis and 8,294,400 pixels per frame. Validating this shape does not verify ownership or real video bytes; the host must inspect, store and verify them.
Fixtures use `kind:"video"`, `format:"mp4"` and explicit `minDuration`, `maxDuration`, `minBytes`, `maxBytes`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight` bounds. See `packages/block-sdk/video-generation-example.js`. Provider fixtures require approval; they are not deterministic equality tests. `validateGeneratedVideoReference`, `defaultCapabilityExpectations` and `validateCapabilityResult` are available through the SDK. Media receipts bind the owner, run, invocation, storage key and SHA-256 digest to the complete output reference. Guest code cannot issue those receipts.
A host advertises only enabled operations. A valid package does not imply every deployment has video generation configured; unavailable hosts must reject it before billing or dispatch. Public video results use the same private media/claim lifecycle as other trial outputs and are retained when continuing in an account.
### Authored state migration routes
A new JavaScript Block release can optionally declare `manifest.state.migrations`
for explicit upgrades of its Step memory. A route's `fromVersion` is an older
positive state schema version; its destination is the enclosing `state.version`.
For example, a declaration for state version 2 can contain:
```json
"migrations": [{
"fromVersion": 1,
"operations": [
{ "op": "rename", "from": "count", "to": "completed" },
{ "op": "default", "key": "labels", "value": [] },
{ "op": "remove", "key": "temporary" }
]
}]
```
Operations run in order on safe ASCII top-level field names. Rename requires an
existing source and absent destination; default writes only if absent; remove
requires the field to exist. Nested values remain intact. There are at most 16
distinct older-version routes and 64 operations per route. Combined declarations
and every intermediate state each fit within 64 KiB. An empty operation list
explicitly advances only the schema version. There is no implicit chaining,
downgrade, deep-path transformation, expression evaluation or code execution.
The portable SDK exports `validateStateMigrations(stateDeclaration)`,
`stateMigrationRoute(manifest, fromVersion)`, and
`migrateBlockState(previousManifest, nextManifest, savedState)`. The last returns
`{state, operations, fromVersion, toVersion}` without modifying its inputs. Both
manifests must describe the same JavaScript Block identity, with valid state
contracts. Matching state versions use an identity transfer; changed versions
require an exact direct route. Runtime execution does not invoke migrations.
Migration declarations change package source: publish a new immutable Block
release and keep its fixtures on the target schema. Do not guess existing state
fields or reset user memory. In StillMade, owners review exact archived/current
source pins, operations and before/after memory, then explicitly apply. The host
rechecks account/project/source/state, retains rollback history and invalidates
downstream results. External hosts must provide equivalent ownership, source
verification, approval and rollback boundaries before accepting the result.
---
# Generate media from JavaScript
URL: https://www.stillmade.shop/docs/build/generate-media
A JavaScript Block can generate images and videos with any model in the shared
catalog (the same models the app's own Canvas and Image Panels use), speech in
any catalog voice, music and sound effects by returning a declared
`media.generate` action. Declare it in the manifest:
```json
"permissions": {"project": [], "actions": ["media.generate"], "network": [], "filesystem": [], "secrets": []},
"outputs": {"request": {"type": "json"}, "media": {"type": "asset[]"}}
```
Return the action from your code. Each item is an `image`, `video`, `speech`,
`music` or `sfx` item with a prompt and optional `settings`. Image and video
items may also set `references` (images), `firstFrame`/`lastFrame` (video),
`negativePrompt` and `seed`. Audio items take only `settings`:
- `speech`: the prompt is the text to speak (up to 3,800 characters);
`settings: {voice, speed}` with a catalog voice id and a speed from 0.25 to 4.
- `music`: `settings: {durationSec}`, 5 to 240 whole seconds (default 30).
- `sfx`: `settings: {durationSec}`, 0.5 to 22 seconds in tenths (default 4).
```js
return {request: {
schemaVersion: 1, kind: "stillmade.media-action", operation: "media.generate",
resultPort: "media",
request: {items: [
{key: "hero", operation: "image", prompt: input.prompt,
settings: {model: "nano-banana-pro", aspectRatio: "16:9", resolution: "2K"},
references: input.styleImages},
{key: "clip", operation: "video", prompt: "Slow push in on the harbor",
settings: {model: "kling-v3-turbo", duration: 5, aspectRatio: "16:9"}},
{key: "intro", operation: "speech", prompt: "Welcome back to the show.",
settings: {voice: "adam", speed: 1}},
{key: "bed", operation: "music", prompt: "Warm lo-fi piano", settings: {durationSec: 30}},
{key: "whoosh", operation: "sfx", prompt: "A fast whoosh", settings: {durationSec: 1.5}}
]}
}};
```
StillMade turns every item into one exact generation request and asks the server
for one maximum price for the whole batch, using StillMade's own prices. The user
sees that maximum and approves it once; credits are reserved before any provider
call, each call is settled from provider evidence, and unused credits are
returned when the batch closes. The approval is bound to this Block's exact
content digest, so changed code needs a new approval. A Block cannot name a
price, a provider, a key or another Block. References and frames must be images
saved in the user's own library. Up to 50 items per action.
When it finishes, the declared `resultPort` receives an array of
`{key, kind, assetId, versionId, url}` in item order (`kind` is `image`,
`video` or `audio`); failed items are listed
separately and are not charged beyond what the provider actually received.
---
# Project defaults and onboarding
URL: https://www.stillmade.shop/docs/project-types/onboarding
### Typed shared inputs
The optional `projectInputs` array declares up to 32 shared input contracts using
`key`, `type`, `semantic`, `required` and `description`, plus the usual optional
schema, accepted meanings, source policy, default, sensitivity, freshness,
cardinality, batching and numeric limits. Keys are unique lowercase identifiers
up to 64 characters. For example:
```json
{"projectInputs":[{"key":"brand_tone","type":"text","semantic":"brand_tone","required":true,"description":"How the brand should sound","sources":["default","user"],"default":"Clear and friendly"}]}
```
Defaults must validate against their contracts and permit the `default` source.
Credential-sensitive defaults are rejected. New projects initialize these values
in typed context with Project Type provenance; matching Block inputs must allow
`project` sources. Existing values are never overwritten by initialization.
Other equally ranked ready project values take precedence over these fallback
records unless a source preference explicitly selects the default. The composer
and connected preview use the defaults too; explicit sample values,
settings and connections remain authoritative. Freshness limits produce expiring
context values. These contracts grant no permissions or execution authority.
Native controls can inspect or replace the declarations. The visual setup editor
can add shared information from project-capable Block requirements, preserving
their types, meanings and schemas, then edit descriptions, required flags and
defaults. Advanced contract editing handles custom declarations and complex
values. Edits stay local until Apply shared inputs; validation is shared with
native edits. Missing values do not create an upfront questionnaire. The next
local recipe/JavaScript Block can request a uniquely matching shared input through
the existing reviewed-answer flow. The declaration controls user/Chat sources;
both its contract and the consuming Block's contract must accept the answer.
Saved answers carry workflow provenance and source revisions, and permitted
Blocks can reuse them. Ambiguous declarations are not chosen automatically.
The existing saved-answer editor can update reviewed shared answers and newly
initialized defaults against an unchanged declaration and a current permitted
consumer. Edits require the current revision, remove default fallback priority,
and mark dependent results stale. Credential-sensitive and dependency-derived
records are not exposed in this editor. Older unsigned defaults are preserved
without being silently enrolled in editing. Richer value pickers and hosted-Block
collection remain unfinished.
Existing releases are not rewritten.
Connected previews accept shared sample answers at `context.sharedInputs`, keyed
by the declared shared-input key. The preview form uses the existing typed-answer
dialogs; complex values can use the sample context JSON. Samples must satisfy
their declarations and permit user answers; credential-sensitive samples are
rejected. They replace matching template defaults only in the preview registry,
and matching consumers must still allow project sources. Explicit Step inputs
and connected results retain precedence. Sample answers are never saved into the
Project Type or a real project. Existing sample digests and server rehearsal
replay include the whole context, so shared samples follow the same verification
path. This does not simulate trusted connections or allow missing hosted results.
A Project Type can declare project defaults and up to eight setup questions. These are optional additions to the schema-1 Project Type contract; they do not replace protected Chat, configure provider credentials or start production work.
```json
{
"defaults": {"aspectRatio": "9:16", "brief": "Create a short product launch video."},
"onboarding": [
{"id": "product", "label": "What is the product?", "type": "text", "required": true},
{"id": "audience", "label": "Who is it for?", "type": "textarea"},
{"id": "tone", "label": "Tone", "type": "select", "options": ["Clear and direct", "Playful"], "default": "Clear and direct"}
]
}
```
Merge these fields into an existing valid Project Type with `schemaVersion`, `id`, `version`, `name`, `description` and its enabled `stages`. This fragment is not a standalone package.
`defaults.aspectRatio` accepts `16:9`, `9:16` or `1:1`; `defaults.brief` is at most 4,000 characters. Question IDs are unique lowercase identifiers up to 40 characters. Labels are at most 120 characters. Text and textarea answers are at most 1,000 characters. A select question declares 2–12 unique options of at most 120 characters; its default must match an option. `required` is optional and boolean.
The Project Type Builder exposes these fields under **Project defaults & starting questions**. Using the Project Type opens one project setup dialog. The host records the chosen project name and format, and retains the brief plus labeled answers in the existing project's production context (`bible.extraInfo`). Script Writer receives this brief. Answers are project data, not edits to a marketplace release or its version identity; they are not published or sent to acquisition analytics. Read Blueprint/project context only through the permissions already documented by the SDK.
The empty Project Type starter is an editable draft. Add an enabled Block before validation, runtime execution, or publication; empty drafts are never installable releases.
---
# Build a Project Type
URL: https://www.stillmade.shop/docs/project-types/build
## Start from a workflow
Open [Project types](/marketplace/project-types) to inspect an existing type, or choose **Project type template** in the builder. Describe the workflow in chat or arrange the ordered list directly.
## Add and configure Steps
Use **Add Step** at an insertion position to browse Blocks, import a package, or create the missing capability with AI. Each instance keeps its own ID, label, configuration, enabled state, and exact Block version.
## Automatic insertion
`insertStep(type, package, index, catalog)` verifies both neighboring primary connections. It preserves secondary connections and returns a new type definition. It never mutates the original. `insertionLocations(type, package, catalog)` reports compatible positions when insertion fails.
## Persisted structure
The JSON `stages` array stores ordered Steps. Each includes `id`, `blockId`, `version`, and `label`; optional fields include `config`, `enabled`, and `condition`. Embedded SDK Blocks live in `packages`. Explicit `connections` are the technical representation of typed relationships.
A custom Block's declared `script` output can explicitly connect to Voiceover's `input`. Run the Block, then review and apply its script in Voiceover; this does not generate audio or require a Canvas node. See [script handoff](/docs/build/script-handoff) for the connection and acceptance rules.
## Scaffold and test outside the app
```sh
node packages/block-cli/cli.js create-type my-type.json
node packages/block-cli/cli.js validate my-type.json
node packages/block-cli/cli.js test my-type.json
node packages/block-cli/cli.js preview my-type.json
node packages/block-cli/cli.js pack my-type.json my-type.stillmade.json
```
The SDK includes `examples/text-workflow.stillmade.json`. Host workspaces use existing project context and controls; switching to a Step does not automatically call a paid generation provider.
## Protect existing projects
Saved projects retain their pinned type definition. Installing a newer type never silently replaces a workflow in progress. See [conditions](/docs/project-types/conditions), [testing](/docs/project-types/testing), and [versioning](/docs/distribute/versioning).
---
# Conditional Steps
URL: https://www.stillmade.shop/docs/project-types/conditions
A persisted Step may include:
```json
{"condition":{"input":"shot","path":["dialogue"],"operator":"present"}}
```
Conditions are bounded data predicates: `present`, `equals`, `not-equals`; equality
operators compare a scalar `value`. Paths contain up to eight safe field names.
Predicate controls use their declared defaults, just as a normal SDK run does.
An explicitly invalid primary value is rejected even when the condition skips.
No JavaScript expressions are executed. The conditional block must have a primary
input compatible with its single output, so a false condition passes through that
input without inventing data. Conditional host workspaces are not supported yet.
Settings are stored in the Step's `config`. `enabled:false` disables a Step and
reconnects a compatible primary chain. Block packages and versions remain pinned;
removing or reordering a Step never edits the reusable package. When upstream data
changes, dependent SDK results are marked stale, retained as previews and excluded
from connected inputs until rerun.
---
# Run on a selection or project
URL: https://www.stillmade.shop/docs/project-types/batches
The Step workspace's **Apply to** control can run a Block with a primary
image/video/audio/asset input on selected assets, one scene, selected shots, or all
compatible current project media (up to 100 per batch). Selections are saved with
the Step's existing project document. Scene/shot membership uses explicit source-document
production metadata; it is never inferred from image content. Reference roles
are preserved, and a Block requiring a semantic role only receives matching
assets. Uploaded audio is correctly typed as audio.
Sources are pinned by asset/version, processed sequentially in sandbox workers,
and never overwritten. A changed source or revoked edit access stops the batch;
completed results remain available. Choose one result explicitly to continue a
single-asset sequence. The project retains the latest 200 batch previews.
New batch previews also retain the host-assigned item number and total, the input
digest, and bounded captured input identities. The explicitly selected primary
asset is recorded as project input; it does not inherit the connection replaced
by that selection. Private text, answers, media addresses and pixels are not
included in the inspection evidence. Sensitive ports remain redacted.
Flow history includes retained batch items. Selecting a result preserves its
original execution identity and captured inputs alongside the new semantic
selection reference. Older previews without recorded inputs show history as
unavailable. The Flow snapshot reads at most the last 100 batch previews and
returns the newest 30 combined history entries, with incomplete history labeled.
For trusted hosted capability Blocks, each completed item is acknowledged in the
saved project before its request recovery slot is released and the next item
can start. If the response or save is interrupted, **Check submitted run** reads
the original request. A recovered batch item is retained for explicit selection;
it is never automatically applied as the Block's current output. Repeating a
recovery check recognizes an already-retained provider result. Older recoveries
without the bound batch selection receipt are kept as unused results for review.
The platform helpers `batchCandidates`, `batchTargets`, and `runBatch` are included
in the downloadable SDK. `batchTargets(manifest, assets, scope)` accepts:
```js
{kind: 'all-assets'}
{kind: 'selected-assets', assetIds: ['asset-1']}
{kind: 'scene', sceneId: 'scene_1'}
{kind: 'selected-shots', shotIds: ['shot-1', 'shot-2']}
```
Asset references may carry explicit `sceneId`, `shotId`, `label`, and `role`.
Missing or incompatible selections fail before execution. These are interactive
batches over the shared project media index. A separate durable background runner
supports future saved versions from Canvas, Panels, White Board Motion, voiceover
and the editor, using the same identities and scoped context described above.
---
# Run connected steps
URL: https://www.stillmade.shop/docs/project-types/running
### Inspect a specific run
In a project's Flow view, select a live run to see its captured input connections,
or select a historical attempt to inspect its saved evidence. Current connections
remain a separate planned view. Missing captured input records are shown as
unavailable; the host does not reconstruct them from today's source choices.
**Ask chat about this run** captures the selected receipt, including batch item,
attempt and loop pass when recorded. The native controller resolves that receipt
again through authorized saved-project inspection. Unsaved or evicted receipts
are unavailable to that saved-project read; another attempt is never substituted.
Legacy run-only targets are accepted only when exactly one receipt matches.
Host integrations use `projectFlowRunReceiptIdentity(record)` from
`packages/block-platform/flow-inspection.js` for the bounded
`receipt::` conversation scope. This is a locator, not an
access grant. Resolve it within the authorized project's placement history and
require exactly one match. Inspect/read operations neither run a Block nor change
its source. Existing retained-history limits still apply.
### Failure and recovery evidence
Flow distinguishes `EXECUTION_FAILED` from `RESULT_DELIVERY_FAILED` (a result
returned, but delivery to the project was interrupted) and `RESULT_SAVE_FAILED` (a result
returned, but project saving was not confirmed). The latter states show **Needs
recovery** rather than implying that generation produced nothing. Recovery must
use the supported request/result controls before a new paid run is considered.
The original failed-attempt record remains in history. If a matching provider
result or exact run/input/item receipt is later retained, its history entry gains
`recovered: true` and the current failure warning clears. This does not mean a
retained result was selected. Diagnostics contain bounded codes and duration;
raw provider messages, prompts and media addresses are excluded.
### Run connected Steps
Inside a Project Type, **Run connected steps** runs the selected recipe or isolated
JavaScript Step and the following supported connected Steps in workflow order.
The host captures the exact Project Type, package versions, current settings,
conditions and connections. Every input comes from current project data, supplied
values, declared context, or an actual upstream output. Production runs never
prefill missing values from package test fixtures.
A connected run stops before a manual workspace, a Block requiring separate
runtime approval, or an unconnected Step. The UI shows the ordered sequence and
stopping point; it does not ask users to wire a graph. ComfyUI, hosted workspaces
and generation are not silently dispatched as part of this local sequence. They
retain their own execution and confirmation paths.
The shared host helper is `packages/block-platform/segment-runner.js`:
```js
const plan = await planSegment(projectType, canvas, startingStepId);
const result = await runSegment(plan, {
source: selectedSource, // stable source/version identity, when applicable
inputs: { [startingStepId]: currentInputs },
externalInputs: currentInputsFromOutsideTheSegment,
resolveContext: (step, source) => scopedContextFor(step, source),
execute: (pkg, input, { signal, check }) => runInTrustedHost(pkg, input, { signal, check }),
check: () => verifyCurrentSourceSettingsPermissions(),
onStep: record => retainCompletedResult(record),
signal,
});
```
Custom durable hosts can opt into stateful sandboxed segments with
`planSegment(type, canvas, startingStepId, {background: true})`. Then call
`runSegment(plan, {background: true, states, ...options})`, where `states` supplies
`{version, value}` for **every** stateful Step ID. Background plans pin code,
settings and connections independently of current memory. The runner never
mutates the supplied memory. Keep the starting memory snapshot with the job;
resuming `previousResults` verifies it and refuses changed inputs or memory.
Persist returned states only in the same transaction as the completed workflow
results. A failed later Step can retain earlier provisional results, but those
results must not advance shared memory prematurely. This mode does not authorize
hosted APIs or ComfyUI. StillMade's scheduled multi-Block service uses this mode with shared Canvas
editing enabled. It pins the job's starting states, retains provisional
checkpoints after failures, and commits all memory changes with the completed
workflow. Selecting a retained result does not apply its memory a second time.
This helper is a host orchestration API, not a capability exposed to guest code.
The host owns permissions, quarantine checks, media decoding/storage and durable
adoption. `externalInputs` is keyed by target Step ID then port name, and may only
fill declared connections from outside the segment. Internal connections always
use outputs produced earlier in the current run. Context access follows each
Block's own declarations. A false condition uses the existing typed bypass rule.
Missing inputs, incompatible roles or stale package/settings pins stop execution.
Pinned safe connection conversions run in the host before each receiving Block.
They recheck actual media kinds and authorized current context; a failed
conversion retains completed upstream results and stops before the receiving
Step. They do not add a provider call or silently select replacement media.
For an explicit batch selection, a trusted host can set `overridePrimary: true`
with its already-authorized media `source`. This replaces only the first Step's
primary input; it must match that input's type and role. Do not put this
receiver-typed source into `externalInputs` as though it were a raw upstream shot
or scene. Ordinary connected runs leave the option `false`. The host records
`primaryInputOverride` when a connection was bypassed, and must preserve that
choice when validating retained results. This option grants no asset access:
the host still validates ownership, selection, permissions and source freshness.
The immutable plan has `workflowDigest`, `planDigest`, `startStepId`, ordered
`steps`, `nextStageId`, and `stopReason`. `result.steps` contains one record per
completed placement: `stepId`, `packageDigest`, `inputDigest`, `outputs`, `skipped`,
`at`, and optional host `mediaReceipts`. Identical Blocks at two positions retain
separate records. On failure, `error.partialResult` retains completed records;
later Steps are not reported as completed. A host can supply `previousResults`
only as an ordered prefix whose source, input and package digests still match.
Cancellation or failure cannot turn a stale partial result into a new success.
## Project handoffs and Flow inspection
Saved shared tasks use `PROJECT_FLOW_OPERATIONS.inspectTasks` (`inspect_project_tasks`, `projects:read`) and `controlTask` (`control_project_task`, separately granted `projects:control` plus `projects:read`). These do not require navigation or a live tab. Inspection returns bounded current task pages or one exact `taskId`. Control accepts pause/takeover/cancel/resume with exact requesterId, taskRevision, instructionRevision, controlEpoch and a unique requestId; current project and connection authority are checked in the transaction. Identical replay returns its original receipt plus current task state without reapplying. Resume queues the existing revalidation process and never approves dispatch, spending or edits; takeover preserves requester/payer. Accepted provider work may still finish after pause/cancel. Native task controls share the coordinator through their existing UI, so their registry `nativeAction` is null. Native and external controls both require the complete observed fence; older native payloads fail with `PROJECT_AI_REFRESH_REQUIRED` instead of changing newer task intent. See [saved task control details](/docs/native-mcp.md#saved-project-task-controls).
`recover_project_capability_request` retrieves one actor-owned project request under `projects:read` + `runs:read` and the existing current edit-access requirement. It verifies retained source/media and an unchanged request ledger before returning bounded, redacted stored outputs with `applied:false` and `selectionChanged:false`. It does not poll a provider, execute, settle, save or select a result. Truncated output envelopes are excerpts. Applying a retained result remains a separate reviewed native recovery action.
The in-app Project Type Builder accepts pinned built-in stages and embedded recipe
packages in an optional `packages` array. Each stage uses `{id,blockId,version,label}`;
connections use `{from:{stage,port},to:{stage,port}}`. Duplicate dependencies,
missing versions, incompatible ports, duplicate input producers, cycles, and root
chat overrides are rejected. Embedded recipe fixture tests must pass before launch
or release.
Executable custom types combine embedded SDK processing Blocks with the existing
production workspaces: Voiceover, Script Writer, Shots, Blueprint, Canvas,
Image Panels, White Board Motion, animated assets, Editor and Export. The
workspace adapter selects the existing production screen for each placement.
Legacy component adapters remain available to saved types; they do not become
separate workspace listings. Custom JavaScript, recipes and reviewed ComfyUI
packages open in their own Step workspace. Run them explicitly; navigation never
triggers paid generation or automatic reruns.
Connections use declared SDK ports or an implemented host port adapter. Canvas
and Editor workspaces share persistent project documents; listing a port does
not invent an executable conversion. The builder validates supported handoffs
and explains when a host port or conversion is not implemented. Script Writer's
script-to-Voiceover handoff uses the existing narration document. Starting Chat
remains protected before the configurable flow.
### Project Flow inspection contract
The SDK exports `ProjectFlowTarget`, `projectFlowTarget()` and
`projectFlowSnapshot()` from `@stillmade/block-sdk`. Targets use stable project,
placement, input/output, run, attempt and repeated-item IDs; display names and
screen positions are not target identity. A target may name the `planned`,
`active_run` or `historical_run` perspective. The snapshot validator checks its
contract version, definition identity, unique placement IDs, receiving edges,
completeness flag and documented size bounds. External `inspect_project_flow`
reads also return a host-computed `snapshotCursor`; use that exact cursor when
reviewing or applying a saved-project source or result-pin change.
`redactProjectFlowMediaAddresses()` recursively removes media address fields
from preview projections before they cross into chat or another client surface.
When present, a placement's optional `condition` summary contains its input key
and safe label, bounded field path, supported operator, and the last applied
outcome (`unknown`, `ran`, or `skipped`). It deliberately omits the comparison
value. A current skipped conditional output and its receiving input use the
`skipped` state with no materialized value; this is distinct from a missing
result. Historical skipped attempts retain a `skipped` history status.
Saved activity and history may include a bounded `execution` summary: execution
kind, whether output was captured or omitted by a conditional skip, and a capped
media-receipt count. It contains no provider run IDs or playable URLs and does
not establish provider activity for an attempt without a saved Block receipt.
`PROJECT_FLOW_OPERATIONS` maps the shared operation IDs to the native action
and external tool names. `updateAffected` maps to the existing native
`update_affected_steps` action; it runs eligible live-workspace Steps, with a
whole-update budget and separate exact generation review for hosted execution, and has
no saved-project external tool. `useCompletedResults` maps to the native
`use_completed_results` action; it adopts retained connected-workflow outputs
in the live workspace after the host revalidates their receipts, without
dispatching generation, and has no saved-project external tool. These SDK helpers validate a data shape and map
operation names; they do not grant access to a project, authorize a mutation,
or dispatch a Block. Hosts resolve every ID and filter
labels, previews, candidates and history against current permissions. The
current saved-project operations are `inspect_project_flow`,
`list_project_flow_placements`, `list_project_flow_ports`,
`list_project_flow_history`, `list_project_flow_run_inputs`,
`read_project_flow_text`, `recover_project_media`,
`diagnose_project_flow`, `preview_project_flow_source`,
`change_project_flow_source`, `preview_project_flow_pin`,
`pin_project_flow_result` and `clear_project_flow_pin`; write operations require
the current `projects:write` grant and exact preview revisions. They never run
a Block. Reruns, retries and costly review remain separate authorized actions.
Native `list_placements`, `list_ports` and `list_history` use the same bounded
projection as those three saved-project page tools. Each requires the exact
`revision` from its own preceding inspection: native surface revisions and
saved-project revisions are different and cannot be interchanged. A placement
page accepts at most 8 rows; ports and retained history accept at most 16.
Later pages of `inspect_project_flow` also require `expectedRevision`.
Membership and project ownership are checked again after saved reads.
For example, after `inspect_project_flow({projectId})` returns `revision`, read
`list_project_flow_history({projectId, placementId, revision, offset:0, limit:8})`
and continue with the returned `nextOffset`. Each entry includes a `receiptId`
and `targetKey` identifying the exact attempt/item, plus recorded input evidence.
Use `targetKey` for chat receipt targeting; several entries may share `runId`.
`historyComplete:false` distinguishes incomplete retained history from the end
of a page. `retainedHistoryComplete:false` still applies at the last page; this
operation cannot recover evicted records or missing historical input evidence.
`inputsTruncated:true` means only a bounded subset of captured input identities
was included. The receipt exposes `inputsCount`, `inputsRevision`,
`inputsNextOffset` and `inputsRetainedComplete`. Use native `list_run_inputs` or
external `list_project_flow_run_inputs` with `{placementId, receiptId,
revision: inputsRevision, offset, limit}` (plus `projectId` externally) to read
at most 16 named captured-input identities per page. This captured-input
revision is different from the surface/Flow revision. Every page rechecks the
exact retained receipt and current read permission; changes beyond the initial
64-input projection also invalidate its revision. A source's present selection
never replaces the recorded identity. Follow `nextOffset` until null; a final
page with `retainedComplete:false` still indicates missing original evidence.
The run capture stores at most 128 named inputs. A capture at that limit may
have omitted identities and is conservatively marked incomplete. Preview
attempts also have a 64-input cap and a byte limit. An older preview may have
retained fewer identities than the original run; paging cannot recover those
lost entries. Nested collection member identities
remain bounded within each input; this operation pages named inputs, not raw
values, full text, or the members of captured collections. The historical
inspector's Previous/Next inputs controls keep its map on that exact input page. None of these reads starts work, applies results, or proves media
playback. The native summary exposes `historyTotal` and `historyNextOffset` so
three displayed receipts are never reported as the whole retained history.
Retained unselected results have a separate read-only inventory. The Result tab
shows eight candidates at a time. Use native `list_retained_results` or external
`list_project_flow_retained_results` with `{placementId, revision:
unusedResultsRevision, offset, limit}` (plus `projectId` externally); list pages
contain at most eight candidates. Only records still retained for the current
Block package are included: unused single results, unselected connected results,
and batch records explicitly marked unused after a source change. The ordinary
batch picker is separate. `historyComplete:false` means evicted history is not
recoverable through this inventory, even at its final page.
Each candidate has an exact `candidateId`, `outputDigest`, run/item/attempt
metadata, and captured-input/output counts and cursors. Native
`inspect_retained_result` and external `inspect_project_flow_retained_result`
accept `{placementId, revision: unusedResultsRevision, candidateId, section,
offset, limit}`. Sections `inputs` and `outputs` page at most 16 identities or
previews. Sections `text` and `collection` also require `outputKey` and the exact
preview `path`; text requires its returned `identity` and permits at most 4,000
characters, collections at most 16 members. Reuse the exact candidate and list
revision on every page. Changes anywhere in retained records, package or preview
restrictions invalidate that revision. Inspect fresh state after a conflict.
Current project read access is rechecked after saved reads; native/UI caches
reset when the authorized reader changes. External/native results redact media
addresses. Sensitivity and prior preview restrictions still apply; a restricted
candidate cannot become inspectable through a later page. Captured inputs remain
bounded metadata and may be incomplete; nested structured previews disclose
omitted fields rather than claiming a complete document. These reads never
select a candidate, restore media, dispatch work, or substitute current output
for a retained value.
Mapped native Canvas outputs use the installed exact-version host port adapter. Flow can show a saved image, video or text value as `ready` with `observation.kind: "native-canvas-output"` and `runReceiptAvailable:false`. The Inspector labels that value **Available**; it does not invent a completed run, receipt or result pin. The exact selected asset/version is preserved in Result and Using. Missing values, mismatched host identities and navigation-only workspace ports remain unavailable. `ProjectFlowNativeObservation` describes this read-only origin; normal output/source paging retains its identity. Native production-workspace connections also use the exact installed host manifest and declared port handles; a newer catalog release cannot redefine an older pin. Original standalone nodes without placement/version metadata retain their historical 1.0.0 contract. Unknown, mismatched and non-host pins cannot impersonate a production workspace.
Whole-project Script retains its original narration source. Transcript uses the selected Voiceover take’s recorded transcript and word timings, independently of the editable narration draft. Older takes without stored transcript text derive it from their own timed words. A new Transcript-only run protects the original Voiceover document; changing an unrelated narration draft does not invalidate it. Original legacy receipts that also include Script remain valid with their stricter source checks. An older proposal cannot replace original reads with current documents, and a legacy result lacking derived proof must be rerun before guarded adoption. Other derived fields and arbitrary dynamic reads require separate attribution. Scoped narration and supported shot/scene collections capture their original Blueprint, Shot Plan, Animation, Panels and Board contributors, including empty originals whose later additions could change the result. They do not substitute the whole-project Script. Native asset inventories, generation records, scoped version histories and character/location/style collections can now retain their original contributor and membership proof when canonical reconstruction exactly matches the consumed value. A bounded v2 Canvas projection excludes only the exact host-dispatched producing placements while retaining their IDs/types, so saving their own output does not invalidate native collection membership. Current saved executable asset/generation members can retain their original selected-output, state and native-source evidence when exact canonical reconstruction matches the consumed collection. A producing placement’s own consumed previous output is captured separately when provable. Ambiguous, legacy, archived, private or unsupported collection members, local-only media, whole-project version metadata and noncanonical source documents remain unproven. v1 source reads stay compatible; new v2 captures require explicit server support. Media-dependent narration and arbitrary dynamic reads still require separate attribution.
Stateful host runs retain the exact starting-memory identity and resulting-state digest alongside original input/output evidence. Original native reads carry through subsequent memory transitions, connected batches, loops and checkpoint recovery. Declared initial/reset memory is distinct from legacy saved memory even when its bytes match; old receipts cannot gain proof from current documents or by removing a warning. Missing or changed state proof blocks protected workspace adoption. This is host-maintained provenance, not cryptographic attestation against rewriting an entire saved receipt. Older retained plans/checkpoints may require their existing recovery/reset path when the new memory evidence changes the plan identity.
Task-authored final Canvas adoption can bind the original native read union to the same transaction as the current task and Canvas execution checks. The host's `taskSourceReadSet` is retained in the exact save/recovery receipt, with explicit `assistantSourceReadSetVersions` capability negotiation. A lost response is recovered by reading the original commit; it does not resubmit an old proposal over newer work. Stale completed results may still be retained for review. Manual selection in saved Canvas projects now uses an acknowledged source save with the original native read union and `sourceCanvasPrecondition`, negotiated with `sourceCanvasPreconditionVersion:1`. Source roots and current execution content are checked under the same ordered transaction locks; layout-only changes are preserved. Exact request recovery distinguishes applied, not-applied and unresolved saves, including no-op selection, without replaying old work over newer selections. Servers without the capability refuse selection. Local drafts and unused-result retention remain separate. Legacy results do not gain missing original-source proof. Remaining native/background producers require separate coverage.
The saved-project inspector and compact handoff can open the exact source Block workspace through existing draft-save navigation guards. Account, project, read access, placement and matching package source are checked after drafts finish saving. The existing Editor opening suppresses automatic narration import/export for this navigation request. Previews without workspace navigation say **Inspect source Block**. Compact handoffs show required inputs needing attention even when the primary value is ready; **Using** targets the first such input.
Large Flow connection lists page 40 placements and offer **Find a Block** with direct keyboard access. Exact input links remain available after search. The map keeps its own bounded pages and selected neighborhood. Unchanged bounded scalar activity reports reuse the shared projection; new captured evidence, source changes and revoked access invalidate the relevant view.
External controllers can review one complete retained local connected run with `preview_project_flow_completed_results`, then select it using `use_project_flow_completed_results`. Apply requires the exact returned Flow, Canvas and candidate digests, a new request ID and the current task fence. Every selected row must have its original sealed v2 source receipt. The server validates the existing plan and original source roots, rechecks project/connection/task authority, and saves through shared Canvas operations. An acknowledged retry returns its original receipt without selecting the result again, even if the current selection has changed.
This external path supports complete recipe/JavaScript connected runs with current inputs, asset-descriptor batches with non-media outputs, and shared complete local loop receipts. All-assets, scene and selected-shots batches require the original complete inventory receipt captured and acknowledged before dispatch; it binds the original scope, ordered targets, exact plan and eight contributing workspace roots. Preview and apply check those original roots again, including workspaces that were empty when the batch started. JavaScript memory transitions require exact initial, reset or retained starting identity and resulting state with original sealed proof. Preview exposes digest-only `stateChanges`; apply saves the original outputs and memory without executing them again. Legacy/unproven or changed memory is unavailable. Hosted execution, decoded/generated media, incomplete runs and failure-recovery results still use their existing workspace review. Broad batches without original inventory proof remain unavailable; current inventory cannot be substituted for missing historical evidence. Inventory membership and each consumed item’s output or memory lineage are separate requirements. A local preview does not establish shared source proof. Preview and apply never dispatch a Block, load media, request model analysis or spend credits. A source/authority conflict keeps the retained result available for review; it does not silently rerun the workflow.
Completed local loops can retain their exact whole receipt in shared Canvas history without selecting outputs or advancing memory. A failed shared save preserves the original device checkpoint. Shared `retainedLoopRuns` summaries expose bounded run identities, pass counts and current selection for review; they do not claim revalidation. Retention is limited to four envelopes, 512 KiB per envelope and 1 MiB total per starting placement; exceeding a bound keeps device recovery and reports the limit rather than evicting old history. External selection validates the original whole loop and every consecutive pass against current sources, memory, task and authority, then uses the existing adoption path without dispatch. In the native workspace, another editor can explicitly choose a shared run without its original device checkpoint. Review validates the original complete receipt; Use rechecks its shared identity and the reviewed Canvas execution state before acknowledged selection. A newer selection or changed source requires fresh review.
Shared loop pass inspection uses the existing `list_project_flow_retained_results` and `inspect_project_flow_retained_result` operations. Initial discovery exposes bounded historical identities; explicit inspection verifies the original whole envelope before returning captured input identities and paged output/text/collection previews. It validates original evidence without requiring today's inputs to match, and does not select or execute the pass. Changed original history or revoked read access invalidates the response. Native Result review uses the same verified reader through **Inspect saved pass**.
Existing-media and host-action selections carry explicit selection metadata. Flow separates them from worker execution receipts and attempt history; a media selection identity is not a run ID. Saved selection still checks current authority and the reviewed Canvas/source state.
External connections must explicitly opt into `projects:write` alongside `projects:read` for reviewed Flow input, pin, result/memory changes and saved-media restoration. Defaults and existing connections do not gain permissions. Shared Flow reads recheck current connection authority after asynchronous work; all four Flow mutations recheck it in their write transaction, including replay. Media restoration rechecks through its existing before-write guard. Revocation, expiry, project/scope changes and suspension invalidate stale authority.
### Completed background results
In a saved Block workspace, open **Background results** beneath the result area to review already completed jobs. Editors can explicitly select a result whose original input, output and memory proof remains valid for the current project; viewers can inspect it. Selection rechecks the exact job, project, Block version, sources and shared Canvas and does not execute the Block or advance saved memory. Legacy jobs, pending validation journals and results with missing original source proof remain preview-only. Current source documents never replace missing original evidence. Connected fallback/bypass and unsupported derived sources may therefore remain unavailable for selection.
Use the **+** between project placements to add a Block. **Edit workflow** in
that dialog opens the ordered builder for adding, removing, disabling,
duplicating, replacing, reordering and configuring placements. Changes remain a
draft until **Save workflow**. A changed source requires a new Block version;
the same ID/version cannot silently acquire different code.
Removed, disabled, replaced or changed placements retain their SDK package
(or built-in host contract), settings, prior results and connections in
**Workflow history**. The old placement
is inactive; it is not run by autosave or future-media automation. Re-enabling
the same placement and source can recover its results for review, marked stale
until rerun. Changing a producer or its inputs also marks affected downstream
results stale. Each history entry offers **Download Block + SDK**. SDK packages
come from that retained instance; built-in downloads follow the current-client
source behavior described above.
Scoped SDK `versions` context includes retained media as inactive previous
versions; archived results do not become current assets or new generations.
The full definition and selected stage travel with project/session saves. A newer
installed type cannot replace an existing project's pinned definition.
Use `node packages/block-cli/cli.js create-type my-type.json`, then `test`, `preview`,
and `pack` on that JSON file. `preview` rehearses connected sandbox Blocks using
fixture inputs and actual upstream outputs. It marks hosted workspaces as requiring
a real project and never calls paid providers. `examples/text-workflow.stillmade.json` is a complete example.
The broader editor command/workspace SDK is not yet exposed to external recipes.
Do not invent `ctx.editor`, `ctx.sql`, `ctx.shell`, `ctx.fs` or `ctx.capabilities`
methods; none exists in 0.1. Keep unsupported requirements explicit.
---
# Process future media
URL: https://www.stillmade.shop/docs/project-types/automation
The Step workspace can save an owner-confirmed rule for **This step**, or for
**Connected steps** when at least two supported recipe/isolated JavaScript Steps
are connected. Its first primary input must accept media. The same manifest,
semantic roles, context permissions and execution API apply; Block authors do not
add a scheduler or write server code.
Save the project and review automation before activation. A connected review
runs every included package's admission and fixture tests, then a connected
sample. Fixtures are review data: only actual upstream sample outputs fill the
following connected inputs. The preview shows each completed Step. Confirming
pins the full plan, scope, settings and conditions. Changing a downstream Step or
connection invalidates the review, just as changing the starting Block does.
Existing media is skipped at activation; new retained versions in the project,
scene or selected shots become independent durable jobs.
The host supplies each queued primary source and resolves declared secondary
inputs from the saved project. Host image decoding and S3 output storage remain
outside the guest. Each completed Step is retained before the next executes.
`step_results` records the ordered per-placement outputs; `outputs` is the final
Step's output only after the connected job completes. If a later Step fails,
completed results remain previewable and can be explicitly adopted into their
matching placements. They never overwrite every Step with the final output.
Originals and current workflow outputs are preserved until that choice. Teams
editors can use results; only the project owner manages rules. Job history
paginates, and result media loads when expanded.
The rule pins the complete plan digest and pauses for review when the saved
workflow or settings change. Retries verify retained prefix inputs and source
identities before resuming. Ordinary processing does not invoke ComfyUI, local
models, hosted generation or arbitrary dependencies. The existing guest CPU,
heap and JSON limits apply alongside bounded host decoding, storage and job
execution. A one-megapixel decode ceiling is not a guarantee that every decoded
RGBA image fits the guest budget.
Automation requires its database migrations and a configured background worker;
see `docs/BLOCK_AUTOMATIONS.md` for deployment and bounds. Unretained/unsaved
versions and media outside saved project source documents are not discoverable.
SDK Block outputs are excluded from future-input discovery to prevent recursive
processing. A connected job's intermediate outputs are forwarded only inside
that job; they do not create new jobs. Unsaved browser-only changes are not
visible to the worker, and local/blob media must be shared before execution.
---
# Rehearse connected workflows
URL: https://www.stillmade.shop/docs/project-types/testing
```sh
node packages/block-cli/cli.js preview my-type.json
node packages/block-cli/cli.js preview my-type.json sample-inputs.json
```
`sample-inputs.json` is keyed by Step ID, then input name:
```json
{ "first": { "text": " My sample " } }
```
`previewProjectType(type, options)` from `packages/block-platform/type-preview.js`
rehearses the pinned workflow without changing it. Options accept `inputs`, scoped
`context`, an abort `signal`, `onResult`, and a sandbox `run` implementation for
browser workers. The CLI uses the same isolated Node runtime as Block tests.
Unconnected inputs start with the Block's first fixture, then Step config and
custom sample values; connected values always come from actual upstream output.
Conditions use the production bypass rules. Inputs and outputs are validated at
each boundary. Offline checks share a 15-second deadline and 4 MB combined
sample budget. Interactive browser previews permit 64 MB of recorded data;
waiting for hosted consent and execution does not consume the local deadline.
The report contains a digest and per-Step results. `passed` means no executed
Step failed; **only `complete: true` means all enabled Steps completed**. Hosted
workspaces, missing context and dependent blocked Steps produce an incomplete
report. The same connected rehearsal also runs within type fixture validation and server
import admission; an executed combination that fails is rejected. Incomplete host
coverage is marked explicitly. This does not replace static admission checks,
individual fixtures, or actual host/media testing in a project. In the builder choose **Preview connected workflow**
to inspect per-Step inputs, outputs and failures.
### Interactive hosted rehearsal
In the Project Type Builder, choose **Preview connected workflow**. For included
hosted Blocks, complete or reuse each account-bound package review first. The
preview then asks for a model, payment choice, and cost confirmation for each
actual hosted invocation. A package review never substitutes its sample output
for an upstream result. Cancel at any point to stop downstream execution.
Use **Custom sample inputs** for values keyed by Step ID. Use **Sample project
context** for a JSON object containing the context fields requested by Blocks:
```json
{"metadata":{"schemaVersion":1,"hasDialogue":true}}
```
Each Block receives only its declared context inputs and permissions. Context
samples are copied; testing cannot write through to a production project.
Conditions read the resolved inputs and apply the same pass-through rules used
in production. Changing either sample clears the displayed prior result.
Before passing hosted output downstream, StillMade re-fetches the authenticated
run and checks the package, input, model selection, quote, output and media
receipts. Generated image references can feed recipe image inputs: the browser
decodes the verified media without resizing it. The recipe limit remains one
megapixel. Arbitrary URLs in sample JSON do not authorize image decoding. Local
JavaScript Blocks retain reference-based image contracts.
The browser host opts into `interactive: true`, supplies `verifyHostedResult`,
and may supply `resolveInput` for authorized media conversion. These are trusted
host callbacks, never functions supplied by imported packages. Omitting the
verifier prevents hosted execution. Keep interactive mode off in automated
admission and CLI checks; enabling it does not provide provider credentials,
payment authorization, or a server-certified workflow receipt.
A successful sample rehearsal proves that those enabled Steps ran with those
inputs. It does not prove every possible project, workspace interaction, or
conditional branch. Manual StillMade workspaces still need their working preview
or a real project. Changing accounts cancels the active hosted preview.
---
# Review hosted Project Types
URL: https://www.stillmade.shop/docs/project-types/hosted-review
A Project Type can include reviewed `text.generate`, `audio.speech`, and
`image.generate` Blocks alongside local recipe/JavaScript Blocks and StillMade
workspaces. Import checks two separate things: whether every embedded package is
safe and reviewed, and which parts of the connected workflow have actually run.
Passing independent Block fixtures does not prove their connected production flow.
### Prepare portable source
The source contains ordinary pinned packages and connections. It never contains
account receipts, chosen payment methods, API keys or provider approval. For
example, run this from the extracted SDK directory to create a script-to-speech
Project Type using the included complete Block examples:
```js
import {writeFile} from 'node:fs/promises';
import {textGenerate} from './packages/block-sdk/capability-example.js';
import {audioSpeech} from './packages/block-sdk/speech-example.js';
const type = {
schemaVersion: 1,
id: 'creator.script-and-speech',
version: '1.0.0',
name: 'Script and speech',
description: 'Draft a short narration script and turn it into reviewed speech.',
license: 'MIT',
stages: [
{id: 'draft', blockId: textGenerate.manifest.id, version: '1.0.0', label: 'Draft narration'},
{id: 'voice', blockId: audioSpeech.manifest.id, version: '1.0.0', label: 'Narrate script'}
],
packages: [textGenerate, audioSpeech],
connections: [
{from: {stage: 'draft', port: 'script'}, to: {stage: 'voice', port: 'script'}}
]
};
await writeFile('script-and-speech.stillmade.json', JSON.stringify(type, null, 2), {flag: 'wx'});
```
This writes source only. It has not generated narration or speech. The offline
validator checks its structure, versions and typed connections. Local fixture
checks still execute in isolation; hosted fixture expectations require an account
review in StillMade. Source packaging does not install a package or authorize a
provider call.
### Review in StillMade
Open **Import package** and select the JSON or ZIP. Review each hosted Block with
the model, payment method, voice or image quality selected in the host controls.
An exact saved import review can be shown and accepted without new generation.
Running a new review still requires its displayed quote and explicit confirmation
for the fixtures and separate sample. Review actual text, audio or images before
accepting the evidence. Source, account or selection changes invalidate reuse.
Every embedded hosted package needs a matching review, including packages used
only by disabled Steps or currently unused packages. Repeated placements of an
unchanged package share one package review. They do not share a production run:
each placement's actual connected input still requires its own execution approval.
After package reviews, StillMade runs the local checks and the portions of the
sample flow that can execute locally. An enabled hosted Step is marked unexecuted;
its dependent Steps wait for its real output. They never receive the hosted
Block's independent fixture sample as a substitute. Missing independent context,
invalid connections and failing local code still prevent import. A genuinely
disabled Step or a valid conditional bypass keeps its ordinary workflow behavior.
The report distinguishes `passed` (package admission) from `workflowComplete`
(all enabled connected Steps completed). **Confirm Import** installs the reviewed
source, and the report continues to show any required workflow rehearsal. A new
release or another account's installation rechecks the receipts and current
source. ComfyUI-containing Project Types use their separate connection-bound
review map, described below.
Unattended hosted automation and a single complete hosted-workflow execution proof
remain separate work; an import report must not claim either one.
### Host review envelope
The authenticated host passes `typeCapabilityReviews` alongside `content` to
`POST /api/blocks/imports/review` and `/api/blocks/imports`, and alongside the
normal release/install fields to `/api/blocks/releases` and `/api/blocks/install`.
It is a plain object with exactly the distinct embedded hosted package digests:
```js
const typeCapabilityReviews = {
[exactPackageDigest]: {
receiptId: verifiedReview.receiptId,
selection: verifiedReview.selection
}
};
```
Use the SDK `digest(package)` on the entire exact package. The map has at most
100 entries and is at most 64 KiB as JSON. Each entry contains only `receiptId`
and the existing exact host `selection`. Unknown/missing digests, another
account's receipts, changed source, expired receipts or invalid retained media
are rejected. The server rechecks before writing the import, release or install;
these checks never generate output or charge again. They do not extend a review's
one-hour standalone import expiry. Project-only expired evidence cannot be used.
The server returns the sanitized map in the account review report, outside the
portable source. A host may use `verifyHostedProjectTypePackages` from
`packages/block-platform/hosted-type-reviews.js` with its trusted authenticated
receipt verifier to obtain the opaque `verifiedHostedPackages` option for
`testProjectType` and `previewProjectType`. Guest JSON cannot construct this
in-memory proof. The helper itself does not authenticate receipts; ordinary Block
authors should use StillMade's review UI.
### Retained samples and setup
Saved imports, releases and hosted installations retain their reviewed sample
media through account-bound server reports. Installed review metadata stores
no extra source copy and grants no release access. Removing team access still stops new private source downloads.
Retaining a historical sample does not extend its import-approval expiry. Legacy
installations without a saved report are not retroactively assigned a receipt.
The current storage and encrypted BYOK vault are reused.
---
# Block manifest
URL: https://www.stillmade.shop/docs/reference/manifest
## Package envelope
Recipe packages contain `manifest`, `recipe`, and `tests`. JavaScript packages contain `manifest`, `code`, and `tests`. Unknown envelope and manifest fields are rejected.
## Manifest example
```json
{
"schemaVersion": 1,
"license": "MIT",
"provenance": {
"notice": "MIT License\n\nCopyright (c) 2026 StillMade SDK contributors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and \nassociated documentation files (the \"Software\"), to deal in the Software without restriction, including \nwithout limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell \ncopies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the \nfollowing conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial \nportions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT \nLIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO \nEVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER \nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE \nUSE OR OTHER DEALINGS IN THE SOFTWARE.\n"
},
"sdkVersion": "0.1.0",
"id": "example.word-count",
"version": "1.0.0",
"name": "Word count",
"description": "Counts words and returns trimmed text using isolated JavaScript.",
"kind": "task",
"runtime": "javascript",
"inputs": {
"text": {
"type": "text",
"semantic": "plain_text",
"required": true,
"description": "Text whose surrounding whitespace should be removed."
}
},
"outputs": {
"text": {
"type": "text",
"semantic": "plain_text",
"description": "The trimmed source text."
},
"words": {
"type": "integer",
"semantic": "word_count",
"description": "The number of whitespace-separated words."
}
},
"permissions": {
"project": [],
"network": [],
"filesystem": [],
"secrets": []
},
"ui": [
{
"control": "text",
"port": "text"
}
]
}
```
## Required fields
| Field | Contract |
| --- | --- |
| schemaVersion | Integer 1 |
| sdkVersion | Exact stable version 0.1.0, caret, tilde, or comparator intersection satisfied by this host |
| id | Lowercase namespace.name; sm. is reserved |
| version | Three-part SemVer, such as 1.0.0 |
| name | 1–120 characters |
| description | 1–1000 characters describing behavior |
| kind | task, workspace, editor-extension, or composite |
| runtime | recipe, javascript, reviewed capability, or reviewed comfyui adapter |
| inputs / outputs | Named port objects; at most 64 in each direction |
| permissions | Explicit supported permission families |
## SDK compatibility
The runtime remains SDK `0.1.0`; exact declarations remain valid. A Block may also declare a bounded stable range such as `^0.1.0`, `~0.1.0`, or `>=0.1.0 <0.2.0`. `sdkCompatibility(declared)` reports `supported`, `unsupported` or `invalid` against that actual runtime version. `validateManifest` enforces the same result. A supported range never bypasses permissions, runtime availability, source admission or fixtures.
Supported grammar is an exact stable three-part version, caret, tilde, or up to four space-separated comparator intersections (`=`, `>`, `>=`, `<`, `<=`). Numeric components are bounded to 999999. Tags, prereleases, wildcards, alternatives and hyphen ranges are unsupported. Major-zero caret rules apply: `^0.1.0` excludes `0.2.0`; `^0.0.3` excludes `0.0.4`. A declaration is preserved verbatim in its package identity; changing it requires a new immutable release. Do not widen it merely to silence an incompatibility.
This is compatibility checking, not automatic code adaptation or a promise of future SDK support. Explicit reviewed Step-state migration routes are documented separately; changing an SDK range never runs a migration or changes project pins.
## Port fields
| Field | Meaning |
| --- | --- |
| type | A supported type, optionally followed by [] |
| required | Defaults to required; set false for omission |
| default | Used when no value is supplied; must match the type |
| min / max | Inclusive numeric bounds |
| primary | At most one true per direction |
| role | Lowercase semantic identifier such as source_image |
| context | A supported input-only context field with matching type and permission |
| description | Help text explaining the port |
## Optional metadata
`ui` binds controls to inputs. `license` records source licensing. `provenance` records remix or adaptation origin. `summary` records the source-linked description. `category` groups related capabilities. `entry` is descriptive metadata; execution uses the packed source.
## Platform compatibility
Block cards and listing pages show Phone, Browser, and Desktop SVG indicators.
Phone means a phone browser; Browser means a desktop web browser; Desktop means
the downloaded StillMade app. Add optional `manifest.platforms` to declare all
three environments. Each entry has `supported` (boolean) and an optional `reason`
(up to 240 characters); unsupported environments require a reason. At least one
environment must be supported. For example, a Block with a desktop-only interface:
```json
{
"platforms": {
"phone": {"supported": false, "reason": "This interface requires the StillMade desktop app."},
"browser": {"supported": false, "reason": "This interface requires the StillMade desktop app."},
"desktop": {"supported": true}
}
}
```
A declaration is a request to support a platform, **not verification**. An
unsupported declaration keeps that platform unavailable. `supported:true` or an
omitted declaration never lights an icon on its own. Runtime portability does not
prove usable UI. Unknown, stale, or missing checks display **Not verified**.
StillMade's server-owned import and release review runs `testPlatforms` against
the actual working preview, using the same isolated runtime and UI frame as the
app. Reports are stored with the existing import/release report and bound to the
exact source digest. Package-supplied reports are rejected. Changing code, UI,
fixtures, or compatibility metadata invalidates the previous source report.
Built-in audits also track the production UI source fingerprint.
The current `mobile-ui-1` gate checks 320 × 720, 390 × 844, 768 × 1024,
844 × 390 phone landscape, and 1280 × 800 desktop containers in Light, Dark,
and White. It uses the real embedded Block/Project Type host and custom sandbox,
then checks the main touch task, empty/populated/loading/error/success states,
long labels, enlarged text, and continuity through resize, rotation and panel
toggles. Ordinary touch controls target 44 × 44 CSS pixels. Fields require
accessible labels; draggable actions require a visible tap or menu alternative.
Intentional canvas/timeline panning remains distinct from page-wide overflow.
See [Mobile-compatible Block UI](/docs/build/mobile-ui) for authoring patterns.
For a custom interface with multiple buttons, put
`data-stillmade-action="run"` on the button that runs the included sample through
`StillMade.run`. The test clicks that control inside the disposable sandbox;
no generation provider, account API, or device permission can be invoked there.
A single-button interface can use its sole button. Interfaces requiring more
complex interaction remain unverified until their host adapter covers it.
The SDK exports `testPlatforms`, `measurePlatformLayout`,
`assessPlatformCapture`, and `checkedPlatformTargets`. A trusted browser adapter
renders the real interface, calls `measurePlatformLayout` in every frame, tries
its interaction, and returns frames plus interaction, embedding, state, and
continuity evidence. Use the checker in
an external test harness as follows:
```js
const fixtures = await testPackage(pkg);
const devices = await testPlatforms(pkg, {
runtimePassed: fixtures.passed,
probe: trustedBrowserAdapter,
});
```
`admitPackage` accepts the same adapter as `platformProbe` and includes
`platformChecks` in its result. Without a browser adapter it reports unverified
platforms; CLI `validate --mobile` exposes the exact required profile but remains
pending until a trusted embedded browser run, and code-only fixture tests never claim device support.
In StillMade, use **Run mobile UI profile** in import review to inspect the
server's result. Import confirmation repeats the server-owned checks.
Cloud-side checks cover Phone and Browser. They do not masquerade as an Electron
check: Desktop stays unverified without an actual desktop-runner report. The
built-in audit runner tests the Electron renderer separately. Browser emulation
is not a physical iOS-device test, and macOS Electron does not certify a Windows
installer or native recording capability. Cloud/ComfyUI operations still require
their normal configured connection; checks never make a paid sample call.
Project Types combine enabled, version-pinned Blocks. An unavailable dependency
makes that platform unavailable; an unverified dependency keeps it unverified.
These indicators do not grant filesystem, process, system recording, or other
native permissions. Unsupported native APIs still fail admission/runtime checks.
## UI schema
Use an array of `{control, port}` objects. Controls are `text`, `number`, `slider`, `checkbox`, and `asset`. Every control binds to a declared input. The host renders these fallback controls in StillMade’s style. A separate package-level `view` may supply a [sandboxed custom interface](/docs/build/custom-interface); it never loads into the parent app DOM.
## Permissions
Supported families are `project`, `network`, `filesystem`, `secrets`, and `capabilities`. Network, filesystem, and secrets arrays must be empty. A capability runtime declares exactly one supported operation: `text.generate`, `audio.speech`, `audio.transcribe`, `audio.music`, `audio.sfx`, `image.generate`, `video.generate`, `image.describe`, `media.analyze`, `web.fetch`, `web.research`, `timeline.propose`, `workspace.propose`, `connection.execute`; reviewed ComfyUI declares `comfyui.execute`. These permissions do not grant JavaScript or a custom interface direct host access. Supported context read permissions are listed in [project context](/docs/build/context).
---
# Types and compatibility
URL: https://www.stillmade.shop/docs/reference/types
## Supported types
- `text`
- `number`
- `integer`
- `boolean`
- `json`
- `object`
- `image`
- `video`
- `audio`
- `asset`
- `script`
- `transcript`
- `brief`
- `bible`
- `shot-plan`
- `panel-document`
- `board-document`
- `scene-document`
- `timeline`
- `timeline-edit`
- `file-bundle`
- `workspace-edit`
- `timeline-range`
- `research`
- `character`
- `location`
- `style`
- `pipeline`
- `approval`
- `scene`
- `shot`
- `mask`
- `depth`
- `pose`
- `metadata`
- `project-context`
- `file`
- `document`
- `table`
- `url`
- `date`
- `color`
Any listed type can use an array suffix, for example `shot[]` or `image[]`.
## Runtime values
| Type family | Value |
| --- | --- |
| text | A string up to 1,000,000 characters |
| number / integer | A finite number, with integer and bounds checks as declared |
| boolean | true or false |
| json | Safe finite JSON data |
| image | Portable media reference or bounded RGBA image |
| video / audio / asset | Portable media reference |
| file-bundle | Validated file or ZIP download data; media requires host authorization |
| Production documents | JSON object with schemaVersion: 1 |
| Arrays | Array whose members match the base type |
A media reference is `{assetId, versionId, kind}`. A decoded image is `{width, height, data}`, with width × height × 4 integer RGBA channels. The current pixel ceiling is 1,048,576.
## Compatibility rules
The raw `compatible` check requires exact type matching except **integer → number**. Arrays match their full declared type. If an input declares a semantic role, its producer must declare the same role. The host connection registry can additionally apply the guarded conversions below.
```js
compatible({type: 'image', role: 'source_image'},
{type: 'image', role: 'source_image'});
// { compatible: true, adapter: null }
```
## Primary ports
Declare one primary input and output for automatic insertion. A sole port is inferred for older packages. Multiple candidates without a declared primary do not produce an arbitrary connection.
## Safe connection conversions
StillMade can insert a versioned, deterministic conversion between compatible
Steps. This is part of the host connection contract; it does not add a visible
Step or ask the user to wire ports. Advanced connection controls show the
conversion and preserve its version when the Project Type is saved.
`compatible(outputPort, inputPort)` remains the strict raw-value check: exact
types and matching required roles, with the existing integer-to-number widening.
Use `connectionCompatibility(outputPort, inputPort)` to discover a supported
connection conversion. Pass the complete port objects, including `role`.
`validateConnectionAdapter(outputPort, inputPort, {permissions, adapter})` also
checks the receiving Block's `manifest.permissions.project` grants and the saved
adapter pin. Static compatibility is a possibility, not a promise that any value
can run: the actual output and authorized context must pass runtime validation.
| Conversion | What the host accepts | Receiving Block permissions |
| --- | --- | --- |
| integer → number | A valid whole number, preserving its value | None |
| image / video / audio → asset | A saved media reference of that kind | None |
| asset → image / video / audio | A media reference whose actual `kind` matches the requested type | None |
| scene → shot[] | The scene's authorized shots, in its recorded project order | `context.scenes.read`, `context.shots.read` |
| shot → image | That shot's unambiguous, current, host-selected image, present in authorized project assets | `context.shots.read`, `context.assets.read` |
The conversion IDs are `integer-to-number`, `image-to-asset`, `video-to-asset`,
`audio-to-asset`, `asset-to-image`, `asset-to-video`, `asset-to-audio`,
`scene-to-shots`, and `shot-to-selected-image`. Each is currently version `1`.
New cross-type Project Type connections must include the exact pin:
```json
{
"from": { "stage": "source", "port": "asset" },
"to": { "stage": "process", "port": "image" },
"adapter": { "id": "asset-to-image", "version": 1 }
}
```
The builder adds this metadata automatically. Existing direct and
integer-to-number connections remain valid without an explicit pin. Changing a
pin changes the workflow identity and invalidates connected-run and automation
reviews. The host uses `adaptConnectionValue(outputPort, inputPort, value,
{context, permissions, adapter})` before validating the receiving input; a
conversion returns copied data and cannot modify the project.
Roles are preserved, never invented: a `character_reference` image cannot become
a `source_image`. Scene and shot conversions use scoped host context rather than
trusting a Block's embedded shot list or selected-image claim. Missing,
ambiguous, stale or unauthorized selections stop the run. StillMade does not
choose the first image or fall back to a historical version. Inline RGBA pixels
must be saved as a media reference before a media-to-asset conversion. No
conversion here downloads, decodes, generates, or uploads media. Video-to-audio,
video-to-frames, model inference and other semantic transformations still require
an explicit supported capability and its own execution permissions.
Additional shared types include `scene`, `shot`, `shot[]`, `mask`, `depth`, `pose`,
`metadata` and `project-context`. Their current values are versioned JSON documents
(`schemaVersion: 1`); declaring a context port does not grant project access.
Scoped context and conditional/batch execution are available for supported SDK
Blocks. ComfyUI uses the explicit account connection and review described below;
unattended remote automation and additional media adapters remain separate work.
## Context bindings
| Field | Exact type |
| --- | --- |
| script | script |
| transcript | transcript |
| shots | shot[] |
| shotPlan | shot-plan |
| scenes | scene[] |
| characters | character[] |
| locations | location[] |
| styles | style[] |
| assets | asset[] |
| files | file[] |
| generations | metadata[] |
| versions | metadata[] |
| timeline | timeline |
| editor | timeline |
| canvas | pipeline |
| blueprint | bible |
| panels | panel-document |
| board | board-document |
| animation | scene-document |
| metadata | metadata |
## Downloadable Block results
Declare an output with `type: "file-bundle"` (or `file-bundle[]`). The shared Block
preview automatically offers Download, including when the Block has a custom
view. A file result is inert data: it grants no filesystem access and never mounts
HTML, runs code or triggers a download before the user clicks. This output type
is separate from the `.stillmade-block` archive used to install the Block itself.
```json
{
"schemaVersion": 1,
"format": "file",
"name": "notes.txt",
"files": [{"path": "notes.txt", "text": "My notes"}]
}
```
For ZIP output use `format: "zip"`, a `.zip` name, and multiple files. Each member
contains exactly `{path,text}` or `{path,source}`. A `source` retains the complete
selected media reference (`kind`, `assetId`, `versionId`, `url`, and its existing
role/metadata); never invent a reference or substitute a URL. Media entries need
`asset.read` in `manifest.permissions.project` and an authorized selection from
the host's existing media inputs or declared project context. Declaration alone
is not permission. The host checks the full reference identity and current
account/selection before transferring bytes. Detached history views can download
text-only results; media ZIPs need the Block's current authorized media context.
Unavailable, unselected or unsupported media aborts the ZIP instead of returning
an archive with missing members. The host does not install dependencies.
File downloads contain exactly one text member whose path equals `name`.
Supported text suffixes: `.txt`, `.md`, `.json`, `.csv`, `.xml`, `.fcpxml`, `.srt`,
`.vtt`. ZIP media suffixes: `.png`, `.jpg`, `.jpeg`, `.webp`, `.gif`, `.mp4`,
`.webm`, `.mov`, `.mp3`, `.wav`, `.ogg`, `.m4a`, `.flac`, `.bin`.
Use safe relative ASCII paths, at most 180 characters, with no traversal,
backslashes, empty segments, trailing dots, reserved device names or case
collisions. Include 1–256 files and at most 3,000,000 UTF-8 text bytes total;
the existing runtime JSON/memory limits also apply. Media transfers use the
host limits above. Files are delivered as downloads with a non-executable MIME
type; no archive member becomes a host filesystem path.
`isFileBundle(value)` and the normal `validate`/`test`/`pack` commands enforce this
contract. Include normal and edge fixtures containing the complete expected
file data. The [File notes example](/block-sdk/examples/file-notes.stillmade-block)
is a complete portable package using this output. Import it, change the text,
run it and click Download. No custom download code is needed in the Block view.
## Original media file admission
The SDK exports `MEDIA_IMPORT_FORMATS`, `MEDIA_IMPORT_MAX_BYTES` and
`inspectMediaImportFile({name,size,type?})` for trusted hosts handling files the
user selected. The inspector returns `{kind,extension}` and rejects unsupported
extensions, conflicting MIME categories, empty/nonintegral sizes and files above
50 MiB. It does not read file bytes, grant filesystem access or authorize an upload.
Supported originals are PNG/JPG/JPEG/WebP/GIF images; MP4/MOV/WebM videos; and
MP3/WAV/M4A/AAC/OGG audio. Each format specifies the canonical upload MIME type.
An absent MIME type or `application/octet-stream` may use the recognized extension.
The existing authenticated media upload endpoint uses the same table and limit,
preserves original bytes through its signed PUT, and returns a durable media URL.
No image downscaling is applied to audio or video originals.
The host's existing Canvas upload adapter accepts optional `signal` and
`assertCurrent` controls. The callback must throw when the owning session is no
longer current; checks run before upload and before caching or returning results.
Isolated previews retain local files without a cloud request. A file selected in
a generic Block preview is still temporary unless a supported host persistence
flow explicitly saves it; declaring `asset.create` alone does not upload it.
These utilities extend the shared upload boundary. They are not a new guest file
API or proof that the native Import media workspace is a portable sandbox Block.
---
# Execution API
URL: https://www.stillmade.shop/docs/reference/execution
## Import the SDK
The downloaded pack contains a local ES module. There is no global npm installation required.
```js
import {readFile} from 'node:fs/promises';
import {validatePackage, testPackage, runPackageAsync}
from './packages/block-sdk/index.js';
const pkg = JSON.parse(await readFile('./examples/word-count.stillmade.json', 'utf8'));
validatePackage(pkg);
const report = await testPackage(pkg);
if (!report.passed) throw new Error('Fix the fixtures before continuing');
const result = await runPackageAsync(pkg, pkg.tests[0].input);
console.log(result.outputs);
```
## Main functions
| Function | Result and behavior |
| --- | --- |
| sdkCompatibility(declared, runtimeVersion?) | supported, unsupported or invalid; defaults to the actual host SDK version |
| validateManifest(manifest) | valid plus field-level errors |
| validatePackage(package) | true, or throws BlockError |
| testPackage(package, options) | Promise of digest-bound fixture report |
| runPackage(package, input, options) | Synchronous recipe-only outputs |
| runPackageAsync(package, input, options) | Promise of RunPackageResult; recipe progress is optional, and hosted runtimes require an explicit trusted executor |
| capabilityOperation(manifest) | Validated hosted generation, analysis, or timeline proposal operation |
| prepareCapabilityInvocation(package, input) | Bounded request for the declared hosted operation |
| capabilityOutputs(package, generatedValue) | Declared typed output, including strict timeline proposals and canonical media references |
| defaultCapabilityExpectations(manifest) | Operation-specific bounded output checks |
| validateCapabilityResult(manifest, result) | Detached typed result; optional generated-media receipts are shape-checked and bound to outputs, not authenticated |
| scanPackage(package) | Schema, dependency, permission, and static checks |
| admitPackage(package, options) | Scan, fixtures, and optional supplied sample runner |
| resolveContextInputs(manifest, values, snapshot) | Copied, permission-checked context bindings |
| compatible(outputPort, inputPort) | Compatibility, adapter, or failure reason |
| connectionCompatibility(outputPort, inputPort) | Direct match or a possible versioned host conversion; actual values still require checking |
| validateConnectionAdapter(outputPort, inputPort, options) | Checks the saved adapter pin and receiving project-read permissions |
| adaptConnectionValue(outputPort, inputPort, value, options) | Host-only copied data projection using authorized context; no I/O or provider execution |
| digest(value) | Promise of canonical JSON SHA-256 digest |
## Async result and progress contract
Packages declaring native desktop tools require a trusted `options.desktop` executor and an explicit package-level `desktop` mapping (editable `src/desktop.json`). Their local JavaScript/recipe must never count as native execution. The conservative guard also covers legacy packages without changing their pinned bytes. Offline native admission is source-only with zero executed fixtures; connected execution and saved-result reuse require a bound native receipt. See [Native desktop execution](/docs/reference/desktop-execution) for the complete contract and current Natron-only action support.
`runPackage` is synchronous and recipe-only. `runPackageAsync` dispatches the supported runtime and resolves `RunPackageResult`, containing final `outputs` and runtime-appropriate optional metadata/media receipts or JavaScript `state`. External hosts must validate and accept Step state atomically with outputs; the SDK does not persist it. A capability executor returns `CapabilityResult`, which cannot contain Step state: `validateCapabilityResult` rejects that field. Stateful JavaScript output is a different runtime result, not a provider response.
Both recipe entrypoints accept `onProgress({stepId, complete, total})`; asynchronous dispatch forwards this callback when executing a recipe. It is not a provider percentage or a new queued-job protocol. An `AbortSignal` requests cancellation through the existing runtime/host adapter. Provider job polling, progress UI, retries, costs and retained-result adoption remain host responsibilities. Do not return an invented `{pending: ...}` envelope as declared Block outputs or assume a canceled network wait undoes provider work.
## Host integration
Node creates a disposable worker for JavaScript execution. Browsers run recipe/JavaScript in disposable workers. ComfyUI uses a host-controlled account connection with explicit confirmation; it never gives the worker or iframe network credentials. Pass an AbortSignal for cancellation. A returned output object does not mutate a project or authorize storage writes.
Connected Project Type rehearsal can carry pixel images through asset ports using a bounded, preview-only PNG data URL and exact-reference in-memory bridge. It works in the CLI and browser without a media service; it never downloads unknown references or creates project assets. See [media previews](/docs/build/media).
## TypeScript declarations
```ts
/** Exact stable SemVer, ^version, ~version, or space-separated comparator intersection. */
export type SdkVersionRange = string;
export * from './semantic-contracts.js';
export * from './workflow-events.js';
export * from './workflow-approvals.js';
export * from './package-semantics.js';
export * from './define-block.js';
export * from './adapter-registry.js';
export * from './block-storage.js';
export type {BlockViewDocumentTools,DocumentDraftOperation,DocumentEntry,DocumentPath} from './document-drafts.js';
export type {BlockViewDocumentOptions,BlockViewDocumentConnection} from './document-store-binding.js';
export interface BlockViewBridge { connectDocument(options:import('./document-store-binding.js').BlockViewDocumentOptions):Readonly; }
export interface SdkCompatibility { status:'supported'|'unsupported'|'invalid'; declared:unknown; runtimeVersion:string; message:string; }
export function sdkCompatibility(declared:unknown,runtimeVersion?:string):SdkCompatibility;
export type Port = { type: string; imageMode?: 'pixels'|'reference'; role?: string; context?: keyof typeof CONTEXT_FIELDS; primary?: boolean; required?: boolean; default?: unknown; min?: number; max?: number; description?: string } & Partial;
export interface Manifest { schemaVersion: 1; id: string; name: string; version: string; sdkVersion: SdkVersionRange; description: string; kind: 'task'|'workspace'|'editor-extension'|'composite'; inputs: Record; outputs: Record; runtime: 'recipe'|'javascript'|'module'|'comfyui'|'capability'; jobs?: {kind:'cooperative';version:1}; state?: {scope:'step';version:number;initial:Record;migrations?:StateMigration[]}; entry?: string; permissions: { project: string[]; desktop?: DesktopPermission[]; capabilities?: ['comfyui.execute']|['text.generate']|['audio.speech']|['audio.transcribe']|['image.generate']|['video.generate']|['image.describe']|['timeline.propose']|['workspace.propose']|['connection.execute']; network?: never[]; filesystem?: never[]; secrets?: never[] }; desktopTools?:DesktopToolDeclaration[]; uiLayout?: {kind:'tabs'|'sections';groups:{id:string;label:string;inputs:string[];outputs:string[];collapsed?:boolean}[]}; ui?: ({ control: 'text'|'number'|'slider'|'checkbox'|'asset'; port: string; label?: string; when?: {port:string;equals:string|boolean} } | {control:'select'|'multiselect';port:string;options:string[];label?:string;when?:{port:string;equals:string|boolean}} | {control:'gallery';port:string;label?:string} | {control:'before-after';port:string;before:string;label?:string})[]; }
export interface BlockView { html: string; css?: string; themedCss?: string; javascript?: string; appearance?: 'host'|'original'; }
/** A frame view: any interface code (frameworks, SVG, canvas, drag) with assets named by SHA-256. */
export interface FrameView { runtime: 'frame'; html: string; css?: string; themedCss?: string; javascript?: string; appearance?: 'host'|'original'; files?: {schemaVersion: 1; files: Record}; }
export interface BlockPackage { portable?: PortableMetadata; manifest: Manifest; view?: BlockView|FrameView; code?: string; module?: ModuleDescriptor; comfyui?: ComfyWorkflow; capability?: ConnectedCapability|TextGenerationCapability|SpeechCapability|TranscriptionCapability|ImageGenerationCapability|VideoGenerationCapability|ImageDescriptionCapability|TimelineProposalCapability; recipe?: { schemaVersion: 1; steps: { id: string; op: string; args: Record }[]; outputs: Record }; tests: { name: string; input: Record; expected?: Record; state?: BlockState; expectedState?: BlockState; expectations?: Record }[]; }
/** Named sandbox action with fixed declared input settings and produced ports. */
export interface BlockOperation {id:string;label:string;description:string;inputs:Record;outputs:string[]}
export interface Manifest {
operations?: BlockOperation[];
storage?: import('./block-storage.js').BlockStorageDeclaration;
/** Events the Block can run on by itself, once the project owner turns them on. */
triggers?: import('./triggers.js').BlockTrigger[];
semantics?: import('./semantic-contracts.js').SemanticDefinition[];
provenance?: { remixSource?: BlockPackage; remixPolicy?: {allowRemixing: boolean; sourceInspection?: "yes"|"no"|"license-controlled"}; remixAncestors?: {id: string; version: string; digest: string}[]; remixedFrom?: { id: string; version: string; digest: string }; [key: string]: unknown };
summary?: Record;
license?: string;
category?: string;
/** Listing compatibility only; this does not grant native device permissions. */
platforms?: Record<'phone'|'browser'|'desktop', { supported: boolean; reason?: string }>;
}
export interface TestReport { digest: string; sdkVersion: string; passed: boolean; execution?: 'host-executor'|'desktop-host-executor'|'unavailable'; liveVerified?: false; reviewRequired?: true; results: { name: string; passed: boolean; code?: string; message?: string }[]; }
export function validatePackage(pkg: unknown): true;
export function testPackage(pkg: BlockPackage, options?: { timeoutMs?: number; comfyui?: ComfyExecutor; capability?: CapabilityExecutor; desktop?:DesktopExecutor }): Promise;
export const SDK_VERSION: '0.1.0';
export const ROOT_CHAT_ID: 'stillmade.starting-chat';
export function stableStringify(value: unknown): string;
export function validateManifest(value: unknown): { valid: boolean; errors: { path: string; message: string }[] };
export function validateRecipe(recipe: BlockPackage['recipe']): true;
export interface RecipeProgress {stepId:string;complete:number;total:number}
export type JavaScriptRequirementResult = {resolved:false;requirement:Port & {key:string}}|{resolved:true;requirement:Port & {key:string};value:unknown};
export interface JavaScriptUserRequest {$stillmadeUserRequest:1;key:string}
export interface JavaScriptChatRequest {$stillmadeChatRequest:1;key:string}
export interface JavaScriptBlockSDK {requestFromChat(key:string):JavaScriptChatRequest|JavaScriptRequirementResult}
export interface JavaScriptBlockSDK {requestFromUser(key:string):JavaScriptUserRequest|JavaScriptRequirementResult}
export interface RunPackageOptions {requestFromUser?:(request:{requirement:Port & {key:string};signal?:AbortSignal})=>{value:unknown}|Promise<{value:unknown}>}
export interface RunPackageResult {userAnswers?:Record}
export interface RunPackageOptions {requestFromChat?:(request:{requirement:Port & {key:string};signal?:AbortSignal})=>{value:unknown}|Promise<{value:unknown}>}
export interface RunPackageResult {chatValues?:Record}
export interface JavaScriptBlockSDK {emit(key:string,value:unknown):void;resolveRequirement(key:string):JavaScriptRequirementResult;getProjectContext():Record;getProjectContext(field:string):unknown;validate(value:unknown,schema:import('./semantic-contracts.js').SemanticContract['schema']):boolean}
/** Recipe Step progress only; provider/native job progress belongs to the host. */
export interface RecipeRunOptions {signal?:AbortSignal;onProgress?:(progress:RecipeProgress)=>void}
export function runPackage(pkg: BlockPackage, values: Record, options?: RecipeRunOptions): { outputs: Record };
export function compatible(output: string|Port, input: string|Port, options?: {semantics?: import('./semantic-contracts.js').SemanticRegistry}): { compatible: boolean; adapter?: string|null; reason?: string };
/** Host-owned deterministic conversion, pinned on a workflow connection. */
export interface ConnectionAdapterPin { id: string; version: 1; }
export interface ConnectionCompatibility { compatible: boolean; adapter?: ConnectionAdapterPin|null; requiresValue?: boolean; contextFields?: string[]; reason?: string; }
export interface ConnectionAdapterOptions { semantics?: import('./semantic-contracts.js').SemanticRegistry; permissions?: string[]; adapter?: ConnectionAdapterPin|null; context?: Record; }
export const ADAPTERS: readonly Readonly[];
export function connectionCompatibility(output: string|Port, input: string|Port, options?: Pick): ConnectionCompatibility;
export function validateConnectionAdapter(output: string|Port, input: string|Port, options?: Omit): ConnectionCompatibility;
export function adaptConnectionValue(output: string|Port, input: string|Port, value: unknown, options?: ConnectionAdapterOptions): unknown;
export function digest(value: unknown): Promise;
export class BlockError extends Error { code: string; details: unknown[]; }
export interface ContinuationProgress {kind:'continuation';turn:number;complete:number|null;total:number|null}
export interface CooperativeJobContext {readonly continuation:unknown;pending(continuation:unknown,progress?:{complete:number;total:number}):{$stillmadeJob:1;continuation:unknown;progress:{complete:number;total:number}|null}}
export const COOPERATIVE_JOB_LIMITS:Readonly<{turns:32}>;
export function continuationProgressValid(value:unknown,previous?:ContinuationProgress|null):value is ContinuationProgress;
export interface RunPackageOptions extends Omit {onProgress?:(progress:RecipeProgress|ContinuationProgress)=>void;state?:BlockState;comfyui?:ComfyExecutor;capability?:CapabilityExecutor}
/** Final runtime result; only JavaScript Blocks can return Step state. */
export interface RunPackageResult extends CapabilityResult {state?:BlockState}
export function runPackageAsync(pkg: BlockPackage, values: Record, options?: RunPackageOptions): Promise;
export interface AdmissionCheck { name: string; passed: boolean; message?: string; }
export type ModelBackendKind='managed-api'|'huggingface-endpoint'|'comfyui'|'local-worker';
export interface ModelPortabilityCheck extends AdmissionCheck {runtimeVerified:false;contract:'generation-backend/1';mode:'host-capability'|'reviewed-comfyui'|'host-action'|'not-applicable';operations:string[];compatibleBackends:ModelBackendKind[];code?:'MODEL_PORTABILITY'}
export const MODEL_PORTABILITY_CONTRACT:'generation-backend/1';
export const PORTABLE_GENERATION_OPERATIONS:readonly ['text.generate','audio.speech','audio.transcribe','image.generate','video.generate','image.describe','timeline.propose','workspace.propose'];
export type HostedCapabilityOperation='text.generate'|'audio.speech'|'audio.transcribe'|'image.generate'|'video.generate'|'image.describe'|'timeline.propose'|'workspace.propose'|'connection.execute';
export interface HostedCapabilityOperationInfo{readonly executor:'capability-run'|'connection';readonly mode:'sync'|'async';readonly byok:boolean;readonly summary:string}
/** The single source of host-mediated operations; every route, filter and doc derives from it. */
export const HOSTED_CAPABILITY_OPERATIONS:Readonly>;
export const HOSTED_OPERATION_NAMES:readonly HostedCapabilityOperation[];
export const CAPABILITY_RUN_OPERATIONS:readonly HostedCapabilityOperation[];
export const SYNC_CAPABILITY_RUN_OPERATIONS:readonly HostedCapabilityOperation[];
export const BYOK_CAPABILITY_OPERATIONS:readonly HostedCapabilityOperation[];
export const UI_INPUT_CONTROLS:readonly ['text','number','slider','checkbox','asset','select','multiselect'];
export const UI_OUTPUT_PRESENTATIONS:readonly ['gallery','before-after'];
/** Every field an input or output port may declare; admission rejects any other. See /docs/reference/ports. */
export const PORT_FIELDS:readonly ['type','required','default','min','max','description','role','primary','context','imageMode','key','semantic','accepts','schema','sources','sensitivity','freshnessPolicy','cardinality','batchable'];
/** Offline source constraint only; it never contacts, approves, or verifies a live model backend. */
export function reviewModelPortability(pkg:BlockPackage):ModelPortabilityCheck;
export interface AdmissionReport { execution?:'not-run'|'desktop-host-executor'; tests?:number; fixtureContracts?:number; platformChecks?: PlatformCheckReport; passed: boolean; liveVerified?: false; reviewRequired?: true; results: AdmissionCheck[]; digest: string; preview?: { outputs: Record }; }
export function scanPackage(pkg: BlockPackage): { passed: boolean; results: AdmissionCheck[]; execution?: 'not-run'; liveVerified?: false; reviewRequired?: true };
/** Execution safety only; not a completion or installation certificate. */
export function scanPackageSource(pkg: BlockPackage): { passed: boolean; results: AdmissionCheck[]; execution?: 'not-run'; liveVerified?: false; reviewRequired?: true };
/** New creation/import admission, including model portability and integration constraints. */
export function scanAdmissionPackage(pkg:BlockPackage):{passed:boolean;results:AdmissionCheck[];execution?:'not-run';liveVerified?:false;reviewRequired?:true};
export function admitPackage(pkg: BlockPackage, options?: { test?: (pkg: BlockPackage) => Promise; run?: (pkg: BlockPackage, input: Record) => Promise<{ outputs: Record }>; sampleInput?: Record; platformProbe?:PlatformProbe; desktop?:DesktopExecutor }): Promise;
/** Read-only media identity supplied by the host. URLs do not grant guest network access. */
export interface ContextMediaReference { assetId: string; versionId: string; kind: 'image'|'video'|'audio'; url: string; role?: string; sceneId?: string; shotId?: string; label?: string; source: string; }
export interface ContextMediaVersion { schemaVersion: 1; id: string; assetId: string; versionId: string; asset: ContextMediaReference; source: string; active: boolean; sceneId?: string; shotId?: string; timestamp?: number; createdAt?: number|string; model?: string; provider?: string; duration?: number; }
export interface ContextDocumentVersion { schemaVersion: 1; id: string; versionId: string; documentType: 'shot-plan'|'timeline'|'bible'; source: string; active: boolean; label?: string; name?: string; createdAt?: number|string; }
export type ContextGeneration = ContextMediaVersion & { status: 'completed' };
export type ContextSelection = {kind:'project'} | {kind:'scene';sceneId:string} | {kind:'selected-shots';shotIds:string[]} | {kind:'selected-assets';assetIds:string[]};
export const CONTEXT_FIELDS: { script: 'script'; transcript: 'transcript'; shots: 'shot[]'; shotPlan: 'shot-plan'; scenes: 'scene[]'; characters: 'character[]'; locations: 'location[]'; styles: 'style[]'; assets: 'asset[]'; generations: 'metadata[]'; versions: 'metadata[]'; timeline: 'timeline'; editor: 'timeline'; canvas: 'pipeline'; blueprint: 'bible'; panels: 'panel-document'; board: 'board-document'; animation: 'scene-document'; metadata: 'metadata' };
export function resolveContextInputs(manifest: Manifest, values: Record, context?: Record): Record;
/** What `stillmade.project.read(fields)` returns: metadata plus the declared context fields. Requires `project.read`. */
export function projectReadSnapshot(manifest: Manifest, context?: Record, fields?: string[]): Record;
export function validateView(view: unknown): true;
export const FRAME_VIEW_LIMITS: Readonly<{htmlBytes:262144;cssBytes:262144;javascriptBytes:4194304;assetFiles:32;assetBytes:8388608;taskMs:250;longTaskMs:1000}>;
export const FRAME_VIEW_ASSET_TYPES: readonly string[];
export function isFrameView(view: unknown): view is FrameView;
export function validateFrameView(view: FrameView): true;
/** Frame view JavaScript is one classic script (an IIFE bundle). */
export function parseFrameViewJavaScript(source: string): {ast: unknown};
/** Platform compilation: every function and loop body calls the task guard. */
export function instrumentFrameViewJavaScript(source: string, guard: string): string;
export function describeViewFiles(files: Record): Promise>;
/** In a frame view, the StillMade object also offers its asset files. */
export interface FrameViewAssets { asset(path: string): string; assetBytes(path: string): ArrayBuffer; }
export const VIEW_LIMITS: Readonly<{htmlBytes:65536;cssBytes:32768;javascriptBytes:65536;messageBytes:4194304;runsPerMinute:30;imagePreviewsPerMinute:60;imagePreviewMs:15000;imagePreviewEdge:512}>;
/** A display-only copy. Never substitute this URL for the original run input. */
export interface BlockImagePreview { url: `data:image/png;base64,${string}`; width: number; height: number; }
export interface BlockImagePixels { width: number; height: number; data: number[]; role?: string; }
export interface BlockImageReference { kind: 'image'; assetId: string; versionId: string; url: string; role?: string; }
export type DesktopPermission = "screen.capture"|"cursor.track"|"camera.capture"|"microphone.capture"|"clipboard.read"|"clipboard.write"|"media.pick"|"notifications.show"|"power.keep-awake"|"native.tools";
export type DesktopToolDeclaration = {id:'org.natron.Natron';adapterVersion:1;actions:('launch'|'project.import'|'project.open'|'project.render')[]} | {id:'org.stillmade.PythonAdapter';adapterVersion:1;actions:('adapter.import'|'adapter.inspect'|'adapter.execute'|'adapter.describe'|'adapter.call')[]};
export type DesktopExecution = {schemaVersion:1;tool:'org.natron.Natron';action:'project.render';arguments:Record<'projectId'|'input'|'reader'|'writer'|'firstFrame'|'lastFrame',{input:string}>;outputs:Record} | {schemaVersion:1;tool:'org.stillmade.PythonAdapter';action:'adapter.execute';arguments:Record<'adapterId'|'sourceDigest'|'functionId'|'input',{input:string}>;outputs:Record} | {schemaVersion:2;tool:'org.stillmade.PythonAdapter';action:'adapter.call';source:{code:string;digest:string;license:string};functionId:string;arguments:Record;outputs:{result:'result'}};
export interface DesktopExecutionRequest {schemaVersion:1;requestId:string;packageDigest:string;inputDigest:string;tools:DesktopToolDeclaration[]}
export interface DesktopExecutionReceipt {schemaVersion:1;requestId:string;packageDigest:string;inputDigest:string;outputDigest:string;tool:'org.natron.Natron'|'org.stillmade.PythonAdapter';adapterVersion:1;action:'project.render'|'adapter.execute'|'adapter.call';executionId:string}
export interface DesktopExecutionResult {outputs:Record;desktopReceipt:DesktopExecutionReceipt}
/** Trusted host only; issue evidence after the approved native action and output registration. */
export type DesktopExecutor=(pkg:BlockPackage,input:Record,options:{signal?:AbortSignal;request:DesktopExecutionRequest})=>Promise;
export interface BlockPackage {desktop?:DesktopExecution}
export interface RunPackageOptions {desktop?:DesktopExecutor}
export interface RunPackageResult {desktopReceipt?:DesktopExecutionReceipt}
export function requiresDesktopExecution(pkg:unknown):boolean;
export function assertLocalDesktopExecutionUnavailable(pkg:unknown):void;
export function validateDesktopExecution(pkg:BlockPackage,options?:{required?:boolean}):true;
export function validateDesktopExecutionRecord(pkg:BlockPackage,input:Record,result:DesktopExecutionResult):Promise;
export function desktopInvocation(pkg:BlockPackage,input:Record):{tool:'org.natron.Natron';operation:'tool.project.render';args:Record;outputs:Record};
export function desktopOutputs(pkg:BlockPackage,result:Record):Record;
export function prepareDesktopExecution(pkg:BlockPackage,input:Record):Promise<{package:BlockPackage;input:Record;request:DesktopExecutionRequest}>;
export function validateDesktopExecutionResult(pkg:BlockPackage,request:DesktopExecutionRequest,result:unknown):Promise;
export type DesktopToolOperation='tool.status'|'tool.launch'|'tool.project.import'|'tool.project.open'|'tool.project.render'|'tool.adapter.import'|'tool.adapter.inspect'|'tool.adapter.execute'|'tool.adapter.describe'|'tool.adapter.call';
export type BlockViewTag = 'div'|'section'|'article'|'header'|'footer'|'p'|'span'|'strong'|'em'|'small'|'h1'|'h2'|'h3'|'h4'|'label'|'button'|'input'|'textarea'|'select'|'option'|'ul'|'ol'|'li'|'table'|'thead'|'tbody'|'tr'|'th'|'td'|'details'|'summary'|'output'|'progress'|'img';
export interface BlockViewNode { tag: BlockViewTag; text?: string; children?: BlockViewNode[]; id?: string; className?: string; action?: string; value?: string|number; type?: 'text'|'number'|'range'|'checkbox'|'radio'|'color'; checked?: boolean; disabled?: boolean; placeholder?: string; min?: string|number; max?: string|number; step?: string|number; label?: string; }
export interface BlockViewAction { action: string; value: string; checked: boolean; event: 'click'|'input'|'change'; }
export interface BlockMediaReference { kind: 'video'|'audio'; assetId: string; versionId: string; url: string; role?: string; }
export interface BlockMediaPreview { kind: 'video'|'audio'; url: string; mimeType: string; bytes: number; }
export interface BlockViewBridge { useOutput(outputs: Record): Promise>; previewMedia(value: BlockMediaReference): Promise; releaseMedia(url: string): void; render(targetId: string, tree: BlockViewNode[]): void; onAction(callback: (event: BlockViewAction) => void|Promise): () => void; desktop(operation: DesktopPermission|DesktopToolOperation|import('./block-storage.js').BlockStorageOperation|"record.start"|"record.stop"|"record.cancel"|"session.close", args?: Record): Promise>; readonly input: Record; onInput(callback: (input: Record) => void): () => void; run(input: Record): Promise>; previewImage(value: BlockImageReference|BlockImagePixels): Promise; }
/** Validate hosted UI colors independently of the preserved original stylesheet. */
export function validateViewAppearance(view?: BlockView, options?: {appearance?:'host'|'original'}): true;
export const APPEARANCE_TOKENS: readonly string[];
/** Inert API prompt for reviewed core image nodes. Endpoints and credentials belong to the host. */
export interface ComfyWorkflow { schemaVersion: 1; prompt: Record}>; inputs: Record; outputs: Record; }
export interface ComfyImageExpectation { kind: 'image'; count: 1; width?: number; height?: number; maxBytes?: number; sha256?: string; }
export interface ComfyImageMetadata { width: number; height: number; bytes: number; mimeType: 'image/png'; frames: 1; sha256?: string; }
export type ComfyExecutor = (pkg: BlockPackage, input: Record, options: {signal?: AbortSignal}) => Promise<{outputs: Record; metadata: Record}>;
export function validateComfyWorkflow(workflow: ComfyWorkflow, manifest: Manifest): {order: string[]; classes: string[]};
export function validateComfyExpectations(manifest: Manifest, expectations: Record): true;
export function testComfyOutputs(manifest: Manifest, outputs: Record, expectations: Record, options: {metadata: Record}): {passed: true; checks: unknown[]};
export function validateComfyInput(workflow: ComfyWorkflow, manifest: Manifest, input: Record): Record;
export function digestComfyInput(manifest: Manifest, input: Record): Promise;
export function prepareComfyPrompt(workflow: ComfyWorkflow, manifest: Manifest, input: Record, uploads: Record): ComfyWorkflow['prompt'];
export function validateComfyBackend(workflow: ComfyWorkflow, manifest: Manifest, objectInfo: unknown): {compatible: true; classes: string[]; contracts: Record};
export function extractComfyOutputs(workflow: ComfyWorkflow, manifest: Manifest, history: unknown, options: {promptId: string}): Record;
export interface PlatformCapture { frames?: { interface?: 'host'|'guest'; width?: number; height?: number; touch?: boolean; controls: number; issues: { code: string; message: string; severity?: 'warning'|'error' }[] }[]; interactionPassed?: boolean|null; embedded?:boolean; continuityPassed?:boolean; states?:Record<'empty'|'populated'|'loading'|'error'|'success'|'long-labels'|'enlarged-text',boolean>; error?: string; unavailable?: string; }
export interface PlatformProbeOptions { content: BlockPackage; platform: 'phone'|'browser'|'desktop'; viewport: {width:number;height:number;touch:boolean;label?:'compact'|'phone'|'tablet'|'phone-landscape'|'desktop'}; theme:'light'|'dark'|'white'; }
export type PlatformProbe = (options: PlatformProbeOptions) => PlatformCapture|Promise;
export interface PlatformTargetCheck { supported:boolean|null; reason:string; checks:{viewport:{width:number;height:number;touch:boolean};theme:string;passed:boolean|null;message:string;findings?:{code:string;message:string}[]}[]; }
export interface PlatformCheckReport {schemaVersion:2;policy:'mobile-ui-1';subject:{id:string;version:string;runtime:string};digest:string;checkedAt:string;versions:{sdk:string;renderer:number;checks:number};targets:Record<'phone'|'browser'|'desktop',PlatformTargetCheck>;}
export function testPlatforms(content:BlockPackage,options?:{probe?:PlatformProbe;runtimePassed?:boolean;runtimeMessage?:string;sourceDigest?:string;targets?:('phone'|'browser'|'desktop')[]}):Promise;
export function measurePlatformLayout(options?:{touch?:boolean;nativeWorkspace?:boolean}):NonNullable[number];
export function assessPlatformCapture(capture:PlatformCapture):{passed:boolean|null;message:string};
export function checkedPlatformTargets(report:PlatformCheckReport,expectedDigest:string):PlatformCheckReport['targets']|null;
export function mobileActivationStatus(report:PlatformCheckReport|undefined,expectedDigest:string,manifest?:Manifest):{ready:boolean;reason:string};
export const PLATFORM_CHECK_POLICY:'mobile-ui-1';
export const PLATFORM_CHECK_VERSION:2;
export const PLATFORM_RENDERER_VERSION:2;
export const PLATFORM_CHECK_TARGETS:Readonly>;
export const PLATFORM_CHECK_THEMES:readonly ['light','dark','white'];
/** Static import inspection only. It never connects a backend or runs a test. */
export interface ComfyImportInspection {
format: 'api'|'api-wrapper'; source: Record; prompt: ComfyWorkflow['prompt'];
nodes: {id: string; classType: string; title?: string; label: string}[];
inputCandidates: {node: string; input: string; label: string; type: 'image'|'integer'|'text'; encoding: 'uploaded-image'|'scalar'; required: boolean; default?: unknown; options?: string[]}[];
outputCandidates: {node: string; label: string; type: 'image'; collection: 'images'; index: 0; required: true}[];
warnings: string[]; execution: 'not-run';
}
export function inspectComfyImport(raw: string|Record): ComfyImportInspection;
/** Requires explicit typed mappings and fixtures; returned source still needs host review and Confirm Import. */
export function buildComfyImportPackage(raw: string|Record, options: {
manifest: Manifest; inputs: ComfyWorkflow['inputs']; outputs: ComfyWorkflow['outputs']; tests: BlockPackage['tests'];
}): BlockPackage;
/** Declarative requests. Model, provider, voice, speed, image size/quality, keys, payment and approval belong to the host. */
export interface TextGenerationCapability { schemaVersion: 1; operation: 'text.generate'; prompt: {$input: string}; system: string; maxTokens: number; output: string; /** Other declared inputs to send as read-only project context. */ context?: string[]; /** With a json output port: the JSON schema the reply must match. */ schema?: JsonSchema; /** One json input of earlier {role, content} turns sent before the prompt. */ history?: {$input: string}; }
export interface SpeechCapability { schemaVersion: 1; operation: 'audio.speech'; text: {$input: string}; output: string; }
export interface TranscriptionCapability { schemaVersion: 1; operation: 'audio.transcribe'; audio: {$input: string}; language: {$input: string}; output: string; }
export interface ImageGenerationCapability { schemaVersion: 1; operation: 'image.generate'; prompt: {$input: string}; output: string; }
export interface TextGenerationInvocation { operation: 'text.generate'; system: string; prompt: string; maxTokens: number; history?: {role:'user'|'assistant';content:string}[]; }
/** text.generate history bounds: turns, characters per turn and in total. */
export const TEXT_HISTORY_LIMITS: Readonly<{turns:20;turn:8000;total:24000}>;
export interface SpeechInvocation { operation: 'audio.speech'; text: string; }
export interface TranscriptionInvocation { operation: 'audio.transcribe'; audio: {kind:'audio';assetId:string;versionId:string;url:string;mimeType?:string}; language: string; }
export interface ImageGenerationInvocation { operation: 'image.generate'; prompt: string; }
export type CapabilityInvocation = TextGenerationInvocation|SpeechInvocation|TranscriptionInvocation|ImageGenerationInvocation|VideoGenerationInvocation|ImageDescriptionInvocation|TimelineProposalInvocation;
export interface CapabilityTextExpectation { kind: 'text'|'script'; minLength: number; maxLength: number; includes?: string[]; }
export interface CapabilityAudioExpectation { kind: 'audio'; format: 'wav'; minDuration: number; maxDuration: number; minBytes: number; maxBytes: number; }
export interface CapabilityTranscriptExpectation {kind:'transcript';minWords:number;maxWords:number;}
export interface CapabilityImageExpectation { kind: 'image'; format: 'png'; width: 1024; height: 1024; minBytes: number; maxBytes: number; }
export interface CapabilityTimelineExpectation {kind:'timeline-edit';minCommands:number;maxCommands:number;}
/** Measured by the host from canonical bytes; these fields alone do not prove the bytes or ownership. */
export interface SpeechAudioReference { kind: 'audio'; assetId: string; versionId: string; url: string; mimeType: 'audio/wav'; duration: number; bytes: number; sampleRate: 24000; channels: 1; }
/** Measured MP3 or WAV from catalog voices, music or sound effects. */
export interface GeneratedAudioReference { kind: 'audio'; assetId: string; versionId: string; url: string; mimeType: 'audio/mpeg'|'audio/wav'; duration: number; bytes: number; sampleRate: number; channels: 1|2; }
export interface GeneratedImageReference { kind: 'image'; assetId: string; versionId: string; url: string; mimeType: 'image/png'; width: number; height: number; bytes: number; }
/** Bounded and bound to an output; authenticated owner/run verification belongs to the host client. */
export interface SpeechMediaReceipt extends SpeechAudioReference { schemaVersion: 1|2; ownerId: string; runId: string; index: number; storageKey: string; sha256: string; }
export interface GeneratedAudioMediaReceipt extends GeneratedAudioReference { schemaVersion: 3; ownerId: string; runId: string; index: number; storageKey: string; sha256: string; }
export interface GeneratedImageMediaReceipt extends GeneratedImageReference { schemaVersion: 1|2|3; ownerId: string; runId: string; index: number; storageKey: string; sha256: string; }
export interface CapabilityResult { outputs: Record; metadata?: Record; mediaReceipts?: Record; }
/** Inject only from a trusted host or an explicit test harness; guest code cannot supply this callback. */
export type CapabilityExecutor = (pkg: BlockPackage, input: Record, options: {signal?: AbortSignal}) => Promise;
export const CAPABILITY_LIMITS: Readonly<{prompt:32000;system:8000;output:32768;maxTokens:4096;fixtures:3;includes:8;includeLength:256;packageBytes:262144;metadataBytes:8192}>;
/** media.analyze bounds: images per run, question length and the closer-inspected video range in seconds. */
export const MEDIA_ANALYZE_LIMITS: Readonly<{images:4;question:2000;range:12}>;
/** web.fetch and web.research bounds: URL, instruction and topic lengths, sources kept and answer size. */
export const WEB_CAPABILITY_LIMITS: Readonly<{url:2048;instruction:4000;topic:500;sources:12;answer:32768}>;
/** image.describe with an image[] input sends one to four images together. */
export const IMAGE_DESCRIBE_LIMITS: Readonly<{images:4}>;
export const SPEECH_CAPABILITY_LIMITS: Readonly<{text:3800;duration:1400;bytes:67108864;sampleRate:24000;channels:1}>;
/** Measured catalog audio (voices, music, sound effects): MP3 or WAV as generated. Music lengths are whole seconds; sound effects are tenths of a second. */
export const AUDIO_CAPABILITY_LIMITS: Readonly<{bytes:134217728;duration:1800;music:Readonly<{min:5;max:240;default:30}>;sfx:Readonly<{min:0.5;max:22;default:4}>}>;
export const IMAGE_CAPABILITY_LIMITS: Readonly<{prompt:32000;bytes:12000000;width:1024;height:1024;maxSide:8192;maxPixels:40000000;maxBytes:67108864}>;
export interface ImageGenerationModel {label:string;aspectRatios:readonly string[];resolutions:readonly string[];qualities:readonly string[];references:number;seed:boolean}
export interface VideoGenerationModel {label:string;aspectRatios:readonly string[];resolutions:readonly string[];durations:readonly number[];firstFrame:boolean;lastFrame:boolean;requiresFirstFrame:boolean;seed:boolean;nativeAudio:boolean}
export interface ImageSettings {model:string;aspectRatio:string;resolution:string;quality:string}
export interface VideoSettings {model:string;duration:number;resolution:string;aspectRatio:string;generateAudio:boolean}
/** Every image model a Block can use; the same catalog as the app's own Blocks. */
export const IMAGE_GENERATION_MODELS: Readonly>;
/** Every video model a Block can use; the same catalog as the app's own Blocks. */
export const VIDEO_GENERATION_MODELS: Readonly>;
export const DEFAULT_IMAGE_MODEL: string;
export const DEFAULT_VIDEO_MODEL: string;
/** The value of a setting a model has no knob for. */
export const DEFAULT_SETTING: 'default';
export const GENERATION_LIMITS: Readonly<{references:8;negativePrompt:2000;seed:2147483647}>;
/** Fill and check image settings; `strict` rejects values the model does not accept. */
export function imageSettings(value?:Partial,options?:{strict?:boolean}):ImageSettings;
/** Fill and check video settings, including Veo's 1080p-is-8-seconds rule. */
export function videoSettings(value?:Partial,options?:{strict?:boolean;firstFrame?:boolean}):VideoSettings;
export function validateSeed(seed:number):number;
export const MEDIA_GENERATE_LIMITS: Readonly<{items:50;prompt:4000}>;
export const MEDIA_GENERATE_OPERATIONS: readonly ['image','video','speech','music','sfx'];
/** Voices, music and sound effects on the app's own generators. */
export const AUDIO_GENERATION: Readonly<{speech:Readonly<{text:3800;defaultVoice:'asteria';speed:Readonly<{min:0.25;max:4;default:1}>}>;music:Readonly<{min:5;max:240;default:30}>;sfx:Readonly<{min:0.5;max:22;default:4}>}>;
export function audioSettings(operation:'speech',value?:{voice?:string;speed?:number},options?:{strict?:boolean}):{voice:string;speed:number};
export function audioSettings(operation:'music'|'sfx',value?:{durationSec?:number},options?:{strict?:boolean}):{durationSec:number};
export function mediaGenerateRequest(request:unknown):unknown;
export function capabilityOperation(manifest: Manifest): 'text.generate'|'audio.speech'|'audio.transcribe'|'image.generate'|'video.generate'|'image.describe'|'timeline.propose'|'workspace.propose';
export function validateCapability(capability: TextGenerationCapability|SpeechCapability|TranscriptionCapability|ImageGenerationCapability|VideoGenerationCapability|ImageDescriptionCapability|TimelineProposalCapability, manifest: Manifest): true;
export function prepareCapabilityInvocation(pkg: BlockPackage, input: Record): CapabilityInvocation;
export function capabilityOutputs(pkg: BlockPackage, text: string): Record;
export function capabilityOutputs(pkg: BlockPackage, audio: SpeechAudioReference): Record;
export function capabilityOutputs(pkg: BlockPackage, transcript: {schemaVersion:1;text:string;words:{text:string;start:number;end:number}[]}): Record;
export function capabilityOutputs(pkg: BlockPackage, image: GeneratedImageReference): Record;
export function capabilityOutputs(pkg: BlockPackage, text: string): Record;
export function validateSpeechAudioReference(value: SpeechAudioReference|GeneratedAudioReference): true;
/** Measured MP3 or WAV reference returned by audio.music, audio.sfx and catalog voices. */
export function validateGeneratedAudioReference(value: GeneratedAudioReference): true;
export function validateGeneratedImageReference(value: GeneratedImageReference): true;
export function defaultCapabilityExpectations(manifest: Manifest): Record;
export function validateCapabilityExpectations(manifest: Manifest, expectations: Record): true;
export function testCapabilityOutputs(manifest: Manifest, outputs: Record, expectations: Record): {passed:true;checks:({output:string;kind:'text'|'script';length:number;includesMatched:number}|{output:string;kind:'audio';format:'wav';duration:number;bytes:number;sampleRate:24000;channels:1}|{output:string;kind:'transcript';words:number}|{output:string;kind:'image';format:'png';width:1024;height:1024;bytes:number}|{output:string;kind:'timeline-edit';commands:number})[]};
/** Bounded output validation only; metadata does not establish a verified live provider receipt. */
export function validateCapabilityResult(manifest: Manifest, result: CapabilityResult): CapabilityResult;
/** Host storage is scoped to the account, project, and Step instance. */
export interface BlockState {version:number;value:Record}
export const STATE_LIMIT_BYTES: 65536;
export function initialBlockState(manifest:Manifest):BlockState;
export function validateBlockState(manifest:Manifest,state:unknown):BlockState;
export interface TimelineEditCommand {
collection: 'clips'|'audioItems'|'captions';
operation: 'move'|'trim'|'split'|'volume'|'mute'|'remove'|'caption.add'|'caption.update'|'grade'|'transition';
before: {id:string; [key:string]:unknown}|null;
values: {type?:string;duration?:number;soundOn?:boolean;soundId?:string;lut?:'None'|'Cold'|'Warm'|'Desaturated'|'High Contrast'|'Filmic'|'Teal and Orange';font?:'Hanken Grotesk';size?:number;highlightColor?:string;background?:string;backgroundOpacity?:number;backgroundPadding?:number;backgroundRadius?:number;position?:'top left'|'top center'|'top right'|'middle left'|'middle center'|'middle right'|'bottom left'|'bottom center'|'bottom right';animation?:'none'|'fade';words?:Array<{text:string;start:number;end:number}>;color?:string|{brightness:number;contrast:number;saturation:number;temperature:number;sharpness:number;vignette:number};id?:string;trackId?:string;text?:string;end?:number;at?:number;newId?:string;start?:number;trimStart?:number;duration?:number;volume?:number;muted?:boolean};
}
export interface TimelineEdit {schemaVersion:1;title:string;timelineId:string;commands:TimelineEditCommand[];}
export function isTimelineEdit(value:unknown):value is TimelineEdit;
export interface WorkspaceEdit {schemaVersion:1;title:string;field:'blueprint'|'panels'|'board'|'canvas'|'animation';before:Record&{schemaVersion:1};after:Record&{schemaVersion:1};}
export function isWorkspaceEdit(value:unknown):value is WorkspaceEdit;
export const WORKSPACE_EDIT_FIELDS:readonly WorkspaceEdit['field'][];
export function validateBlockOutputs(manifest: Manifest, values: Record): Record;
/** Optional schema-1 Project Type setup, consumed by the StillMade host. */
export interface ProjectTypeOnboardingQuestion {id:string;label:string;type:'text'|'textarea'|'select';required?:boolean;options?:string[];default?:string;}
export interface ProjectTypeDefaults {aspectRatio?:'16:9'|'9:16'|'1:1';brief?:string;}
/** Optional Project Type production rule. Only trusted host receipts can satisfy it. */
export interface ProjectTypeOutcome {type:"export"|"delivery"|"project_outcome";dedupe?:"material_project_state";minimumDurationSec?:number;}
export interface VideoGenerationCapability {schemaVersion:1;operation:'video.generate';prompt:{$input:string};output:string;}
export interface VideoGenerationInvocation {operation:'video.generate';prompt:string;}
export interface GeneratedVideoReference {kind:'video';assetId:string;versionId:string;url:string;mimeType:'video/mp4';width:number;height:number;duration:number;bytes:number;}
export interface GeneratedVideoMediaReceipt extends GeneratedVideoReference {schemaVersion:1;ownerId:string;runId:string;index:number;storageKey:string;sha256:string;}
export interface CapabilityVideoExpectation {kind:'video';format:'mp4';minDuration:number;maxDuration:number;minBytes:number;maxBytes:number;minWidth:number;maxWidth:number;minHeight:number;maxHeight:number;}
export const VIDEO_CAPABILITY_LIMITS:Readonly<{prompt:32000;bytes:67108864;duration:60;width:3840;height:3840;pixels:8294400}>;
export function validateGeneratedVideoReference(value:GeneratedVideoReference):true;
export function capabilityOutputs(pkg:BlockPackage,video:GeneratedVideoReference):Record;
export interface ImageDescriptionCapability {schemaVersion:1;operation:'image.describe';image:{$input:string};instruction:string;maxTokens:number;output:string;}
export interface ImageDescriptionInvocation {operation:'image.describe';image:GeneratedImageReference|{width:number;height:number;data:number[]};instruction:string;maxTokens:number;}
export interface TimelineProposalCapability {schemaVersion:1;operation:'timeline.propose';context:Record;instruction:string;maxTokens:number;output:string;}
export interface TimelineProposalInvocation {operation:'timeline.propose';context:Record;instruction:string;maxTokens:number;}
export const COMFY_MODEL_LIMITS: Readonly<{steps:number;prompt:number;dimension:number;seed:number}>;
export function comfyModelRequirements(workflow: ComfyWorkflow): string[];
export type StateMigrationOperation={op:'rename';from:string;to:string}|{op:'default';key:string;value:unknown}|{op:'remove';key:string};
export interface StateMigration {fromVersion:number;operations:StateMigrationOperation[]}
export function validateStateMigrations(state:Manifest['state']):true;
export function stateMigrationRoute(manifest:Manifest,fromVersion:number):StateMigration|null;
export function migrateBlockState(previousManifest:Manifest,nextManifest:Manifest,state:BlockState):{state:BlockState;operations:StateMigrationOperation[];fromVersion:number;toVersion:number};
export const STATE_MIGRATION_LIMITS:Readonly<{routes:16;operations:64;keyLength:80;declarationBytes:65536}>;
/** In-session host job. This handle is never a guest output or durable receipt. */
export type PackageJobStatus = 'queued'|'loading'|'running'|'completed'|'failed'|'cancelled';
export interface PackageJobSnapshot {readonly id:string;readonly status:PackageJobStatus;readonly progress:Readonly|null;readonly error?:Readonly<{code:string;message:string}>}
export interface PackageJob {readonly id:string;readonly result:Promise;getSnapshot():Readonly;subscribe(listener:()=>void):()=>void;cancel():boolean}
export interface PackageJobOptions extends RunPackageOptions {timeoutMs?:number;execute?:(pkg:BlockPackage,input:Record,options:RunPackageOptions)=>Promise|RunPackageResult}
export function createPackageJob(pkg:BlockPackage,input:Record,options?:PackageJobOptions):PackageJob;
export {TimelineRange,ApprovalDecision,TIMELINE_RANGE_MAX_MS,isTimelineRange,isApprovalDecision} from './interaction-values.js';
/** Trusted native Script Writer services; does not grant guest permissions. */
export const SCRIPT_HOST_SERVICE_VERSION: 1;
export const SCRIPT_HOST_OPERATIONS: readonly string[];
/** Checks the service version and all required callable operations. */
export function requireScriptHostServices(services:T):T;
export * from './native-workspace-contract.js';
export * from './script-project-browser.js';
export * from './script-collaboration.js';
export * from './script-server-services.js';
export * from './voiceover-host-services.js';
export * from './shot-planning-host-services.js';
export * from './blueprint-host-services.js';
export * from './shot-planning-sandbox-host.js';
export * from './board-editor-host.js';
/** Canonical immediate-use portable profile; legacy packages remain readable. */
export type PortableSource = {license:'MIT'|'BSD-2-Clause'|'BSD-3-Clause'|'ISC'|'Apache-2.0';paths:string[];evidence:string[];changes:string} & ({repository:string;commit:string;kind?:never}|{kind:'original';repository?:never;commit?:never});
export interface PortableMetadata {format:'stillmade-block/1';dependencies:[];sources:PortableSource[];files:Record;lineage:{parents:{id:string;version:string;digest:string}[];changes:string}}
export const PORTABLE_FORMAT:'stillmade-block/1';
export const PORTABLE_LIMITS:Readonly<{archive:number;expanded:number;files:number}>;
export function recognizedLicense(text:string):PortableSource['license']|null;
export function validatePortableMetadata(pkg:BlockPackage):void;
export function withPortableAttribution(pkg:BlockPackage):BlockPackage;
export function withOriginalSource(pkg:BlockPackage):BlockPackage;
export function portableFiles(pkg:BlockPackage):Record;
export function packageFromPortableFiles(files:Record):BlockPackage;
export function readPortableArchive(bytes:Uint8Array,codec:{unzipSync:(bytes:Uint8Array)=>Record}):BlockPackage;
export function writePortableArchive(pkg:BlockPackage,codec:{zipSync:Function}):Uint8Array;
export function verifyPortableSources(pkg:BlockPackage,options?:{fetcher?:typeof fetch;signal?:AbortSignal}):Promise<{passed:boolean;legacy?:boolean;results?:{name:string;passed:boolean;message:string}[]}>;
export * from './workspace-application.js';
export * from './workspace-navigation.js';
export * from './canvas-workspace-host.js';
export * from './native-host-providers.js';
export {blockRemixPolicy,assertRemixReady,REMIX_AUTHORING_REQUIREMENT} from './remix-policy.js';
export interface SdkCompatibility {status:'supported'|'unsupported'|'invalid';declared:unknown;runtimeVersion:string;message:string}
export interface Manifest { schemaVersion: 1; id: string; name: string; version: string; sdkVersion: SdkVersionRange; description: string; kind: 'task'|'workspace'|'editor-extension'|'composite'; inputs: Record; outputs: Record; runtime: 'recipe'|'javascript'|'module'|'comfyui'|'capability'; jobs?: {kind:'cooperative';version:1}; state?: {scope:'step';version:number;initial:Record;migrations?:StateMigration[]}; entry?: string; permissions: { project: string[]; desktop?: DesktopPermission[]; capabilities?: ['comfyui.execute']|['text.generate']|['audio.speech']|['audio.transcribe']|['image.generate']|['video.generate']|['image.describe']|['timeline.propose']|['workspace.propose']|['connection.execute']; network?: never[]; filesystem?: never[]; secrets?: never[] }; uiLayout?: {kind:'tabs'|'sections';groups:{id:string;label:string;inputs:string[];outputs:string[];collapsed?:boolean}[]}; ui?: ({ control: 'text'|'number'|'slider'|'checkbox'|'asset'; port: string; label?: string; when?: {port:string;equals:string|boolean} } | {control:'select'|'multiselect';port:string;options:string[];label?:string;when?:{port:string;equals:string|boolean}} | {control:'gallery';port:string;label?:string} | {control:'before-after';port:string;before:string;label?:string})[]; }
export interface BlockPackage { portable?: PortableMetadata; manifest: Manifest; view?: BlockView|FrameView; code?: string; module?: ModuleDescriptor; comfyui?: ComfyWorkflow; capability?: ConnectedCapability|TextGenerationCapability|SpeechCapability|TranscriptionCapability|ImageGenerationCapability|VideoGenerationCapability|ImageDescriptionCapability|TimelineProposalCapability; recipe?: { schemaVersion: 1; steps: { id: string; op: string; args: Record }[]; outputs: Record }; tests: { name: string; input: Record; expected?: Record; state?: BlockState; expectedState?: BlockState; expectations?: Record }[]; }
export type CapabilityInvocation = TextGenerationInvocation|SpeechInvocation|TranscriptionInvocation|ImageGenerationInvocation|VideoGenerationInvocation|ImageDescriptionInvocation|TimelineProposalInvocation;
export interface CapabilityResult { outputs: Record; metadata?: Record; mediaReceipts?: Record; }
export function capabilityOperation(manifest: Manifest): 'text.generate'|'audio.speech'|'audio.transcribe'|'image.generate'|'video.generate'|'image.describe'|'timeline.propose'|'workspace.propose';
export function validateCapability(capability: TextGenerationCapability|SpeechCapability|TranscriptionCapability|ImageGenerationCapability|VideoGenerationCapability|ImageDescriptionCapability|TimelineProposalCapability, manifest: Manifest): true;
export function defaultCapabilityExpectations(manifest: Manifest): Record;
export function validateCapabilityExpectations(manifest: Manifest, expectations: Record): true;
export function testCapabilityOutputs(manifest: Manifest, outputs: Record, expectations: Record): {passed:true;checks:({output:string;kind:'text'|'script';length:number;includesMatched:number}|{output:string;kind:'audio';format:'wav';duration:number;bytes:number;sampleRate:24000;channels:1}|{output:string;kind:'transcript';words:number}|{output:string;kind:'image';format:'png';width:1024;height:1024;bytes:number}|{output:string;kind:'timeline-edit';commands:number})[]};
export * from './voiceover-workspace-host.js';
export * from './blueprint-workspace-host.js';
export * from './canvas-chat-host.js';
export * from './canvas-timing.js';
export * from './canvas-chat-edit-broker.js';
export * from './canvas-chat-build-broker.js';
export * from './canvas-chat-descriptor-broker.js';
export * from './presenter-host.js';
export * from './presenter-placement-broker.js';
export * from './presenter-insertion-broker.js';
export * from './presenter-build-broker.js';
export * from './presenter-gap-broker.js';
export * from './presenter-provision-broker.js';
export * from './native-preview-contract.js';
export * from './animated-assets-host.js';
export * from './pipeline-execution-host.js';
export * from './pipeline-node-generation-host.js';
export * from './pipeline-runtime-host.js';
export * from './canvas-generation-host.js';
export * from './media-description-host.js';
export * from './audio-score-host.js';
export * from './video-assets-host.js';
export * from './editor-execution-host.js';
export * from './canvas-node-host.js';
export * from './media-upload-host.js';
export * from './youtube-connection-host.js';
export * from './youtube-host.js';
export interface TransitionSettings {type:string;duration:number;soundOn:boolean;soundId:string;}
export const TRANSITION_SETTINGS: readonly {readonly type:string;readonly sounds:readonly string[]}[];
export function isTransitionSettings(value:unknown):value is TransitionSettings;
/** Host-installed media export dependency; unavailable to sandbox guest code. */
export interface MediaExportSession {read(url:string):Promise;close():void}
export interface MediaExportHost {version:1;open(urls:string[],options?:{signal?:AbortSignal;checkCurrent?:()=>void}):MediaExportSession}
export const MEDIA_EXPORT_LIMITS:Readonly<{files:256;fileBytes:268435456;totalBytes:536870912;concurrency:6;attempts:3}>;
export function requireMediaExportHost(host:unknown):MediaExportHost;
/** Inert, user-requested file download result; not a Block package archive. */
export interface FileBundleSource {[key:string]:unknown;kind:'image'|'video'|'audio';assetId:string;versionId:string;url:string;role?:string}
export interface FileBundle {schemaVersion:1;format:'file'|'zip';name:string;files:({path:string;text:string}|{path:string;source:FileBundleSource})[]}
export const FILE_BUNDLE_LIMITS:Readonly<{files:256;textBytes:3000000;pathLength:180}>;
export function isFileBundle(value:unknown):value is FileBundle;
/** Host admission for user-selected originals; no guest upload permission. */
export const MEDIA_IMPORT_MAX_BYTES: number;
export const MEDIA_IMPORT_FORMATS: Readonly>>;
/** Every upload the host stores: media plus general files (kind 'file'). */
export const IMPORT_FORMATS: Readonly>>;
/** Checks a chosen file; `kinds` defaults to media, pass ['file'] for general files. */
export function inspectMediaImportFile(file:{name:string;size:number;type?:string},options?:{kinds?:Array<'image'|'video'|'audio'|'file'>}):{kind:'image'|'video'|'audio'|'file';extension:string;mimeType?:string};
export * from './data-values.js';
export * from './cloud-storage.js';
export * from './triggers.js';
export * from './mock-host.js';
export * from './view-bridge.js';
export * from './port-types.js';
export * from './trigger-examples.js';
/** Metadata only; filled values do not imply media access or execution approval. */
export interface AgentValuePresence {status:'missing'|'empty'|'invalid'|'filled';filled:boolean;characters?:number;items?:number;fields?:number;availability?:'verified'|'unknown'}
export interface AgentInputPresence extends AgentValuePresence {id:string;type:string;required:boolean;source:'project'|'provided'|'default'|'unset';locked:boolean}
export function inspectAgentValue(value:unknown,port?:{type:string;[key:string]:unknown},options?:{mediaVerified?:boolean}):AgentValuePresence;
export function inspectAgentPorts(ports?:Record,values?:Record,options?:{offset?:number;limit?:number;locked?:string[]}):{fields:AgentInputPresence[];total:number;nextOffset:number|null};
export function inspectAgentBlock(content:unknown,values?:Record,options?:{offset?:number;limit?:number;outputOffset?:number;outputs?:Record;locked?:string[]}):{schemaVersion:1;block:Record;inputs:ReturnType;outputs:ReturnType;prerequisites:{inputs:Array<{inputKey:string;type:string;source:string;locked:boolean}>;partial:boolean};permissions:Record;platforms:Record;stateContract:{scope:string|null;version:number|null}|null;execution:{runtime:string|null;cooperativeJobs:boolean};readiness:'partial'|'needs-input'|'inputs-present';admission:'not-checked';mediaAccess:'not-checked';executionApproval:'not-granted'};
export interface AgentDocumentField {path:string[];type:string;status:'missing'|'empty'|'filled';filled:boolean;characters?:number;items?:number;fields?:number;childrenInspected?:number;childrenOmitted?:boolean}
export function inspectAgentDocument(document:unknown,options?:{limit?:number;maxDepth?:number}):{fields:AgentDocumentField[];partial:boolean;inspection:'presence-only';validation:'not-checked';mediaAccess:'not-checked'};
export interface AgentEditableInput {id:string;type:string;description?:string;min?:number;max?:number;options?:unknown[];selection?:string;optionsPartial?:boolean}
export function agentInputChoices(manifest:{inputs?:Record;ui?:any[]},locked?:string[],options?:{offset?:number}):AgentEditableInput[];
export function parseAgentInput(manifest:Parameters[0],args:unknown,options?:{locked?:string[];busy?:boolean;disabled?:boolean}):{field:string;value:unknown};
export {defineNativeAgentAdapter,bindNativeAgentActions,bindNativeWorkspaceActions,NATIVE_WORKSPACE_ACTION_LIMIT} from './native-agent-contract.js';
export type {NativeAgentAction,NativeAgentAdapter,NativeWorkspaceAction} from './native-agent-contract.js';
export * from './export-policy.js';
export {createUniversalResolver} from './universal-resolver.js';
export type {ResolutionSource,ResolutionDependency,ResolutionValue,ResolutionOptions,ResolutionCandidate,ResolutionDecision,ResolutionAnswer,ResolutionResult,UniversalResolver,UniversalResolverServices} from './universal-resolver.js';
export function inspectAgentDocumentPage(document:unknown,options?:{path?:string[];offset?:number;limit?:number}):{focus:AgentDocumentField;fields:AgentDocumentField[];total:number;offset:number;nextOffset:number|null;inspection:'presence-only';validation:'not-checked';mediaAccess:'not-checked'};
export * from './asset-generation-host.js';
export * from './pipeline-asset-host.js';
export * from './byok-payment-host.js';
export * from './repository-application.js';
export * from './portable-media-actions.js';
export type ProjectFlowFailureCode='EXECUTION_FAILED'|'RESULT_DELIVERY_FAILED'|'RESULT_SAVE_FAILED';
export const PROJECT_FLOW_FAILURE_CODES:readonly ProjectFlowFailureCode[];
export type ProjectFlowPerspective='planned'|'active_run'|'historical_run';
export interface ProjectFlowTarget {projectId:string;placementId?:string;inputKey?:string;outputKey?:string;runId?:string;attemptId?:string;itemId?:string;perspective?:ProjectFlowPerspective}
export type ProjectFlowValuePath=Array<{kind:'field';key:string}|{kind:'item';index:number}>;
export interface ProjectFlowLocalMediaReference {assetId:string;versionId:string;kind:'image'|'video'|'audio'}
export interface ProjectFlowLocalMediaInspection extends ProjectFlowLocalMediaReference {availability:'present'|'not-on-this-device'|'version-mismatch'|'unavailable';scope:'this-device';cloudAvailable:false;playbackVerified:false;execution:'none'}
export interface ProjectFlowTextPreview {kind:'text';excerpt:string;characters?:number;revision?:string;path?:ProjectFlowValuePath;identity?:string;offset?:number;limit?:number;nextOffset?:number|null;truncated?:boolean;pageable?:boolean;redacted?:boolean}
export interface ProjectFlowTextPageRequest {placementId:string;portKind:'input'|'output';portKey:string;path:ProjectFlowValuePath;identity:string;offset:number;limit:number}
export interface ProjectFlowCandidate {id:string;label:string;reason?:string|null;sourcePlacementId?:string|null;outputKey?:string|null;preview?:unknown;adapter?:{id:string;version:number}|null;adapterDetail?:ProjectFlowAdapterDetail|null}
export interface ProjectFlowAdapterDetail {id?:string;version?:number|string;available:boolean;inputType?:string;outputType?:string;effect?:string;status?:string;steps?:Array<{id:string;version:number|string}>;contextFields?:string[]}
export interface ProjectFlowInput {key:string;label:string;required:boolean;state:string;reason?:string|null;source?:string|null;sourceKind?:string|null;sourcePlacementId?:string|null;sourceOutputKey?:string|null;explicit?:boolean;selectionReason?:string|null;valuePolicy?:string|null;materialized?:boolean;preview?:unknown;candidates:ProjectFlowCandidate[];candidatesOmitted?:number;candidateCount?:number;candidateRevision?:string;candidateOffset?:number;candidateLimit?:number;candidateNextOffset?:number|null;[key:string]:unknown}
export interface ProjectFlowNativeObservation {kind:'native-canvas-output';nodeId:string;blockId:string;blockVersion:string;outputKey:string;handle:string;runReceiptAvailable:false;revision:string}
export interface ProjectFlowOutput {key:string;label:string;state:string;runId?:string|null;preview?:unknown;observation?:ProjectFlowNativeObservation|null;availabilityReason?:string|null}
export type ProjectFlowConditionOperator='present'|'equals'|'not-equals'|'greater-than'|'at-least'|'less-than'|'at-most';
export interface ProjectFlowCondition {inputKey:string;inputLabel:string;path:string[];operator:ProjectFlowConditionOperator;outcome:'unknown'|'ran'|'skipped'}
export interface ProjectFlowExecution {kind:'StillMade hosted capability'|'Connected ComfyUI service'|'Block runtime'|'Condition bypass';delivery:'captured'|'not-produced';mediaReceiptCount:number}
export interface ProjectFlowInputEvidence {key:string;label:string;kind:string|null;assetId?:string|null;versionId?:string|null;count?:number|null;sourceId?:string|null;sourceRevision?:string|null;sourceReference?:{id:string;version:string;path:Array}|null;sourceKind?:string|null;sourceStepId?:string|null;sourcePort?:string|null;sourceRunId?:string|null;sourceAdapterId?:string|null;sourceAdapterVersion?:string|null;items?:Array<{kind:string|null;assetId:string|null;versionId:string|null;count:number|null}>;truncated?:boolean}
export interface ProjectFlowActiveRun {scope:'this-tab'|'shared-tab';kind:'single'|'connected'|'batch'|'loop';startedAt:string;runId:string|null;progress:string|null;actorLabel?:string;attempt?:number|null;batchIndex?:number;batchCount?:number;iteration?:number;additionalRuns?:number;initialInputs:ProjectFlowInputEvidence[]|null;inputsComplete:boolean}
export interface ProjectFlowFailureDiagnostic {code:ProjectFlowFailureCode;durationMs:number|null}
export interface ProjectFlowFailedAttempt extends ProjectFlowFailureDiagnostic {runId:string;at:string;attempt?:number;batchIndex?:number;batchCount?:number}
export interface ProjectFlowHistoryEntry {receiptId?:string;inputs?:ProjectFlowInputEvidence[]|null;inputsRevision?:string;inputsCount?:number|null;inputsOffset?:number;inputsLimit?:number;inputsNextOffset?:number|null;inputsRetainedComplete?:boolean;inputsTruncated?:boolean;execution?:ProjectFlowExecution;diagnostic?:ProjectFlowFailureDiagnostic;recovered?:boolean;[key:string]:unknown}
export interface ProjectFlowSelection {kind:'existing-media'|'host-action';selectionId:string;at?:string}
export interface ProjectFlowActivity {selection?:ProjectFlowSelection;failedAttempt?:ProjectFlowFailedAttempt;execution?:ProjectFlowExecution;active?:ProjectFlowActiveRun;applied?:boolean;at?:string|null;runId?:string|null;skipped?:boolean;[key:string]:unknown}
export interface ProjectFlowRetainedCandidate {candidateId:string;kind:'unused'|'connected'|'batch'|'loop';loopRunId?:string;loopReceiptDigest?:string;loopStartPlacementId?:string;historical?:true;selection?:ProjectFlowSelection;verification?:'verified-original'|'requires-inspection';runId:string|null;at:string|null;outputDigest:string;packageDigest:string|null;inputDigest:string|null;attempt?:number|null;batchIndex?:number|null;batchCount?:number|null;iteration?:number|null;reason:string;applied:false;previewRestricted:boolean;inputs:ProjectFlowInputEvidence[]|null;inputsCount:number|null;inputsOffset:number;inputsNextOffset:number|null;inputsRetainedComplete:boolean;outputs:ProjectFlowOutput[];outputsCount:number;outputsOffset:number;outputsNextOffset:number|null}
export interface ProjectFlowRetainedListRequest {placementId:string;revision:string;offset:number;limit:number}
export interface ProjectFlowRetainedInspectRequest extends ProjectFlowRetainedListRequest {candidateId:string;section:'inputs'|'outputs'|'text'|'collection';outputKey?:string;path?:Array<{kind:'field';key:string}|{kind:'item';index:number}>;identity?:string}
export interface ProjectFlowPlacement {id:string;state:string;localMediaInputs?:string[];condition?:ProjectFlowCondition;activity?:ProjectFlowActivity|null;history?:ProjectFlowHistoryEntry[];retainedLoopRuns?:{runId:string;planDigest:string;receiptDigest:string;loopDigest:string;iterations:number;selected:boolean;location:"shared-project";validation:"requires-review"}[];unusedResults?:ProjectFlowRetainedCandidate[];unusedResultsRevision?:string;unusedResultsCount?:number;unusedResultsOffset?:number;unusedResultsNextOffset?:number|null;inputs:ProjectFlowInput[];outputs:ProjectFlowOutput[];[key:string]:unknown}
export interface ProjectFlowEdge {from:string;to:string;inputKey:string;outputKey?:string|null;sourceRunId?:string|null;state?:string;materialized?:boolean;adapter?:unknown}
export interface ProjectFlowIssue {message:string;placementId?:string|null;inputKey?:string|null}
export interface ProjectFlowSnapshot {contractVersion:1;projectId:string|null;snapshotCursor?:string;revision?:string;scope?:'project'|'placement';definition:{id:string;version:string}|null;placements:ProjectFlowPlacement[];edges:ProjectFlowEdge[];issues:ProjectFlowIssue[];partial:boolean;projectFlowPartial?:boolean;total?:number;nextOffset?:number|null;pageComplete?:boolean;candidatePage?:{placementId:string;inputKey:string;total:number;candidateRevision:string;offset:number;limit:number;nextOffset:number|null}}
export const PROJECT_FLOW_CONTRACT_VERSION:1;
export const PROJECT_FLOW_PERSPECTIVES:readonly ProjectFlowPerspective[];
export interface ProjectTaskControlRequest { projectId:string; taskId:string; action:'pause'|'resume'|'takeover'|'cancel'; requestId:string; requesterId:string; taskRevision:string; instructionRevision:number; controlEpoch:number; reason?:string; }
export interface ProjectTaskControlReceipt { requestId:string; taskId:string; action:'pause'|'resume'|'takeover'|'cancel'; status:string; instructionRevision:number; controlEpoch:number; }
export interface ProjectCapabilityRequestRead { projectId:string; requestId:string; expectedRevision?:string; }
export interface ProjectCapabilityRequestDelivery { projectId:string; requestId:string; runId:string; revision:string; state:string; recoveryState:'retrieved'|'pending'|'unavailable'; packageDigest:string; inputDigest:string; quoteDigest:string; outputs:Record|null; execution:'none'; applied:false; selectionChanged:false; providerPolled:false; validatedFor:'delivery-only'; }
export const PROJECT_FLOW_OPERATIONS:Readonly>>;
export const PROJECT_FLOW_LIMITS:Readonly<{placements:1000;edges:4000;issues:1000;bytes:12000000}>;
export function projectFlowTarget(value:unknown):Readonly;
export function projectFlowSnapshot(value:T):T;
export function redactProjectFlowMediaAddresses(value:T):T;
// Pure future application contracts; Manifest/BlockPackage remain unchanged.
export * from './browser-application.js';
export * from './browser-application-execution.js';
export * from './browser-application-guest.js';
export * from './browser-application-artifacts.js';
export type ConnectionBackend='pipedream'|'composio'|'nango'|'generated';
export interface ConnectionPin {backend:ConnectionBackend;id:string;version:string;schemaHash:string;integrationId?:string}
export interface ConnectedCapability {schemaVersion:1;operation:'connection.execute';appId:string;capabilityId:string;pin:ConnectionPin;bindings:Record;output:string;inputSchema:Record;outputSchema:Record}
export interface ConnectedResultExpectation {kind:'connected-result';maximumBytes:number}
export interface ConnectedAppMetadata {id:string;blockId?:string;name:string}
export interface ConnectedTaskMetadata extends ConnectionPin {capabilityId:string;definitionHash:string;name:string;description?:string;inputSchema:Record;outputSchema:Record;kind?:'action'|'trigger'|'sync';semantics?:string}
export function connectedAppBlock(app:ConnectedAppMetadata,route:ConnectedTaskMetadata):BlockPackage;
export interface ConnectionConfigurationRequest {operation:'reload'|'options'|'configure';propName?:string;query?:string;page?:number;previousContext?:Record}
export interface ConnectionConfigurationResult {schemaVersion:1;operation:'reload'|'options'|'configure';inputSchema:Record;schemaDigest:string;dynamicPropsId?:string;propName?:string;options?:Array<{label:string;value:unknown}>;previousContext?:Record}
/** Pure package construction; the authenticated host separately validates the current exact-account configuration receipt. Trigger metadata is not ordinary action admission. */
export function configuredConnectionBlock(app:ConnectedAppMetadata,route:ConnectedTaskMetadata,basePackage:BlockPackage,configuration:ConnectionConfigurationResult):Promise;
export function validateConnectedCapability(capability:ConnectedCapability,manifest:Manifest):true;
export function prepareConnectedInvocation(pkg:BlockPackage,raw:Record):{operation:'connection.execute';appId:string;capabilityId:string;pin:ConnectionPin;input:Record};
export interface SchemaFields {currencyDigits(currency:string):number;minorUnits(decimal:string,currency:string):number;formatMoney(minor:number,currency:string):string;preview(value:unknown,options?:{maximumFields?:number}):BlockViewNode[];read(root:unknown,path:Array):unknown;set(root:unknown,path:Array,value:unknown):Record;controls(schema:Record,bindings:Record,values:Record,options?:{canEdit?:boolean;busy?:boolean}):{tree:unknown[];actions:unknown[]};change(values:Record,actions:unknown[],event:unknown):Record;fromState(input:Record,state:Record,bindings:Record):Record;edits(values:Record,state:Record,bindings:Record):{patch:Record;before:Record