Generation bridge reference
This is the field-level contract for spending a viewer's Buzz on a generation from a block: the WorkflowBody your block sends, the useBuzzWorkflow() lifecycle that carries it (estimate → submit → poll → cancel), and the BlockWorkflowSnapshot you get back.
The field tables below are generated from the published @civitai/app-sdk and @civitai/blocks-react type definitions — the same JSDoc your editor shows — so they can't drift from the packages you install. For the narrative version (with worked img2img / LoRA examples and the page-vs-model rules) start with the text-to-image generation guide; for the ComfyUI recipe path see Comfy on Civitai.
Before you design against the field tables, read what the bridge can and cannot do — the bridge is a narrower surface than the orchestrator, so orchestrator step JSON can't be sent from a block, and reaching a model is a matter of naming the right modelVersionId rather than describing an engine.
Trust model
useBuzzWorkflow() is host-mediated: the host resolves the viewer from the block token and runs the estimate/submit/cancel on Civitai's side of the iframe boundary, re-checking scope + budget every time. Your block never holds an orchestrator credential.
useBuzzWorkflow() lifecycle
useBuzzWorkflow(): UseBuzzWorkflowReturnOrchestrates the estimate → confirm → submit → poll dance through the host-mediated `postMessage` path. The host enforces budget rules (`cost_estimate <= token.buzzBudget`) before forwarding to the orchestrator; submit() will reject if the host refuses. Block apps should call `useBuzzPurchase().openPurchaseModal()` when that happens. The hook does NOT auto-poll: after `submit` flips `status` to `'polling'`, the caller runs a `useEffect` that calls `poll(workflowId)` on a backoff until the snapshot is terminal. `status === 'confirming'` is IDLE (estimate landed, user reviewing cost) — keep the Generate button enabled. `estimate`/`submit` take a full {@link WorkflowBody} — the discriminated union keyed by `kind`, so either a `textToImage` body (`{ kind, modelId, modelVersionId, params }`) or a `customComfy` recipe body (`{ kind, recipe, params }`), not a bare `{ prompt }`. The hook forwards the body to the host verbatim and never reads variant-specific fields, so both members flow through unchanged.
| member | type | notes |
|---|---|---|
estimate | (body: WorkflowBody) => Promise<BlockWorkflowSnapshot> | |
submit | (body: WorkflowBody) => Promise<BlockWorkflowSnapshot> | |
poll | (workflowId: string) => Promise<BlockWorkflowSnapshot> | |
cancel | (workflowId: string) => Promise<BlockWorkflowSnapshot> | Cancel a running workflow on the orchestrator (a real server-side stop, not just client-side untracking). The host re-derives ownership from the viewer's orchestrator token, so this can only cancel workflows the viewer owns; the orchestrator rejects others. Resolves with the workflow's (now-canceled) snapshot. |
status | WorkflowStatus | |
result | BlockWorkflowSnapshot | null | |
error | Error | null |
WorkflowBody union
Body the block sends to `useBuzzWorkflow().{submit,estimate}`. A real discriminated union keyed by `kind`: - {@link WorkflowBodyTextToImage} (`kind: 'textToImage'`) — the original checkpoint/LoRA/img2img generation body (unchanged, back-compatible). - {@link WorkflowBodyCustomComfy} (`kind: 'customComfy'`) — a bounded, server-registered ComfyUI recipe (post-paid; the iframe never sends a graph). New kinds extend this union as the host gains support for them. Narrow on `body.kind` before touching member-specific fields (e.g. `modelId`/`params` live only on the `textToImage` member).
WorkflowBodyTextToImageWorkflowBodyCustomComfy
WorkflowBodyTextToImage object
The text-to-image member of the {@link WorkflowBody} discriminated union (`kind: 'textToImage'`). This is the original, single-member shape — kept byte-identical for backward compatibility. An existing `{ kind: 'textToImage', modelId, modelVersionId, params }` body must continue to satisfy {@link WorkflowBody} unchanged. Both `modelId` and `modelVersionId` are required even though they're conceptually redundant — the host validates that `modelId` matches the JWT's `ctx.modelId` (context binding) AND that the version belongs to that model (DB lookup). The block always has both values from `useBlockContext().context as ModelSlotContext`.
| field | type | notes |
|---|---|---|
kind | 'textToImage' | |
modelId | number | |
modelVersionId | number | |
additionalResources? | Array<{ modelVersionId: number; strength?: number; }> | Optional additional generation resources (LoRAs) layered on top of the checkpoint (`modelVersionId`). Mirrors civitai's `blockWorkflowBodySchema`: - max 5 entries - each: { modelVersionId: positive int, strength?: number in [-1, 2], default 1 } - the server is LoRA-only for additional resources (non-LoRA versions are rejected) and enforces base-model-family compatibility with the checkpoint + per-resource entitlement (early-access/Private) before any Buzz spend. Omit for a checkpoint-only generation (backward compatible). |
sourceImage? | BlockSourceImage | Optional img2img init/source image (App Blocks IMAGE bridge). When present, the block bridge emits an `img2img` graph workflow instead of `txt2img`; omit for a plain text-to-image generation (backward compatible). Constraints (all SERVER-ENFORCED — mirrors civitai's merged `blockTextToImageBodySchema`): - `url` must be a Civitai-hosted https image (an uploaded image from {@link BlockUploadedImageInfo.url} qualifies) — never an arbitrary remote URL. - SD-family checkpoints ONLY (a non-SD-family checkpoint is rejected fail-closed). - PAGE apps only — the server rejects `sourceImage` on a model-bound token. |
sharedContentKey? | string | Optional shared-storage key of the published content this generation runs on behalf of. The server resolves it to the content's author for attribution (see the G5 civitai PR). Opaque string — the block passes back the `key` it got from `useSharedStorage()` for the content being generated against; omit when not applicable. |
accountType? | BuzzAccountType | Optional preferred Buzz pool to fund this generation from — a *preference*, not a guarantee. The host clamps it server-side to what the viewer actually holds and to the domain-allowed pools (a `blockWorkflowBodySchema` field on civitai/civitai; preferred-first, then falls back). Omit for today's default host-chosen funding order (backward compatible). Whichever pool ended up the primary funder is echoed back on {@link BlockWorkflowSnapshot.spentAccountType}. |
params | BlockTextToImageParams |
BlockTextToImageParams object
Generation parameters a block can override. All optional — the host fills sensible defaults (sampler='Euler', steps=25, dimensions from the base-model family) when omitted, so the simplest block can submit `{ kind: 'textToImage', modelId, modelVersionId, params: { prompt } }`. Bounds mirror civitai/civitai's `blockWorkflowBodySchema` zod gate; over- limit values are rejected server-side before reaching the orchestrator.
| field | type | notes |
|---|---|---|
prompt | string | |
negativePrompt? | string | |
cfgScale? | number | Range 1–30. |
sampler? | string | Sampler name (e.g. 'Euler', 'DPM++ 2M Karras'). Defaults to 'Euler'. |
steps? | number | Range 1–50. |
seed? | number | null | `null` lets the orchestrator pick. |
width? | number | Range 64–2048. Defaults to 1024 for SDXL/Flux, 512 for SD1/SD2. |
height? | number | Range 64–2048. Same defaults as width. |
clipSkip? | number | Per-resource CLIP layer skip count (SD1/SDXL). Range 0–12. Flux ignores it. |
quantity? | number | Range 1–4. Defaults to 1. |
BlockSourceImage object
A Civitai-hosted source image for an img2img generation. Mirrors civitai's merged `blockTextToImageBodySchema.sourceImage` (`{ url, width, height }`). `url` MUST resolve to a Civitai-controlled host — the server rejects an arbitrary remote URL (SSRF / arbitrary-fetch). An image obtained from {@link BlockUploadedImageInfo.url} (via `useImageUpload`) satisfies this. `width`/`height` are the intrinsic dimensions the graph uses for its denoise/aspect derivation.
| field | type | notes |
|---|---|---|
url | string | |
width | number | |
height | number |
WorkflowBodyCustomComfy object
The `customComfy` member of the {@link WorkflowBody} discriminated union (`kind: 'customComfy'`) — runs a **server-registered, code-reviewed ComfyUI recipe** end-to-end. This is a bounded, fail-closed primitive: the iframe NEVER sends a Comfy graph. It sends only a registered `recipe` id plus a small, per-recipe-validated `params` object; the civitai server owns the entire graph (built by object construction, so the prompt is a leaf value that cannot perturb graph topology). Trust / safety model (all SERVER-ENFORCED — mirrors civitai's forthcoming `blockCustomComfyBodySchema`): - `recipe` is a **registered recipe id** resolved against a code-reviewed, in-repo recipe registry. An unknown id is **rejected server-side, fail- closed** (the schema enum is derived from the registry keys) — there is no way for a block to run an arbitrary/unreviewed graph. - `params` are **bounded and validated per-recipe** by the server's `.strict()` Zod param schema (extra fields rejected); each recipe pins its own resources (checkpoint/LoRA/diffusion AIRs) — the block cannot supply them. Billing is **post-paid** (the underlying orchestrator `customComfy` step bills measured GPU runtime at a fixed rate, so there is NO exact pre-price): - `estimate` returns a per-recipe **display estimate**, not a firm quote — surface it as an estimate; the actual cost is known only on terminal. - Each recipe declares a hard per-job `maxBuzz` ceiling backed by an aggressive step `timeout`; the orchestrator physically caps the job at that ceiling server-side (worst-case Buzz = the timeout in seconds), and the host gates `maxBuzz <= token.buzzBudget` before submit. A single job therefore cannot exceed the recipe's declared ceiling no matter what.
| field | type | notes |
|---|---|---|
kind | 'customComfy' | |
recipe | string | A **registered recipe id** (e.g. `'seamless-pano-360'`). Resolved server- side against the code-reviewed recipe registry; an unknown id is rejected fail-closed. The recipe fixes the graph, the resource allowlist, the checkpoint policy, and the `maxBuzz`/`timeout` budget ceiling — none of which the block can influence beyond selecting the recipe + `params`. |
params | { prompt: string; seed?: number | null; engine?: string; accountType?: BuzzAccountType; } | Bounded, per-recipe-validated parameters. Only the fields a recipe's `.strict()` Zod schema accepts are honored; extra fields are rejected server-side. The common shape: - `prompt` — the generation prompt (a leaf string; injected into the server-built graph by object construction, never string-templated). - `seed` — optional; `null`/omitted lets the orchestrator pick. - `engine` — optional recipe engine-variant selector (e.g. a DiT engine); the recipe defaults it when omitted. - `accountType` — optional preferred Buzz pool (a preference, clamped server-side to pools the viewer actually holds; see {@link BuzzAccountType}). |
BlockWorkflowSnapshot object
The host-mediated view of an orchestrator workflow that an iframe block receives over `postMessage`. This is intentionally a flattened **subset** of `WorkflowSnapshot` from `../orchestrator/` — the host (civitai.com) maps the full orchestrator payload down to this shape before forwarding. Notable differences: - `workflowId` here = orchestrator's `id` - `imageUrls` here = flattened from `steps[].output.images[].url` - `cost.total` is the host-attested total (not the raw orchestrator field) - the status union omits orchestrator-internal states like `unassigned` If the orchestrator gains a status the host doesn't recognize, the host is responsible for mapping it to one of the values here (typically `processing` or `failed`).
| field | type | notes |
|---|---|---|
workflowId | string | |
status | 'pending' | 'processing' | 'succeeded' | 'failed' | 'expired' | 'canceled' | |
cost? | { total: number; } | |
imageUrls? | string[] | |
error? | string | |
spentAccountType? | BuzzAccountType | The Buzz pool that was the PRIMARY FUNDER of this generation — i.e. the account with the LARGEST debit, which the host stamps onto the workflow snapshot server-side. This is NOT necessarily "the paid account": a generation covered mostly by free/earned Buzz reports `spentAccountType: 'blue'`. Populated by the host from the Phase-1 backend `spentAccountType` field; absent when the host predates it or no spend occurred. Informational only — surface it (e.g. "funded from your yellow balance") but don't gate on it. |
autoClaim? | { type: 'dailyBoost'; amount: number; accountType: 'yellow' | 'blue' | 'red' | 'green'; } | Set when the host opportunistically claimed a Buzz reward on the user's behalf during submit. Currently the host only fires this for the daily boost (25 blue Buzz, one per UTC day) when the user's balance would otherwise have been short by less than the boost amount. Informational only — the block has no obligation to reconcile state (the claim already settled in the orchestrator). A typical block UX surfaces a small "+25 daily boost claimed" notice next to the succeeded result. |
AppWorkflow object
The clean, wire-stable projection of ONE orchestrator workflow in the calling app's own generator SUBQUEUE — what `QUERY_APP_WORKFLOWS` / `CANCEL_APP_WORKFLOW` return. Mirrors civitai/civitai's `AppWorkflow` / `projectAppWorkflow` (`src/server/services/blocks/workflow.service.ts`, PR #3164) EXACTLY — KEEP IN LOCKSTEP; a drift here silently strands the reply's transport validator. The host deliberately DROPS every internal/sensitive workflow field (steps, params, prompts, resources, tokens, transactions, metadata, tags) so a block can never read generation internals of a queue it only owns by tag. status: the block-contract status — the orchestrator's `unassigned`/`preparing`/`scheduled` all collapse to `pending`. images: only `available` blobs with a non-null url (see {@link AppWorkflowImage}). cost: the workflow's realized/estimated buzz total, or `null` when absent. createdAt: ISO-8601 string.
| field | type | notes |
|---|---|---|
workflowId | string | |
status | 'pending' | 'processing' | 'succeeded' | 'failed' | 'expired' | 'canceled' | |
images | AppWorkflowImage[] | |
cost | number | null | |
createdAt | string | ISO-8601. |
AppWorkflowImage object
One result image on an {@link AppWorkflow}. Mirrors civitai/civitai's `AppWorkflowImage` projection (`src/server/services/blocks/workflow.service.ts`, PR #3164) — keep in lockstep. Only `available` blobs with a non-null url are surfaced by the host (pending/blocked blobs are dropped rather than handing the block dead links). `width`/`height` are `null` until the orchestrator populates them; `nsfwLevel` is the numeric civitai browsing-level bitflag (1/2/4/8/16), `null` for an unrated blob.
| field | type | notes |
|---|---|---|
url | string | |
width | number | null | |
height | number | null | |
nsfwLevel | number | null |
What the bridge can and cannot do
The generation bridge is a deliberately narrower surface than the orchestrator, not a thin proxy in front of it. The body your block sends is a two-member discriminated union keyed by kind, and anything outside those two shapes is rejected at the wire schema — in the host, before any orchestrator call is made.
The two members are the whole surface:
kind | what it addresses | how you name the model |
|---|---|---|
textToImage | a Civitai checkpoint | numeric modelId + modelVersionId |
customComfy | a server-registered ComfyUI recipe | a registered recipe id |
Orchestrator step JSON cannot be sent from a block
If you have been handed an orchestrator step — a $type object shaped like this:
{
"$type": "imageGen",
"input": {
"engine": "sdcpp",
"ecosystem": "zImage",
"model": "turbo",
"operation": "createImage"
}
}— that is correct for the orchestrator and unusable from a block. The bridge body has no $type field and no imageGen kind, and none of ecosystem / model / operation / engine is how a block names a model. Such a body fails the kind union before the host does anything else.
The symptom is distinctive: every generation fails identically, on every model, with no per-model variation — because nothing model-specific ever ran. If you are seeing "it fails on anything", check the body shape first.
Orchestrator step JSON belongs to the Orchestration REST API, where you hold a Bearer token. Blocks never hold one — see not to be confused with orchestration recipes.
"The model I want isn't reachable" — what to do
Most of the time it is reachable, and the fix is naming the right modelVersionId. Work down this path:
- Is it a Civitai checkpoint? — if it has a Civitai model version, use
textToImagewith itsmodelId+modelVersionId. Models that feel "orchestrator-only" usually aren't: Z-Image and Qwen are ordinary checkpoints with ordinary ids. - Do you need an edit? — pass a
sourceImageand name the edit version (see the worked example). The variant is chosen bysourceImage, not by the version id — this is the single most common mistake on the bridge. - Is it genuinely outside the union? — today that means multi-image editing (
sourceImageis singular) and background removal (no union member reaches it).customComfyis the only other path, so this becomes a registered recipe question. Say so explicitly when you ask: both recipes today are prompt-only txt2img, so anything taking an image input is new ground, not a variation on an existing recipe. - Recipes are not self-serve. The registry is server-side and code-reviewed; there is no runtime, manifest, or dashboard way to add one. Adding a recipe is a platform request — ask through the same channel as beta access, described in requesting a new recipe.
The ids you probably want
These are the checkpoints developers most often assume are out of reach. They are not — they are normal textToImage targets:
| model | modelId | modelVersionId | use it for |
|---|---|---|---|
| Z Image Turbo | 2168935 | 2442439 | txt2img |
| Qwen — "Image Edit 2511" | 2268063 | 2558804 | edit (send a sourceImage) |
| Qwen — "fp8_e4m3fn" | 2268063 | 2552908 | txt2img |
The Qwen model name and its edit version disagree
Both Qwen versions live under one modelId (2268063), and the model is named "Qwen-Image-2512" while its edit version is named "Image Edit 2511". Reading 2512 off the model and treating it as the version you want lands you on the txt2img version. The edit version is 2558804.
Omitting sourceImage silently switches you to a different MODEL
The workflow variant is derived from whether sourceImage is present, not from the version id you name. Name the edit version but leave sourceImage off, and the bridge builds a txt2img graph — and then, because the edit version isn't valid for txt2img, it re-maps your model to that version's txt2img sibling and generates with that. For Qwen, asking for 2558804 without a source image gets you 2552908. It does not warn you, and it does not fail.
That is the worst failure mode available here, because it looks like success: the workflow succeeds, images render, and nothing in the BlockWorkflowSnapshot reports either substitution. You did not get a weaker version of what you asked for — you got a different model, and the only tell is that the output ignores your source image and doesn't behave like an edit.
The fix is in your body, not in a support request: send sourceImage whenever you mean to edit, and name the edit version (2558804) explicitly.
Worked example: Qwen single-image edit
The case people get wrong. Note both halves: the edit version id and the sourceImage. This is a page app — sourceImage is rejected on a model-bound token.
import { useBuzzWorkflow, useImageUpload } from '@civitai/blocks-react';
import type { WorkflowBodyTextToImage } from '@civitai/app-sdk/blocks';
// PAGE APP ONLY — `sourceImage` is rejected fail-closed on a model-bound token.
export function QwenEdit() {
const { submit } = useBuzzWorkflow();
const { open } = useImageUpload({ purpose: 'generationSource' });
const run = async () => {
const source = await open(); // Civitai-hosted { url, width, height }
if (!source) return;
const body: WorkflowBodyTextToImage = {
kind: 'textToImage',
modelId: 2268063,
modelVersionId: 2558804, // "Image Edit 2511" — NOT the model's default
sourceImage: { url: source.url, width: source.width, height: source.height },
params: { prompt: 'make the sky stormy' },
};
await submit(body);
};
return <button onClick={run}>Edit image</button>;
}Drop the sourceImage line and this silently becomes a txt2img generation.
What sourceImage can and cannot do
sourceImage is the optional img2img / edit input on a textToImage body. Its limits are structural, not tuning knobs:
- One image, never several.
sourceImageis a single{ url, width, height }object, not an array. Multi-image editing — a reference plus a target, or compositing two inputs — is not expressible. - Civitai-hosted
httpsURLs only.civitai.com,civitai.red,civitai.greenand their subdomains. An arbitrary remote URL is rejected. - Page apps only.
sourceImageis rejected fail-closed on a model-bound token, the same restrictionadditionalResourcescarries. See page-vs-model constraints. - You do not choose edit vs img2img — the checkpoint's ecosystem does. Edit-capable ecosystems (Qwen, Qwen2, Seedream, NanoBanana, OpenAI, Flux2 and the Flux2-Klein variants) get an
img2img:editgraph. SD-family ecosystems get plainimg2img("Image Variations") instead. A checkpoint whose ecosystem supports neither is rejected fail-closed. There is no body field that overrides this.
Not supported today
Two things are genuinely out of reach, and they are the only two worth opening a platform request for:
| Not available through the bridge | Where it stands |
|---|---|
| Multi-image editing (two or more inputs) | sourceImage is singular, and no registered recipe takes an image input — a platform request |
| Background removal (e.g. BiRefNet) | a first-class orchestrator step, not a Comfy graph and not an imageGen operation — no union member reaches it |
These are bounded by the union's shape, not by configuration:
| Constraint | What to do instead |
|---|---|
sourceImage on a model-bound (model.*) block | build a page app |
| Shipping your own ComfyUI graph | never available — graphs stay server-side, by design |
| Choosing edit vs img2img yourself | it follows from the checkpoint's ecosystem; pick the checkpoint accordingly |
Note what is not on these lists: single-image editing and Z-Image both work through textToImage today — see the ids you probably want.
The registered recipes
customComfy accepts only a registered recipe id — an unregistered id is rejected at the union, before the recipe is resolved. Today there are exactly two:
recipe | what it does | params (.strict()) | per-generation Buzz ceiling |
|---|---|---|---|
seamless-pano-360 | 360° seamless panorama, fixed 2048×1024 | { prompt, seed?, engine?, accountType? } — engine is one of zimage-turbo, flux2-klein, qwen-image | 90 / 150 / 180, by engine |
starter-comfy-txt2img | single-step Z-Image txt2img, fixed 1024×1024 | { prompt, seed?, accountType? } | 30 |
Both param schemas are .strict(): a field that isn't listed is rejected, not ignored. Note what is not exposed — neither recipe takes width / height, steps, or CFG. Those are fixed server-side (the starter recipe runs at the Z-Image turbo defaults).
Don't reach for starter-comfy-txt2img to get Z-Image
It is a fixed-resolution, prompt-and-seed demo starter, not the Z-Image path. For Z-Image generation use textToImage with 2168935 / 2442439, which gives you the full param surface (dimensions, steps, sampler, quantity). Reach for the recipe only when you want exactly what it does.
See also
- What the bridge can and cannot do — the boundary vs the orchestrator, the ids for Z-Image and Qwen edit, and the two gaps that need a platform request.
- Generation guide — the narrative walkthrough (img2img, LoRAs, page-vs-model).
- Comfy on Civitai (customComfy) — the recipe-gated ComfyUI path.
- Hooks reference — every
@civitai/blocks-reacthook. - Messages reference — the
postMessageprotocol these hooks sit on.