> Public product help. This page has no customer entries, projects, chat transcripts, API keys, or horizon contents.
> Reading that data requires `Authorization: Bearer` (a signed-in app session JWT, or a Headless access token).
> Do not call Supabase `/rest/v1` with a Headless JWT. Data reads go through `/casc/v1/headless/v1/rest`.

# Headless user choice (dialog bridge)

Website builders do not embed the Cascade React chat UI. They own the **front** (modals, lists, buttons)
and a **BFF** that talks to Zagger Headless. When the agent needs a human decision, Headless does not
guess. It returns `needs_ui_action` with a pending action. Your UI collects the choice; your BFF posts it
back; you poll the chat request again.

Secrets stay on the BFF. Browsers never hold `client_secret`. Use the same Headless Bearer token for
send, wait, search, lock, and resolve.

API base: `/casc/v1/headless/v1`. Overview: [headless](headless.md). Endpoint list: repo `cascade-node-server/docs/headless-api.md`.

## Roles

| Piece | Owns |
| --- | --- |
| Your website | Composer, message list, dialogs (Find Record, unlock, archive, …) |
| Your BFF | `client_id` / `client_secret`, token mint, all Headless HTTP |
| Zagger Headless | Agent, tools, session metadata, pending action store |
| End user | Identified as stable `sub_user` on token mint |

## Front and back loop

1. BFF: `POST /token` with `sub_user` (and optional `project_id`).
2. BFF: `POST /chat/send` with `content`, optional `session_id`, `project_id`, `workflow_id`, `files`.
3. BFF: `GET /chat/requests/{request_id}/wait?timeout=120` (or poll `/result`).
4. If `result.status` is `fulfilled` / `errored` / `canceled`, render `result.markdown` and stop.
5. If `result.status` is `needs_ui_action`, read `result.pending_ui_action` and open the matching dialog in your site.
6. User confirms or cancels in your UI.
7. BFF: `POST /ui-actions/{action_id}/resolve` with `{ "result": { … } }` or `{ "cancel": true }`.
8. BFF: poll wait/result again until a terminal status (or another `needs_ui_action`).

`wait` returns early when a **blocking** action is pending. That is intentional: the agent
is waiting on your user, not on more model time.

## Pending action object

Returned on wait/result as `pending_ui_action`, and on `GET /ui-actions/{action_id}`.

```
{
  "action_id": "uuid",
  "type": "open_find_record",
  "request_id": "uuid",
  "session_id": "uuid",
  "project_id": "uuid",
  "created_at": "ISO-8601",
  "expires_at": "ISO-8601",
  "blocking": true,
  "status": "pending",
  "payload": {},
  "result": null
}
```

| Field | Use in your UI |
| --- | --- |
| `action_id` | Path param for GET and resolve |
| `type` | Which dialog to render |
| `session_id` | Which chat the choice applies to |
| `blocking` | If true, wait/result stays `needs_ui_action` until resolve or expiry |
| `payload` | Hints (for Find Record: optional `group_slug`) |
| `expires_at` | Treat as cancel if the user returns too late (HTTP 410 on resolve) |

## Action types and result bodies

| type | Who creates it | Your dialog | Success resolve body | Cancel |
| --- | --- | --- | --- | --- |
| `open_find_record` | Automatic when wait/result sees a session find-record request | Pick ingestion group → search entries → confirm lock | `{ "result": { "group_slug", "entry_id", "label?" } }` | `{ "cancel": true }` clears the find request; does not unlock an existing lock |
| `confirm_unlock_record` | Your BFF: `POST /sessions/{sessionId}/ui-actions` with `{ "type": "confirm_unlock_record" }` | Yes / No unlock | `{ "result": { "confirm": true } }` clears the lock | `{ "cancel": true }` no-op |
| `confirm_archive_session` | Same create endpoint with `confirm_archive_session` | Yes / No archive | `{ "result": { "confirm": true } }` archives the session | `{ "cancel": true }` no-op |
| `confirm_workflow_warnings` | Optional; usually skip and send `workflow_id` after your own confirm | Missing tools/groups warning | `{ "result": { "proceed": true } }` | `{ "cancel": true }` |

