# Authentication (/docs/authentication) Every API call is authenticated with a bearer token: ``` Authorization: Bearer ``` The one exception is the node catalog — `GET /v1/nodes` and `GET /v1/nodes/:id` are public, so you can read a node's ports and enums before you have a token. Anything that reads your data or spends money needs a credential. There are two kinds of tokens, both sent the same way. ## Personal access tokens (PATs) [#personal-access-tokens-pats] Best for scripts, CI, and servers. * Created in the Studio under **Settings → API tokens**. * Shaped `blit_…`. The full token is **shown exactly once** at creation — BlitFlow stores only a hash and cannot show it again. * Optional expiry, up to 365 days. * Revocable at any time from the same settings page. Treat a PAT like a password: keep it in an environment variable or secret manager, never in source control. ## Device login (OAuth device flow) [#device-login-oauth-device-flow] Best for a developer's own machine. The [CLI](/docs/cli) signs you in through the browser and saves a short-lived session token to `~/.blitflow/config.json`: ```bash blitflow login ``` For CI and headless environments, set `BLITFLOW_TOKEN` (a PAT) instead of logging in. The MCP hosted server uses the same OAuth machinery — MCP clients authenticate interactively, no token pasting required. See [MCP server](/docs/mcp). ## Base URLs [#base-urls] | Client | Base URL | | ---------------------------- | ----------------------------------------------------------------------- | | Public API | `https://studio.blitflow.com/api` | | A Studio deployment directly | `https:///api` (the API is served under the `/api` prefix) | Contract paths are relative to the base: `/v1/nodes` is `https://studio.blitflow.com/api/v1/nodes`, or `https:///api/v1/nodes` when calling a Studio host directly. The SDK defaults to `https://studio.blitflow.com/api` when you pass an `apiKey`, and accepts a `baseUrl` override — see [SDK setup](/docs/sdk). ## Scopes [#scopes] Each operation has a scope. Tokens act on behalf of your user account: | Scope | Operations | Effect | | ------ | ------------------------------------------ | ----------------------------- | | `read` | `nodes.list`, `nodes.get`, `workflows.get` | No cost, no side effects | | `run` | `runs.create`, `runs.node` | Executes nodes — spends money | Runs are charged to your organization's prepaid balance and admitted while it is positive. See [Runs & streaming](/docs/concepts/runs). ## Errors [#errors] | Status | Meaning | | ------ | ---------------------------------------------------------------- | | `401` | Missing, invalid, expired, or revoked token | | `403` | Authenticated but not allowed (e.g. cross-origin cookie request) | Error bodies are JSON: `{ "error": "" }`. Browser sessions (cookies) also work against the API for same-origin calls from the Studio itself, but API clients should always use bearer tokens — cookie auth is CSRF-guarded and rejects cross-origin mutations. # CLI (/docs/cli) The `blitflow` CLI wraps the same contract operations for terminal use — handy for trying nodes, running YAML-defined workflows, and scripting. ## Install [#install] The CLI ships on npm as [`blitflow`](https://www.npmjs.com/package/blitflow) — a single self-contained bundle, no runtime dependencies. Requires Node.js 20+. ```bash npm install -g blitflow # or run it without installing: bunx blitflow --help npx blitflow --help ``` ## First use [#first-use] Sign in once, then run something: ```bash # opens a code to approve in the browser; the token is saved locally blitflow login # run a single node — progress on stderr, results on stdout blitflow node rd-fast -i prompt="a brass key" ``` ## Commands [#commands] ``` blitflow login Sign in via the browser (OAuth device flow) blitflow logout Remove the saved token blitflow whoami Show the signed-in user blitflow nodes [query] List or search the node palette blitflow node [--input k=v]... Run a single node (no workflow) blitflow run [--input k=v]... Run a workflow blitflow mcp Serve the BlitFlow MCP server over stdio ``` `run` takes either a local YAML file or a reference to a published workflow: the published `/` address (preferred, e.g. `acme/sprite-pack@1.2.0`) or the workflow's internal UUID, optionally `@` — see [Versioning](/docs/concepts/versioning). The CLI disambiguates by a simple rule: **if the argument exists as a file on disk it is a file (a directory does not count); otherwise, anything that parses as a workflow ref is a ref.** | Option | Description | | ----------------- | ------------------------------------------------------------- | | `-i, --input k=v` | Input value (repeatable). Use `@` for a file/image input | | `--json` | Machine-readable output on stdout | | `--url U` | API base URL (else `BLITFLOW_URL`, else saved, else default) | ## Examples [#examples] ```bash # sign in once — saves a session token to ~/.blitflow/config.json blitflow login # explore the palette blitflow nodes sprite # run one node blitflow node rd-fast \ -i prompt="a brass key" -i removeBg=true -i seed=7 # run a published workflow by its address blitflow run acme/sprite-pack@2.1.0 \ -i prompt="isometric stone tower" # the internal UUID form works too blitflow run 8f61e451-40a7-4f65-8fa9-69704576b6d4@2.1.0 -i prompt="a brass key" # run a local YAML workflow definition blitflow run flow.yaml -i photo=@https://example.com/tower.jpg ``` ## Inputs [#inputs] * Plain values are scalars — numbers and booleans are coerced (`seed=7`, `removeBg=true`). * For `run`, values become inline artifacts typed by the workflow's declared input connector. * `@https://…/x.png` becomes a **ref artifact**, kind inferred from the file extension. YAML workflow files use the same shape as the JSON [workflow definition](/docs/concepts/workflows). ## Configuration [#configuration] The token from `blitflow login` lives in `~/.blitflow/config.json`. For CI or headless use, set `BLITFLOW_TOKEN` (a [personal access token](/docs/authentication)) instead of logging in; `BLITFLOW_URL` overrides the API base URL. # Introduction (/docs/introduction) BlitFlow runs AI workflows: graphs of nodes (image models, LLMs, media utilities) that turn inputs into generated outputs. You can design workflows visually in the Studio editor, or build them dynamically in code — and run either kind programmatically. ## Three ways in, one contract [#three-ways-in-one-contract] Every operation is defined once in the BlitFlow **API contract**. The HTTP API, the TypeScript SDK, and the MCP server are all derived from the same contract, so they expose the same five operations with identical inputs and outputs: | Operation | HTTP | SDK | MCP tool | | --------------- | ----------------------- | -------------------------------------- | --------------- | | `nodes.list` | `GET /v1/nodes` | `client.searchNodes(…)` | `nodes_list` | | `nodes.get` | `GET /v1/nodes/:id` | `client.getNode(id)` | `nodes_get` | | `runs.create` | `POST /v1/runs` | `client.run(…)` / `client.startRun(…)` | `runs_create` | | `workflows.get` | `GET /v1/workflows/:id` | `client.getWorkflow(ref)` | `workflows_get` | | `runs.node` | `POST /v1/runs/node` | `client.runNode(…)` | `runs_node` | Pick the surface that fits: | Surface | You are… | Reach for it when | | ------------------------- | ---------------------------------------- | --------------------------------------------- | | **HTTP API** | any language, any runtime | you want plain REST + SSE with a bearer token | | **SDK** (`@blitflow/sdk`) | a TypeScript app | you're building a product on top of BlitFlow | | **MCP** | an AI agent / assistant (Claude, Cursor) | you want a model to call BlitFlow as a tool | | **CLI** (`blitflow`) | a person in a terminal / CI | you're scripting or experimenting | Prefer learning by task? The [Guides](/docs/guides/browse-nodes) show the same task on every surface, side by side. ## Two ways to get a workflow [#two-ways-to-get-a-workflow] 1. **Reference a published workflow.** Design it in the Studio editor, publish a version, and run it by reference — `@` such as `abc123@2.1.0` or `abc123@latest`. See [Versioning](/docs/concepts/versioning). 2. **Send an inline definition.** Build the workflow JSON in code (by hand or with the [SDK builder](/docs/sdk/building-workflows)) and pass it directly to a run — no publishing step required. ## Where to start [#where-to-start] ## For AI agents [#for-ai-agents] These docs are machine-readable: append `.md` to any page URL for clean Markdown, or fetch [`/llms.txt`](/llms.txt) (an index of every page) and [`/llms-full.txt`](/llms-full.txt) (the full text of all pages). # Quickstart (/docs/quickstart) ## Create an access token [#create-an-access-token] In the Studio, open **Settings → API tokens** and create a personal access token. It looks like `blit_…` and is **shown once** — store it somewhere safe (an environment variable, a secret manager). ```bash export BLITFLOW_TOKEN="blit_..." ``` All authenticated calls send it as a bearer token: `Authorization: Bearer $BLITFLOW_TOKEN`. See [Authentication](/docs/authentication) for the details (and for `blitflow login`, the browser-based alternative for your dev machine). ## Explore the node palette [#explore-the-node-palette] Nodes are the units of work: image models, LLMs, media utilities. Search the palette to find what you need: ```bash curl "https://studio.blitflow.com/api/v1/nodes?q=image&limit=5" \ -H "Authorization: Bearer $BLITFLOW_TOKEN" ``` ```ts import { BlitClient } from "@blitflow/sdk"; const client = new BlitClient({ apiKey: process.env.BLITFLOW_TOKEN }); const specs = await client.searchNodes({ query: "image", limit: 5 }); ``` Each result is a **node spec** listing the node's exact input and output ports, including defaults and allowed enum values. Always read the spec before calling a node — enum values are not guessable, and a wrong value fails validation. See [Nodes & specs](/docs/concepts/nodes). ## Run a workflow [#run-a-workflow] A run takes either an **inline workflow definition** or a **reference to a published workflow** (`@`), plus input values. The response is a stream of events, ending in `run.completed` with the outputs. ```ts import { BlitClient } from "@blitflow/sdk"; const client = new BlitClient({ apiKey: process.env.BLITFLOW_TOKEN }); // Run a workflow you published from the Studio editor: // (or "abc123@2.1.0", or an inline Workflow object) const outputs = await client.run("abc123@latest", { prompt: { value: "a brass key on black velvet" }, }); ``` `run()` streams the events for you and resolves with the final outputs (or throws if the run fails). Use `startRun()` instead if you want to observe per-step progress — see [Running workflows](/docs/sdk/running-workflows). ```bash curl -N -X POST "https://studio.blitflow.com/api/v1/runs" \ -H "Authorization: Bearer $BLITFLOW_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "workflowRef": "abc123@latest", "inputs": { "prompt": { "value": "a brass key on black velvet" } } }' ``` The `-N` flag matters: the response is a **Server-Sent Events stream**, not a single JSON body. Read `data:` lines until `run.completed` (or `run.failed`); the stream ends with `data: [DONE]`. See [POST /v1/runs](/docs/api/runs). ## Read the outputs [#read-the-outputs] Outputs are [Artifacts](/docs/concepts/artifacts). Small values (text, numbers) arrive **inline**; binary results (images, audio, video) arrive as a **ref** — a URL you fetch to get the bytes: ```json { "image": { "kind": "image", "ref": "https://…/output.png", "mimeType": "image/png" } } ``` ```ts const artifact = outputs.image; if ("ref" in artifact) { const bytes = await fetch(artifact.ref).then((r) => r.arrayBuffer()); } ``` ## Next steps [#next-steps] # Artifacts (/docs/api/artifacts) Run inputs are **Artifact-only**: binary media crosses the run boundary as a `{ kind, ref }` [Artifact](/docs/concepts/artifacts), never as inline bytes, base64, or a `data:` URI. These endpoints turn media you have — a fetchable URL, or bytes on disk — into an organization-owned, first-party Artifact you pass unchanged to `POST /v1/runs` or `POST /v1/runs/node`. Two paths: * **Remote import** — you have a public URL; blitflow fetches it server-side. * **Signed upload** — you have local bytes; you upload them directly to storage with a short-lived, single-pathname authorization. The bytes never transit blitflow's API servers. ## Import a remote URL [#import-a-remote-url] Fetch a public http(s) URL server-side and copy it into first-party storage as an owned Artifact. ### Request body [#request-body] | Field | Type | Description | | ----------- | ------ | ------------------------------------------------------------------------------------- | | `url` | string | The http(s) URL to import. Must be publicly reachable | | `org` | string | Organization slug; omit for your personal organization | | `kind` | string | Override the kind derived from the content type: `image`, `audio`, `video`, or `file` | | `retention` | string | `temporary` (default) or `durable` — see [retention](#limits-and-retention) | Private and internal destinations (localhost, private IP ranges, cloud metadata endpoints) are rejected, including on redirects. The same size cap and content-type allowlist as direct uploads apply. ```bash curl -X POST "https://studio.blitflow.com/api/v1/artifacts/import" \ -H "Authorization: Bearer $BLITFLOW_TOKEN" \ -H "Content-Type: application/json" \ -d '{"url": "https://example.com/tower.jpg"}' ``` ### Response [#response] ```json { "id": "9c2b1e1a-…", "artifact": { "kind": "image", "ref": "https://…/blitflow/uploads/2026-08/1f9d….jpg", "mimeType": "image/jpeg" }, "contentType": "image/jpeg", "sizeBytes": 482133, "expiresAt": "2026-09-17T12:00:00.000Z" } ``` Pass `artifact` **unchanged** as a run input: ```json { "inputs": { "photo": { "kind": "image", "ref": "https://…/1f9d….jpg" } } } ``` ### Errors [#errors] | Status | Cause | | ------ | ------------------------------------------------------------------------- | | `400` | Unreachable URL, blocked (private/internal) destination, or fetch failure | | `413` | Remote file over the 50 MB cap | | `415` | Content type outside the allowlist | ## Upload local bytes [#upload-local-bytes] Local bytes use a two-step signed upload: **prepare** an authorization, PUT the bytes **directly to storage**, then **complete** to record the Artifact. ### Prepare [#prepare] Create an upload intent and mint a short-lived token constrained to one randomized pathname, the declared content type, and the declared size. | Field | Type | Description | | ------------- | ------- | ------------------------------------------------------------------------------------- | | `contentType` | string | MIME type of the bytes (see [allowlist](#limits-and-retention)) | | `sizeBytes` | integer | Size of the bytes; the upload is rejected beyond it. Max 52428800 (50 MB) | | `org` | string | Organization slug; omit for your personal organization | | `kind` | string | Override the kind derived from the content type: `image`, `audio`, `video`, or `file` | | `retention` | string | `temporary` (default) or `durable` | ```json { "id": "5d0a7c22-…", "pathname": "blitflow/uploads/2026-08/8ba3….png", "uploadUrl": "https://vercel.com/api/blob/blitflow/uploads/2026-08/8ba3….png", "token": "vercel_blob_client_…", "contentType": "image/png", "maxSizeBytes": 482133, "tokenExpiresAt": "2026-08-18T13:00:00.000Z", "expiresAt": "2026-08-18T14:00:00.000Z" } ``` The token authorizes **exactly one pathname** with the declared content type and size cap, and expires at `tokenExpiresAt` (1 hour). The intent itself expires at `expiresAt` (2 hours): complete before then or the intent — and any uploaded bytes — is discarded. ### Upload the bytes [#upload-the-bytes] Directly to storage — not to blitflow's API. With `@vercel/blob`: ```ts import { put } from "@vercel/blob/client"; await put(prepared.pathname, bytes, { access: "public", token: prepared.token, }); ``` Or raw HTTP: ```bash curl -X PUT "$UPLOAD_URL" \ -H "Authorization: Bearer $UPLOAD_TOKEN" \ -H "x-content-type: image/png" \ -H "x-vercel-blob-access: public" \ --data-binary @./tower.png ``` ### Complete [#complete] Verify the uploaded blob against the intent and record the owned Artifact. Verifies the blob at the prepared pathname — existence, size within the declared cap, matching content type, your organization — then records exactly one artifact ledger row and returns the same shape as `/v1/artifacts/import`. **Idempotent:** completing the same intent again (a retry, a race) returns the same Artifact. | Status | Cause | | ------ | ----------------------------------------------------------------- | | `400` | Nothing uploaded at the prepared pathname yet | | `404` | Unknown intent, or an intent belonging to another organization | | `409` | Uploaded blob doesn't match the declared size cap or content type | | `410` | Intent expired before completion | ## Limits and retention [#limits-and-retention] | Limit | Value | | ------------- | ------------------------------------------------------------------------------ | | Max size | 50 MB per artifact (uploads and imports) | | Content types | `image/*`, `audio/*`, `video/*`, `application/pdf`, `application/octet-stream` | Retention follows the `retention` field: * **`temporary`** (default) — the artifact expires 30 days after creation, the same window as run outputs. Right for one-off run inputs. * **`durable`** — never expires; for workflow Library and recipe assets. A temporary artifact referenced by a workflow graph is promoted to durable instead of expiring — the same [part-of-the-recipe rule](/docs/concepts/artifacts#storage-and-retention) that protects run outputs. The hosted MCP server exposes these operations as `artifacts_import`, `artifacts_uploads_prepare`, and `artifacts_uploads_complete`. No tool accepts base64, byte arrays, `data:` URIs, or local file paths — models must never serialize file bytes into tool arguments. # API overview (/docs/api) The HTTP API exposes the BlitFlow contract over plain REST + Server-Sent Events. Use it from any language; the [TypeScript SDK](/docs/sdk) and [MCP server](/docs/mcp) are typed wrappers over exactly these operations. ## Base URL & auth [#base-url--auth] ``` https://studio.blitflow.com/api ``` All endpoints require a bearer token: ```bash curl "https://studio.blitflow.com/api/v1/nodes" \ -H "Authorization: Bearer $BLITFLOW_TOKEN" ``` See [Authentication](/docs/authentication) for token types. When calling a Studio deployment directly, the API lives under the `/api` prefix (`https:///api/v1/…`). ## Operations [#operations] | Operation | Method & path | Scope | Response | | -------------------------------------- | ----------------------- | ------ | ------------------------------ | | [`nodes.list`](/docs/api/nodes) | `GET /v1/nodes` | `read` | `NodeSpec[]` | | [`nodes.get`](/docs/api/nodes) | `GET /v1/nodes/:id` | `read` | `NodeSpec` | | [`workflows.get`](/docs/api/workflows) | `GET /v1/workflows/:id` | `read` | Published version + definition | | [`runs.create`](/docs/api/runs) | `POST /v1/runs` | `run` | **SSE stream** of run events | | [`runs.node`](/docs/api/runs) | `POST /v1/runs/node` | `run` | `{ outputs }` | ## Conventions [#conventions] * Request and response bodies are JSON (`Content-Type: application/json`), except `runs.create`, which responds with `text/event-stream`. * `GET` operations take parameters as query strings; `POST` operations take a JSON body. * Inputs are validated against the contract schemas — unknown fields are rejected, not ignored. ## Errors [#errors] Errors are JSON with a human-readable message: ```json { "error": "workflow abc123@9.9.9 not found" } ``` | Status | Meaning | | ------ | ------------------------------------------------------------------------------------------------------------------ | | `400` | Invalid JSON, failed schema validation, bad version selector, or both/neither of `workflow`/`workflowRef` provided | | `401` | Missing, invalid, expired, or revoked token | | `403` | Cross-origin cookie-authenticated mutation (use a bearer token) | | `404` | Workflow, version, or node not found | For streaming runs, failures **after the stream starts** arrive as a `run.failed` event on the stream, not as an HTTP error status. # Nodes (/docs/api/nodes) ## List or search nodes [#list-or-search-nodes] ``` GET /v1/nodes ``` Returns the node palette as an array of [node specs](/docs/concepts/nodes). The node catalog is **public** — `GET /v1/nodes` and `GET /v1/nodes/:id` work without a token. A spec is the published interface of a node and contains nothing caller-specific, which is what lets [blitflow.com/models](https://blitflow.com/models) browse it. Every other endpoint still requires a credential. ### Query parameters [#query-parameters] | Parameter | Type | Description | | ---------- | ------- | -------------------------------------------------------------- | | `q` | string | Case-insensitive match against node id, title, and description | | `category` | string | Filter by category (e.g. `image`) | | `limit` | integer | Maximum number of results | ### Example [#example] ```bash curl "https://studio.blitflow.com/api/v1/nodes?q=sprite&category=image&limit=3" ``` ```json [ { "id": "rd-fast", "kind": "model", "title": "RD Fast", "category": "image", "description": "Fast image generation…", "inputs": { "prompt": { "name": "prompt", "connector": { "id": "TEXT", "isOptional": false, "isArray": false } } }, "outputs": { "image": { "name": "image", "connector": { "id": "IMAGE", "isOptional": false, "isArray": false } } } } ] ``` ## Get one node [#get-one-node] ``` GET /v1/nodes/:id ``` Fetches a single spec by id (e.g. `rd-fast`). The response is one `NodeSpec` object with the full `inputs`/`outputs` port map — including `defaultValue`, `optionValues` (enums), and `minValue`/`maxValue` constraints. This operation is currently served through the [MCP tools](/docs/mcp/tools) and in-process clients. Over plain HTTP, fetch the palette with `GET /v1/nodes?q=` and match on `id`. Build node inputs from the spec you just fetched — never from memory. A value outside a port's `optionValues` fails validation. See [Nodes & specs](/docs/concepts/nodes). # Runs (/docs/api/runs) ## Run a workflow [#run-a-workflow] Start a workflow run and stream its events back as Server-Sent Events. Runs a workflow and streams [run events](/docs/concepts/runs) back as **Server-Sent Events**. ### Request body [#request-body] | Field | Type | Description | | ------------- | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `workflow` | object | Inline [workflow definition](/docs/concepts/workflows) — for one-off runs | | `workflowRef` | string | `@` reference to a published workflow — the id is the published `/` address (preferred, e.g. `acme/sprite-pack@2.1.0`) or the workflow's UUID (e.g. `8f61e451-…@2.1.0`); omit the version or use `@latest` for the current one | | `inputs` | object | Run inputs — [Artifacts](/docs/concepts/artifacts) keyed by the workflow's input names. `kind` is optional; the input node declares it | Provide **exactly one** of `workflow` or `workflowRef` — both or neither is a `400`. ### Response: SSE stream [#response-sse-stream] `Content-Type: text/event-stream`. Each event is a `data:` line of JSON; the stream ends with `data: [DONE]`: ```bash curl -N -X POST "https://studio.blitflow.com/api/v1/runs" \ -H "Authorization: Bearer $BLITFLOW_TOKEN" \ -H "Content-Type: application/json" \ -d '{"workflowRef": "acme/sprite-pack@latest", "inputs": {"prompt": {"value": "isometric stone tower"}}}' ``` ``` data: {"type":"step.started","nodeId":"gen"} data: {"type":"step.completed","nodeId":"gen","usage":{"costUsd":0.012,"ms":4183}} data: {"type":"run.completed","outputs":{"image":{"kind":"image","ref":"https://…/out.png","mimeType":"image/png"}}} data: [DONE] ``` Keep the connection open (`curl -N`) and read to the terminal event: `run.completed` carries the outputs; `run.failed` carries `{ error: { code, message } }`. Failures after the stream starts arrive as events, not HTTP status codes. ### Errors (before the stream starts) [#errors-before-the-stream-starts] | Status | Cause | | ------ | ------------------------------------------------------------------------------------------------------------ | | `400` | Invalid JSON, schema violation, bad `workflowRef`, or not exactly one of `workflow`/`workflowRef` | | `401` | Bad token | | `402` | The organization's balance is not positive — `{ code: "insufficient_credit", balanceUsd }`; add funds to run | | `404` | `workflowRef` doesn't resolve to a published version | ## Get a run (with its outputs) [#get-a-run-with-its-outputs] ``` GET /v1/runs/:id ``` Fetches one run by id: status, per-node steps (duration, cost, exact error for failed nodes), and — for completed runs — `outputs`, the same `name → Artifact` record the `run.completed` event carried. Outputs are **persisted at completion**, so a caller who lost the SSE stream (or polls later) can still retrieve exactly what the run produced. Run ids come from the `run.started` event, or from `GET /v1/runs` (run history). ```json { "id": "8f61e451-…", "status": "completed", "steps": [{ "nodeId": "gen", "status": "completed", "costUsd": 0.012 }], "outputs": { "image": { "kind": "image", "ref": "https://…/out.png" } } } ``` `outputs` is `null` while the run is pending/running and for failed runs. Output refs are BlitFlow-hosted blob URLs; after the artifact retention window a ref's blob may no longer resolve even though the envelope is still recorded. ## Run a single node [#run-a-single-node] ``` POST /v1/runs/node ``` Executes one node by id — no workflow graph needed. Ideal for one-off inference. Supports `model` and `llm` nodes; use `POST /v1/runs` for graphs. ### Request body [#request-body-1] | Field | Type | Description | | -------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | | `node` | string | Node id, e.g. `rd-fast` | | `inputs` | object | Keyed by the node's input port names. Scalars (`string`, `number`, `boolean`, `null`) for value ports; [Artifacts](/docs/concepts/artifacts) for data ports | ### Response [#response] A single JSON body (no stream): ```json { "outputs": { "image": { "kind": "image", "ref": "https://…/key.png", "mimeType": "image/png" } } } ``` `runs.node` is currently served through the [MCP tools](/docs/mcp/tools) and the SDK's in-process clients. Over plain HTTP, wrap the node in a minimal one-node workflow and use `POST /v1/runs`. ### Example (SDK) [#example-sdk] ```ts const { outputs } = await client.ops["runs.node"]({ node: "rd-fast", inputs: { prompt: "a brass key", removeBg: true, seed: 7 }, }); ``` # Workflows (/docs/api/workflows) ## Get a published workflow [#get-a-published-workflow] ``` GET /v1/workflows/:id ``` Fetches the canonical definition of a published version. `:id` is either the workflow's published `/` address (preferred, e.g. `acme/sprite-pack`) or its internal UUID. When using the address in a URL path, encode the `/` as `%2F`. The version can be given inline in the path segment using the same `@` notation the SDK and MCP clients use — e.g. `/v1/workflows/acme%2Fsprite-pack@2.1.0` — or as the `version` query parameter. Giving it **both** inline and as `?version=` is a `400`. ### Query parameters [#query-parameters] | Parameter | Type | Description | | --------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------- | | `version` | string | `latest` (default — follows the current pointer, including rollbacks), a full semver like `2.1.0`, or a bare major like `2` (its highest version) | This is the query-string form of the `@` notation — see [Versioning](/docs/concepts/versioning). ### Example [#example] ```bash # by published address (the "/" is URL-encoded) curl "https://studio.blitflow.com/api/v1/workflows/acme%2Fsprite-pack?version=2.1.0" \ -H "Authorization: Bearer $BLITFLOW_TOKEN" # same thing with the version inline in the path curl "https://studio.blitflow.com/api/v1/workflows/acme%2Fsprite-pack@2.1.0" \ -H "Authorization: Bearer $BLITFLOW_TOKEN" # or by internal UUID curl "https://studio.blitflow.com/api/v1/workflows/8f61e451-40a7-4f65-8fa9-69704576b6d4?version=2.1.0" \ -H "Authorization: Bearer $BLITFLOW_TOKEN" ``` ```json { "id": "8f61e451-40a7-4f65-8fa9-69704576b6d4", "address": "acme/sprite-pack", "version": "2.1.0", "sequence": 7, "workflow": { "version": 2, "nodes": [ { "id": "prompt", "uses": "input", "outputType": "TEXT" }, { "id": "gen", "uses": "rd-fast", "inputs": { "prompt": "${prompt.value}" } }, { "id": "image", "uses": "output", "inputs": { "value": "${gen.image}" } } ] } } ``` | Field | Description | | ---------- | ------------------------------------------------------------------------------- | | `id` | Workflow id (UUID) | | `address` | Published `/` address; `null` if no slug is claimed | | `version` | The resolved full semver | | `sequence` | Monotonic publish counter — increases with every publish, independent of semver | | `workflow` | The immutable [workflow definition](/docs/concepts/workflows) | ### Errors [#errors] | Status | Cause | | ------ | -------------------------------------------------------------------------- | | `400` | Invalid selector (e.g. `version=1.2` — partial versions are rejected) | | `400` | Version given twice — inline in the path (`@2.1.0`) **and** as `?version=` | | `404` | Unknown workflow id/address, or no published version matches the selector | Publishing, rollback, and version history are Studio actions — design and publish in the editor, then consume the published versions from code. # Artifacts (/docs/concepts/artifacts) Every run input and output is an **Artifact**: a small envelope that pairs a `kind` with either an inline value or a URL reference. ## The two forms [#the-two-forms] Binary data — images, audio, video, files — travels by reference. `ref` is a URL; **fetch it to get the bytes**: ```json { "kind": "image", "ref": "https://…/output.png", "mimeType": "image/png", "metadata": { "width": 1024, "height": 1024 } } ``` `mimeType` and `metadata` are optional. Small scalars, text, and structured data travel inline: ```json { "kind": "text", "value": "a brass key on black velvet" } ``` ```json { "kind": "structured", "value": { "tags": ["key", "brass"] } } ``` ## Kinds [#kinds] `image`, `text`, `audio`, `video`, `float`, `int`, `boolean`, `embedding`, `file`, `structured` — matching the workflow input connector types. ## Passing artifacts as inputs [#passing-artifacts-as-inputs] Workflow run inputs (`inputs` on `POST /v1/runs`) are artifacts keyed by the workflow's declared input names. **`kind` is optional on the way in** — the workflow's input node (or, for `runs.node`, the node's port) already declares the type, and the server fills it in: ```json { "inputs": { "prompt": { "value": "isometric stone tower" }, "photo": { "ref": "https://example.com/tower.jpg" } } } ``` Sending `kind` explicitly is still accepted, and every artifact you get **back** carries one — outputs are always complete envelopes. To feed your own media into a node (e.g. an `init` port for image-to-image), first turn it into an **owned artifact** — [import a remote URL](/docs/api/artifacts) server-side, or upload local bytes with the signed-upload flow — and pass the returned ref artifact unchanged. In the studio and on a model page, every image/audio/video/file input runs that same signed upload for you: drop a file on the field, or pick one with its upload button, and the field fills with the resulting ref. Single-node runs (`runs.node`) are more lenient: value ports accept bare scalars (`"a prompt"`, `7`, `true`) and data ports take artifacts. A code node accepts FILE, IMAGE, AUDIO, or VIDEO only from a compatible BlitFlow Artifact reference in the workflow graph. It does not accept an external URL, `data:` URI, base64 value, or descriptor literal. See [Binary artifacts in code nodes](/docs/sdk/code-nodes#binary-artifacts). Don't try to read image bytes out of an artifact's `value` — binary outputs are always **ref** artifacts. Download the `ref` URL, and persist the bytes yourself if you need them beyond the 30-day retention window below. ## Storage and retention [#storage-and-retention] Node outputs are served from **blitflow's own storage**: before a run records a step result, any output the model provider returned is copied to blitflow blob storage and the artifact's `ref` is rewritten to that URL. You never receive a short-lived provider URL — output refs stay fetchable after the provider's own links have expired. Two retention rules apply: * **Run artifacts expire after 30 days.** Outputs produced by a run are retained for 30 days from creation, then deleted. Download anything you want to keep longer. * **Temporary uploads and imports expire after 30 days.** Media you bring in via [`/v1/artifacts/import` or the signed-upload flow](/docs/api/artifacts) defaults to the same window (`expiresAt` on the response says exactly when); pass `retention: "durable"` for assets that should live forever. * **Recipe assets don't expire.** An asset that is part of a workflow's definition — e.g. an image set as a default value in the graph — lives as long as the workflow does, even if it started life as a run output or a temporary upload (a graph reference promotes it to durable). # Community (/docs/concepts/community) A workflow is private to its organization until you list it in the **community**. Listing one makes its published versions readable by anyone — signed in or not — at its permanent address, and lets other people **fork** it into an organization of their own. ## What "public" means [#what-public-means] Visibility is a property of the workflow, and it applies only to what you have already [published](/docs/concepts/versioning). Two consequences follow: * **You must publish a version before you can list a workflow.** The community reads immutable `workflowVersion` snapshots, so a workflow with nothing published has nothing to show. * **Your working copy stays private.** The editor's draft — the thing autosave and the workflow agent write to — is never served publicly. Visitors see the version you deliberately pinned, and nothing you have changed since. Nothing else about your organization becomes visible: runs, costs, comments, members, balances, and API keys are all unaffected. What *is* visible is the full graph of every published version, including node settings, prompts, and constant values. Treat a listed workflow as published source: don't leave anything in a `const` node you wouldn't put in a README. ## Listing and withdrawing [#listing-and-withdrawing] In the Studio editor, open **Share** in the left rail, add a short description, and choose **Publish to community**. The workflow then appears in the Studio's **Community** section and at its public page: ``` https://studio.blitflow.com/w// ``` That URL is the workflow's [published address](/docs/concepts/versioning), so it is permanent. A `?v=` query pins the page to one version — `?v=1.2.0` for an exact semver, `?v=1` for that major's highest — using the same selectors as the API. Without it the page follows `@latest`, including after a rollback. **Make private** takes the listing and the public page offline again. Existing forks are independent copies and keep working; the slug stays claimed, so re-listing later restores the same address. ## Inspecting [#inspecting] The public page renders the published graph read-only: pan and zoom the canvas, switch versions, and read the workflow's derived interface — its inputs, their types and which are required, and its named outputs. This is the same interface the API exposes, so it tells you exactly what you would be calling. You cannot run a workflow from its public page. A run spends an organization's balance, so running one means forking it into an organization of yours first. ## Forking [#forking] **Fork** copies the version you are looking at into an organization you choose: * The copy is seeded from the **pinned published version**, not the original's working copy. * It starts as an ordinary private draft — no claimed slug, no version history, not listed. Published names belong to the organization that claimed them, so your fork claims its own on its first publish. * It records where it came from. The editor shows the source address beside the workflow name, and the original's public page counts its forks. From there it is your workflow: edit it, run it, and publish it under your own name. The original is untouched, and later versions of it do not flow into your fork. You need an account to fork, and write access (`admin` or `member`) in the organization you fork into. # Nodes & specs (/docs/concepts/nodes) Nodes are the executable units of a workflow: hosted models, LLMs, media utilities. The **node palette** is the catalog of everything you can run, and each entry is described by a **node spec**. In a workflow, a node's input ports are fed by other nodes (or by workflow inputs) and its output ports feed the next node: You can also [run a node on its own](/docs/guides/run-a-node) — same ports, no graph. ## Node specs [#node-specs] A spec describes a node's exact interface — its identity (`id`, `kind`, display metadata) and its `inputs` / `outputs` ports: ```json { "id": "rd-fast", "kind": "model", "title": "RD Fast", "category": "image", "description": "Fast image generation…", "inputs": { "prompt": { "name": "prompt", "connector": { "id": "TEXT", "isOptional": false, "isArray": false } }, "style": { "name": "style", "connector": { "id": "TEXT", "isOptional": true, "isArray": false, "defaultValue": "game_asset", "optionValues": ["game_asset", "character_turnaround", "item_sheet"] } } }, "outputs": { "image": { "name": "image", "connector": { "id": "IMAGE", "isOptional": false, "isArray": false } } } } ``` The same spec, rendered as the interface a caller programs against: Each port's `connector` carries the type and constraints: | Connector field | Meaning | | ----------------------- | ------------------------------------------------------------------------------------------------------------------- | | `id` | `IMAGE`, `TEXT`, `AUDIO`, `VIDEO`, `FLOAT`, `INT`, `BOOLEAN`, `EMBEDDING`, `FILE`, `STRUCTURED`, or `CUSTOM:` | | `isOptional` | Whether the port may be omitted | | `isArray` | Whether the port takes a list | | `defaultValue` | Default used when the port is omitted | | `optionValues` | Allowed enum values, when constrained | | `minValue` / `maxValue` | Numeric bounds, when constrained | ## Discover before you call [#discover-before-you-call] **Never guess inputs or enum values.** Fetch the node's spec first and build your inputs from its `inputs` ports. Enum names (`style`, modes, sizes) are not guessable, and a value outside `optionValues` fails validation before anything runs. The palette is searchable — filter by free text and category, then inspect the candidate: ```bash # search curl "https://studio.blitflow.com/api/v1/nodes?q=sprite&category=image" \ -H "Authorization: Bearer $BLITFLOW_TOKEN" ``` Via the SDK use `searchNodes` / `getNode` ([SDK: nodes](/docs/sdk/nodes)); via MCP use the `nodes_list` / `nodes_get` tools ([MCP tools](/docs/mcp/tools)). ## Practical tips [#practical-tips] * **Reproducibility** — if a node exposes a `seed` input, set it. Same seed + same inputs → same output; hold the seed while iterating a prompt. * **Respect locked modes** — some model modes fix output dimensions or formats. Read the spec's defaults and bounds instead of fighting them. * **Prefer the cheapest node that clears the bar** — step up to a higher-fidelity (more expensive) node only when quality actually demands it. # Runs & streaming (/docs/concepts/runs) Starting a run executes a workflow graph. Because runs can take a while (model inference, multi-step graphs), the API **streams progress events** instead of blocking on a single response. ## Run events [#run-events] A run emits a stream of events, a discriminated union on `type`: | Event | Payload | Meaning | | ---------------- | ------------------------------------ | ----------------------------- | | `step.started` | `nodeId` | A node began executing | | `step.progress` | `nodeId`, `message` | Progress within a node | | `step.completed` | `nodeId`, `usage: { costUsd, ms }` | Node finished, with cost/time | | `step.failed` | `nodeId`, `error: { code, message }` | Node failed | | `run.completed` | `outputs: Record` | **Terminal** — the results | | `run.failed` | `error: { code, message }` | **Terminal** — the run failed | Read the stream to completion and collect `run.completed.outputs`. Treat `run.failed` and `step.failed` as terminal: surface the error message — don't retry blindly. Over HTTP the stream is [Server-Sent Events](/docs/api/runs); the SDK parses it into an async iterable for you: ```ts const handle = await client.startRun("abc123@latest", inputs); for await (const event of handle.events()) { if (event.type === "step.completed") { console.log( `${event.nodeId}: $${event.usage.costUsd} in ${event.usage.ms}ms`, ); } if (event.type === "run.completed") return event.outputs; if (event.type === "run.failed") throw new Error(event.error.message); } ``` ## Cost control [#cost-control] Runs spend money (model inference), charged to your organization's prepaid balance. A run is admitted while the balance is positive, runs to completion, and is charged its metered cost when it ends. There is no per-run spend cap: a run is never failed for what it cost, so an expensive step never leaves you paying for a run that returned nothing. A run that exhausts the balance still completes; the next run is refused with `402 insufficient_credit` until you add funds in the Studio's billing settings. Practical habits: * Know what a run will cost before you start it: `runs.list` and `runs.get` report `costUsd` per run and per step, so comparable past runs are the estimate. * Generate at the smallest useful size / fewest variations; scale up winners. * Persist output `ref` URLs and don't regenerate assets you already have. * Watch `step.completed.usage.costUsd` to learn what each node actually costs. ## Single-node runs [#single-node-runs] For one-off inference you don't need a workflow at all — `runs.node` executes a single node by id with plain inputs and returns its outputs directly (no stream). Available through the [SDK](/docs/sdk/nodes) and the [`runs_node` MCP tool](/docs/mcp/tools). # Versioning (/docs/concepts/versioning) Publishing a workflow from the Studio editor creates an **immutable version**: a frozen snapshot of the definition, addressed by semver. Your integrations run against these versions — the editor's draft state never changes a published version underneath you. ## Version references [#version-references] Everywhere a workflow reference is accepted (`workflowRef` on runs, the SDK's `run`/`getWorkflow`, MCP tools, the CLI's `run`), the notation is `@`. The id half takes two forms: * **Published address** (preferred): `/`, e.g. `acme/sprite-pack` — see [Published addresses](#published-addresses) below. * **Workflow UUID** (internal form, still accepted everywhere), e.g. `8f61e451-40a7-4f65-8fa9-69704576b6d4`. | Reference | Resolves to | | ------------------------- | ------------------------------------------ | | `acme/sprite-pack` | Latest version (implicit `@latest`) | | `acme/sprite-pack@latest` | The current **latest pointer** (see below) | | `acme/sprite-pack@2.1.0` | Exactly version `2.1.0` | | `acme/sprite-pack@2` | The highest published `2.x.x` | The same selectors work with the UUID form (`8f61e451-40a7-4f65-8fa9-69704576b6d4@2.1.0`). Partial versions like `@2.5` are **rejected** as ambiguous — use a full semver (`@2.5.0`) or a bare major (`@2`). Over raw HTTP the same selectors appear as a query parameter: `GET /v1/workflows/acme%2Fsprite-pack?version=2.1.0` — the address's `/` is URL-encoded in the path (see [the endpoint reference](/docs/api/workflows)). ## Published addresses [#published-addresses] A workflow's address is `/`, both in kebab-case (lowercase letters, digits, `-`). The **first publish claims the slug**, and from then on the address permanently identifies that workflow: * Renaming the workflow's display name does not change the slug. * Deleting a published workflow retires its name forever — the slug is tombstoned and never re-registered, so an old address can never start resolving to a different workflow. * The organization's own slug is likewise frozen once it namespaces any published workflow. * Platform namespaces (`blitflow`, `core`, and a few others) are reserved and can never be org slugs. The address resolves wherever a workflow ref is accepted — the HTTP API, the SDK, the MCP tools, and the CLI. The UUID remains the internal id (it appears in run details and observability) and keeps working as a ref forever. ## Semver and the latest pointer [#semver-and-the-latest-pointer] * Versions are `MAJOR.MINOR.PATCH` (no prerelease/build tags). When publishing, you choose which component to bump. * **Versions are immutable**; the definition behind `2.1.0` never changes. * **`@latest` is a mutable pointer.** It normally tracks the newest publish, but can be **rolled back** in the Studio to point at an earlier version — without deleting anything. New version numbers always continue from the highest ever published, so rollbacks never cause collisions. * Each response from `workflows.get` also carries a monotonically increasing `sequence` number, useful for detecting "did anything change" regardless of semver. ## Choosing a selector [#choosing-a-selector] | Use case | Recommended ref | | --------------------------------------- | -------------------------------- | | Production integration, no surprises | Exact — `acme/sprite-pack@2.1.0` | | Track compatible updates within a major | Major — `acme/sprite-pack@2` | | Internal tools, always newest | `acme/sprite-pack@latest` | Pin exact versions in production: `@latest` and `@2` both move when new versions are published (or `@latest` is rolled back). ## Fetching a published definition [#fetching-a-published-definition] You can fetch the canonical JSON of any published version — useful for auditing, diffing, or running it locally as an inline workflow: ```ts import { BlitClient } from "@blitflow/sdk"; const client = new BlitClient({ apiKey: process.env.BLITFLOW_TOKEN }); const { id, version, sequence, workflow } = await client.getWorkflow("acme/sprite-pack@2"); ``` # Workflow definitions (/docs/concepts/workflows) A workflow is a JSON document describing a graph of nodes. The Studio editor produces this format when you publish; the SDK builder produces it in code; and you can also write it by hand. Either way, the same definition can be sent inline to [`POST /v1/runs`](/docs/api/runs) or published and run by reference. ## Shape [#shape] The graph below runs two `input` nodes into an `rd-fast` model and out through an `output` node — the same definition written as JSON just after: ```json { "version": 2, "nodes": [ { "id": "photo", "uses": "input", "outputType": "IMAGE", "label": "Source photo" }, { "id": "style", "uses": "input", "outputType": "TEXT", "inputs": { "default": "isometric" } }, { "id": "gen", "uses": "rd-fast", "inputs": { "prompt": "${style.value}", "init": "${photo.value}", "seed": 7 } }, { "id": "result", "uses": "output", "inputs": { "value": "${gen.image}" } } ] } ``` | Field | Description | | --------- | ----------------------------------------------------------------------------------------------- | | `version` | Schema version. Always `2`. | | `nodes` | The graph. Every node is `{ id, uses, inputs }` (plus `outputType`/`label` on interface nodes). | There are no top-level input/output blocks: **a workflow's public interface is derived from its `input` and `output` nodes.** An input node's `id` is the API parameter name callers provide at run time; `label` is display-only; an input without a `default` is required. ## The `uses` field [#the-uses-field] `uses` identifies what a node runs — never where it runs: | Form | Meaning | | -------------------- | --------------------------------------------------------------- | | `rd-fast`, `llm` | A platform node (curated model or operator) | | `input`, `output` | Interface nodes — declare the workflow's API | | `const` | A fixed inline value (`outputType` + `inputs.value`) | | `acme/sprite-pack@1` | A published workflow mounted as a node (reserved — coming soon) | The ref grammar reserves version selectors (`@latest`, `@1`, `@1.2.0` — same notation as [workflow references](/docs/concepts/versioning)), but node versions don't resolve yet: today only the implicit/explicit `@latest` runs, and a pinned selector fails at run time. `outputType` on `input`/`const` nodes is a connector id: `IMAGE`, `TEXT`, `AUDIO`, `VIDEO`, `FLOAT`, `INT`, `BOOLEAN`, `EMBEDDING`, `FILE`, or `STRUCTURED`. ## Value references [#value-references] Node inputs accept either literal JSON values (strings, numbers, booleans, `null`, objects, arrays) or `${nodeId.port}` references — a ref is only recognized as the full string value, never inside a nested structure: | Reference | Meaning | | ---------------- | ------------------------------------------------------------------------------------------ | | `${photo.value}` | The `value` port of the node `photo` (input and const nodes expose their value on `value`) | | `${gen.image}` | Output port `image` of node `gen` | References are how edges are expressed — there is no separate edge list, and there is exactly one reference namespace: every ref points at a node's port. ## Authoring options [#authoring-options] * **Studio editor** — design visually, then [publish a version](/docs/concepts/versioning) and run it by reference from code. * **SDK builder** — construct the graph programmatically with validation; see [Building workflows](/docs/sdk/building-workflows). * **Raw JSON / YAML** — write the definition directly; the [CLI](/docs/cli) runs YAML files with the same shape. # Browse the node palette (/docs/guides/browse-nodes) Before running anything, find the node you want and read its spec. All surfaces hit the same `nodes.list` / `nodes.get` contract ops (scope: `read`), and both are public — no token needed to read the catalog. To browse it in a page instead, [blitflow.com/models](https://blitflow.com/models) renders the same catalog, with a form per node generated from its spec. Signed in, the studio's **Nodes** section (`G` then `N`) is the same catalog scoped to your organization — see [Run a single node](/docs/guides/run-a-node). ```jsonc { "name": "nodes_list", "arguments": { "q": "image", "limit": 20 } } ``` Then inspect one: ```jsonc { "name": "nodes_get", "arguments": { "id": "rd-fast" } } ``` ```ts import { BlitClient } from "@blitflow/sdk"; const client = new BlitClient({ apiKey: process.env.BLITFLOW_TOKEN }); const nodes = await client.searchNodes({ query: "image", limit: 20 }); const spec = await client.getNode("rd-fast"); ``` ```bash blitflow nodes image # search the palette blitflow nodes --json # full machine-readable list ``` ```bash curl "https://studio.blitflow.com/api/v1/nodes?q=image&limit=20" \ -H "Authorization: Bearer $BLITFLOW_TOKEN" ``` See [GET /v1/nodes](/docs/api/nodes) for the response shape. Read the spec's `inputs` ports — defaults, `optionValues` (enums), and bounds — before calling the node. Guessed enum values fail validation. See [Nodes & specs](/docs/concepts/nodes). # Run a single node (/docs/guides/run-a-node) The fastest way to get output: pick a node from the [palette](/docs/guides/browse-nodes), pass its inputs, and run it. Inputs and outputs are [artifacts](/docs/concepts/artifacts); an image input is a reference `{ kind: "image", ref }`. This example runs `rd-fast` with a text prompt and gets back an image. All tabs call the same contract op, `runs.node` (scope: `run`). To do it without writing any code, open the studio's **Nodes** section, pick a node, and fill in the form — it is generated from the node's spec, so the widget for each input already enforces that port's enum values and bounds. Image, audio, video and file inputs take a local file too: drop one on the field (or use its upload button) and it is stored as an owned [artifact](/docs/concepts/artifacts) whose ref fills the field. The run is charged to the organization whose dashboard you are in. Call the `runs_node` tool with the node id and its inputs: ```jsonc { "name": "runs_node", "arguments": { "node": "rd-fast", "inputs": { "prompt": "an isometric wooden desk, pixel art", "seed": 7 }, }, } ``` The result contains the output artifacts, e.g. `{ "outputs": { "image": { "kind": "image", "ref": "https://…" } } }`. ```ts import { BlitClient } from "@blitflow/sdk"; const client = new BlitClient({ apiKey: process.env.BLITFLOW_TOKEN }); const { outputs } = await client.runNode("rd-fast", { prompt: "an isometric wooden desk, pixel art", seed: 7, }); console.log(outputs.image); // { kind: "image", ref: "https://…", … } ``` ```bash blitflow node rd-fast \ --input prompt="an isometric wooden desk, pixel art" \ --input seed=7 ``` Progress streams to stderr; the final output artifacts print to stdout (add `--json` for machine-readable output). Pass a file/URL as a reference input with `--input image=@https://…`. Over plain HTTP `runs.node` isn't served yet — wrap the node in a minimal one-node workflow and use [POST /v1/runs](/docs/api/runs). To chain several nodes, build a [workflow](/docs/guides/run-a-workflow). # Run a workflow & stream events (/docs/guides/run-a-workflow) A [workflow](/docs/concepts/workflows) chains nodes. Provide either a published reference (`@`, see [Versioning](/docs/concepts/versioning)) or an inline definition, plus the workflow's inputs, and follow the [event stream](/docs/concepts/runs) to the final outputs. All tabs call the `runs.create` contract op (scope: `run`). ```jsonc { "name": "runs_create", "arguments": { "workflowRef": "abc123@2.1.0", // or "workflow": { …inline definition… } "inputs": { "prompt": { "value": "a cozy isometric office" }, }, }, } ``` The tool consumes the event stream internally and returns the final output artifacts. ```ts import { BlitClient } from "@blitflow/sdk"; const client = new BlitClient({ apiKey: process.env.BLITFLOW_TOKEN }); const inputs = { prompt: { value: "a cozy isometric office" } }; // Just the final outputs: const outputs = await client.run("abc123@2.1.0", inputs); // Or follow per-step progress: const handle = await client.startRun("abc123@2.1.0", inputs); for await (const event of handle.events()) { console.log(event.type); if (event.type === "run.completed") return event.outputs; } ``` ```bash # published workflow by reference blitflow run abc123@2.1.0 --input prompt="a cozy isometric office" # or a local YAML definition blitflow run workflow.yaml --input prompt="a cozy isometric office" ``` Progress streams to stderr; final outputs print to stdout. ```bash curl -N -X POST "https://studio.blitflow.com/api/v1/runs" \ -H "Authorization: Bearer $BLITFLOW_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "workflowRef": "abc123@2.1.0", "inputs": { "prompt": { "value": "a cozy isometric office" } } }' ``` The response is a Server-Sent Events stream — read `data:` lines until `run.completed` / `run.failed`. See [POST /v1/runs](/docs/api/runs). # MCP server (/docs/mcp) BlitFlow speaks the [Model Context Protocol](https://modelcontextprotocol.io), so AI agents — Claude Code, Claude Desktop, Cursor, or anything MCP-capable — can discover nodes, inspect workflows, and run them as **tools**. The tools map one-to-one onto the [API contract](/docs/api): same operations, same schemas. ## Hosted server (recommended) [#hosted-server-recommended] The hosted server lives at: ``` https://mcp.blitflow.com ``` It uses **streamable HTTP** transport and **OAuth** — your MCP client opens a browser to sign you in; there are no tokens to paste. ```bash claude mcp add --transport http blitflow https://mcp.blitflow.com ``` For clients configured with a `.mcp.json`-style file: ```json { "mcpServers": { "blitflow": { "type": "http", "url": "https://mcp.blitflow.com" } } } ``` ## Local server (stdio) [#local-server-stdio] The [CLI](/docs/cli) also serves MCP over stdio via `blitflow mcp` — useful for headless environments or when pointing at a non-production API host. It authenticates like every other CLI command: the session saved by `blitflow login`, or a [personal access token](/docs/authentication) in `BLITFLOW_TOKEN` (which takes precedence). | Env var | Required | Description | | ---------------- | -------- | ------------------------------------------------ | | `BLITFLOW_TOKEN` | no\* | Your `blit_…` token (\*unless already logged in) | | `BLITFLOW_URL` | no | Override the API base URL | ```json { "mcpServers": { "blitflow": { "command": "npx", "args": ["-y", "blitflow", "mcp"], "env": { "BLITFLOW_TOKEN": "blit_..." } } } } ``` On a machine where you've already run `blitflow login`, drop the `env` block entirely. ## What the agent can do [#what-the-agent-can-do] | Tool | Scope | Purpose | | --------------- | ------ | ---------------------------------------------- | | `nodes_list` | `read` | Search the node palette | | `nodes_get` | `read` | Read one node's exact inputs and enum values | | `workflows_get` | `read` | Fetch a published workflow definition | | `runs_create` | `run` | Run a workflow (inline or by `@`) | | `runs_node` | `run` | Run a single node | See [Tools](/docs/mcp/tools) for full parameter schemas, and note that `run`-scoped tools **spend money** from the organization's prepaid balance — there is no per-run cap, so agents should know a node's cost before running it. Tool results come back as JSON text. On failure a tool returns an error result with the message — agents should read it and adjust, not retry blindly. # Tools (/docs/mcp/tools) All tools mirror the [API contract](/docs/api) exactly — parameters are validated against the same schemas. **Tool naming:** both servers (hosted and `blitflow mcp` stdio) expose underscore names (`nodes_list`) — contract op names use dots (`nodes.list`), which some MCP clients can't call as tool names. In a client like Claude Code the tools surface as `mcp__blitflow__nodes_list` etc. ## `nodes_list` [#nodes_list] List or search the node palette. Returns `NodeSpec[]`. | Parameter | Type | Required | Description | | ---------- | ------- | -------- | ----------------------------------------- | | `q` | string | no | Free-text search (id, title, description) | | `category` | string | no | Filter by category | | `limit` | integer | no | Max results | ## `nodes_get` [#nodes_get] Fetch one node spec by id — its exact input/output ports, defaults, enum `optionValues`, and numeric bounds. | Parameter | Type | Required | Description | | --------- | ------ | -------- | ----------------------- | | `id` | string | yes | Node id, e.g. `rd-fast` | Call this **before** running a node and build inputs from the returned spec. Guessed enum values fail validation — they are not in the model's memory. ## `workflows_get` [#workflows_get] Fetch a published workflow version's canonical definition. | Parameter | Type | Required | Description | | --------- | ------ | -------- | ----------------------------------------------------------------------------------------------- | | `id` | string | yes | Published `/` address (e.g. `acme/sprite-pack`) or the workflow's UUID | | `version` | string | no | `latest` (default), a full semver (`2.1.0`), or a major (`2`) | Returns `{ id, address, version, sequence, workflow }` (`address` is `null` until the workflow's slug is claimed) — see [Versioning](/docs/concepts/versioning). ## `runs_create` [#runs_create] Run a workflow. Provide **exactly one** of `workflow` or `workflowRef`. The tool consumes the run's event stream internally and returns the terminal outputs. | Parameter | Type | Required | Description | | ------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------- | | `workflow` | object | one of | Inline [workflow definition](/docs/concepts/workflows) | | `workflowRef` | string | one of | `@` reference — a published address like `acme/sprite-pack@2.1.0` (preferred) or a workflow UUID like `8f61e451-…@2.1.0` | | `inputs` | object | no | [Artifacts](/docs/concepts/artifacts) keyed by workflow input name — `kind` is optional, the input node declares it | Result: `{ outputs: Record }`. If the run fails, the tool returns the `run.failed` error — read the message; don't loop. ## `runs_node` [#runs_node] Run a single node by id — no workflow needed. Supports `model`/`llm` nodes. | Parameter | Type | Required | Description | | --------- | ------ | -------- | -------------------------------------------------------------------------------------------------------------------- | | `node` | string | yes | Node id | | `inputs` | object | yes | Keyed by input port name — scalars for value ports, Artifacts for data ports (`kind` optional, the port declares it) | Returns `{ outputs: Record }`. ## `artifacts_import` [#artifacts_import] Import a publicly fetchable http(s) URL as an organization-owned [Artifact](/docs/concepts/artifacts) — the way to feed remote media into `runs_node` / `runs_create`. Pass the returned `artifact` object **unchanged** as a run input. | Parameter | Type | Required | Description | | ----------- | ------ | -------- | ------------------------------------------------------------------------------------- | | `url` | string | yes | Public http(s) URL. Private/internal destinations are rejected | | `org` | string | no | Organization slug; omit for the personal organization | | `kind` | string | no | Override the kind derived from the content type: `image`, `audio`, `video`, or `file` | | `retention` | string | no | `temporary` (default, 30 days) or `durable` | Returns `{ id, artifact, contentType, sizeBytes, expiresAt }` — see the [HTTP flow](/docs/api/artifacts) for limits. ## `artifacts_uploads_prepare` / `artifacts_uploads_complete` [#artifacts_uploads_prepare--artifacts_uploads_complete] The signed-upload control plane for bytes a programmatic client holds locally: `prepare` returns a short-lived direct-upload URL and token constrained to one pathname, declared content type, and maximum size; after PUTting the bytes there, `complete` (with the returned `id`) verifies the blob and records the Artifact, idempotently. See the [signed-upload flow](/docs/api/artifacts) for the full steps. No BlitFlow tool accepts base64, byte arrays, `data:` URIs, or local file paths. Never serialize file bytes into tool arguments — import a URL, or drive the signed-upload flow from your own runtime. ## Agent playbook [#agent-playbook] 1. **Discover** — `nodes_list` with `q`/`category`, then `nodes_get` the candidate. Build inputs strictly from the spec. 2. **Mind spend** — runs are charged to the organization's balance with no per-run cap; prefer the cheapest node that clears the quality bar, and read `usage.costUsd` before scaling up. 3. **Fetch refs** — binary outputs are ref artifacts; download the `ref` URL for bytes, and reuse persisted refs instead of regenerating. 4. **Media in, by reference** — to use an image/audio/video input, first `artifacts_import` its URL (or run the signed-upload flow), then pass the returned `artifact` unchanged. Never inline bytes or `data:` URIs. 5. **Stop on failure** — a `run.failed` / error result is terminal. Read the message, fix the inputs, then retry deliberately. # Building workflows (/docs/sdk/building-workflows) Beyond referencing workflows published from the Studio editor, you can build definitions **dynamically in code** and run them inline. Two tools help, both producing a plain [`Workflow`](/docs/concepts/workflows) object. ## The draft builder [#the-draft-builder] `createWorkflow()` gives you an imperative builder with connection-type checking and validation: ```ts import { BlitClient, createWorkflow } from "@blitflow/sdk"; const client = new BlitClient({ apiKey: process.env.BLITFLOW_TOKEN }); const draft = createWorkflow(client); // declare workflow inputs (returns a ref string like "${prompt.value}") const promptRef = draft.addInput("prompt", "TEXT"); // add nodes by spec id, wiring inputs to refs or literals const gen = draft.addNode("rd-fast", { prompt: promptRef, removeBg: true, seed: 7, }); // or connect ports explicitly — returns { ok, reason? } on type mismatch // draft.connect(gen.id, "image", upscale.id, "init"); // expose outputs draft.addOutput("sprite", `\${${gen.id}.image}`); // validate before running const problems = draft.validate(); if (problems.length > 0) throw new Error(problems.map((p) => p.message).join("\n")); const workflow = draft.toWorkflow(); const outputs = await client.run(workflow, { prompt: { value: "a brass key" }, }); ``` | Method | Purpose | | ----------------------------------------- | ---------------------------------------------------- | | `addInput(name, connector)` | Add an `input` node; returns its `${name.value}` ref | | `addNode(specId, inputs?)` | Add a node from the palette | | `connect(fromId, fromPort, toId, toPort)` | Wire an edge with connector-type checking | | `addOutput(name, ref)` | Add an `output` node exposing a result | | `validate()` | Returns a list of validation errors (empty = OK) | | `toWorkflow()` | Produce the final `Workflow` JSON | Code nodes use the same workflow version `2` and can be mixed with catalog nodes. See [Code nodes](/docs/sdk/code-nodes) for the module contract, typed ports, runtime limits, and isolation rules. Fetch node specs first (`getNode` / `searchNodes`) so you wire real port names and valid enum values — see [Nodes & specs](/docs/concepts/nodes). ## `graphToWorkflow()` — convert a visual graph [#graphtoworkflow--convert-a-visual-graph] If your application maintains its own node-and-edge graph model (like the Studio editor does), `graphToWorkflow()` converts it into a definition. It is spec-aware: it resolves port types, turns exposed ports into `input` nodes, and materializes primitives as `input` or `const` nodes. ```ts import { graphToWorkflow } from "@blitflow/sdk"; const workflow = graphToWorkflow({ nodes: [ { id: "n1", specId: "rd-fast", values: { seed: 7 }, exposed: ["prompt"], }, ], edges: [ // { source: "n1", sourceHandle: "image", target: "n2", targetHandle: "init" } ], specsById, // Record — from searchNodes()/getNode() outputNodes: [{ id: "out1", label: "sprite" }], primitives: [], }); ``` | Argument | Description | | ------------- | ------------------------------------------------------------------------------------------------- | | `nodes` | Graph nodes: `specId`, inline socket `values`, and `exposed` input ports (become workflow inputs) | | `edges` | Connections: `source`/`sourceHandle` → `target`/`targetHandle` | | `specsById` | Node specs used to resolve port types | | `outputNodes` | Output sinks — each `label` becomes a workflow output name | | `primitives` | Constants and workflow-input placeholders feeding the graph | ## Publishing [#publishing] Programmatically built workflows are best run **inline** — pass the object straight to `run()` / `POST /v1/runs`. Publishing versioned workflows (and rolling back `@latest`) happens in the Studio; publish there when you want a stable `@` ref for other systems to consume. # Code node runtime (/docs/sdk/code-node-runtime) Code nodes run in an immutable BlitFlow Sandbox. Choose `runtime: "node"` or `runtime: "python"`; the runtime decides the source contract and installed packages. This page is the authoritative list of what each environment contains — packages from your own application are **not** available inside a Sandbox. ## Runtime matrix [#runtime-matrix] | Capability | Node | Python | | -------------------- | -------------------------- | -------------------------- | | API | `blitflow.code/v1` | `blitflow.code/v1` | | Workflow node | `code@1.0.0` | `code@1.0.0` | | Supported versions | `1`, `2` | `1` | | Authoring default | `2` | `1` | | Python environment | — | `1` | | Language | JavaScript | Python | | Module format | ECMAScript module (ESM) | Python source module | | Runtime | Node.js `24.x` | Python `3.13.x` | | Context output path | `context.outputDir` | `context.output_dir` | | Network | denied | denied | | Package installation | not available | not available | | Filesystem | ephemeral except Artifacts | ephemeral except Artifacts | Node v1, Node v2, and Python v1 are independent immutable snapshots. Their exact patch versions and package inventories are captured when each snapshot is built. A workflow retains its selected version; changing the authoring default does not rewrite existing definitions. ## Entry points [#entry-points] ### Node.js [#nodejs] BlitFlow saves Node source as an `.mjs` module and invokes its default export: ```js export default async function (inputs, context) { return { result: inputs.value }; } ``` The one-argument form remains valid. Node source must be executable JavaScript ESM: TypeScript syntax, CommonJS `require()`, and `module.exports` are not transpiled or supported. ### Python [#python] BlitFlow saves Python source as `source.py` and invokes `main`: ```py def main(inputs, context): return {"result": inputs["value"]} ``` `main(inputs)` and `main(inputs, context)` are both valid. `main` may be synchronous or `async`. The result must be a plain `dict` with string keys and strict JSON values. NaN, infinity, bytes, sets, non-string keys, cycles, and custom objects are rejected — including NumPy and pandas scalars and arrays: return `float(x)`, `int(x)` or `.tolist()`. An exception raised inside `main` is a code failure; BlitFlow does not reinterpret it as an argument-count error. For both languages, `inputs` is keyed by declared ports, required outputs must exist, optional outputs may be omitted, and extra outputs fail the step. stdout and stderr are bounded diagnostics and never outputs. ## Node.js imports [#nodejs-imports] The Node.js 24 standard library is installed. Import built-ins through `node:` specifiers so generated code cannot confuse them with third-party packages. | Task | Available imports | | ----------------------- | --------------------------------------------------------------------------- | | Files and paths | `node:fs`, `node:fs/promises`, `node:path` | | Bytes and streams | `node:buffer`, `node:stream`, `node:stream/promises`, `node:string_decoder` | | Hashing and compression | `node:crypto`, `node:zlib` | | URLs and utilities | `node:url`, `node:util`, `node:events`, `node:timers/promises` | ```js import { createHash } from "node:crypto"; import { readFile, writeFile } from "node:fs/promises"; ``` Node v1 includes one external package: | Package | Typical use | | ------------------------- | ----------------------------------------------- | | `simple-statistics@7.9.3` | descriptive statistics, quantiles, and variance | Node v2 requires Node.js `>=24.12.0 <25` and includes this exact public catalog: | Category | Package | | ------------------------ | --------------------------------------------------------------------------------------------------- | | Image | `sharp@0.35.3`, `jimp@1.6.1`, `@napi-rs/canvas@1.0.7`, `exifr@7.1.3` | | Audio and video | `@napi-rs/webcodecs@1.4.0` | | glTF and GLB | `@gltf-transform/core@4.4.2`, `@gltf-transform/extensions@4.4.2`, `@gltf-transform/functions@4.4.2` | | Statistics and tables | `simple-statistics@7.9.3`, `mathjs@15.2.0`, `arquero@8.0.3`, `apache-arrow@21.2.0` | | Analytical SQL | `@duckdb/node-api@1.5.5-r.4` | | Documents | `pdf-lib@1.17.1`, `exceljs@4.4.0`, `docx@9.7.1` | | HTML, XML, validation | `cheerio@1.2.0`, `fast-xml-parser@5.11.0`, `ajv@8.20.0`, `zod@4.4.3` | | Structured formats | `yaml@2.9.0`, `csv-parse@7.0.2`, `csv-stringify@6.8.3` | | Dates, text, compression | `date-fns@4.4.0`, `fastest-levenshtein@1.0.16`, `fflate@0.8.3` | | Graphs | `graphology@0.26.0`, `graphology-library@0.8.0` | `sharp` is not available in Node v1. New Node definitions use v2; explicitly pin version 1 only when preserving its smaller legacy environment. `axios` and `@blitflow/sdk` are not available inside either Sandbox environment. Other packages from your application are not installed. Network modules may exist in the standard library, but outbound connections are denied. Do not run npm, npx, yarn, pnpm, or bun install. ## Python imports [#python-imports] Python v1 includes the following exact public catalog. Use the import name in the second column, not necessarily the distribution name. Transitive packages are locked for the snapshot but are not a stable authoring surface. | Installed distribution | Import name | Primary use | | --------------------------------- | ------------- | --------------------------------------- | | `beautifulsoup4@4.15.0` | `bs4` | HTML parsing | | `duckdb@1.5.5` | `duckdb` | in-process SQL analytics | | `imageio@2.37.4` | `imageio` | image reading and writing | | `jsonschema@4.26.0` | `jsonschema` | JSON Schema validation | | `lxml@6.1.1` | `lxml` | XML and HTML parsing | | `matplotlib@3.11.1` | `matplotlib` | headless plotting to files | | `networkx@3.6.1` | `networkx` | graph algorithms | | `numpy@2.5.2` | `numpy` | arrays and numerical computing | | `opencv-python-headless@5.0.0.93` | `cv2` | computer vision without a GUI | | `openpyxl@3.1.5` | `openpyxl` | reading and writing XLSX files | | `orjson@3.12.0` | `orjson` | fast JSON encoding and decoding | | `pandas@3.0.5` | `pandas` | tabular dataframes | | `pillow@12.3.0` | `PIL` | image processing | | `plotly@6.9.0` | `plotly` | chart figures and structured chart data | | `polars@1.43.2` | `polars` | columnar dataframes | | `pyarrow@25.0.1` | `pyarrow` | Arrow and Parquet data | | `pydantic@2.13.4` | `pydantic` | typed data validation | | `pypdf@6.16.1` | `pypdf` | reading and transforming PDFs | | `python-dateutil@2.9.0.post0` | `dateutil` | date parsing and arithmetic | | `python-docx@1.2.0` | `docx` | reading and writing DOCX files | | `pyyaml@6.0.3` | `yaml` | YAML parsing and serialization | | `rapidfuzz@3.14.5` | `rapidfuzz` | fuzzy string matching | | `reportlab@5.0.0` | `reportlab` | PDF generation | | `scikit-image@0.26.0` | `skimage` | image processing and analysis | | `scikit-learn@1.9.0` | `sklearn` | classical machine learning | | `scipy@1.18.0` | `scipy` | scientific algorithms | | `seaborn@0.13.2` | `seaborn` | statistical visualization | | `statsmodels@0.14.6` | `statsmodels` | statistical models and tests | | `sympy@1.14.0` | `sympy` | symbolic mathematics | | `xlsxwriter@3.2.9` | `xlsxwriter` | generating XLSX files | * PyTorch is not installed. * TensorFlow is not installed. * Transformers is not installed. * Diffusers is not installed. Choose a catalog/provider node for workloads that require those model runtimes. Python runs with user site packages disabled, bytecode writes disabled, a headless Matplotlib backend, and bounded native-library thread counts. Do not run pip, uv, conda, poetry, or another installer from user code. ## Filesystem and Artifacts [#filesystem-and-artifacts] Every attempt receives a fresh filesystem. FILE, IMAGE, AUDIO, and VIDEO inputs are read-only local descriptors in both languages: ```ts type CodeArtifactInput = { path: string; mimeType: string; sizeBytes: number; }; ``` Use the language-specific context path for outputs: ```js // Node export default async function ({ text }, { outputDir }) { const path = outputDir + "/report.txt"; await (await import("node:fs/promises")).writeFile(path, text); return { report: { path, mimeType: "text/plain" } }; } ``` ```py # Python from pathlib import Path def main(inputs, context): path = Path(context.output_dir) / "report.txt" path.write_text(inputs["text"], encoding="utf-8") return {"report": {"path": str(path), "mimeType": "text/plain"}} ``` Only declared FILE, IMAGE, AUDIO, and VIDEO outputs are collected and persisted. Never hard-code internal Sandbox paths. The returned `mimeType` is checked against the detected bytes. Typed media ports accept closed allowlists; FILE takes any passive MIME: | Port | Accepted `mimeType` | | ----- | ------------------------------------------------------------------------------------------------------------------------ | | IMAGE | `image/png`, `image/jpeg`, `image/webp`, `image/gif`, `image/avif` | | AUDIO | `audio/wav`, `audio/mpeg`, `audio/aac`, `audio/flac`, `audio/ogg` (MP4/M4A audio is not accepted) | | VIDEO | `video/mp4`, `video/webm`, `video/quicktime`, `video/matroska` | | FILE | any passive MIME, defaulting to `application/octet-stream`; a declared MIME must be compatible with the bytes' signature | SVG, HTML, XML, JavaScript, and other active content are rejected on every port. See [Code nodes](/docs/sdk/code-nodes#binary-artifacts) for size limits and chaining examples. ## Sandbox restrictions [#sandbox-restrictions] These restrictions apply identically to Node and Python: | Area | Restriction | | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Lifetime | Every attempt uses a fresh, non-persistent Sandbox. The Sandbox and its full process tree stop after success, failure, or timeout; background processes do not survive. | | Network | The snapshot is switched to deny-all outbound networking before publication and execution has no exposed ports. | | Identity | User code runs as an unprivileged user with no Linux capabilities, no supplementary groups, and no ability to gain privileges. | | Environment | User code receives a sanitized environment allowlist. Application secrets and provider credentials are not forwarded. | | Runtime | Installed runtimes and packages are a root-owned, read-only runtime. Runtime package installation is unavailable. | | Filesystem | Temporary writes are ephemeral. Only declared FILE/IMAGE/AUDIO/VIDEO outputs written below the supplied output directory become durable Artifacts. | The process supervisor also enforces these per-attempt limits: | Limit | Current contract | | ------------------- | --------------------------------------------------- | | Source | 64 KiB UTF-8 | | Ports | 16 inputs and 16 outputs | | Encoded JSON input | 10 MiB | | Encoded JSON result | 10 MiB | | stdout / stderr | 1 MiB each | | Binary Artifact | 200 MiB per file; 400 MiB across inputs and outputs | | Processes | 128 processes | | Address space | 4 GiB per user process | | Timeout | 1–900 seconds; default 300 | | Compute | 1, 2, or 4 vCPUs; default 2 | Timeouts, missing imports, syntax errors, exceptions, and invalid results fail the step. Public errors do not contain stdout, stderr, internal paths, private Artifact URLs, or provider details. # Code nodes (/docs/sdk/code-nodes) A code node runs your own Node.js or Python function as a workflow step, inside a fresh, isolated Sandbox. Reach for one when the node catalog doesn't already provide the operation: reshaping JSON, computing statistics, filtering records, or processing FILE, IMAGE, AUDIO, or VIDEO data. Before choosing imports, read the [code node runtime reference](/docs/sdk/code-node-runtime). Node v1 provides Node.js built-ins and `simple-statistics@7.9.3`; Node v2 adds the pinned image, media, analytics, document, parsing, graph, and glTF catalog. Python v1 has its own independent catalog. Packages from your application are not automatically available. ## Write the function [#write-the-function] For Node, the `code` string is an ES module with one default-exported function, sync or async: ```js export default async function (inputs, context) { return { result: inputs.value }; } ``` * `inputs` — a plain object keyed by your declared input ports. * Return a plain object keyed by your declared output ports: every required output present, optional ones omissible, extra keys fail the step. * `context.outputDir` — the only directory binary outputs may be written to. Scalar-only nodes can use the one-argument form and skip `context`. * `console.log()` and `console.error()` are diagnostics, never outputs. The source must be plain JavaScript ESM — no TypeScript syntax, CommonJS `require()`, or `module.exports`. For Python, export a function named `main`: ```py def main(inputs, context): return {"result": inputs["value"]} ``` Python accepts `main(inputs)` and `main(inputs, context)`, sync or async. Binary Artifact producers use `context.output_dir`. Return a plain `dict` containing strict JSON values. NaN, infinity, bytes, sets, non-string keys, cycles, and arbitrary objects are rejected. Choose the immutable environment matching the work: | Need | Node v1 | Node v2 | Python v1 | | ---------------------- | ------------------- | ------------------------------------- | ------------------------------------------------------ | | Statistics | `simple-statistics` | `simple-statistics`, `mathjs`, DuckDB | `numpy`, `scipy`, `statsmodels` | | Tabular data | JavaScript arrays | Arquero, Arrow, DuckDB | `pandas`, `polars`, `duckdb`, `pyarrow` | | Images | byte/file APIs | sharp, Jimp, Canvas | `PIL`, `cv2`, `skimage`, `imageio` | | Audio and video | byte/file APIs | WebCodecs | installed Python libraries where supported | | Documents | byte/file APIs | PDF, XLSX, and DOCX libraries | `openpyxl`, `xlsxwriter`, `docx`, `pypdf`, `reportlab` | | Parsing and validation | JSON | HTML, XML, YAML, CSV, AJV, and Zod | `pydantic`, `yaml`, `lxml`, `bs4`, `jsonschema` | | 3D assets | byte/file APIs | glTF Transform for glTF/GLB editing | byte/file APIs | The [runtime reference](/docs/sdk/code-node-runtime) lists every installed distribution, exact version, import name, and Sandbox restriction. Do not infer that a dependency from your application is available inside a code node. ## Define and run a node with the SDK [#define-and-run-a-node-with-the-sdk] Write the port contract before the source. `defineCodeNode()` validates the definition, and `WorkflowDraft.toWorkflow()` validates the code node together with its call-site inputs and references: ```ts import { BlitClient, CODE_ENVIRONMENT_VERSION, CODE_NODE_API_VERSION, CODE_RUNTIME_ENVIRONMENT_VERSIONS, createWorkflow, defineCodeNode, } from "@blitflow/sdk"; const summarize = defineCodeNode({ apiVersion: CODE_NODE_API_VERSION, runtime: "node", environmentVersion: CODE_ENVIRONMENT_VERSION, code: ` export default function ({ values, title }) { const total = values.reduce((sum, value) => sum + value, 0); return { summary: { title, count: values.length, total }, label: title + ": " + total, }; } `, inputs: [ { name: "values", type: "STRUCTURED" }, { name: "title", type: "TEXT" }, ], outputs: [ { name: "summary", type: "STRUCTURED" }, { name: "label", type: "TEXT" }, ], compute: { timeoutSeconds: 30, vcpus: 1 }, }); const draft = createWorkflow(); const code = draft.addCodeNode(summarize, { values: [4, 8, 15, 16, 23, 42], title: "Sequence", }); draft.addOutput("summary", `\${${code.id}.summary}`); draft.addOutput("label", `\${${code.id}.label}`); const client = new BlitClient({ apiKey: process.env.BLITFLOW_TOKEN }); const outputs = await client.run(draft.toWorkflow(), {}); ``` `WorkflowDraft.toWorkflow()` emits workflow version `2`. Code nodes are an additive node kind in that contract and can be mixed with catalog nodes. For Python, set `runtime: "python"` and `environmentVersion: CODE_RUNTIME_ENVIRONMENT_VERSIONS.python`; the rest of the definition and workflow APIs are unchanged. New Node definitions use `CODE_ENVIRONMENT_VERSION`, the promoted Node v2 catalog. Set `environmentVersion: 1` only when intentionally preserving the legacy Node v1 environment. Existing definitions retain their recorded environment version. ```ts const pythonSummary = defineCodeNode({ apiVersion: CODE_NODE_API_VERSION, runtime: "python", environmentVersion: CODE_RUNTIME_ENVIRONMENT_VERSIONS.python, code: ` def main(inputs): values = inputs["values"] return {"total": float(sum(values))} `, inputs: [{ name: "values", type: "STRUCTURED" }], outputs: [{ name: "total", type: "FLOAT" }], }); ``` ## Supported port types [#supported-port-types] | Port type | Value inside user code | | ------------ | ----------------------------------------------------------- | | `TEXT` | string | | `INT` | safe integer (`Number.isSafeInteger(value)`) | | `FLOAT` | finite number | | `BOOLEAN` | boolean | | `EMBEDDING` | non-empty array of finite numbers | | `STRUCTURED` | JSON-compatible value: null, scalar, array, or plain object | | `FILE` | read-only `{ path, mimeType, sizeBytes }` input descriptor | | `IMAGE` | read-only `{ path, mimeType, sizeBytes }` input descriptor | | `AUDIO` | read-only `{ path, mimeType, sizeBytes }` input descriptor | | `VIDEO` | read-only `{ path, mimeType, sizeBytes }` input descriptor | Do not return `undefined`, `bigint`, functions, symbols, `NaN`, infinities, class instances, cyclic objects, getters, sparse arrays, or undeclared properties. Port names must be JavaScript identifiers and cannot be `__proto__`, `prototype`, or `constructor`. ## Cookbook [#cookbook] The snippets below are module bodies for the `code` field. Match their reads and returned keys with the definition's `inputs` and `outputs` exactly. ### Return scalar and structured outputs [#return-scalar-and-structured-outputs] Declare `label` as TEXT, `ratio` as FLOAT, and `details` as STRUCTURED: ```js export default function ({ name, completed, total }) { const ratio = total === 0 ? 0 : completed / total; return { label: `${name}: ${completed}/${total}`, ratio, details: { completed, total, done: completed === total }, }; } ``` ### Use simple-statistics [#use-simple-statistics] `simple-statistics@7.9.3` is available in Node v1 and v2: ```js import { mean, median, standardDeviation } from "simple-statistics"; export default function ({ values }) { return { mean: mean(values), report: { median: median(values), standardDeviation: standardDeviation(values), }, }; } ``` Declare `values` as STRUCTURED, `mean` as FLOAT, and `report` as STRUCTURED. ### Use NumPy and Pandas [#use-numpy-and-pandas] Choose `runtime: "python"`. NumPy and Pandas are preinstalled in Python v1: ```py import numpy as np import pandas as pd def main(inputs): frame = pd.DataFrame(inputs["rows"]) values = frame[inputs["column"]].to_numpy(dtype=float) return { "mean": float(np.mean(values)), "rows": int(len(frame)), } ``` Declare `rows` as STRUCTURED, `column` as TEXT, `mean` as FLOAT, and the output `rows` as INT. Convert NumPy scalar values to Python `float` or `int` before returning them. ### Use async Python [#use-async-python] Python `main` may be async. This is useful for local asynchronous work, but it does not enable network access: ```py import asyncio async def main(inputs): await asyncio.sleep(0) return {"result": inputs["value"]} ``` Declare `value` and `result` with the same compatible scalar or STRUCTURED type. ### Create an IMAGE with Pillow [#create-an-image-with-pillow] Python IMAGE producers write below `context.output_dir` and return the same descriptor shape as Node producers: ```py from pathlib import Path from PIL import Image def main(inputs, context): path = Path(context.output_dir) / "swatch.png" Image.new("RGB", (64, 64), inputs["color"]).save(path, format="PNG") return {"image": {"path": str(path), "mimeType": "image/png"}} ``` Declare `color` as TEXT and `image` as IMAGE. The collector verifies the PNG signature and persists a durable Artifact. ### Read a FILE [#read-a-file] A FILE input is a read-only local descriptor, not a Blob URL: ```js import { createHash } from "node:crypto"; import { createReadStream } from "node:fs"; export default async function ({ source }) { const hash = createHash("sha256"); for await (const chunk of createReadStream(source.path)) { hash.update(chunk); } return { size: source.sizeBytes, metadata: { mimeType: source.mimeType, sha256: hash.digest("hex"), }, }; } ``` Declare `source` as FILE, `size` as INT, and `metadata` as STRUCTURED. Never persist or return `source.path`; it is internal to this attempt. ### Create a FILE [#create-a-file] Write below the supplied output directory and return a descriptor, not bytes: ```js import { writeFile } from "node:fs/promises"; import { join } from "node:path"; export default async function ({ rows }, { outputDir }) { const path = join(outputDir, "report.json"); await writeFile(path, JSON.stringify(rows, null, 2), "utf8"); return { report: { path, mimeType: "application/json" } }; } ``` Declare `rows` as STRUCTURED and `report` as FILE. BlitFlow collects the file, uploads it, and replaces the descriptor with a durable file Artifact. ### Read and copy an IMAGE [#read-and-copy-an-image] Image inputs use the same read-only descriptor. This example copies supported image bytes without assuming an unavailable image-processing package: ```js import { copyFile } from "node:fs/promises"; import { join } from "node:path"; export default async function ({ image }, { outputDir }) { const path = join(outputDir, "copy.bin"); await copyFile(image.path, path); return { copy: { path, mimeType: image.mimeType } }; } ``` Declare both `image` and `copy` as IMAGE. The collector detects the real MIME from the bytes and requires it to match the returned MIME. ### Chain code nodes [#chain-code-nodes] Binary inputs in a workflow definition must be references to a compatible output from an earlier node. After wrapping the FILE recipes above as `createReport` and `inspectReport`, connect them through the producer's output ref: ```ts const created = draft.addCodeNode(createReport, { rows: [{ id: 1, status: "ready" }], }); const inspected = draft.addCodeNode(inspectReport, { source: `\${${created.id}.report}`, }); draft.addOutput("report", `\${${created.id}.report}`); draft.addOutput("metadata", `\${${inspected.id}.metadata}`); ``` The first step's durable Artifact is materialized as a new read-only local descriptor for the second step. User code never receives the private storage capability URL. ### See a deterministic invalid-output failure [#see-a-deterministic-invalid-output-failure] This source fails when `result` is declared as INT because the returned value has the wrong type: ```js export default function () { return { result: "not an integer" }; } ``` Fix the returned value or the declared port. Retrying the same definition will not change a deterministic contract failure. ## Binary artifacts [#binary-artifacts] FILE, IMAGE, AUDIO, and VIDEO workflow inputs must reference a compatible output from an earlier node. Descriptor literals, external URLs, `data:` URIs, base64 strings, and raw buffers are rejected before a Sandbox starts. BlitFlow materializes its own Artifact as a local descriptor: ```ts type CodeArtifactInput = { path: string; mimeType: string; sizeBytes: number; }; ``` To produce a binary Artifact, write a regular file below `context.outputDir` (Node) or `context.output_dir` (Python), then return `{ path, mimeType? }` for the declared port. The final workflow output is a durable [`Artifact`](/docs/concepts/artifacts) reference with MIME, byte size, and SHA-256 metadata. It is not bytes or base64. IMAGE accepts PNG, JPEG, WebP, GIF, and AVIF. AUDIO accepts AAC, FLAC, MPEG, OGG, and WAV. VIDEO accepts MP4, M4V, WebM, QuickTime, and Matroska. Every typed media MIME is detected from the bytes and must match a declared MIME. SVG, HTML, XML, JavaScript, and other active content cannot be typed media. FILE defaults to `application/octet-stream`; a declared FILE MIME must be valid, non-active, and compatible with any detected binary signature. Each file is limited to 200 MiB. Binary inputs and outputs together are limited to 400 MiB per attempt. Missing files, paths outside `outputDir`, symlinks, hardlinks, sparse files, special files, undeclared outputs, MIME mismatches, and limit violations fail the step. ## Run through API, SDK, or MCP [#run-through-api-sdk-or-mcp] A code node always executes as part of a workflow. The workflow can be inline or a published `workflowRef`; its contract is identical on all surfaces. | Surface | Start the workflow | Read a known run id | | -------------- | ---------------------- | ------------------- | | HTTP API | `POST /v1/runs` (SSE) | `GET /v1/runs/:id` | | TypeScript SDK | `run()` / `startRun()` | `getRun()` | | MCP | `runs_create` | `runs_get` | ### HTTP API [#http-api] Send the workflow JSON produced by `draft.toWorkflow()` in the `workflow` field, or send a published reference: ```bash curl -N "https://studio.blitflow.com/api/v1/runs" \ -H "Authorization: Bearer $BLITFLOW_TOKEN" \ -H "Content-Type: application/json" \ -d '{"workflowRef":"acme/code-report@latest","inputs":{}}' ``` Keep the SSE connection open through `run.completed` or `run.failed`. Preserve the id from `run.started` if you need to fetch the durable run later. ### TypeScript SDK [#typescript-sdk] Use `client.run(workflow, inputs)` when only terminal outputs matter. Use `startRun()` to inspect every event, and `client.getRun(id)` to fetch a known durable record. The complete inline builder example at the top uses `run()`. ### MCP [#mcp] Call `runs_create` with exactly one of `workflow` or `workflowRef`, plus any run inputs. It consumes the event stream and returns terminal outputs. Use `runs_get` when you already have a run id and need its durable state. `runs.node` / `runNode()` / `runs_node` execute registered model/llm catalog nodes. They do not accept dynamic code definitions. Use the workflow operation for every code node. ## Limits and isolation [#limits-and-isolation] | Limit | Current contract | | ------------------ | -------------------------- | | Source code | 64 KiB UTF-8 | | Input/output ports | 16 inputs and 16 outputs | | Input payload | 10 MiB encoded JSON | | Result payload | 10 MiB encoded JSON | | stdout / stderr | 1 MiB each | | Timeout | 1–900 seconds; default 300 | | vCPUs | 1, 2, or 4; default 2 | Each attempt uses a fresh, non-persistent Sandbox. Outbound network access is denied, no ports are exposed, runtime package installation is unavailable, and the Sandbox stops after the attempt. Infrastructure failures are retried by the durable workflow; deterministic source, input, and output failures are not. Public errors never include captured stdout or stderr. ## Troubleshooting [#troubleshooting] | Symptom | Cause | Fix | | ----------------------- | ------------------------------------------------------ | -------------------------------------------------------------------------- | | Missing package/module | The import is not in the selected immutable runtime | Use the runtime catalog or a catalog node | | Invalid entry point | Node default export or Python `main` is missing | Export the required function | | Required output missing | A branch omitted a declared output | Return every required key on every branch | | Invalid output type | Returned value does not match its port | Correct the value or the declared port | | Undeclared output | Return object has an extra key or file | Declare it or stop returning it | | Artifact path rejected | Path is outside the runtime output directory or unsafe | Derive it from `context.outputDir` (Node) or `context.output_dir` (Python) | | Media MIME mismatch | Declared MIME or port kind differs from detected bytes | Return a supported matching MIME or correct the file bytes | | Network request fails | Outbound networking is denied | Use workflow inputs or a catalog node with provider access | | Timeout | Work exceeded `compute.timeoutSeconds` | Reduce work or choose an allowed timeout up to 900 seconds | ## Final checklist [#final-checklist] * Define ports before writing code and use their names exactly. * Read the [runtime reference](/docs/sdk/code-node-runtime) before importing. * Use exported API and environment constants; do not invent versions. * Return only JSON-compatible values matching declared scalar port types. * Read FILE/IMAGE/AUDIO/VIDEO descriptors and write outputs only below `context.outputDir` (Node) or `context.output_dir` (Python). * Return binary descriptors, never bytes, base64, or internal paths. * Treat stdout and stderr as diagnostics, never results. * Assume no network, runtime package installation, or persistent disk. * Run `defineCodeNode()` and `draft.toWorkflow()` locally before starting a run. # SDK setup (/docs/sdk) `@blitflow/sdk` is a typed TypeScript client for the BlitFlow API. It works in Node.js, Bun, and the browser, and covers the full contract: node discovery, workflow runs with streamed events, published-workflow fetching, and a programmatic workflow builder. The SDK currently ships from the BlitFlow monorepo as the workspace package `@blitflow/sdk`; npm publication is on the way (the `blitflow` npm name is already reserved). ## Create a client [#create-a-client] ```ts import { BlitClient } from "@blitflow/sdk"; const client = new BlitClient({ apiKey: process.env.BLITFLOW_TOKEN, // "blit_…" PAT or session token }); ``` ### Options [#options] | Option | Default | Description | | ------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | | `apiKey` | — | Bearer token. When set, requests go to the public API with `Authorization: Bearer …` | | `baseUrl` | `https://studio.blitflow.com/api` (with `apiKey`), `/api` (without) | API origin override — point it at a Studio host's `/api` if needed | | `credentials` | `"include"` in session mode | `fetch` credentials mode | ### Two auth modes [#two-auth-modes] * **API-key mode** — pass `apiKey`; defaults to the public API. This is what scripts, servers, and CI use. * **Session mode** — construct with no `apiKey` in a browser that's signed in to the Studio: requests go to the same-origin `/api` with cookies. This is how the Studio's own frontend uses the SDK. ## The `ops` surface [#the-ops-surface] Every contract operation is available, fully typed, on `client.ops` — inputs are validated before sending and outputs parsed on receipt: ```ts const specs = await client.ops["nodes.list"]({ q: "image", limit: 5 }); const version = await client.ops["workflows.get"]({ id: "abc123", version: "2", }); const events = await client.ops["runs.create"]({ workflowRef: "abc123@2" }); // AsyncIterable ``` On top of `ops`, the client carries an ergonomic method per common operation — start with these: | Method | Purpose | | -------------------------------------------------- | ------------------------------------------------ | | `client.run(workflow, inputs, opts?)` | Run to completion, return outputs | | `client.startRun(workflow, inputs, opts?)` | Start a run, iterate events yourself | | `client.runNode(node, inputs, opts?)` | Run one node, no workflow needed | | `client.getWorkflow(ref)` | Fetch a published definition by `@` | | `client.searchNodes(opts?)` / `client.getNode(id)` | Node palette discovery | | `createWorkflow()` / `graphToWorkflow(…)` | Build workflow definitions in code | Errors from non-OK responses throw with the HTTP status and message — catch and inspect rather than retrying blindly. `@blitflow/sdk` is a **client** — it never touches model providers directly. The server-side execution engine (providers, node definitions) lives in `@blitflow/backend-sdk` and is a separate, self-hosting concern. # Nodes & single runs (/docs/sdk/nodes) ## Search the palette [#search-the-palette] ```ts import { BlitClient } from "@blitflow/sdk"; const client = new BlitClient({ apiKey: process.env.BLITFLOW_TOKEN }); const specs = await client.searchNodes({ query: "sprite", category: "image", limit: 10, }); ``` All options are optional — omit them to list the whole palette. ## Fetch one spec [#fetch-one-spec] ```ts const spec = await client.getNode("rd-fast"); // The spec's ports tell you exactly what to send: for (const [name, port] of Object.entries(spec.inputs)) { console.log( name, port.connector.id, port.connector.optionValues ?? "", port.connector.defaultValue ?? "", ); } ``` Read `optionValues` before sending enum-like inputs (`style`, modes, sizes) — values outside the enum fail validation. See [Nodes & specs](/docs/concepts/nodes). ## Run a single node [#run-a-single-node] For one-off inference, skip the workflow entirely: ```ts const { outputs } = await client.runNode("rd-fast", { prompt: "a brass key on black velvet", removeBg: true, seed: 7, // data ports take Artifacts: // init: { ref: "https://…/base.png" }, }); console.log(outputs.image); // { kind: "image", ref: "https://…", … } ``` Scalars (`string` / `number` / `boolean` / `null`) work for value ports; [Artifacts](/docs/concepts/artifacts) for images and other binary data. The call returns the node's outputs directly — no event stream. # Running workflows (/docs/sdk/running-workflows) Both run helpers accept the same `workflow` argument: an **inline `Workflow` object** or a **string reference** to a published workflow — `"@"`, where the id is the published `/` address (preferred, e.g. `"acme/sprite-pack@2.1.0"`) or the workflow's internal UUID (see [Versioning](/docs/concepts/versioning)). ## `run()` — run to completion [#run--run-to-completion] The simple path: streams events internally and resolves with the final outputs, or throws if the run fails. ```ts import { BlitClient } from "@blitflow/sdk"; const client = new BlitClient({ apiKey: process.env.BLITFLOW_TOKEN }); const outputs = await client.run("acme/sprite-pack@2.1.0", { prompt: { value: "isometric stone tower" }, photo: { ref: "https://example.com/tower.jpg" }, }); // outputs: Record console.log(outputs.image); // { kind: "image", ref: "https://…", … } ``` Inputs are [Artifacts](/docs/concepts/artifacts) keyed by the workflow's declared input names. ## `startRun()` — observe progress [#startrun--observe-progress] Returns immediately with a handle; iterate `events()` for per-step progress: ```ts const handle = await client.startRun("acme/sprite-pack@latest", inputs); for await (const event of handle.events()) { switch (event.type) { case "step.started": console.log(`▶ ${event.nodeId}`); break; case "step.progress": console.log(` ${event.nodeId}: ${event.message}`); break; case "step.completed": console.log( `✔ ${event.nodeId} ($${event.usage.costUsd}, ${event.usage.ms}ms)`, ); break; case "run.completed": return event.outputs; case "run.failed": throw new Error(`${event.error.code}: ${event.error.message}`); } } ``` The event union is documented in [Runs & streaming](/docs/concepts/runs). ## Running an inline definition [#running-an-inline-definition] Pass a `Workflow` object instead of a ref for one-off runs — nothing needs to be published: ```ts const outputs = await client.run( { version: 2, nodes: [ { id: "prompt", uses: "input", outputType: "TEXT" }, { id: "gen", uses: "rd-fast", inputs: { prompt: "${prompt.value}", seed: 7 }, }, { id: "image", uses: "output", inputs: { value: "${gen.image}" } }, ], }, { prompt: { value: "a brass key" } }, ); ``` See [Building workflows](/docs/sdk/building-workflows) for constructing these programmatically with validation. ## `getWorkflow()` — fetch a published definition [#getworkflow--fetch-a-published-definition] ```ts const { id, version, sequence, workflow } = await client.getWorkflow("acme/sprite-pack@2"); // version: "2.4.1" — the resolved highest 2.x.x ``` Useful for auditing what a ref resolves to, diffing versions, or grabbing a definition to modify and re-run inline.