Generation hooks
useBuzzWorkflow and the hooks that list, publish and resource a generation.
This page documents 4 of the 47 hooks in the Hooks reference, which indexes every hook by name. Generated from the published @civitai/blocks-react@0.73.0 package's type definitions and README.
Other pages: Context, frame and analytics hooks · Batch, confirmed-step and training hooks · Buzz, purchase and tip hooks · Viewer, consent and navigation hooks · Storage, picker and media hooks
useBuzzWorkflow
useBuzzWorkflow(): UseBuzzWorkflowThe generation flow: `estimate` → `submit` → `poll`, host-mediated. Returns `{ estimate, submit, poll, status, result, error }`.
import type { WorkflowBody } from '@civitai/app-sdk/blocks';
const { estimate, submit, poll, status, result } = useBuzzWorkflow();
declare const modelId: number, modelVersionId: number, userPrompt: string;
const body: WorkflowBody = {
kind: 'textToImage',
modelId,
modelVersionId,
params: { prompt: userPrompt },
};
// The viewer-facing copy is a string YOUR APP owns, chosen by `err.code`.
// Nothing on the error may be rendered: `err.message` is developer-facing and
// its wording is not a contract; `err.snapshot.error` is server-authored and
// unsanitised.
const estimateFailureMessage = (err: WorkflowEstimateError) =>
err.code === 'no-cost'
? 'We could not get a price for this configuration. Try adjusting it.'
: 'Pricing is unavailable right now. Please try again shortly.';
// 🔴 estimate() REJECTS when the reply carries no usable price. ALWAYS catch it.
let priced = false;
try {
await estimate(body); // status 'estimating' → 'confirming' (cost in result.cost.total)
priced = true;
} catch (err) {
if (!(err instanceof WorkflowEstimateError)) throw err;
// status is now 'error'. Log both for the developer; render neither.
logForDebugging(err.message, err.snapshot.error);
showError(estimateFailureMessage(err));
}
if (priced) {
// 🔴 submit() REJECTS when the reply carries no usable workflow outcome. A
// priced refusal is different — it RESOLVES.
try {
const snap = await submit(body); // status 'submitting' → 'polling'
if (snap.status === 'failed') {
// 🔴 A RESOLVED `failed` IS A SERVER OUTCOME, NEVER A TOP-UP CUE: a spend
// cap or limit buying Buzz does not raise, or a run that may already have
// spent. The complete list is on `useBuzzWorkflow`'s `submit` docs.
// Running OUT of Buzz rejects instead — see the catch below.
showError(submitOutcomeMessage(snap)); // YOUR app owns this copy
} else {
await poll(snap.workflowId); // you loop this on a backoff until terminal
}
} catch (err) {
if (!(err instanceof WorkflowSubmitError)) throw err;
// Log both for the developer; render neither.
logForDebugging(err.message, err.snapshot.error);
// 🔴 TWO SEPARATE QUESTIONS — DO NOT CONJOIN THEM. `code` decides what you may
// say about MONEY; the id decides only whether there is something to POLL.
// Folding the id test into the `code` test sends a 'workflow-failed' reply
// whose id is 'whatif' into the reassuring arm — the exact blind-retry
// invitation this whole guard exists to remove.
if (err.code === 'workflow-failed') {
// 🔴 Spend MAY ALREADY BE COMMITTED. Do not tell the viewer it was free,
// and do not retry blindly — a retry mints a fresh idempotency key, i.e. a
// SECOND reservation.
showError('The generation may have started but did not complete. Check your history.');
// Only NOW ask about pollability: 'whatif' is a non-workflow sentinel.
if (err.snapshot.workflowId !== 'whatif') await poll(err.snapshot.workflowId);
} else {
// 🔴 'exception' means the host had no workflow to report — USUALLY nothing
// was queued, but a lost response or an in-progress idempotency conflict
// reaches this arm too. Retry with the SAME idempotencyKey, not a fresh one.
showError('Could not start the generation. Please try again.');
}
}
}import type { BlockWorkflowSnapshot, WorkflowBody } from '@civitai/app-sdk/blocks';
const { estimate } = useBuzzWorkflow();
declare const body: WorkflowBody;
function priceLine(cost: NonNullable<BlockWorkflowSnapshot['cost']>): string {
// Absent = not itemised. Show the total alone; do NOT treat it as a 0 fee.
if (cost.authorFee === undefined) return `${cost.total} Buzz`;
// 0 = a fee was looked up and none applies to this request.
if (cost.authorFee === 0) return `${cost.total} Buzz (no app fee)`;
const generation = cost.total - cost.authorFee;
return `${cost.total} Buzz (generation ${generation} + app fee ${cost.authorFee})`;
}
const quote = await estimate(body); // rejects without a usable price — see above
if (quote.cost) showPrice(priceLine(quote.cost));import { useBuzzWorkflow, WorkflowEstimateError } from '@civitai/blocks-react';
import type { WorkflowBody } from '@civitai/app-sdk/blocks';
const { estimate } = useBuzzWorkflow();
declare const body: WorkflowBody;
try {
await estimate(body);
} catch (err) {
if (!(err instanceof WorkflowEstimateError)) throw err;
switch (err.snapshot.errorCode) {
case 'training-quote-rejected': // the request itself was refused — change it
showError('These training settings were refused. Check them and try again.');
break;
case 'training-quote-unavailable': // no answer right now — retrying may work
case 'training-quote-unpriced': // answered without a price
showError('Training cannot be priced right now. Try again shortly.');
break;
default: // no code, or one a later host added
showError('Could not get a price.');
}
}import { useBuzzWorkflow } from '@civitai/blocks-react';
import type { WorkflowBody } from '@civitai/app-sdk/blocks';
const { estimate, submit, watch } = useBuzzWorkflow();
// Step 1 draws an image; step 2 removes the background from step 1's result.
const body: WorkflowBody = {
kind: 'workflow',
arguments: { prompt: 'a lighthouse at dusk' },
steps: [
{
name: 'draw',
$type: 'imageGen',
input: {
engine: 'google',
model: 'nano-banana-2',
// Read from `arguments` above.
prompt: { $ref: '$arguments', path: 'prompt' },
},
// An intermediate step: its image is not surfaced.
suppressOutput: true,
},
{
name: 'cutout',
$type: 'imageBackgroundRemoval',
// Read from the EARLIER step named `draw`.
input: { image: { $ref: 'draw', path: 'output.images[0].url' } },
},
],
};
const quote = await estimate(body);
showPrice(`About ${quote.cost?.total} Buzz`); // an estimate, not a maximum
const submitted = await submit(body);
const done = await watch(submitted.workflowId);
for (const step of done.steps ?? []) {
if (step.status === 'failed') showError(`${step.name} failed`);
}
// Even when `done.status === 'failed'`, earlier steps may have left images.
showImages(done.imageUrls ?? []);useAppWorkflows
useAppWorkflows(params?: AppWorkflowsParams): UseAppWorkflowsThe calling app's **own** generator subqueue — the tag-scoped list of generations **this app** produced for the viewer (newest-first), plus a fail-closed `cancel`. The host self-binds the account off the block token and **forces** the per-app tag filter, so a block only ever sees the queue it produced — never the viewer's personal queue or another app's. Returns `{ workflows, cursor, loading, error, refetch, cancel }`; each `AppWorkflow` is `{ workflowId, status, images[], cost, createdAt }`. Pass the returned `cursor` back as `params.cursor` to page forward. Requires `ai:write:budgeted` (same trust boundary as submit); host-mediated over `QUERY_APP_WORKFLOWS` / `CANCEL_APP_WORKFLOW`. `cancel(workflowId)` sends `CANCEL_APP_WORKFLOW`, resolves once the host confirms the terminal state (which is optimistically spliced into `workflows` in place — no refetch round-trip), and rejects with the host's error on failure.
const { workflows, cursor, loading, error, refetch, cancel } = useAppWorkflows({ limit: 20 });
if (!loading && !error) {
workflows.forEach((w) => console.log(w.workflowId, w.status, w.images.length, w.cost));
}
async function onCancel(id: string) {
try {
await cancel(id); // optimistically flips the row to `canceled`
} catch (err) {
console.error('cancel failed', err);
}
}useGenerationResources
useGenerationResources(): UseGenerationResourcesRehydrate a saved set of generation resources by version id — WITHOUT re-opening the picker. Returns the same widened projection `useResourcePicker` yields (recommended weights, trigger words, clipSkip). DISCOVERY ONLY.
const { fetch } = useGenerationResources();
const resources = await fetch([691639, 666002]); // by saved versionIds
const first = resources[0]; // .versionId / .strength / .trainedWords / .clipSkipusePublishGenerationOutputs
usePublishGenerationOutputs(): UsePublishGenerationOutputsPublish selected outputs of one of the calling app's OWN generations into bare, real-scanned public `Image` rows via the host-mediated `PUBLISH_GENERATION_OUTPUTS` → `PUBLISH_RESULT` bridge. Token-bound + fail-closed: the host self-binds the account off the block token, re-derives (viewer, app, workflowId) ownership before reading the workflow, and re-uploads + FULL-scans each selected output server-side (no url ever crosses from the iframe). The result is a set of bare (post-less) scanned `Image` row ids — no Post, no gallery attach, no rewards/notifications. Host-chrome shows a consent confirm before anything is published, and because that confirm waits on a human the request carries {@link HUMAN_INTERACTION_TIMEOUT_MS}, not the default protocol timeout. `dev:live` refuses this bridge: publishing requires the viewer's signed-in civitai.com session, which the local harness does not have, so `publish()` rejects with a message saying so. Test publishing against the mock host — `createMockHost` or `Harness` from `@civitai/blocks-react/testing`, with the `publishImageIds` / `publishError` options.
const { publish } = usePublishGenerationOutputs();
const imageIds = await publish({ workflowId: w.workflowId, imageIndexes: [0, 2] });
// …store imageIds via useSharedStorage() so the grid can read them back gated.