Resolve is idempotent: a second POST on the same `action_id` returns `already_resolved: true` and does not lock twice.

## Find Record (end to end)

1. User (or agent tool) requests Find Record. Wait/result returns `needs_ui_action` with `type: open_find_record`.
2. If `payload.group_slug` is set, skip group pick. Otherwise list groups via PostgREST `GET /rest/groups` or your cached project groups.
3. Search rows: `GET /projects/{projectId}/groups/{groupSlug}/entries?q=&limit=`.
4. User picks a row. BFF posts resolve with `group_slug` and `entry_id` (optional `label`).
5. Zagger sets `locked_record` on the session (same as Cascade **Find Record** confirm).
6. Poll wait/result. Later tool calls in that session can use the locked row.

You can also lock without a pending action: `PUT /sessions/{sessionId}/locked-record` with `group_slug` and `entry_id`. Unlock: `DELETE` that URL, or the confirm-unlock action above. Inspect: `GET /sessions/{sessionId}/locked-record`.

## Unlock and archive from your chrome

These are not auto-emitted. When the user clicks Unlock or Archive in *your* header:

1. `POST /sessions/{sessionId}/ui-actions` `{ "type": "confirm_unlock_record" }` (or `confirm_archive_session`).
2. Show your confirm modal using the returned `pending_ui_action`.
3. `POST /ui-actions/{action_id}/resolve` with confirm or cancel.

Alternatively archive with `PATCH /sessions/{sessionId}` `{ "archived": true }` if you do not want a two-step confirm.

## Ingest is REST, not a dialog type

Conversation ingest (planes → preview → apply) is a separate REST flow:
`/ingest/planes`, `/plan`, `/modify`, `/discard`, `/apply`, `/result/:previewId`.
Drive that from your own wizard. Do not wait for `needs_ui_action` for ingest.

## Create / update confirm in chat (session wait)

In the Cascade UI, `entry_create` / `entry_update` pause for a confirm modal. That payload is stored on the session as
`entry_update_wait_request` (also `tool_chain_wait_request`, `embedding_wait_request`).
Headless `GET /sessions/{sessionId}/locked-record` returns those objects so you can **render** the proposed fields.

**Headless default:** integrations have `require_entry_confirmation: false`, so entry create/update
**apply immediately** on Headless chat (like the scheduler). Toggle **Entry confirm** on in
Headless → Integrations (or `PATCH /admin/headless/integrations/:id` with
`{ "require_entry_confirmation": true }`) to use the Cascade-style wait. That wait is still not resolved via
`/ui-actions/:id/resolve` — only the Cascade session UI can confirm/cancel it today.

## Example: resolve Find Record

```
POST /casc/v1/headless/v1/ui-actions/ACTION_UUID/resolve
Authorization: Bearer ACCESS
Content-Type: application/json

{ "result": { "group_slug": "innfall-articles", "entry_id": "ENTRY_UUID", "label": "Optional title" } }
```

Cancel:

```
{ "cancel": true }
```

## Status values you must handle

| result.status | Meaning |
| --- | --- |
| `fulfilled` | Show markdown (and yuml if present) |
| `errored` / `canceled` | Show error / cancelled |
| `needs_ui_action` | Open dialog from `pending_ui_action`; do not treat as a finished reply |
| In-flight (processing) | Keep polling wait/result |

## Not the same as…

- **Scheduler Run now** — unattended; no user choice; creates/updates apply immediately.
- **Settings → System** — instance config, not this loop.
- **Supabase `/rest/v1`** — Headless JWT is not a Supabase JWT. Data queries go through `/casc/v1/headless/v1/rest` ([headless-postgrest](headless-postgrest.md)).
