Comfy Cloud (customComfy)
Most generation from an App Block goes through a bounded text-to-image body: you send a prompt, model, and a few params, and the host builds the generation graph for you. Comfy Cloud (customComfy) is the other path — it lets your block drive a server-owned ComfyUI workflow by name, for effects a simple txt2img body can't express (a panorama stitch, a multi-stage pipeline, a custom-node graph).
The defining constraint: your block never ships a ComfyUI graph. It sends a tiny body that names a workflow the platform already owns:
import type { WorkflowBodyCustomComfy } from '@civitai/app-sdk/blocks';
// The block picks a registered recipe id + a small, per-recipe-validated params
// object. It never sends a graph — the server owns the workflow in full.
const body: WorkflowBodyCustomComfy = {
kind: 'customComfy',
recipe: 'starter-comfy-txt2img', // a SERVER-registered, code-reviewed id
params: {
prompt: 'a serene alpine lake at golden hour',
// seed?: number | null — omit to let the orchestrator pick
},
};recipe selects the workflow; params is a small, bounded object the recipe validates. That's the entire wire surface.
Closed beta — access is limited
Comfy Cloud is part of the closed-beta Apps platform and is mod-gated. You can scaffold and run the sample against the local mock host today (see Try it locally); real generation needs closed-beta builder access, and each recipe is added by the Civitai team.
The recipe-gated model
A recipe is a fixed, server-registered, code-reviewed ComfyUI workflow, identified by a stable id (for example starter-comfy-txt2img). The recipe — not your block — owns:
- the ComfyUI graph itself,
- the resource allowlist (which checkpoints / LoRAs the graph may use),
- the checkpoint policy, and
- a hard per-generation Buzz ceiling (
maxBuzz) backed by an aggressive step timeout.
Your block influences none of that beyond choosing the recipe id and its params. An unknown or unregistered recipe is rejected fail-closed at the server boundary, and any params field the recipe's schema doesn't accept is stripped.
Why it works this way — the security model
A block runs in an untrusted sandboxed iframe. If a block could ship its own ComfyUI graph, it could pull in arbitrary custom nodes, arbitrary weights, and arbitrary compute — an unbounded, unreviewable surface. Pinning generation to a small set of reviewed, in-repo recipes keeps the graph, the resources, and the spend ceiling on Civitai's side of the boundary. It's the same "the host brokers, you don't" principle as the rest of the bridge, applied to ComfyUI.
The trade-off is deliberate: you cannot bring your own graph. If the registered recipes don't cover what you need, a new recipe has to be authored and reviewed on Civitai's side.
Requesting a new recipe
Because a recipe is a reviewed, in-repo artifact, adding one isn't self-serve — it's a change the Civitai team makes. Request one through the same channel as beta access: open a Request access issue on the civitai/cli repo describing the workflow you need (what the graph should do, the models involved, rough runtime), or reach out to the Civitai team if you already have builder access. There is no runtime or dashboard way to register a graph yourself.
Submitting a generation
You submit a customComfy body through the same useBuzzWorkflow() hook you use for text-to-image generation — the hook takes a full WorkflowBody discriminated union, so switching to Comfy Cloud is just a different body:
import { useBuzzWorkflow } from '@civitai/blocks-react';
import type { WorkflowBodyCustomComfy } from '@civitai/app-sdk/blocks';
export function RunButton({ prompt }: { prompt: string }) {
const { estimate, submit, poll, status, result } = useBuzzWorkflow();
const run = async () => {
const body: WorkflowBodyCustomComfy = {
kind: 'customComfy',
recipe: 'starter-comfy-txt2img',
params: { prompt },
};
await estimate(body); // display estimate → result.cost.total
const snap = await submit(body);
await poll(snap.workflowId); // drive to a terminal snapshot
};
return <button onClick={run} disabled={status !== 'confirming'}>Generate</button>;
}The host runs the estimate and submit server-side against your block token, re-checking scopes and budget every time — your block never talks to the orchestrator directly. estimate() returns a display estimate, not a firm quote; the exact charge is known only when the workflow reaches a terminal state (see How generation is billed).
Requirements
To use Comfy Cloud, your app must be a page app (Comfy Cloud is not offered to slot apps) and its manifest must:
- Request the
ai:write:budgetedscope. This is the budgeted-generation capability — the same scope text-to-image generation uses. - Set
page.buzzBudgetPerGen≥ the recipe's per-generation Buzz ceiling. The host mints each generation token withbuzzBudgetPerGenas its budget and gates every submit onrecipe.maxBuzz ≤ token.buzzBudget. If your per-gen budget is below the recipe's ceiling, every submit is rejected before it runs. Set it at or above the recipe's ceiling (with a little headroom).
{
"$schema": "https://civitai.com/schemas/app-block/v1.json",
"blockId": "my-comfy-app",
"version": "0.1.0",
"name": "My Comfy App",
"type": "block",
"scopes": ["ai:write:budgeted"],
"page": {
"path": "/",
"title": "My Comfy App",
"buzzBudgetPerGen": 40
},
"contentRating": "g",
"minApiVersion": "1.0",
"buildCommand": "npm run build",
"outputDir": "dist"
}The scaffold's Comfy Cloud sample pairs buzzBudgetPerGen: 40 with the starter-comfy-txt2img recipe (per-generation ceiling 30 Buzz) — comfortably above the ceiling, so submits are never budget-gated out.
How generation is billed
Comfy Cloud is post-paid with a hard ceiling — you don't pay a fixed price up front:
- Reserve the ceiling. On submit, the host reserves the recipe's declared
maxBuzzagainst the token budget (this is whybuzzBudgetPerGenmust cover it). - Cap the runtime. The recipe runs under an aggressive step timeout that physically bounds GPU-seconds, so the worst-case charge can't exceed the ceiling no matter what the graph does (roughly ~1 Buzz per GPU-second).
- Settle to actual. When the workflow reaches a terminal state, the charge settles down to the real runtime — a fast job costs less than the ceiling; the reservation only bounds the maximum.
So a recipe's ceiling is a worst case you provision for, not a flat price. Surface the estimate() value as an estimate in your UI, and read the final result.cost.total on completion.
Try it locally
The civitai CLI's generation scaffold (civitai app create, the page-app template that wires up Buzz + generation) ships a Comfy Cloud sample as a secondary path — a ready-made src/comfy.ts that builds a starter-comfy-txt2img body exactly like the snippets above, plus a unit test.
The mock host in npm run dev:harness implements the customComfy message handlers, so the sample runs with no backend — you can wire up the body, click generate, and see the mocked estimate/submit/poll round-trip locally before you ever have beta access:
civitai app create my-app # generation template ships src/comfy.ts
cd my-app && npm install
npm run dev:harness # mock host — Comfy Cloud sample runs, no backendReal generation (against a registered recipe) needs closed-beta access and npm run dev:live / a submitted app; see the Quickstart.
Not to be confused with orchestration recipes
Civitai uses the word "recipe" in two unrelated places:
- App Blocks
customComfyrecipes (this page) — server-registered ComfyUI workflows a block invokes by id through the host bridge. The block sends{ kind: 'customComfy', recipe, params }; it never holds a token for, or calls, the orchestrator directly. - Orchestration recipes — task-oriented examples for the public Orchestration REST API (WAN, Flux, upscalers, TTS, …), where you hold a Bearer token and POST workflows to
orchestration.civitai.comyourself.
They're different products with different auth models. If you're building an App Block, you want customComfy recipes; if you're calling the orchestration API directly, you want Orchestration recipes.
Next
- Quickstart — scaffold, run the harness, submit for review.
- Concepts — the block / trust-frame / bridge model.
- Scopes reference —
ai:write:budgetedand the full scope set. - Manifest reference —
page.buzzBudgetPerGenand every manifest field.