---
url: /apps/reference/messages.md
description: >-
  The full postMessage protocol between a Civitai App and its host — payloads,
  directions, request/reply pairing, and page-only messages.
---

> Documentation index: https://developer.civitai.com/llms.txt — every page on this site has a markdown version at its URL + `.md`.

# Message bridge

A block and its host talk over `window.postMessage`. Every message is a
`{ type, payload }` object discriminated by `type`. The tables below are
generated from the published `@civitai/app-sdk` message unions (for payload
shapes) joined with the `civitai` host **parity inventory** (for direction,
request/reply pairing, and page-only flags).

Most builders never send these by hand — the [React hooks](https://developer.civitai.com/apps/reference/hooks.md) wrap them.
This page is the contract for advanced use and non-React SDK consumers.

## Conventions

* **block → host** vs **host → block** — the direction of the message.
* **request → reply** — a request-style message that **awaits** a specific reply
  type (shown). An unhandled request-style message hangs the block until its SDK
  timeout, so the host always registers a handler.
* **fire-and-forget** — a block → host message with no reply; ignoring it is a
  silent no-op, never a hang.
* **page-only** — handled only by the full-page host (a page app at
  `/apps/run/<slug>`), not by the model-slot host today. Slot apps are deferred
  during the closed beta, so build page apps and you get the full surface.
* **A host → block push added after your block was built is inert until you
  rebuild.** Your bundle compiles in the `@civitai/blocks-react` you built
  against, so it only understands the messages that version knew about. A push
  the host began sending later — `THEME_CHANGE` is the current example — is
  silently discarded: the transport finds no validator for the type, matches no
  pending request and no push listener, and no-ops. **No error, no warning, and
  nothing to see in the console.** Nothing rebuilds a deployed block on your
  behalf, so it stays frozen at its build-time SDK until you resubmit; rebuilding
  against the current SDK picks up every push added since, automatically.
* **The *Per-app storage* and *Shared storage* families are deprecated.** They
  carry the legacy `useAppStorage` / `useSharedStorage` stores, which are shut
  down on 2026-11-10; from then on these messages stop working and their
  requests fail with an error. The [document store](https://developer.civitai.com/apps/guide/storage.md) that replaces them is
  not on the bridge: blocks call it over HTTP.

**Lifecycle**

**`BLOCK_ERROR`** — block → host · fire-and-forget

payload:

```ts
{
    message: string;
    fatal: boolean;
}
```

**`BLOCK_INIT`** — host → block

payload:

```ts
BlockInitPayload
```

**`BLOCK_READY`** — block → host · fire-and-forget

payload:

```ts
{
    height: number;
    sdkVersion?: string;
    sdkName?: 'blocks-react' | 'sdk';
}
```

**`NAVIGATE`** — block → host · fire-and-forget · page-only

payload:

```ts
{
    path: string;
    scope?: BlockNavigateScope;
    target: 'current' | 'new_tab';
}
```

Model slot: model slot is an embedded panel; host-navigation is out of remit (no NAVIGATE bridge)

**`RESIZE_IFRAME`** — block → host · fire-and-forget

payload:

```ts
{
    height: number;
}
```

**`RESUME`** — host → block

payload: (none)

**`SUSPEND`** — host → block

payload: (none)

**`TRACK_EVENT`** — block → host · fire-and-forget

payload:

```ts
{
    eventName: string;
    properties?: Record<string, unknown>;
}
```

**Auth & token**

**`CONSENT_UNAVAILABLE`** — host → block

payload:

```ts
ConsentUnavailablePayload
```

**`REQUEST_CONSENT`** — block → host · fire-and-forget

payload (optional):

```ts
{
    scopes?: string[];
}
```

**`REQUEST_SIGN_IN`** — block → host · fire-and-forget

payload (optional):

```ts
{
    returnUrl?: string;
}
```

**`REQUEST_TOKEN`** — block → host · request → reply

payload:

```ts
{
    requestId: string;
    blockInstanceId: string;
}
```

reply `TOKEN_REFRESH_RESPONSE`:

```ts
{
    requestId: string;
    token: WrappedToken;
}
```

or a TOKEN\_REFRESH push when no requestId was sent

**`TOKEN_REFRESH`** — host → block

payload:

```ts
{
    token: WrappedToken;
}
```

**Generation workflows**

**`CANCEL_WORKFLOW`** — block → host · request → reply

payload:

```ts
{
    requestId: string;
    workflowId: string;
}
```

reply `WORKFLOW_CANCELED`:

```ts
{
    requestId: string;
    snapshot: BlockWorkflowSnapshot;
}
```

**`ESTIMATE_WORKFLOW`** — block → host · request → reply

payload:

```ts
{
    requestId: string;
    body: WorkflowBody;
}
```

reply `ESTIMATE_RESULT`:

```ts
{
    requestId: string;
    snapshot: BlockWorkflowSnapshot;
}
```

**`ESTIMATE_WORKFLOW_BATCH`** — block → host · request → reply

payload:

```ts
{
    requestId: string;
    bodies: WorkflowBody[];
}
```

reply `ESTIMATE_BATCH_RESULT`:

```ts
{
    requestId: string;
    snapshots?: BlockWorkflowSnapshot[];
    aggregate?: BlockEstimateBatchAggregate;
    error?: string;
}
```

**`POLL_WORKFLOW`** — block → host · request → reply

payload:

```ts
{
    requestId: string;
    workflowId: string;
    waitSeconds?: number;
}
```

reply `WORKFLOW_STATUS`:

```ts
{
    requestId: string;
    snapshot: BlockWorkflowSnapshot;
}
```

**`SUBMIT_WORKFLOW`** — block → host · request → reply

payload:

```ts
{
    requestId: string;
    body: WorkflowBody;
    idempotencyKey?: string;
}
```

reply `WORKFLOW_SUBMITTED`:

```ts
{
    requestId: string;
    snapshot: BlockWorkflowSnapshot;
}
```

**App subqueue**

**`CANCEL_APP_WORKFLOW`** — block → host · request → reply · page-only

payload:

```ts
{
    requestId: string;
    workflowId: string;
}
```

reply `CANCEL_APP_WORKFLOW_RESULT`:

```ts
{
    requestId: string;
    result?: {
        workflow: AppWorkflow;
    };
    error?: string;
}
```

Model slot: app subqueue is a page-only affordance today; slot-apps deferred

**`QUERY_APP_WORKFLOWS`** — block → host · request → reply · page-only

payload:

```ts
{
    requestId: string;
    params?: AppWorkflowsParams;
}
```

reply `APP_WORKFLOWS_RESULT`:

```ts
{
    requestId: string;
    result?: {
        workflows: AppWorkflow[];
        cursor: string | null;
    };
    error?: string;
}
```

Model slot: app subqueue is a page-only affordance today; slot-apps deferred

**Buzz**

**`GET_BUZZ_ACCOUNTS`** — block → host · request → reply · page-only

payload:

```ts
{
    requestId: string;
}
```

reply `BUZZ_ACCOUNTS_RESULT`:

```ts
{
    requestId: string;
    result?: {
        accounts: BlockBuzzAccount[];
    };
    error?: string;
}
```

Model slot: buzz self-read dashboard is a page-only affordance; slot-apps deferred

**`GET_BUZZ_BALANCE`** — block → host · request → reply

payload:

```ts
{
    requestId: string;
}
```

reply `BUZZ_BALANCE_RESULT`:

```ts
{
    requestId: string;
    balance?: {
        blue: number;
        green: number;
        yellow: number;
    };
    error?: string;
}
```

**`GET_BUZZ_TRANSACTIONS`** — block → host · request → reply · page-only

payload:

```ts
{
    requestId: string;
    params?: BlockBuzzTransactionsParams;
}
```

reply `BUZZ_TRANSACTIONS_RESULT`:

```ts
{
    requestId: string;
    result?: {
        cursor?: string;
        transactions: BlockBuzzTransaction[];
    };
    error?: string;
}
```

Model slot: buzz self-read dashboard is a page-only affordance; slot-apps deferred

**`GET_DAILY_COMPENSATION`** — block → host · request → reply · page-only

payload:

```ts
{
    requestId: string;
    params?: BlockDailyCompensationParams;
}
```

reply `DAILY_COMPENSATION_RESULT`:

```ts
{
    requestId: string;
    result?: {
        resources: BlockDailyCompensationResource[];
        hasPublishedResources: boolean;
    };
    error?: string;
}
```

Model slot: buzz self-read dashboard is a page-only affordance; slot-apps deferred

**`OPEN_BUZZ_PURCHASE`** — block → host · request → reply

payload:

```ts
{
    requestId: string;
    suggestedAmount?: number;
}
```

reply `BUZZ_PURCHASE_RESULT`:

```ts
{
    requestId: string;
    purchased: boolean;
    newBalance?: number;
}
```

**Viewer**

**`GET_VIEWER`** — block → host · request → reply · page-only

payload:

```ts
{
    requestId: string;
}
```

reply `VIEWER_RESULT`:

```ts
{
    requestId: string;
    viewer?: BlockViewer;
    error?: string;
}
```

Model slot: viewer self-read is a page-only affordance; slot-apps deferred

**Pickers & upload**

**`IMAGE_SCAN_RESOLVED`** — host → block

payload:

```ts
{
    requestId: string;
    imageId: number;
    result: BlockImageScanResult;
}
```

**`OPEN_CHECKPOINT_PICKER`** — block → host · request → reply

payload:

```ts
{
    requestId: string;
    /** Optional ecosystem-family filter. Absent ⇒ unconstrained. Never ''. */
    baseModelGroup?: string;
    /** Currently-selected versionId so the picker can pre-highlight it. */
    currentVersionId?: number;
}
```

reply `CHECKPOINT_PICKER_RESULT`:

```ts
{
    requestId: string;
    selected?: BlockCheckpointInfo;
}
```

**`OPEN_IMAGE_PICKER`** — block → host · request → reply · page-only

payload:

```ts
{
    requestId: string;
    /**
     * Which of the viewer's libraries to offer. OMIT the key for both. The
     * host de-duplicates repeats and refuses an empty array or an unknown
     * member.
     */
    sources?: BlockImagePickerSource[];
    /**
     * Ask for SEVERAL images in one picker session. `max` is a whole
     * number ≥ 1; the host clamps anything above 20 to 20. The reply is
     * then `selectedImages`, not `selected`. OMIT the key for a single
     * pick.
     */
    multiple?: {
        max: number;
    };
    /**
     * Offer only images in a published, non-`Private` post: `true`
     * narrows the `'posted'` library to those. It does not filter by
     * rating or by any other post setting. The `'generated'` library is
     * unaffected. OMIT it (or `false`) for today's behaviour, which also
     * offers images in a `Private` post. Send a boolean or nothing: the
     * host refuses any other value with `{ requestId, error }`.
     *
     * Set it when the pick becomes PUBLIC-FACING, e.g. an App Store
     * sub-listing card image (`imageId` on
     * `POST /api/v1/blocks/sub-listings/upsert`, `app.subListings.upsert`
     * in `@civitai/sdk`), which the store refuses for a non-public image.
     * Leave it off when the pick is a private input (a generation source).
     *
     * 🔴 A HOST THAT PREDATES THIS FIELD IGNORES IT and still offers
     * `Private`-post images, so an app setting it must still handle the
     * store's `image_not_public` refusal.
     */
    publicOnly?: boolean;
}
```

reply `IMAGE_PICKER_RESULT`:

```ts
{
    requestId: string;
    selected?: BlockPickedImageInfo;
    selectedImages?: BlockPickedImageInfo[];
    error?: string;
}
```

Model slot: the image picker is a page-only affordance today; the shared dispatcher NACKs it with an IMAGE\_PICKER\_RESULT error

**`OPEN_IMAGE_UPLOAD`** — block → host · request → reply · page-only

payload:

```ts
{
    requestId: string;
    purpose?: BlockUploadPurpose;
    asyncScan?: boolean;
    /** In-tab image bytes; no picker. Page apps only. See the comment above. */
    bytes?: ArrayBuffer;
    /** Optional name for a `bytes` upload (host-sanitised). */
    filename?: string;
}
```

reply `IMAGE_UPLOAD_RESULT`:

```ts
{
    requestId: string;
    selected?: BlockUploadedImageInfo | BlockGenerationSourceImageInfo | BlockPendingImageInfo | BlockTrainingDataUploadResult;
    /** A `bytes` upload's refusal or failure: a host string or a forwarded server message. */
    error?: string;
}
```

Model slot: host-mediated image upload is a page-only affordance today; the model slot has no such surface

**`OPEN_RESOURCE_PICKER`** — block → host · request → reply · page-only

payload:

```ts
{
    requestId: string;
    resourceType: BlockResourcePickerType;
    /** Optional base-model family hint (ecosystem key or baseModel name). */
    baseModelGroup?: string;
    /**
     * Ask for SEVERAL resources in one picker session. LoRA only — the host
     * refuses it with a Checkpoint. `max` is a whole number ≥ 1; the host
     * clamps anything above 5 (the `additionalResources` cap) to 5. The
     * reply is then `selectedResources`, not `selected`. OMIT the key for
     * a single pick: its absence is what keeps that path unchanged.
     */
    multiple?: {
        max: number;
    };
}
```

reply `RESOURCE_PICKER_RESULT`:

```ts
{
    requestId: string;
    selected?: BlockResourceInfo;
    selectedResources?: BlockResourceInfo[];
    error?: string;
}
```

Model slot: model slot uses the narrower OPEN\_CHECKPOINT\_PICKER; the wider resource picker is a page-only affordance

**`SET_USER_CHECKPOINT`** — block → host · request → reply

payload:

```ts
{
    requestId: string;
    versionId: number | null;
}
```

reply `USER_CHECKPOINT_SET`:

```ts
{
    requestId: string;
    ok?: boolean;
    error?: string;
}
```

**Per-app storage**

**`APP_STORAGE_DELETE`** — block → host · request → reply

payload:

```ts
{
    requestId: string;
    key: string;
}
```

reply `APP_STORAGE_DELETE_RESULT`:

```ts
{
    requestId: string;
    ok?: boolean;
    deleted?: boolean;
    error?: string;
}
```

**`APP_STORAGE_GET`** — block → host · request → reply

payload:

```ts
{
    requestId: string;
    key: string;
}
```

reply `APP_STORAGE_GET_RESULT`:

```ts
{
    requestId: string;
    value: unknown;
    error?: string;
}
```

**`APP_STORAGE_LIST`** — block → host · request → reply

payload:

```ts
{
    requestId: string;
    prefix?: string;
    limit?: number;
    cursor?: string;
}
```

reply `APP_STORAGE_LIST_RESULT`:

```ts
{
    requestId: string;
    keys: Array<{
        key: string;
        updatedAt: string;
    }>;
    nextCursor?: string;
    error?: string;
}
```

**`APP_STORAGE_QUOTA`** — block → host · request → reply

payload:

```ts
{
    requestId: string;
}
```

reply `APP_STORAGE_QUOTA_RESULT`:

```ts
{
    requestId: string;
    usedBytes: number;
    rowCount: number;
    limitBytes: number;
    limitRows: number;
    error?: string;
}
```

**`APP_STORAGE_SET`** — block → host · request → reply

payload:

```ts
{
    requestId: string;
    key: string;
    value: unknown;
}
```

reply `APP_STORAGE_SET_RESULT`:

```ts
{
    requestId: string;
    ok?: boolean;
    error?: string;
    sizeBytes?: number;
}
```

**Shared storage**

**`SHARED_APPEND`** — block → host · request → reply

payload:

```ts
{
    requestId: string;
    value: SharedStorageValue;
}
```

reply `SHARED_APPEND_RESULT`:

```ts
{
    requestId: string;
    key: string;
    error?: string;
    rateLimit?: SharedStorageRateLimit;
}
```

**`SHARED_GET`** — block → host · request → reply

payload:

```ts
{
    requestId: string;
    key: string;
}
```

reply `SHARED_GET_RESULT`:

```ts
{
    requestId: string;
    item: SharedStorageItemWire | null;
    error?: string;
}
```

**`SHARED_GET_COUNT`** — block → host · request → reply

payload:

```ts
{
    requestId: string;
    key: string;
}
```

reply `SHARED_GET_COUNT_RESULT`:

```ts
{
    requestId: string;
    count: number;
    error?: string;
}
```

**`SHARED_GET_COUNTS`** — block → host · request → reply

payload:

```ts
{
    requestId: string;
    keys: string[];
}
```

reply `SHARED_GET_COUNTS_RESULT`:

```ts
{
    requestId: string;
    counts: Record<string, number>;
    error?: string;
}
```

**`SHARED_LIST`** — block → host · request → reply

payload:

```ts
{
    requestId: string;
    prefix?: string;
    limit?: number;
    cursor?: string;
}
```

reply `SHARED_LIST_RESULT`:

```ts
{
    requestId: string;
    items: SharedStorageItemWire[];
    nextCursor?: string;
    error?: string;
}
```

**`SHARED_REPORT`** — block → host · request → reply

payload:

```ts
{
    requestId: string;
    key: string;
    reason?: string;
}
```

reply `SHARED_REPORT_RESULT`:

```ts
{
    requestId: string;
    ok?: boolean;
    error?: string;
    rateLimit?: SharedStorageRateLimit;
}
```

**`SHARED_UNVOTE`** — block → host · request → reply

payload:

```ts
{
    requestId: string;
    key: string;
}
```

reply `SHARED_UNVOTE_RESULT`:

```ts
{
    requestId: string;
    count: number;
    error?: string;
    rateLimit?: SharedStorageRateLimit;
}
```

**`SHARED_UPDATE`** — block → host · request → reply

payload:

```ts
{
    requestId: string;
    key: string;
    value: SharedStorageValue;
}
```

reply `SHARED_UPDATE_RESULT`:

```ts
{
    requestId: string;
    ok?: boolean;
    error?: string;
    rateLimit?: SharedStorageRateLimit;
}
```

**`SHARED_VOTE`** — block → host · request → reply

payload:

```ts
{
    requestId: string;
    key: string;
}
```

reply `SHARED_VOTE_RESULT`:

```ts
{
    requestId: string;
    count: number;
    error?: string;
    rateLimit?: SharedStorageRateLimit;
}
```

**`SHARED_WITHDRAW`** — block → host · request → reply

payload:

```ts
{
    requestId: string;
    key: string;
}
```

reply `SHARED_WITHDRAW_RESULT`:

```ts
{
    requestId: string;
    ok?: boolean;
    deleted?: boolean;
    error?: string;
    rateLimit?: SharedStorageRateLimit;
}
```

**Wildcard packs & resource files**

**`GET_RESOURCE_FILE`** — block → host · request → reply · page-only

payload:

```ts
{
    requestId: string;
    modelVersionId: number;
    fileId?: number;
}
```

reply `RESOURCE_FILE_RESULT`:

```ts
{
    requestId: string;
    file?: BlockResourceFile;
    error?: BlockResourceFileErrorCode;
}
```

Model slot: model slot has no resource-file surface; page-only by decision (civitai-app-starters#664)

**`GET_WILDCARD_PACK`** — block → host · request → reply · page-only

payload:

```ts
{
    requestId: string;
    modelVersionId: number;
}
```

reply `WILDCARD_PACK_RESULT`:

```ts
{
    requestId: string;
    pack?: BlockWildcardPack;
    error?: BlockWildcardPackErrorCode;
}
```

Model slot: model slot has no wildcard-pack import surface; the resolve+parse bridge is a page-only affordance

**Other**

**`BLOCK_HELLO`** — block → host · fire-and-forget

payload: (none)

**`BLOCK_MESSAGE_REJECTED`** — block → host · fire-and-forget

payload:

```ts
{
    type: string;
}
```

**`CREATE_POST_FROM_APP`** — block → host · request → reply · page-only

payload:

```ts
{
    requestId: string;
    /** Image sources, in POST ORDER. At least one; the host caps the total. */
    sources: BlockPostSource[];
    title?: string;
    detail?: string;
    /** Requested tag NAMES. Resolved against EXISTING tags only. */
    tags?: string[];
    /** Optional model-version gallery attach. Gated hard server-side. */
    modelVersionId?: number;
}
```

reply `CREATE_POST_RESULT`:

```ts
{
    requestId: string;
    result?: BlockCreatePostResult;
    error?: string;
}
```

Model slot: every eligible image comes from the app subqueue, which is a page-only affordance; a model-slot block has no source images to post

**`GET_ENTITY_REACTIONS`** — block → host · request → reply

payload:

```ts
{
    requestId: string;
    entityType: BlockReactionEntityType;
    /** 1 to `REACTION_READ_MAX_IDS` positive integers. Duplicates are collapsed. */
    ids: number[];
    /** A ticket per id, keyed by the id as a string. */
    tickets?: Record<string, string>;
}
```

reply `ENTITY_REACTIONS_RESULT`:

```ts
{
    requestId: string;
    result?: BlockEntityReactionsResult;
    error?: BlockEntityReactionErrorCode | string;
}
```

**`GET_IMAGES_BY_IDS`** — block → host · request → reply · page-only

payload:

```ts
{
    requestId: string;
    imageIds: number[];
}
```

reply `IMAGES_RESULT`:

```ts
{
    requestId: string;
    result?: {
        images: BlockGatedImage[];
    };
    error?: string;
}
```

Model slot: shared-grid gated read is a page-only affordance today; the model slot has no such surface

**`OPEN_MEDIA_UPLOAD`** — block → host · request → reply · page-only

payload:

```ts
{
    requestId: string;
    /** What kind of media to upload. The host refuses any other value. */
    kind: BlockMediaKind;
    /** Required. The only accepted value today. */
    purpose: 'generationSource';
    /** In-tab bytes: present = bytes mode (no picker), absent = picker mode. */
    bytes?: ArrayBuffer;
    /** Bytes mode only: the type the app believes it is sending; must match the content. */
    mimeType?: string;
}
```

reply `MEDIA_UPLOAD_RESULT`:

```ts
{
    requestId: string;
    selected?: BlockMediaUploadInfo;
    error?: string;
}
```

Model slot: media upload is a page-only affordance today; the shared dispatcher NACKs it with a MEDIA\_UPLOAD\_RESULT error

**`PUBLISH_GENERATION_OUTPUTS`** — block → host · request → reply · page-only

payload:

```ts
{
    requestId: string;
    workflowId: string;
    imageIndexes?: number[];
    title?: string;
}
```

reply `PUBLISH_RESULT`:

```ts
{
    requestId: string;
    result?: {
        imageIds: number[];
    };
    error?: string;
}
```

Model slot: shared-grid publish is a page-only affordance today; the model slot has no such surface

**`ROUTE_CHANGED`** — host → block

payload:

```ts
{
    subPath: string;
}
```

**`RUN_CONFIRMED_STEP`** — block → host · request → reply · page-only

payload:

```ts
{
    requestId: string;
    body: WorkflowBodyPassThroughStep;
}
```

reply `CONFIRMED_STEP_RESULT`:

```ts
{
    requestId: string;
    snapshot?: BlockWorkflowSnapshot;
    error?: string;
}
```

Model slot: confirmed steps are page-only on the server; the shared dispatcher NACKs it with a CONFIRMED\_STEP\_RESULT error

**`RUN_TRAINING`** — block → host · request → reply · page-only

payload:

```ts
{
    requestId: string;
    body: WorkflowBodyTraining;
}
```

reply `TRAINING_RESULT`:

```ts
{
    requestId: string;
    snapshot?: BlockWorkflowSnapshot;
    error?: string;
}
```

Model slot: training is page-only on the server; the shared dispatcher NACKs it with a TRAINING\_RESULT error

**`SAVE_IMAGE`** — block → host · request → reply · page-only

payload:

```ts
{
    requestId: string;
    /** Own-output URL — origin-allowlisted host-side. Mutually exclusive with `imageId` / `bytes`. */
    url?: string;
    /** Cross-user image id — routed through the gated per-viewer read. Mutually exclusive with `url` / `bytes`. */
    imageId?: number;
    /**
     * In-tab file bytes — classified by CONTENT host-side, size-capped host-side.
     * Page apps only. Mutually exclusive with `url` / `imageId`.
     */
    bytes?: ArrayBuffer;
    /**
     * Optional download filename (host-sanitized). For `bytes`, a `.json`
     * name selects JSON for text that parses as JSON, and the extension is
     * forced from the classified type.
     */
    filename?: string;
}
```

reply `SAVE_IMAGE_RESULT`:

```ts
{
    requestId: string;
    ok?: boolean;
    error?: string;
}
```

Model slot: download bridge is a page-only affordance today; the paid-output apps are page apps, the model slot has no such surface

**`SET_COLLECTION_FOLLOW`** — block → host · request → reply

payload:

```ts
{
    requestId: string;
    /** Positive integer. A numeric STRING is refused, never coerced. */
    collectionId: number;
    /** `true` ⇒ follow, `false` ⇒ unfollow. */
    follow: boolean;
}
```

reply `COLLECTION_FOLLOW_RESULT`:

```ts
{
    requestId: string;
    result?: BlockCollectionFollowResult;
    error?: BlockCollectionFollowErrorCode | string;
}
```

**`SET_ENTITY_REACTION`** — block → host · request → reply

payload:

```ts
{
    requestId: string;
    entityType: BlockReactionEntityType;
    /** Positive integer. A numeric STRING is refused, never coerced. */
    entityId: number;
    /** One of `APP_REACTIONS`. `Dislike` is refused as `invalid-request`. */
    reaction: BlockAppReaction;
    /** `true` ⇒ the reaction exists afterwards; `false` ⇒ it does not. */
    reacted: boolean;
    /** 1 to `REACT_TICKET_MAX_LENGTH` characters. */
    ticket?: string;
}
```

reply `ENTITY_REACTION_RESULT`:

```ts
{
    requestId: string;
    result?: BlockEntityReactionResult;
    error?: BlockEntityReactionErrorCode | string;
}
```

**`THEME_CHANGE`** — host → block

payload:

```ts
{
    theme: Theme;
}
```

::: tip Payloads reference SDK types
Some payload fields are typed as named SDK interfaces (for example
`WorkflowBody`, `BlockViewer`, `AppWorkflow`). Those come from
`@civitai/app-sdk/blocks` — import the package to get the full type definitions
in your editor.
:::
