# Authentication (/docs/authentication) Every API call is authenticated with a bearer token: ``` Authorization: Bearer ``` 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 accept a `maxCostUsd` cap, and the server enforces its own ceiling on top. 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" --max-cost 0.10 ``` ## 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 ``` `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 | | `--max-cost N` | Cap the run's spend in USD | | `--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 \ --max-cost 0.10 # run a published workflow by its address blitflow run acme/sprite-pack@2.1.0 \ -i prompt="isometric stone tower" --max-cost 0.25 # 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. # Overview (/docs) 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` | `searchNodes(client, …)` | `nodes_list` | | `nodes.get` | `GET /v1/nodes/:id` | `getNode(client, id)` | `nodes_get` | | `runs.create` | `POST /v1/runs` | `run(…)` / `startRun(…)` | `runs_create` | | `workflows.get` | `GET /v1/workflows/:id` | `getWorkflow(client, ref)` | `workflows_get` | | `runs.node` | `POST /v1/runs/node` | `client.ops["runs.node"](…)` | `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, searchNodes } from "@blitflow/sdk"; const client = new BlitClient({ apiKey: process.env.BLITFLOW_TOKEN }); const specs = await searchNodes(client, { 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, run } from "@blitflow/sdk"; const client = new BlitClient({ apiKey: process.env.BLITFLOW_TOKEN }); // Run a workflow you published from the Studio editor: const outputs = await run( client, "abc123@latest", // or "abc123@2.1.0", or an inline Workflow object { prompt: { kind: "text", value: "a brass key on black velvet" } }, { maxCostUsd: 0.25 }, ); ``` `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": { "kind": "text", "value": "a brass key on black velvet" } }, "maxCostUsd": 0.25 }' ``` 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] # 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). ### 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" \ -H "Authorization: Bearer $BLITFLOW_TOKEN" ``` ```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] ``` POST /v1/runs ``` 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 | | `maxCostUsd` | number | Spend cap for this run, in USD. Defaults to $0.50 when omitted; the server caps requests at its $1.00 per-run ceiling | 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": {"kind": "text", "value": "isometric stone tower"}}, "maxCostUsd": 0.25}' ``` ``` 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 | | `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 | | `maxCostUsd` | number | Spend cap in USD | ### 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 }, maxCostUsd: 0.1, }); ``` # 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: ```json { "inputs": { "prompt": { "kind": "text", "value": "isometric stone tower" }, "photo": { "kind": "image", "ref": "https://example.com/tower.jpg" } } } ``` To feed an existing image into a node (e.g. an `init` port for image-to-image), pass a **ref artifact** pointing at any fetchable URL. Single-node runs (`runs.node`) are more lenient: value ports accept bare scalars (`"a prompt"`, `7`, `true`) and data ports take 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. * **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. # 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**. ## Node specs [#node-specs] A spec describes a node's exact interface: ```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 } } } } ``` | Field | Description | | ---------------------------------- | ----------------------------------- | | `id` | Node id, e.g. `rd-fast`, `llm` | | `kind` | Node kind (`model`, `llm`, `const`) | | `title`, `category`, `description` | Display metadata | | `inputs` / `outputs` | Ports, keyed by name | 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 startRun(client, "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). Two layers protect you: 1. **Your cap** — pass `maxCostUsd` on every run. If the accumulated step cost would exceed it, the run fails instead of overspending. 2. **The server ceiling** — the platform enforces its own limits regardless of what the client asks for: currently a **$0.50 default** when `maxCostUsd` is omitted, and a **$1.00 per-run ceiling** that client requests are capped to. ```json { "workflowRef": "abc123@latest", "inputs": { … }, "maxCostUsd": 0.25 } ``` Practical habits: * Set your own `maxCostUsd` lower than the ceiling so a bug can't burn budget. * 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**: publishing from the Studio freezes the workflow's slug under its organization, and from then on the address permanently identifies that workflow — the slug does not change when the workflow's display name does, and it is never re-registered for a different workflow. Deleting a published workflow retires its name forever: the slug is tombstoned and can never be claimed by another workflow in the organization, so an old address can never start resolving to something else. An 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, getWorkflow } from "@blitflow/sdk"; const client = new BlitClient({ apiKey: process.env.BLITFLOW_TOKEN }); const { id, version, sequence, workflow } = await getWorkflow( client, "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] ```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. # 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 SDK also ships a stdio MCP server, `blitflow-mcp`, which authenticates with a [personal access token](/docs/authentication) instead of OAuth — useful for headless environments or when pointing at a non-production API host. | Env var | Required | Description | | ------------------ | -------- | ------------------------- | | `BLITFLOW_TOKEN` | yes | Your `blit_…` token | | `BLITFLOW_API_URL` | no | Override the API base URL | ```json { "mcpServers": { "blitflow": { "command": "blitflow-mcp", "env": { "BLITFLOW_TOKEN": "blit_..." } } } } ``` ## 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** — agents should pass `maxCostUsd` on every run. 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:** the hosted server exposes 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. The local stdio server registers the raw dotted names. ## `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 | | `maxCostUsd` | number | no | Spend cap in USD — **set this on every run** | 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 | | `maxCostUsd` | number | no | Spend cap in USD | Returns `{ outputs: Record }`. ## Agent playbook [#agent-playbook] 1. **Discover** — `nodes_list` with `q`/`category`, then `nodes_get` the candidate. Build inputs strictly from the spec. 2. **Cap spend** — always pass `maxCostUsd`; prefer the cheapest node that clears the quality bar. 3. **Fetch refs** — binary outputs are ref artifacts; download the `ref` URL for bytes, and reuse persisted refs instead of regenerating. 4. **Stop on failure** — a `run.failed` / error result is terminal. Read the message, fix the inputs, then retry deliberately. # 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`). ```jsonc { "name": "nodes_list", "arguments": { "q": "image", "limit": 20 } } ``` Then inspect one: ```jsonc { "name": "nodes_get", "arguments": { "id": "rd-fast" } } ``` ```ts import { BlitClient, getNode, searchNodes } from "@blitflow/sdk"; const client = new BlitClient({ apiKey: process.env.BLITFLOW_TOKEN }); const nodes = await searchNodes(client, { query: "image", limit: 20 }); const spec = await getNode(client, "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 }`. Every call accepts a `maxCostUsd` ceiling. 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`). 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 }, "maxCostUsd": 0.5, }, } ``` 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.ops["runs.node"]({ node: "rd-fast", inputs: { prompt: "an isometric wooden desk, pixel art", seed: 7 }, maxCostUsd: 0.5, }); console.log(outputs.image); // { kind: "image", ref: "https://…", … } ``` ```bash blitflow node rd-fast \ --input prompt="an isometric wooden desk, pixel art" \ --input seed=7 \ --max-cost 0.5 ``` 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": { "kind": "text", "value": "a cozy isometric office" }, }, "maxCostUsd": 0.75, }, } ``` The tool consumes the event stream internally and returns the final output artifacts. ```ts import { BlitClient, run, startRun } from "@blitflow/sdk"; const client = new BlitClient({ apiKey: process.env.BLITFLOW_TOKEN }); const inputs = { prompt: { kind: "text", value: "a cozy isometric office" } }; // Just the final outputs: const outputs = await run(client, "abc123@2.1.0", inputs, { maxCostUsd: 0.75 }); // Or follow per-step progress: const handle = await startRun(client, "abc123@2.1.0", inputs, { maxCostUsd: 0.75, }); 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" \ --max-cost 0.75 # 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": { "kind": "text", "value": "a cozy isometric office" } }, "maxCostUsd": 0.75 }' ``` The response is a Server-Sent Events stream — read `data:` lines until `run.completed` / `run.failed`. See [POST /v1/runs](/docs/api/runs). # 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, getNode, run } 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 run(client, workflow, { prompt: { kind: "text", 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 | 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. # 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 SDK exports ergonomic helpers — start with these: | Helper | Purpose | | ---------------------------------------------------- | ------------------------------------------------ | | `run(client, workflow, inputs, opts?)` | Run to completion, return outputs | | `startRun(client, workflow, inputs, opts?)` | Start a run, iterate events yourself | | `getWorkflow(client, ref)` | Fetch a published definition by `@` | | `searchNodes(client, opts?)` / `getNode(client, 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, searchNodes } from "@blitflow/sdk"; const client = new BlitClient({ apiKey: process.env.BLITFLOW_TOKEN }); const specs = await searchNodes(client, { query: "sprite", category: "image", limit: 10, }); ``` All options are optional — omit them to list the whole palette. ## Fetch one spec [#fetch-one-spec] ```ts import { getNode } from "@blitflow/sdk"; const spec = await getNode(client, "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.ops["runs.node"]({ node: "rd-fast", inputs: { prompt: "a brass key on black velvet", removeBg: true, seed: 7, // data ports take Artifacts: // init: { kind: "image", ref: "https://…/base.png" }, }, maxCostUsd: 0.1, }); 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, run } from "@blitflow/sdk"; const client = new BlitClient({ apiKey: process.env.BLITFLOW_TOKEN }); const outputs = await run( client, "acme/sprite-pack@2.1.0", { prompt: { kind: "text", value: "isometric stone tower" }, photo: { kind: "image", ref: "https://example.com/tower.jpg" }, }, { maxCostUsd: 0.25 }, ); // 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 import { startRun } from "@blitflow/sdk"; const handle = await startRun(client, "acme/sprite-pack@latest", inputs, { maxCostUsd: 0.5, }); 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 run( client, { 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: { kind: "text", 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 import { getWorkflow } from "@blitflow/sdk"; const { id, version, sequence, workflow } = await getWorkflow( client, "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.