Skip to content

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(): UseBuzzWorkflowReturn

Orchestrates 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.

membertypenotes
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.
statusWorkflowStatus
resultBlockWorkflowSnapshot | null
errorError | 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).

members
  • WorkflowBodyTextToImage
  • WorkflowBodyCustomComfy

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

fieldtypenotes
kind'textToImage'
modelIdnumber
modelVersionIdnumber
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?BlockSourceImageOptional 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?stringOptional 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?BuzzAccountTypeOptional 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}.
paramsBlockTextToImageParams

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.

fieldtypenotes
promptstring
negativePrompt?string
cfgScale?numberRange 1–30.
sampler?stringSampler name (e.g. 'Euler', 'DPM++ 2M Karras'). Defaults to 'Euler'.
steps?numberRange 1–50.
seed?number | null`null` lets the orchestrator pick.
width?numberRange 64–2048. Defaults to 1024 for SDXL/Flux, 512 for SD1/SD2.
height?numberRange 64–2048. Same defaults as width.
clipSkip?numberPer-resource CLIP layer skip count (SD1/SDXL). Range 0–12. Flux ignores it.
quantity?numberRange 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.

fieldtypenotes
urlstring
widthnumber
heightnumber

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.

fieldtypenotes
kind'customComfy'
recipestringA **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`).

fieldtypenotes
workflowIdstring
status'pending' | 'processing' | 'succeeded' | 'failed' | 'expired' | 'canceled'
cost?{ total: number; }
imageUrls?string[]
error?string
spentAccountType?BuzzAccountTypeThe 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.

fieldtypenotes
workflowIdstring
status'pending' | 'processing' | 'succeeded' | 'failed' | 'expired' | 'canceled'
imagesAppWorkflowImage[]
costnumber | null
createdAtstringISO-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.

fieldtypenotes
urlstring
widthnumber | null
heightnumber | null
nsfwLevelnumber | 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:

kindwhat it addresseshow you name the model
textToImagea Civitai checkpointnumeric modelId + modelVersionId
customComfya server-registered ComfyUI recipea 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:

json
{
  "$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:

  1. Is it a Civitai checkpoint? — if it has a Civitai model version, use textToImage with its modelId + modelVersionId. Models that feel "orchestrator-only" usually aren't: Z-Image and Qwen are ordinary checkpoints with ordinary ids.
  2. Do you need an edit? — pass a sourceImage and name the edit version (see the worked example). The variant is chosen by sourceImage, not by the version id — this is the single most common mistake on the bridge.
  3. Is it genuinely outside the union? — today that means multi-image editing (sourceImage is singular) and background removal (no union member reaches it). customComfy is 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.
  4. 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:

modelmodelIdmodelVersionIduse it for
Z Image Turbo21689352442439txt2img
Qwen — "Image Edit 2511"22680632558804edit (send a sourceImage)
Qwen — "fp8_e4m3fn"22680632552908txt2img

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 appsourceImage is rejected on a model-bound token.

tsx
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. sourceImage is 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 https URLs only. civitai.com, civitai.red, civitai.green and their subdomains. An arbitrary remote URL is rejected.
  • Page apps only. sourceImage is rejected fail-closed on a model-bound token, the same restriction additionalResources carries. 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:edit graph. SD-family ecosystems get plain img2img ("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 bridgeWhere 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:

ConstraintWhat to do instead
sourceImage on a model-bound (model.*) blockbuild a page app
Shipping your own ComfyUI graphnever available — graphs stay server-side, by design
Choosing edit vs img2img yourselfit 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:

recipewhat it doesparams (.strict())per-generation Buzz ceiling
seamless-pano-360360° seamless panorama, fixed 2048×1024{ prompt, seed?, engine?, accountType? }engine is one of zimage-turbo, flux2-klein, qwen-image90 / 150 / 180, by engine
starter-comfy-txt2imgsingle-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

Civitai Developer Documentation