@civitai/app-sdk 0.65.0 → 0.66.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -140,7 +140,7 @@ interface BlockInitPayload {
140
140
  | `isModelSlotContext(ctx)` / `isPageSlotContext(ctx)` | Runtime narrowing for the `slotId`-discriminated `BlockContext` union. Real checks on a value that crossed a `postMessage` boundary — they verify every field they assert, not just `slotId`. |
141
141
  | `fetchNestedDocument(opts)` | Embed one of your OWN bundled documents as an iframe `srcdoc`, because a nested `<iframe src>` of your own content **cannot load** — see [Nested documents](#nested-documents) below. `NestedDocumentError` carries a `.code` of `'invalid-url' \| 'http-error' \| 'network-error'`. |
142
142
  | `isSignedIn(viewer)` | **The sign-in gate.** `isSignedIn(useBlockContext().viewer)` — do not open-code it as `viewer !== null` or `viewer?.signedIn === true`. Which of those is correct has already changed once with the host contract, and this is the one place it is decided. It reads neither `viewer.id` nor `viewer.username` (both `@deprecated`), so nothing written through it changes when those are removed. Need the identity rather than the presence? `useViewer()` — scope-gated and audited per call. |
143
- | types | `BlockManifestV1`, `ManifestSettings` (+ field types), `BlockContext`, `ModelSlotContext`, `BlockCheckpointInfo`, `ShowcaseImage`, `BlockToken`, `WrappedToken`, `BlockSettings`, `ViewerInfo`, `Theme`, `WorkflowBody`, `BlockTextToImageParams`, `WorkflowBodyCustomComfy` (+ its two arms `WorkflowBodyCustomComfyRecipe` / `WorkflowBodyCustomComfyInline`, and `InlineComfyNode`), `WorkflowBodyStep` / `WorkflowBodyPassThroughStep` (the two arms of `kind: 'step'`), `WorkflowBodyTraining` (+ `AiToolkitTrainingParams`, `BlockTrainingQuote`, `BlockTrainingDatasetItem`, `BlockTrainingDatasetResult` and the `BLOCK_TRAINING_*` bounds), `BlockWorkflowSnapshot`, `WorkflowStatus`, `BlockInitPayload`, `ParentToBlockMessage`, `BlockToParentMessage`. |
143
+ | types | `BlockManifestV1`, `ManifestSettings` (+ field types), `BlockContext`, `ModelSlotContext`, `BlockCheckpointInfo`, `ShowcaseImage`, `BlockToken`, `WrappedToken`, `BlockSettings`, `ViewerInfo`, `Theme`, `WorkflowBody`, `BlockTextToImageParams`, `WorkflowBodyCustomComfy` (+ its two arms `WorkflowBodyCustomComfyRecipe` / `WorkflowBodyCustomComfyInline`, and `InlineComfyNode`), `WorkflowBodyStep` / `WorkflowBodyPassThroughStep` (the two arms of `kind: 'step'`), `WorkflowBodyTraining` (+ `AiToolkitTrainingParams`, `BlockTrainingQuote`, `BlockTrainingDatasetItem`, `BlockTrainingDatasetResult` and the `BLOCK_TRAINING_*` bounds), `WorkflowBodyWorkflow` (+ `WorkflowBodyWorkflowStep`, `WorkflowStepRef`, `WithWorkflowStepRefs`, `BlockWorkflowStepSnapshot` and `BLOCK_WORKFLOW_MAX_STEPS`), `BlockWorkflowSnapshot`, `WorkflowStatus`, `BlockInitPayload`, `ParentToBlockMessage`, `BlockToParentMessage`. |
144
144
 
145
145
  `WorkflowBody`'s `customComfy` member is a discriminated union on `mode`, mirroring
146
146
  the host's `blockCustomComfyMemberSchema`. Narrow on the VALUE (`body.mode === 'inline'`),
@@ -221,6 +221,66 @@ and review sessions. The dataset is prepared over `PREPARE_TRAINING_DATASET`
221
221
  (civitai/civitai#5438). Use it through `@civitai/blocks-react`'s `usePrepareTrainingDataset()`
222
222
  and `useRunTraining()`.
223
223
 
224
+ `WorkflowBodyWorkflow` (`kind: 'workflow'`) submits **several orchestrator steps as one
225
+ workflow**, 1 to `BLOCK_WORKFLOW_MAX_STEPS` (8), through the same messages as every other kind.
226
+ A value anywhere in a step's `input` may be a `WorkflowStepRef`
227
+ (`{ $ref, path, default?, format? }`) naming `'$arguments'`, an **earlier** step's `name`, or an
228
+ earlier **unnamed** step by position (`'$0'`). A `name` starts with a lowercase
229
+ letter, uses only lowercase letters, digits, `_` and `-`, and is unique (the exact rule is on
230
+ `WorkflowBodyWorkflowStep.name`). `suppressOutput: true` marks an intermediate step whose
231
+ blobs are not surfaced. There is no `maxBuzz`: the ceiling is the app's per-generation budget × the
232
+ number of steps.
233
+
234
+ ```ts
235
+ import type { WorkflowBody } from '@civitai/app-sdk/blocks';
236
+
237
+ const body: WorkflowBody = {
238
+ kind: 'workflow',
239
+ arguments: { prompt: 'a lighthouse at dusk' },
240
+ steps: [
241
+ {
242
+ name: 'draw',
243
+ $type: 'imageGen',
244
+ input: {
245
+ engine: 'google',
246
+ model: 'nano-banana-2',
247
+ prompt: { $ref: '$arguments', path: 'prompt' },
248
+ },
249
+ suppressOutput: true,
250
+ },
251
+ {
252
+ name: 'cutout',
253
+ $type: 'imageBackgroundRemoval',
254
+ input: { image: { $ref: 'draw', path: 'output.images[0].url' } },
255
+ },
256
+ ],
257
+ };
258
+ ```
259
+
260
+ - 🔴 **The estimate is not a maximum.** The orchestrator re-prices a step once the values it
261
+ references are known and charges the difference.
262
+ - **A workflow the host cannot price still proceeds**, reserved at the ceiling above — unless it
263
+ contains a training step (refused without a quote, as on the single-step arm) or a `repeat`
264
+ step at any depth (also refused without a quote, before any reservation, as
265
+ `workflow_repeat_unbounded`).
266
+ - **One author fee per workflow**, on the quoted base, itemised as `cost.authorFee` on the
267
+ estimate. If the workflow bills more, the fee charged is held to the fee shown; a workflow that
268
+ does not succeed has its whole fee reversed, even if earlier steps delivered output.
269
+ - **`repeat` over `arguments` only, at most 16 iterations** (`BLOCK_WORKFLOW_REPEAT_MAX_ITERATIONS`,
270
+ nested repeats multiplied): `for.$ref` must be `'$arguments'` and `path` must name an array
271
+ there. A `merge` step's `fields` entries each need a string `$ref` (an earlier step or
272
+ `$arguments`) and a `reduce`. Repeat children appear in `snapshot.steps` as `<name>.<n>`. The
273
+ velocity limit counts one unit per step, and a `repeat` one per iteration.
274
+ - `snapshot.steps` reports each step's `status`, `imageUrls`, `output` and `error`. A `failed`
275
+ workflow can still carry `cost.total > 0` and usable `imageUrls`: steps that succeeded before
276
+ the failing one stay charged.
277
+ - A refused body carries `snapshot.errorCode`: `workflow_step_denied` (a platform-internal
278
+ `$type`), `workflow_ref_invalid` (a reference to the step itself, a later step, or nothing, or a
279
+ step name that breaks the naming rules, or a malformed `repeat`/`merge`),
280
+ `workflow_repeat_unbounded` (a `repeat` not over an array in `arguments`, or over 16 iterations)
281
+ or `workflow_too_many_steps`. The single-step pass-through arm refuses `repeat` outright.
282
+ - Requires a host that knows the kind; an older host refuses the body.
283
+
224
284
  ### `defineBlock` validator rules
225
285
 
226
286
  **There is no rule list here, and that is the point.** `defineBlock` does not
@@ -109,7 +109,9 @@ export { isModelSlotContext, isPageSlotContext } from './types.js';
109
109
  * in `types.ts` for which of the two it uses and why.
110
110
  */
111
111
  export { isSignedIn } from './types.js';
112
- export type { BlockContext, KnownSlotId, ModelSlotId, PageSlotContext, PageSlotId, UnknownSlotContext, BlockAnalyticsDeclaration, BlockAnalyticsEventDeclaration, BlockAnalyticsPropertyDeclaration, BlockManifest, BlockManifestGood, BlockManifestV1, BlockSettings, BlockToken, ContentRating, ManifestBooleanField, ManifestIframe, ManifestNumberField, ManifestPage, ManifestPreview, ManifestSettingField, ManifestSettings, ManifestStringField, ManifestTarget, ModelSlotContext, SettingScope, SettingWidget, Theme, ViewerInfo, BlockCheckpointInfo, BlockResourceInfo, BlockResourcePickerType, BlockSourceImage, BlockUploadedImageInfo, BlockGenerationSourceImageInfo, BlockPendingImageInfo, BlockImageScanResult, BlockUploadPurpose, BlockNavigateScope, BlockTextToImageParams, BlockWorkflowSnapshot, BlockEstimateBatchAggregate, BlockModelSubstitution, ModelSubstitutionReason, KnownBlockWorkflowErrorCode, BlockStepToolCall, BlockTrainedEpoch, BlockPublishedModel, BuzzAccountType, ShowcaseImage, WorkflowBody, WorkflowBodyTextToImage, WorkflowBodyCustomComfy, WorkflowBodyCustomComfyRecipe, WorkflowBodyCustomComfyInline, InlineComfyNode, WorkflowBodyStep, WorkflowBodyPassThroughStep, WorkflowStatus, BlockBuzzTransaction, BlockBuzzAccount, BlockDailyCompensationResource, BlockViewer, BlockWildcardPack, BlockWildcardPackErrorCode, AppWorkflow, AppWorkflowImage, BlockGatedImage, BlockCollectionFollowErrorCode, BlockCollectionFollowResult, BlockPostSource, BlockCreatePostRequest, BlockCreatePostResult, BlockCreatePostHostError, WorkflowBodyTraining, AiToolkitTrainingParams, AiToolkitTrainingParamsBase, AiToolkitPlainEcosystem, AiToolkitLrScheduler, AiToolkitOptimizerType, BlockTrainingQuote, BlockTrainingDatasetItem, BlockTrainingDatasetResult, BlockTrainingRejectionReason, BlockPrepareTrainingDatasetHostError, BlockRunTrainingHostError, } from './types.js';
112
+ export type { BlockContext, KnownSlotId, ModelSlotId, PageSlotContext, PageSlotId, UnknownSlotContext, BlockAnalyticsDeclaration, BlockAnalyticsEventDeclaration, BlockAnalyticsPropertyDeclaration, BlockManifest, BlockManifestGood, BlockManifestV1, BlockSettings, BlockToken, ContentRating, ManifestBooleanField, ManifestIframe, ManifestNumberField, ManifestPage, ManifestPreview, ManifestSettingField, ManifestSettings, ManifestStringField, ManifestTarget, ModelSlotContext, SettingScope, SettingWidget, Theme, ViewerInfo, BlockCheckpointInfo, BlockResourceInfo, BlockResourcePickerType, BlockSourceImage, BlockUploadedImageInfo, BlockGenerationSourceImageInfo, BlockPendingImageInfo, BlockImageScanResult, BlockUploadPurpose, BlockNavigateScope, BlockTextToImageParams, BlockWorkflowSnapshot, BlockEstimateBatchAggregate, BlockModelSubstitution, ModelSubstitutionReason, KnownBlockWorkflowErrorCode, BlockStepToolCall, BlockTrainedEpoch, BlockPublishedModel, BuzzAccountType, ShowcaseImage, WorkflowBody, WorkflowBodyTextToImage, WorkflowBodyCustomComfy, WorkflowBodyCustomComfyRecipe, WorkflowBodyCustomComfyInline, InlineComfyNode, WorkflowBodyStep, WorkflowBodyPassThroughStep, WorkflowBodyWorkflow, WorkflowBodyWorkflowStep, WorkflowStepRef, WithWorkflowStepRefs, BlockWorkflowStepSnapshot, WorkflowStatus, BlockBuzzTransaction, BlockBuzzAccount, BlockDailyCompensationResource, BlockViewer, BlockWildcardPack, BlockWildcardPackErrorCode, AppWorkflow, AppWorkflowImage, BlockGatedImage, BlockCollectionFollowErrorCode, BlockCollectionFollowResult, BlockPostSource, BlockCreatePostRequest, BlockCreatePostResult, BlockCreatePostHostError, WorkflowBodyTraining, AiToolkitTrainingParams, AiToolkitTrainingParamsBase, AiToolkitPlainEcosystem, AiToolkitLrScheduler, AiToolkitOptimizerType, BlockTrainingQuote, BlockTrainingDatasetItem, BlockTrainingDatasetResult, BlockTrainingRejectionReason, BlockPrepareTrainingDatasetHostError, BlockRunTrainingHostError, } from './types.js';
113
+ /** Host bound for the App Blocks `kind: 'workflow'` body — see `WorkflowBodyWorkflow`. */
114
+ export { BLOCK_WORKFLOW_MAX_STEPS, BLOCK_WORKFLOW_REPEAT_MAX_ITERATIONS } from './types.js';
113
115
  /**
114
116
  * Host bounds for the App Blocks `kind: 'training'` flow, mirrored from
115
117
  * civitai/civitai's zod — see `WorkflowBodyTraining`.
@@ -108,6 +108,8 @@ export { isModelSlotContext, isPageSlotContext } from './types.js';
108
108
  * in `types.ts` for which of the two it uses and why.
109
109
  */
110
110
  export { isSignedIn } from './types.js';
111
+ /** Host bound for the App Blocks `kind: 'workflow'` body — see `WorkflowBodyWorkflow`. */
112
+ export { BLOCK_WORKFLOW_MAX_STEPS, BLOCK_WORKFLOW_REPEAT_MAX_ITERATIONS } from './types.js';
111
113
  /**
112
114
  * Host bounds for the App Blocks `kind: 'training'` flow, mirrored from
113
115
  * civitai/civitai's zod — see `WorkflowBodyTraining`.
@@ -1168,6 +1168,9 @@ export type WorkflowBodyStep = {
1168
1168
  * checkpoint itself.
1169
1169
  * - `$type` is bounded to **1…64 characters** and `input` to **262144 bytes**
1170
1170
  * (256 KB) serialized. Both REJECT; neither truncates.
1171
+ * - **`repeat` is refused on this arm**, outright: a `repeat` may only
1172
+ * iterate an array in a body's own `arguments`, and this arm has none. Use
1173
+ * a `kind: 'workflow'` body ({@link WorkflowBodyWorkflow}) to repeat a step.
1171
1174
  * - `maxBuzz` is the app's spend knob, and the host's quote can raise the
1172
1175
  * reservation above it — see its own note below.
1173
1176
  *
@@ -1249,6 +1252,196 @@ export type WorkflowBodyPassThroughStep = {
1249
1252
  */
1250
1253
  maxBuzz: number;
1251
1254
  };
1255
+ /**
1256
+ * The most steps one `kind: 'workflow'` body may carry. The host refuses a
1257
+ * longer list with `errorCode: 'workflow_too_many_steps'`; an empty list is
1258
+ * refused too.
1259
+ */
1260
+ export declare const BLOCK_WORKFLOW_MAX_STEPS = 8;
1261
+ /**
1262
+ * The most times one top-level `repeat` step of a `kind: 'workflow'` body may
1263
+ * run its template, NESTED REPEATS MULTIPLIED (a 4-item repeat inside a 4-item
1264
+ * repeat is 16). The host refuses more with
1265
+ * `errorCode: 'workflow_repeat_unbounded'`.
1266
+ */
1267
+ export declare const BLOCK_WORKFLOW_REPEAT_MAX_ITERATIONS = 16;
1268
+ /**
1269
+ * A REFERENCE inside a `kind: 'workflow'` step's `input` — the orchestrator's
1270
+ * own mechanism for feeding one step's result into a later step. Put it
1271
+ * anywhere a value would go, at any depth.
1272
+ *
1273
+ * `$ref` names where the value comes from:
1274
+ * - `'$arguments'` — the body's own {@link WorkflowBodyWorkflow.arguments};
1275
+ * - the `name` of an EARLIER step in the same body;
1276
+ * - `'$<index>'` — an EARLIER step you did NOT name, by its zero-based
1277
+ * position (`'$0'` is the first step). A step that has a `name` is
1278
+ * referenced by that name only.
1279
+ *
1280
+ * An object counts as a reference only when it has BOTH `$ref` and `path` and
1281
+ * no key other than `default` / `format`; anything else is ordinary input.
1282
+ *
1283
+ * `path` selects the value inside that source, e.g. `'output.images[0].url'`
1284
+ * for a step, or `'prompt'` for `$arguments`. The orchestrator resolves it; the
1285
+ * path is not checked by this package or by the host.
1286
+ *
1287
+ * 🔴 A reference may only point BACKWARDS. A step that names itself, a later
1288
+ * step, or a step that does not exist is refused by the host with
1289
+ * `errorCode: 'workflow_ref_invalid'` before anything is priced or reserved.
1290
+ * A body may hold at most 64 references, in inputs nested at most 32 levels.
1291
+ *
1292
+ * `default` is the value used when the path resolves to nothing; `format` is a
1293
+ * template applied to the resolved value. Both are passed to the orchestrator
1294
+ * unmodified.
1295
+ */
1296
+ export type WorkflowStepRef = {
1297
+ $ref: '$arguments' | `$${number}` | (string & {});
1298
+ path: string;
1299
+ default?: unknown;
1300
+ format?: string;
1301
+ };
1302
+ /**
1303
+ * `T` with a {@link WorkflowStepRef} allowed in place of any value, at any
1304
+ * depth. Use it to keep a step input typed while still wiring references in:
1305
+ *
1306
+ * ```ts
1307
+ * type CutoutInput = { image: string; format?: 'png' | 'webp' };
1308
+ * const input: WithWorkflowStepRefs<CutoutInput> = {
1309
+ * image: { $ref: 'draw', path: 'output.images[0].url' },
1310
+ * format: 'png',
1311
+ * };
1312
+ * ```
1313
+ */
1314
+ export type WithWorkflowStepRefs<T> = WorkflowStepRef | (T extends ReadonlyArray<infer U> ? Array<WithWorkflowStepRefs<U>> : T extends object ? {
1315
+ [K in keyof T]: WithWorkflowStepRefs<T[K]>;
1316
+ } : T);
1317
+ /** One step of a {@link WorkflowBodyWorkflow}. */
1318
+ export type WorkflowBodyWorkflowStep = {
1319
+ /**
1320
+ * What later steps call this one in a {@link WorkflowStepRef}, and the `name`
1321
+ * it reports under on {@link BlockWorkflowSnapshot.steps}. Optional: an
1322
+ * unnamed step is named by its position (`'$0'`, `'$1'`, …).
1323
+ *
1324
+ * A lowercase letter followed by up to 31 of `a-z`, `0-9`, `_` or `-`
1325
+ * (`/^[a-z][a-z0-9_-]{0,31}$/`), unique within the body, and not
1326
+ * `'block-step'`, which is reserved. A name that breaks a rule is refused
1327
+ * with `errorCode: 'workflow_ref_invalid'`.
1328
+ */
1329
+ name?: string;
1330
+ /**
1331
+ * The ORCHESTRATOR step type to run, verbatim — e.g. `'imageGen'`. Not
1332
+ * resolved against any allowlist: it is refused only when it names a
1333
+ * platform-internal type (scanners, moderation classifiers, hashing, web
1334
+ * scrape/search, and the retired `textToImage` step type), with
1335
+ * `errorCode: 'workflow_step_denied'`. 1…64 characters.
1336
+ *
1337
+ * Two step types are read by shape, because they name their sources outside
1338
+ * a {@link WorkflowStepRef}:
1339
+ * - **`repeat` — over `arguments` only, at most
1340
+ * {@link BLOCK_WORKFLOW_REPEAT_MAX_ITERATIONS} iterations.** Its input must
1341
+ * be exactly `{ for: { $ref, path, as }, template: { $type, name?, input? } }`
1342
+ * with `for.$ref === '$arguments'` and `path` naming an ARRAY in the body's
1343
+ * `arguments`; its length is the iteration count, nested repeats
1344
+ * multiplied. A repeat over another step's output, a missing or non-array
1345
+ * path, or too many iterations is refused with
1346
+ * `errorCode: 'workflow_repeat_unbounded'`; a wrong shape, or repeats nested
1347
+ * more than 4 deep, with `workflow_ref_invalid`. Inside the template the
1348
+ * `as` name is a source a reference may name; it may not equal a step name.
1349
+ * The template's `$type` is held to the denylist too.
1350
+ * - **`merge`** — every entry of `input.fields` needs a string `$ref` and a
1351
+ * `reduce`, and each `$ref` must name `$arguments` or an EARLIER step,
1352
+ * never a repeat variable. Each counts toward the 64-reference cap.
1353
+ * Otherwise `workflow_ref_invalid`.
1354
+ */
1355
+ $type: string;
1356
+ /**
1357
+ * The step's own input object. Any value in it, at any depth, may be a
1358
+ * {@link WorkflowStepRef}. The orchestrator's per-`$type` schema is the only
1359
+ * authority for what a given `$type` accepts. Every step's `input` plus the
1360
+ * body's `arguments` may total at most 262144 bytes (256 KB) serialized.
1361
+ */
1362
+ input: Record<string, unknown>;
1363
+ /**
1364
+ * `true` marks an INTERMEDIATE step: its blobs are not surfaced, neither on
1365
+ * its own {@link BlockWorkflowStepSnapshot.imageUrls} nor in the top-level
1366
+ * {@link BlockWorkflowSnapshot.imageUrls}. The step still runs and is still
1367
+ * charged.
1368
+ */
1369
+ suppressOutput?: boolean;
1370
+ };
1371
+ /**
1372
+ * The `workflow` member of {@link WorkflowBody} — SEVERAL orchestrator steps
1373
+ * submitted as ONE workflow, where a later step can consume an earlier step's
1374
+ * result through a {@link WorkflowStepRef}. It travels through the same
1375
+ * messages and hooks as every other kind (`estimate`, `submit`, `watch`,
1376
+ * `cancel`, a batch-estimate cell, `useWorkflowBatch`).
1377
+ *
1378
+ * Between 1 and {@link BLOCK_WORKFLOW_MAX_STEPS} steps. There is no `maxBuzz`:
1379
+ * the spend ceiling is the app's per-generation budget × the number of steps,
1380
+ * enforced by the host. The host's schema is strict: an unknown key on the
1381
+ * body or on a step is rejected, not dropped.
1382
+ *
1383
+ * 🔴 THREE THINGS ABOUT THE PRICE, all of them different from a single-step
1384
+ * body:
1385
+ * 1. **The estimate is NOT a maximum.** When a step's input references an
1386
+ * earlier step, the orchestrator re-prices that step once the referenced
1387
+ * values are known and charges the difference. Show the estimate as an
1388
+ * estimate; never as "you will pay at most".
1389
+ * 2. **A workflow the host cannot price still proceeds.** It is reserved at
1390
+ * the ceiling above instead of at a quote — unless it contains a training
1391
+ * step, which is refused without a quote exactly as on the single-step arm
1392
+ * (a `training-quote-*` `errorCode`), or a `repeat` step at any depth,
1393
+ * which is refused without a quote too, before any reservation, with
1394
+ * `errorCode: 'workflow_repeat_unbounded'` — retrying later may succeed.
1395
+ * 3. **One author fee per workflow**, not one per step, computed on the
1396
+ * QUOTED base. The estimate's `cost.authorFee` is that single fee, already
1397
+ * inside `cost.total`. If the workflow bills more than the quote, the fee
1398
+ * charged is held to the fee shown. A workflow that does not succeed has
1399
+ * its WHOLE fee reversed, even when earlier steps delivered output.
1400
+ *
1401
+ * The app's velocity limit counts one unit per step, and a `repeat` step one
1402
+ * per iteration (nested repeats multiplied) instead of one.
1403
+ *
1404
+ * 🔴 A FAILED WORKFLOW CAN STILL HAVE COST AND IMAGES. Steps that succeeded
1405
+ * before the failing one stay charged, so a `failed` snapshot can carry
1406
+ * `cost.total > 0` and usable `imageUrls`. See {@link BlockWorkflowSnapshot.steps}.
1407
+ *
1408
+ * 🔴 REQUIRES A HOST THAT KNOWS THIS KIND. An older host refuses the body.
1409
+ *
1410
+ * @example An image step whose output a second step consumes.
1411
+ * ```ts
1412
+ * const body: WorkflowBody = {
1413
+ * kind: 'workflow',
1414
+ * arguments: { prompt: 'a lighthouse at dusk' },
1415
+ * steps: [
1416
+ * {
1417
+ * name: 'draw',
1418
+ * $type: 'imageGen',
1419
+ * input: {
1420
+ * engine: 'google',
1421
+ * model: 'nano-banana-2',
1422
+ * prompt: { $ref: '$arguments', path: 'prompt' },
1423
+ * },
1424
+ * suppressOutput: true,
1425
+ * },
1426
+ * {
1427
+ * name: 'cutout',
1428
+ * $type: 'imageBackgroundRemoval',
1429
+ * input: { image: { $ref: 'draw', path: 'output.images[0].url' } },
1430
+ * },
1431
+ * ],
1432
+ * };
1433
+ * ```
1434
+ */
1435
+ export type WorkflowBodyWorkflow = {
1436
+ kind: 'workflow';
1437
+ /** The steps, in order. 1…{@link BLOCK_WORKFLOW_MAX_STEPS}. */
1438
+ steps: WorkflowBodyWorkflowStep[];
1439
+ /**
1440
+ * Values the steps can read through `{ $ref: '$arguments', path }` — one
1441
+ * place to put a prompt or a seed that several steps share.
1442
+ */
1443
+ arguments?: Record<string, unknown>;
1444
+ };
1252
1445
  /** Ceiling on what ONE confirmed `kind: 'training'` run may cost, in Buzz. Host constant `BLOCK_TRAINING_MAX_BUZZ_PER_RUN`. */
1253
1446
  export declare const BLOCK_TRAINING_MAX_BUZZ_PER_RUN = 5000;
1254
1447
  /** Max images in one prepared training dataset. Host constant `BLOCK_TRAINING_DATASET_MAX_ITEMS`. */
@@ -1431,6 +1624,10 @@ export interface WorkflowBodyTraining {
1431
1624
  * on a server-prepared dataset of the viewer's own images. ESTIMATE ONLY
1432
1625
  * through `useBuzzWorkflow`; it runs through `useRunTraining()`, whose
1433
1626
  * host-chrome consent dialog is the only way to confirm its quote.
1627
+ * - {@link WorkflowBodyWorkflow} (`kind: 'workflow'`) — up to
1628
+ * {@link BLOCK_WORKFLOW_MAX_STEPS} orchestrator steps submitted as one
1629
+ * workflow, a later step consuming an earlier one's result through a
1630
+ * {@link WorkflowStepRef}. Its estimate is not a maximum — read its doc.
1434
1631
  *
1435
1632
  * `kind: 'step'` is therefore itself a union, discriminated on the PRESENCE of
1436
1633
  * `step` — the same nesting {@link WorkflowBodyCustomComfy} has on `mode`.
@@ -1447,7 +1644,7 @@ export interface WorkflowBodyTraining {
1447
1644
  * over `kind`. Host code that must handle every member gets a compile error
1448
1645
  * pointing at the new one, which is the intended behaviour.
1449
1646
  */
1450
- export type WorkflowBody = WorkflowBodyTextToImage | WorkflowBodyCustomComfy | WorkflowBodyStep | WorkflowBodyPassThroughStep | WorkflowBodyTraining;
1647
+ export type WorkflowBody = WorkflowBodyTextToImage | WorkflowBodyCustomComfy | WorkflowBodyStep | WorkflowBodyPassThroughStep | WorkflowBodyTraining | WorkflowBodyWorkflow;
1451
1648
  /**
1452
1649
  * The host-mediated view of an orchestrator workflow that an iframe block
1453
1650
  * receives over `postMessage`.
@@ -1487,6 +1684,9 @@ export interface BlockWorkflowSnapshot {
1487
1684
  * orchestrator price quote, which refuses before any fee is looked up; and
1488
1685
  * the kinds that price no fee (`customComfy`, pass-through `step`,
1489
1686
  * `training`). **Do not read absent as `0`** — show `total` on its own.
1687
+ *
1688
+ * A `kind: 'workflow'` estimate itemises it too, and it is ONE fee for the
1689
+ * whole workflow however many steps it has.
1490
1690
  */
1491
1691
  authorFee?: number;
1492
1692
  };
@@ -1507,6 +1707,8 @@ export interface BlockWorkflowSnapshot {
1507
1707
  * (which rejects with `WorkflowSubmitError`, before anything is reserved or
1508
1708
  * charged), and to that body's cell in a batch estimate. Read it from the
1509
1709
  * error's `.snapshot.errorCode`. The `kind: 'training'` arm does NOT set it.
1710
+ * A refused `kind: 'workflow'` body ({@link WorkflowBodyWorkflow}) sets it in
1711
+ * the same shape, on the same three replies.
1510
1712
  *
1511
1713
  * The values the host sends today ({@link KnownBlockWorkflowErrorCode}):
1512
1714
  * - `'training-quote-rejected'` — the orchestrator refused the request
@@ -1515,6 +1717,21 @@ export interface BlockWorkflowSnapshot {
1515
1717
  * - `'training-quote-unavailable'` — no usable answer (an outage, a timeout,
1516
1718
  * a rate limit, insufficient funds, …). Retrying later may succeed.
1517
1719
  * - `'training-quote-unpriced'` — the orchestrator answered without a price.
1720
+ * - `'workflow_step_denied'` — a step of a `kind: 'workflow'` body names a
1721
+ * platform-internal `$type`. Remove or replace that step.
1722
+ * - `'workflow_ref_invalid'` — a {@link WorkflowStepRef} in a step's `input`
1723
+ * points at the step itself, a later step, or nothing; or a step `name`
1724
+ * breaks the naming rules (see {@link WorkflowBodyWorkflowStep.name}).
1725
+ * `error` names the step by its position.
1726
+ * - `'workflow_too_many_steps'` — more than
1727
+ * {@link BLOCK_WORKFLOW_MAX_STEPS} steps.
1728
+ * - `'workflow_repeat_unbounded'` — a `repeat` step iterates something other
1729
+ * than an array in the body's `arguments`, or more than
1730
+ * {@link BLOCK_WORKFLOW_REPEAT_MAX_ITERATIONS} times, nested repeats
1731
+ * multiplied; or the workflow has a `repeat` and the host could get no
1732
+ * price quote for it (`error` then reads `… a workflow with a repeat needs
1733
+ * a price quote and none could be obtained; try again`, and a retry may
1734
+ * succeed).
1518
1735
  *
1519
1736
  * 🔴 AN UNKNOWN VALUE IS POSSIBLE: the host may add codes, and the SDK's
1520
1737
  * inbound validator accepts any string here. Handle an unrecognised code as a
@@ -1722,6 +1939,48 @@ export interface BlockWorkflowSnapshot {
1722
1939
  * that predate civitai/civitai#5434.
1723
1940
  */
1724
1941
  trainingQuote?: BlockTrainingQuote;
1942
+ /**
1943
+ * The steps of a `kind: 'workflow'` run ({@link WorkflowBodyWorkflow}), in
1944
+ * the orchestrator's order, each with its own status and results. The steps a
1945
+ * `repeat` step generates follow it, named `<repeat name>.<n>`, so the list
1946
+ * can be longer than the body's.
1947
+ *
1948
+ * The top-level fields keep their meaning: `status` is the workflow's,
1949
+ * `imageUrls` is the union of every step that did NOT set `suppressOutput`,
1950
+ * and `cost` is the whole workflow's.
1951
+ *
1952
+ * 🔴 A `failed` WORKFLOW IS NOT AN EMPTY ONE. Steps that succeeded before the
1953
+ * failing step stay charged and keep their results, so a snapshot with
1954
+ * `status: 'failed'` can carry `cost.total > 0` and usable `imageUrls`. Read
1955
+ * `steps` to see which step failed (`status: 'failed'`, with its `error`)
1956
+ * and which finished.
1957
+ *
1958
+ * Read it from the poll and cancel replies of a `kind: 'workflow'` run; do
1959
+ * not rely on it being on the submit reply. ABSENT for every other kind, on
1960
+ * an estimate (which creates no workflow) and on a host that predates it.
1961
+ */
1962
+ steps?: BlockWorkflowStepSnapshot[];
1963
+ }
1964
+ /** One entry of {@link BlockWorkflowSnapshot.steps}. */
1965
+ export interface BlockWorkflowStepSnapshot {
1966
+ /** The step's `name` from the body, or the name the host gave an unnamed step. */
1967
+ name: string;
1968
+ /** The orchestrator step type that ran. */
1969
+ $type: string;
1970
+ status: 'pending' | 'processing' | 'succeeded' | 'failed' | 'expired' | 'canceled';
1971
+ /**
1972
+ * This step's own image urls. Omitted when it has none, and always omitted
1973
+ * for a step submitted with `suppressOutput: true`.
1974
+ */
1975
+ imageUrls?: string[];
1976
+ /**
1977
+ * The step's raw orchestrator output, minus its blobs — the same treatment
1978
+ * and the same caveats as {@link BlockWorkflowSnapshot.stepOutputs}: its shape
1979
+ * is whatever the orchestrator returns for that `$type`, and it is UNSCANNED.
1980
+ */
1981
+ output?: unknown;
1982
+ /** Why this step failed. Diagnostic text; do not render it verbatim. */
1983
+ error?: string;
1725
1984
  }
1726
1985
  /**
1727
1986
  * The run total a BATCH estimate reports beside its per-cell snapshots
@@ -1781,7 +2040,7 @@ export type ModelSubstitutionReason = 'wrong-workflow' | 'unrecognized' | 'gated
1781
2040
  * and the SDK's inbound validator accepts any string. Handle an unrecognised
1782
2041
  * code as a generic failure.
1783
2042
  */
1784
- export type KnownBlockWorkflowErrorCode = 'training-quote-rejected' | 'training-quote-unavailable' | 'training-quote-unpriced';
2043
+ export type KnownBlockWorkflowErrorCode = 'training-quote-rejected' | 'training-quote-unavailable' | 'training-quote-unpriced' | 'workflow_step_denied' | 'workflow_ref_invalid' | 'workflow_too_many_steps' | 'workflow_repeat_unbounded';
1785
2044
  /** One entry of {@link BlockWorkflowSnapshot.modelSubstitutions}. */
1786
2045
  export interface BlockModelSubstitution {
1787
2046
  /** The checkpoint version id the block asked for. */
@@ -115,6 +115,22 @@ export function isSignedIn(viewer) {
115
115
  return viewer !== null && viewer !== undefined;
116
116
  }
117
117
  // ============================================================
118
+ // App Blocks MULTI-STEP kind (`kind: 'workflow'`)
119
+ // ============================================================
120
+ /**
121
+ * The most steps one `kind: 'workflow'` body may carry. The host refuses a
122
+ * longer list with `errorCode: 'workflow_too_many_steps'`; an empty list is
123
+ * refused too.
124
+ */
125
+ export const BLOCK_WORKFLOW_MAX_STEPS = 8;
126
+ /**
127
+ * The most times one top-level `repeat` step of a `kind: 'workflow'` body may
128
+ * run its template, NESTED REPEATS MULTIPLIED (a 4-item repeat inside a 4-item
129
+ * repeat is 16). The host refuses more with
130
+ * `errorCode: 'workflow_repeat_unbounded'`.
131
+ */
132
+ export const BLOCK_WORKFLOW_REPEAT_MAX_ITERATIONS = 16;
133
+ // ============================================================
118
134
  // App Blocks TRAINING kind (`kind: 'training'`)
119
135
  // ============================================================
120
136
  //
package/dist/index.d.ts CHANGED
@@ -25,11 +25,11 @@
25
25
  *
26
26
  * - **`./blocks`** — importing it RUNS `./safe-storage` for its side effect
27
27
  * (repairing `localStorage`/`sessionStorage` at an opaque origin). Folding
28
- * 160 symbols and that side effect into the root would make every OAuth app
28
+ * 167 symbols and that side effect into the root would make every OAuth app
29
29
  * that writes `import { exchangeCode } from '@civitai/app-sdk'` pay for the
30
30
  * Civitai-Apps contract it does not use. Blocks and OAuth apps are disjoint
31
31
  * audiences; see this package's README.
32
- * 🔴 That 160 is ASSERTED, not merely quoted — by
32
+ * 🔴 That 167 is ASSERTED, not merely quoted — by
33
33
  * `packages/civitai-app-sdk/test/export-surface.test.ts`, which pins
34
34
  * `surface.get('./blocks').size`. It said 119 here against a real 125 until
35
35
  * 2026-10-04, because the existing checks pin the root as an exact union and
package/dist/index.js CHANGED
@@ -25,11 +25,11 @@
25
25
  *
26
26
  * - **`./blocks`** — importing it RUNS `./safe-storage` for its side effect
27
27
  * (repairing `localStorage`/`sessionStorage` at an opaque origin). Folding
28
- * 160 symbols and that side effect into the root would make every OAuth app
28
+ * 167 symbols and that side effect into the root would make every OAuth app
29
29
  * that writes `import { exchangeCode } from '@civitai/app-sdk'` pay for the
30
30
  * Civitai-Apps contract it does not use. Blocks and OAuth apps are disjoint
31
31
  * audiences; see this package's README.
32
- * 🔴 That 160 is ASSERTED, not merely quoted — by
32
+ * 🔴 That 167 is ASSERTED, not merely quoted — by
33
33
  * `packages/civitai-app-sdk/test/export-surface.test.ts`, which pins
34
34
  * `surface.get('./blocks').size`. It said 119 here against a real 125 until
35
35
  * 2026-10-04, because the existing checks pin the root as an exact union and
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@civitai/app-sdk",
3
- "version": "0.65.0",
3
+ "version": "0.66.0",
4
4
  "description": "OAuth + PKCE, encrypted-cookie sessions, scopes, and orchestrator helpers for building third-party Civitai apps.",
5
5
  "license": "MIT",
6
6
  "type": "module",