> ## Documentation Index
> Fetch the complete documentation index at: https://agents.nanonets.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Review Form

> `review_form` composes **one human-review card** that aggregates the structured outputs of several prior steps (for example, an invoice extraction plus a per-row GL-code…

`review_form` composes **one human-review card** that aggregates the structured outputs of several prior steps (for example, an invoice extraction plus a per-row GL-code lookup) into a single form with sections and tables, then **pauses the task** for review.

It is part of the **Nanonets** data-extraction tool group (`data_extraction`, `document_processing`, `validation`, `review`).

## When to use it

| Scenario                                                      | Tool                               |
| ------------------------------------------------------------- | ---------------------------------- |
| Review a **single** prior extraction with validation rules    | `review_data_extraction`           |
| Combine **multiple** prior step outputs into one review card  | `review_form`                      |
| Multi-stage review (finance → ops → manager) with progress UI | `review_form` (one call per stage) |

Use `review_form` instead of chaining multiple `review_data_extraction` calls when a reviewer needs to see data from several blocks together — for example, invoice header fields plus line items with a looked-up GL code column.

## Architecture

The agent does **not** design the form layout. It passes:

1. **variables** — which prior outputs to include (`{name, ref}` pairs where `ref` is a `${VAR_N}` reference)
2. **layout\_guidelines** — plain-English description of how the reviewer should see the data

`review_form` then:

1. Resolves each variable to JSON (markdown fences from `ai_call_response` are stripped automatically)
2. Generates a **layout spec** via a focused LLM call (cold path), or reuses a **frozen** spec from `configured_tools.bindings`
3. Applies the spec **deterministically** — citations (`word_id_groups`) and `__metadata` on source leaves are preserved
4. Optionally runs a **validation** LLM pass when `validation_rules` is set
5. Creates a validation `task_await` and transitions the task to `waiting_for_input`

```mermaid theme={null}
sequenceDiagram
    participant A as Agent (decide_next_action)
    participant W as Worker (review_form)
    participant L as LLM (layout compiler)
    participant D as Database
    participant F as Frontend

    A->>W: variables + layout_guidelines
    W->>W: Resolve ${VAR_N} → JSON
    alt No frozen layout_spec
        W->>L: Data samples + guidelines
        L-->>W: {"sections": [...]}
        W->>D: Persist layout_spec to configured_tools.bindings
    else Frozen layout_spec in bindings
        W->>W: Skip layout LLM
    end
    W->>W: buildArtifactFromSpec (deterministic)
    opt validation_rules set
        W->>L: Validation pass
        W->>W: Merge __metadata.validation on flagged cells
    end
    W->>D: INSERT task_await (validation)
    W-->>A: structured_data + await_id
    F->>F: JsonTreeTable (edits) + ChatInput approve/reject
```

### Layout freezing

On the first successful run for a configured tool instance, the generated layout spec is saved to `configured_tools.bindings.layout_spec`. Subsequent runs reuse that spec — the layout LLM is not called again.

To force regeneration, pass `regenerate_layout: true` (or clear the binding).

## Inputs

| Field               | Required | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| ------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `variables`         | Yes      | Array of `{name, ref}`. `ref` must be a `${VAR_N}` reference to the **whole** variable (not a sub-path). Each resolved value must be a JSON object or array. For a configured tool with a **frozen layout**, the LLM-visible schema is instead an **object** whose keys are the layout's fixed variable names and whose values are the refs (`{"invoice": "${VAR_2}"}`) — the model picks refs but can't drift off the naming contract. Both shapes are accepted at execution.                                                                                                                                                                                                                                                                                                                  |
| `layout_guidelines` | No       | Plain-English layout instructions. Used only when generating a new layout spec. Example: *"Show the invoice header fields, then a line-items table. Add the GL code from 'gl' as a column on the line items, matched by line\_id."*                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| `validation_rules`  | No       | Plain-English rules to flag fields for review (same semantics as `review_data_extraction`). Requires a configured LLM client.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| `static_checks`     | No       | Deterministic CEL validation checks (array of `{expr, scope, target, message}`), evaluated mechanically — no LLM, same result every run. Run in the same pass as `validation_rules` (both merge; deterministic findings win and re-fire on an approved-value change). **Targets must match the composed artifact's leaf keys** — raw field names in `raw_passthrough` mode, the layout's authored labels otherwise — so use `raw_passthrough` or a frozen `layout_spec` so those names are stable. `scope:"unique"` is rejected (one artifact per run). Helper functions in `expr`: `num(x)`, `date(x)`, `abs(x)`, `sum(list)`, and `norm(x)` for case/punctuation/whitespace-insensitive text equality (e.g. `norm(vendor_name) == norm(matched_vendor)`). Config-only (`x-exclude-from-llm`). |
| `title`             | No       | Heading shown above the review form.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| `reviewers`         | No       | Emails or names matching workspace users. Assigns and notifies reviewers via the same path as `ask_user`. The task still pauses if omitted.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| `stage_name`        | No       | Label for this review stage (e.g. `"Finance Review"`). Shown in the stage progress banner.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `stage_index`       | No       | 1-based index of this stage (e.g. `2` for "Stage 2 of 3").                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `stage_total`       | No       | Total number of review stages. Pair with `stage_index` for progress display.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `regenerate_layout` | No       | When `true`, ignore any frozen `layout_spec` and regenerate from `layout_guidelines`. Ignored when `raw_passthrough` is true.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| `raw_passthrough`   | No       | When `true`, skip layout-spec generation entirely — no LLM layout call, no section/field/table reshaping. With one variable, the form *is* that variable's raw JSON. With multiple variables, each is shown verbatim under its own name.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |

`layout_spec` is injected from `configured_tools.bindings` at execution time and is **not** LLM-visible in the input schema.

One further config-only toggle (bound in `configured_tools.bindings`, not LLM-visible): `auto_proceed_on_pass` (skip the human pause when validation rules are set and none fail).

### Frozen variable names

A frozen layout references data as `$<name>.<path>`, but the agent re-authors the `variables` names every task, and they drift. The tool defends in layers:

* **Published names**: a configured tool with a frozen layout advertises `variables` as an object of the layout's fixed names (each an `x-var-ref` string property), so the deciding LLM never invents names — it only picks which `${VAR_N}` holds each one. Names are deliberately not JSON-schema-required: forcing a key when the task lacks that data would make constrained decoding stuff a best-guess ref (a plausible-but-wrong form) instead of failing loudly below.
* **Sanitization**: variable names are trimmed of edge punctuation on intake (`"sold_to_match,"` → `sold_to_match`); spec aliases match through the same fallback.
* **Recovery by id**: a dropdown field whose alias doesn't resolve recovers its value from the task's match/lookup variable carrying the same `match_config_id` / `csv_id` (unambiguous single-row only). A *present* named variable is always authoritative — an empty match result renders an empty dropdown, never a row from another variable.
* **Naming contract** (always on): with a pre-authored layout, a call missing a referenced name that has no id fallback fails with a retryable error listing the exact expected/passed/missing names, instead of rendering those fields blank — the backstop for array-shape calls mimicked from history and for providers without constrained decoding. The error deliberately does not suggest `regenerate_layout` — regenerating overwrites the admin-configured layout and is an admin decision, not a naming-mistake workaround. (A `regenerate_layout: true` call on a frozen-layout tool can only reuse the already-published names — adding a new data source to a frozen form is an admin action in the tool config.)

### Match dropdowns without match data

A `match_config` dropdown whose task has **no** match data — the matcher failed, never ran, or returned an empty `matches` array — renders an **empty, functional dropdown** (options are served live from the config's source) so the reviewer picks manually. Such a pick fills the value but does not teach the matcher (no search term to key a learning on). This requires `value_column` declared on the dropdown config.

### Raw passthrough

`raw_passthrough: true` bypasses the layout compiler entirely: `resolveLayoutSpec` / `generateLayoutSpec` are never called, so there's no LLM cost and no risk of a hallucinated `$path`. Use it when the upstream extraction's own JSON shape is already what the reviewer should see and you don't need to combine sources, add a looked-up column, or otherwise reshape the data — e.g. reviewing a single `structured_data_extraction` output as-is. `validation_rules`, `reviewers`, and the stage fields all still work normally; only the layout step is skipped.

### Variable resolution rules

* Pass the whole variable reference (e.g. `"${VAR_2}"`), not a JSON sub-path.
* Each variable must resolve to structured JSON (object or array). Raw text or unresolved references fail the step.
* `ai_call_response` variables may be markdown-fenced (` ```json … ``` `); the tool strips fences on consume, matching orchestrator path resolution.

## Layout spec (internal)

The layout LLM outputs JSON of the form `{"sections": [ ... ]}`. Sections reference source data with `$<name>.<path>` (e.g. `$invoice.header.total`). Data values are never inlined.

### Section shapes

**Whole source (preferred for simple cases)**

```json theme={null}
{ "label": "Invoice", "source": "$invoice" }
```

Renders all fields from the source. Nested arrays become tables. An array source like `{"label": "Line items", "source": "$invoice.line_items"}` renders as a table.

**Explicit fields**

```json theme={null}
{
  "label": "Header",
  "kind": "fields",
  "fields": {
    "Invoice No": "$invoice.header.invoice_number",
    "Total": "$invoice.header.total"
  }
}
```

**Table with optional join**

Use when adding a looked-up column from a second source:

```json theme={null}
{
  "label": "Line items",
  "kind": "table",
  "rows": "$invoice.line_items",
  "join": {
    "rows": "$gl.line_items",
    "as": "gl",
    "on": { "left": "id", "right": "id" }
  },
  "columns": [
    { "label": "Description", "path": "description" },
    { "label": "Amount", "path": "amount" },
    { "label": "Account", "path": "gl.codings.account.code" }
  ]
}
```

For **`gl_coding`** outputs, publish the tool result as a `${VAR_N}` and pass it into `review_form` as e.g. `gl`. Join `$gl.line_items` onto extraction lines (by `id` or positionally) and expose account codes via `gl.codings.<dimension>.code`. Those paths are auto-stamped with `__metadata.editor = "select"` and a shared `select_option_groups` entry from `$gl.dimensions` so the review UI renders a searchable COA dropdown. Reviewer edits create agent memories/rules that the next `gl_coding` run consumes via `InjectLearnedRules`.

* Omit `on` for a **positional** (by-index) join.
* Omit `columns` to auto-derive columns from the first row (plus joined keys).
* Column `path` names a field (e.g. `"amount"` or `"gl.codings.account.code"`) — never `"amount.value"`. Trailing `.value` on paths is stripped so citations survive.
* Keyed joins normalize `[cite:N]` markers in join-key strings so `ai_call` outputs can match extraction leaves.

### Dropdown (select) editors

When a field's value must be one of a known candidate list — a `gl_coding` dimension, a `csv_lookup` match, any prior step output carrying an enum — the reviewer should correct it via a **constrained dropdown**, not free text. A dropdown correction still flows through the normal edit path (`/api/steps/edit-simple`), so it diffs the artifact and creates learned rules exactly like a typed edit.

**Explicit spec** — mark the column (or a `fields` entry, using the object form) with `editor: "select"` and an `options` locator:

```json theme={null}
{
  "label": "Department",
  "path": "gl.codings.department.code",
  "editor": "select",
  "options": "$gl.dimensions.0.candidates"
}
```

* `options` starting with `$` resolves once against the variables and becomes a **shared option group**: stored a single time in the result `metadata.select_option_groups` catalog (capped at 500 entries, `truncated` flag set when clipped) and referenced from each cell via `__metadata.options_group`.
* `options` without `$` is a **row-relative path** (e.g. `"match.additional_candidates"`): each row's own candidate list is stamped inline on that cell as `__metadata.select_options` (capped at 50).
* Option items may be strings or objects. Objects are read via conventional keys (`value`/`code`/`id`/`match` for the value; `label`/`name`/`match`/`title` for the label; `description`; `confidence_score`/`confidence`). Override with `option_value` / `option_label` / `option_description` — gjson paths into the option element, so nested picks like `"product.Item Code"` work.

**Every row in the CSV, not just the matcher's shortlist:** `csv_lookup_tool`'s `additional_candidates` (per match) is only the LLM matcher's top-K close matches — good for a "did you mean one of these" dropdown, but it isn't the full CSV. When the reviewer should be able to pick *any* row (e.g. any approver, any GL account), reference that lookup's `all_products` array — every row of the CSV.

An **LLM** csv\_lookup variable is published as a wrapper object (exact-match lookups stay a bare `matches` array):

```json theme={null}
{ "match_type": "llm", "matches": [...], "all_products": [...], "csv_id": "..." }
```

so reference the rows via `$lookup.matches` and the full option list via `$lookup.all_products`:

```json theme={null}
{
  "label": "Next Approver",
  "path": "approvals.next_approver",
  "editor": "select",
  "options": "$lookup.all_products",
  "option_value": "internal_id",
  "option_label": "value"
}
```

`all_products` rows are raw `{header: value}` maps straight from the CSV (no `value`/`code`/`match` key of their own), so set `option_value`/`option_label` to the actual column headers. `all_products` is populated only for `matching_strategy: "llm"`. Unlike other shared option groups (which are capped at 500 entries), the `all_products` catalog is **not** capped — every CSV row renders as a choice, so the reviewer can always pick any row. The trade-off is size: a very large CSV produces a very long dropdown, and publishing `all_products` into the variable also inlines it wherever that `${VAR_N}` is referenced, inflating prompt/token size for downstream steps — use `return_columns`/pre-filtering upstream to keep large catalogs reasonable.

**Auto-detection** — no spec changes needed for the two known dropdown-capable sources:

* **`gl_coding` variables** (`{line_items, dimensions: [{name, candidates}]}`): any column/field resolving to a `codings.<dimension>.code` leaf is stamped with that dimension's candidates as a shared group (`<var>:<dimension>`).
* **`csv_lookup` LLM-match variables** (the published matches array): rows carrying `additional_candidates` get the matcher's shortlist stamped inline on the matched (embedding) column's cell — the column is identified deterministically by `candidate.product[col]` equaling the candidate's identity (`match`, falling back to `match_identifier` when the matcher leaves `match` empty).

**Match-row narrowing**: a field/column marked `editor: "select"` whose path lands on a whole match row or its `values` map (including the case where the matched column is literally named `value`, which a `$row.values.value` ref can't reach because trailing `.value` is stripped for citation safety) is narrowed to a canonical leaf holding just the matched column's value, with the row's candidates attached. Stamping refuses non-canonical leaves outright — an object with extra keys renders as nested rows where no dropdown can attach. Narrowing requires explicit select intent; without `editor: "select"`, objects pass through untouched.

Leaves stamped either way carry `__metadata.editor: "select"`; the frontend (`SelectValueCell`) renders a searchable dropdown with inline candidates first (confidence dots), then the shared group, plus the current value and a clear option. Sections rendered as **whole-source passthrough** (`{"source": "$gl"}`) and `raw_passthrough` forms are **not** stamped — enum data must be laid out via table columns or `fields` for dropdowns to attach (the layout compiler prompt steers this).

## Output

```json theme={null}
{
  "status": "completed",
  "result": {
    "structured_data": { "...": "ordered sections → fields/tables with citations" },
    "metadata": {
      "title": "Invoice review",
      "stage_name": "Finance Review",
      "stage_index": 2,
      "stage_total": 3,
      "applied_memories": [{ "rule_id": "rule-4240", "field": "invoice.header.invoice_number", "analysis": "..." }]
    },
    "validation_summary": { "failed_count": 1 },
    "await_id": "<uuid>"
  }
}
```

* `structured_data` uses the same leaf shape as `structured_data_extraction`: `{"value": ..., "word_id_groups": [...], "__metadata": {...}}`.
* When `validation_rules` or `static_checks` is set, flagged cells receive `__metadata.validation.{failed, reason}`, plus `source: "static"` for a deterministic (static-check) verdict — an LLM finding is left unmarked (absent `source` == LLM). The provenance lets a round-2 review recover a carried-through deterministic verdict rather than downgrade it.
* `await_id` is **always** present on success. The orchestrator detects it and suppresses `decide_next_action`, moving the task to `waiting_for_input`.

On error, `structuredContent` is `{"status": "error", "message": "..."}` and `isError` is true.

### Applied memory rules

`applied_memories` surfaces the learned rules (`agent_memories`) that fired on the **source steps** this form aggregates — `review_form` itself has no extraction LLM call, so it never applies rules on its own account. Each entry is tagged with the source variable's name (e.g. `invoice.header.invoice_number`, `gl.codings.department.code`) so rules from different sources are distinguishable even when merged into one list; a rule that fired across multiple rows of a table can appear once per affected row. The key is omitted entirely when no source step recorded any rules. This feeds the same "N learned rules applied" chip already shown under `structured_data_extraction` output — no separate UI for `review_form`.

## Task pause and review flow

`review_form` always creates a validation `task_await` — unlike `review_data_extraction`, which only pauses when validation findings exist.

The orchestrator treats `review_form` the same as `structured_data_extraction` and `review_data_extraction` for pause detection: an `await_id` in `structuredContent.result` triggers `waiting_for_input`.

**Do not call `ask_user` after `review_form`.** The pause is embedded in the tool result.

When reviewers are assigned:

1. `ask_user.PerformAssignment` writes task assignments and sends notifications.
2. Assignment failure is logged but does not cancel the pause.
3. Task assignees are returned on `GET /api/tasks/:id/feed` as `assignees` and shown in the approval composer.

### Approving or rejecting

`review_form` uses a **chat-composer approval surface** (`ChatInput` in `reviewApproval` mode), not the inline Proceed button on the extraction card.

| Action      | UI                                          | API                                                                                               |
| ----------- | ------------------------------------------- | ------------------------------------------------------------------------------------------------- |
| **Approve** | Approve toggle + optional comment + send    | `POST /api/tasks/:id/awaits/:await_id/respond` with `{ "decision": "proceed", "comment": "..." }` |
| **Reject**  | Reject toggle + **required** comment + send | Same endpoint with `{ "decision": "reject", "comment": "..." }`                                   |

On approve, the frontend **saves pending cell edits first** (via a `reviewFormSaveRef` callback registered by the active pause card), then calls the respond endpoint. Reject does not auto-save edits.

The backend records an attributed `message_sent` feed row on respond (decision, optional comment, reviewer name) for the timeline audit trail.

### Edit persistence

Edits saved on the review card are written to the **`review_form` step result** in the task feed (via `submitEditsSimple` / `EditService`), updating the combined `structured_data` artifact on that card. They do **not** write back to the upstream source variables (`${VAR_2}`, etc.) that were only read to compose the form.

`review_form` does not yet register its own `${VAR_N}` on step completion — downstream steps cannot reference reviewer edits via a variable until variable publication is added. Design intent: a future `review_form` output variable would hold the combined artifact; upstream sources stay read-only inputs.

## Multi-stage review

Call `review_form` once per stage (one reviewer/stage per call). Each call creates an auditable feed card.

```text theme={null}
Stage 1: stage_index=1, stage_total=3, stage_name="Data entry review"
Stage 2: stage_index=2, stage_total=3, stage_name="Finance review"
Stage 3: stage_index=3, stage_total=3, stage_name="Manager sign-off"
```

Subsequent stages that need reviewer-corrected data must read from the prior stage's saved **`review_form` feed result** (or a future published output variable once wired). The UI shows a stage progress banner from `metadata.stage_*` in the approval composer (`stage_name`, `stage_index` / `stage_total` as "Stage N of M").

## Human-readable UI

In the task feed, `review_form` renders as `structured_data_extraction` content (same `JsonTreeTable` extraction UI) with:

* Editable cells and citation highlights (clicking a cited cell opens the PDF preview; citation visibility follows `showCitationForCell` rules)
* Validation flags (yellow cells) when `validation_rules` flagged fields
* Full-width card layout — `StepActionMenu` is positioned outside the content column so the table uses the full feed width

### Pause UX (two surfaces)

When the task is `waiting_for_input` / `awaiting_review` and a `review_form` step holds an `await_id`, `TaskFeed` picks the **latest** matching feed entry as the active pause.

**1. Data card — edit bar only**

The sticky bottom bar on the extraction card is for **edits**, not approval:

* Shown for `review_form` only when the reviewer has unsaved changes (or while saving / after a save flash)
* Offers **Discard Edits** and **Save Changes**
* Does **not** show Proceed — approval lives in the chat composer (below)

For `review_data_extraction`, the same bar still includes Proceed.

**2. Chat composer — approve / reject**

When a `review_form` pause is active, the normal message input is replaced by `ChatInput` in `reviewApproval` mode:

* Stage label from `metadata.stage_*` (e.g. "Finance Review · Stage 2 of 3")
* Segmented **Approve / Reject** toggle
* Comment field (optional on approve, required on reject)
* **ReviewAssigneePicker** (task assignees from feed `assignees`)
* Send button (blue on approve, red on reject)

**Fullscreen extraction overlay:** the composer is hidden; a **single-line** approval bar is portaled to the bottom of the viewport (`reviewApprovalSingleLine`), offset for the PDF preview panel width. The former `ReviewFormSidebarPanel` in the sections sidebar was removed — approval is always in this bottom bar.

`TaskFeed` passes `activeReviewFormEntryId` and `reviewFormSaveRef` so the active card registers `saveEditsIfNeeded` before approve.

## Agent instruction guidance

1. **Prior steps must produce structured JSON.** Typical sources: `structured_data_extraction`, `ai_call` (with JSON output), database/query tools, or another `review_form` / `review_data_extraction` output.
2. **Name variables logically** in `variables` (e.g. `"invoice"`, `"gl"`) — the layout compiler uses these names in `$invoice`, `$gl` references.
3. **Describe joins in `layout_guidelines`** when combining sources: which key to match on, which columns to show, and which source supplies each column.
4. **Multi-stage:** after stage N approves, stage N+1 should compose from the prior stage's saved review data (not re-extract from scratch). Until output variables exist, the agent must reference the corrected artifact from the prior `review_form` step result.
5. **Do not chain `ask_user`** for review — `review_form` handles the pause.

### Example workflow

```text theme={null}
1. structured_data_extraction  → ${VAR_2}  (invoice)
2. ai_call / lookup            → ${VAR_5}  (GL codes per line_id)
3. review_form
     variables: [
       {"name": "invoice", "ref": "${VAR_2}"},
       {"name": "gl", "ref": "${VAR_5}"}
     ]
     layout_guidelines: "Show invoice header fields, then a line-items table with Description, Amount, and GL Code from gl matched by line_id."
     validation_rules: "Flag any line where amount is missing or GL code is empty."
     reviewers: ["finance@example.com"]
     stage_name: "Finance Review"
     stage_index: 1
     stage_total: 1
```

## Comparison with `review_data_extraction`

|                     | `review_data_extraction`            | `review_form`                                                 |
| ------------------- | ----------------------------------- | ------------------------------------------------------------- |
| Input data          | Single `extracted_data` reference   | Multiple named `variables`                                    |
| Layout              | Uses extraction shape as-is         | LLM-generated layout spec + deterministic assembly            |
| `validation_rules`  | Optional (or `static_checks`)       | Optional                                                      |
| `static_checks`     | Optional (deterministic, no LLM)    | Optional (deterministic, no LLM)                              |
| Pause behavior      | Only when validation findings exist | Always                                                        |
| Layout caching      | N/A                                 | Frozen in `configured_tools.bindings`                         |
| Multi-source joins  | N/A                                 | Keyed or positional table joins                               |
| Approve / reject UI | Proceed on card review bar          | `ChatInput` approval composer (bottom bar in fullscreen)      |
| Edit save target    | Updates source extraction variable  | Updates `review_form` step result in feed (not upstream vars) |

## Notes

* Layout generation uses the default LLM tier. Optional `validation_rules` uses the same validation pass as `review_data_extraction`.
* Off by default — enable it in the agent's Tools panel.
