> ## Documentation Index
> Fetch the complete documentation index at: https://developer.kodexa.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Bridge API & External Services

> Use the Bridge API in Kodexa data forms for platform data access, HTTP requests, and service bridge integration directly from sandboxed scripts.

The Bridge API is the interface between scripts running in the QuickJS sandbox and the Kodexa platform. Scripts access it through the `kodexa.*` namespace, where each sub-namespace corresponds to a capability gated by the form's bridge permissions.

## Bridge Permissions

The `bridge` property on a data form configures what scripts are allowed to do. The `permissions` array lists capability gates -- a script that attempts to call a method without the required permission will receive a `Permission denied` error.

| Permission      | Grants access to                                                                                                                            |
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `data:read`     | Read data objects, attributes, tag metadata, taxonomies                                                                                     |
| `data:write`    | Modify data objects and attributes                                                                                                          |
| `document:read` | Access the document proxy (`kodexa.document`)                                                                                               |
| `navigation`    | Focus attributes, switch pages, rotate pages, jump to sections and exceptions, switch views (`kodexa.navigation`)                           |
| `viewer`        | Scroll, pan, zoom, show page regions, fit the page, copy the selection, and pop out / re-dock the spatial document viewer (`kodexa.viewer`) |
| `formState`     | Get and set form state values, access node refs                                                                                             |
| `http:get`      | HTTP GET requests via `kodexa.http`                                                                                                         |
| `http:post`     | HTTP POST requests via `kodexa.http` and service bridge calls                                                                               |

Additional bridge configuration:

* **`apiBaseUrl`** -- Base URL for `kodexa.http` and `kodexa.serviceBridge` calls. Defaults to `"/api"`.
* **`maxExecutionMs`** -- Script execution timeout in milliseconds. Defaults to `1000`.

```json theme={null}
{
  "bridge": {
    "permissions": ["data:read", "data:write", "navigation", "http:post"],
    "apiBaseUrl": "https://api.example.com",
    "maxExecutionMs": 2000
  }
}
```

## kodexa.data

Requires `data:read`. Methods that modify data also require `data:write`, and the two focused-field methods that only move focus or reveal values require `navigation` -- each method's gate is listed in the table.

| Method                                      | Parameters                                         | Returns               | Permission   |
| ------------------------------------------- | -------------------------------------------------- | --------------------- | ------------ |
| `getDataObjects(filter?)`                   | `{ path?: string, parentId?: string }`             | `DataObject[]`        | `data:read`  |
| `getDataObject(uuid)`                       | `uuid: string`                                     | `DataObject \| null`  | `data:read`  |
| `getAttributes(dataObjectUuid)`             | `dataObjectUuid: string`                           | `Attribute[]`         | `data:read`  |
| `getAttribute(dataObjectUuid, path)`        | `dataObjectUuid: string, path: string`             | `any`                 | `data:read`  |
| `setAttribute(dataObjectUuid, path, value)` | `dataObjectUuid: string, path: string, value: any` | `void`                | `data:write` |
| `addDataObject(parentUuid, path)`           | `parentUuid: string, path: string`                 | `DataObject`          | `data:write` |
| `deleteDataObject(uuid)`                    | `uuid: string`                                     | `void`                | `data:write` |
| `getTagMetadata(path)`                      | `path: string`                                     | `TagMetadata \| null` | `data:read`  |
| `getTaxonomies()`                           | --                                                 | `Taxonomy[]`          | `data:read`  |
| `clearFocusedValue()`                       | --                                                 | `void`                | `data:write` |
| `deleteFocusedValue()`                      | --                                                 | `void`                | `data:write` |
| `addDataGroup()`                            | --                                                 | `void`                | `data:write` |
| `deleteDataGroup()`                         | --                                                 | `void`                | `data:write` |
| `blurField()`                               | --                                                 | `void`                | `navigation` |
| `showInDocument()`                          | --                                                 | `void`                | `navigation` |

```javascript theme={null}
// Read all line items under a parent object
const items = kodexa.data.getDataObjects({ parentId: parentUuid });
for (const item of items) {
  const amount = kodexa.data.getAttribute(item.uuid, "LineItem/Amount");
  kodexa.log.debug("Amount: " + amount);
}

// Set a computed total
kodexa.data.setAttribute(summaryUuid, "Invoice/Total", calculatedTotal);
```

### Typed writes with `setAttribute`

`setAttribute` stores the value in the column that matches the attribute's own type rather than keeping everything as text, so the write survives a save and reload and is picked up by the field's editor, formulas, and validation rules:

* **Numeric attributes** -- a JavaScript number, or a numeric string with grouping commas such as `"1,234.56"`, is stored as the numeric value.
* **Date attributes** -- an ISO date-time is stored as the date value, and a date-only literal such as `"2026-01-01"` is accepted and stored as midnight on that day.
* **Boolean attributes** -- `true` / `false` set the boolean value.
* **Text and untyped attributes** -- the value is stored as text; a number written to a text attribute is stored in its string form.

A value that cannot be parsed as a number for a numeric attribute falls back to the text column and logs a warning to the browser console, so scripts that pre-formatted numbers or dates to work around values not saving no longer need to.

```javascript theme={null}
// Each of these lands in the attribute's own typed column
kodexa.data.setAttribute(uuid, "Invoice/Total", "1,234.56");   // numeric
kodexa.data.setAttribute(uuid, "Invoice/IssuedOn", "2026-01-01"); // date
kodexa.data.setAttribute(uuid, "Invoice/Approved", true);       // boolean
```

The update goes through the same audited edit path as a reviewer's own edit, so it is persisted, survives reload, and appears in the change history. The attribute's original extracted text is never overwritten -- `setAttribute` changes the current value only.

When the data object has no attribute at `path` yet, one is created from the supplied value, provided the form's taxonomy metadata resolves that path. If it does not, the call logs a warning to the browser console and makes no change.

### Focused-field methods

The last six methods take **no arguments** and act on whichever field the reviewer currently has focused, so one form-declared shortcut works across every field in the form:

* **`clearFocusedValue()`** blanks the field's value while keeping the attribute record.
* **`deleteFocusedValue()`** removes the attribute entirely. Both leave the field looking blank, but an absent attribute is not an empty one -- the distinction can matter for export and validation.
* **`addDataGroup()`** adds a sibling row after the focused field's data group.
* **`deleteDataGroup()`** deletes the data group the focused field belongs to.
* **`blurField()`** drops focus from the field without changing any data.
* **`showInDocument()`** reveals the focused field's value in the document viewer.

All six are safe no-ops when no field has focus -- a warning is logged to the browser console and no error is thrown. This is the common case rather than an edge case: a form-declared shortcut fires whether or not focus is in a field. The four methods that change data require `data:write`; `blurField` and `showInDocument` need only `navigation`, so a read-only form can still move focus and reveal values.

These methods exist to back data-entry keyboard shortcuts. Bind them declaratively as form `shortcuts` entries (see [Keyboard Shortcuts](/guides/data-forms/shortcuts)) whose scripts call, for example, `kodexa.data.clearFocusedValue()`.

## kodexa.navigation

Requires `navigation`. Spatial-viewer methods route to the document viewer for the form's first document family by default; pass an explicit `documentFamilyId` when a form is bound to more than one document.

| Method                                                   | Parameters                                                       | Returns               | Description                                                                                              |
| -------------------------------------------------------- | ---------------------------------------------------------------- | --------------------- | -------------------------------------------------------------------------------------------------------- |
| `focusAttribute(dataObjectUuid, attributePath, viewId?)` | `dataObjectUuid: string, attributePath: string, viewId?: string` | `void`                | Highlight an attribute in the document viewer (and broadcast to sidecars)                                |
| `setPage(page, documentFamilyId?)`                       | `page: number, documentFamilyId?: string`                        | `void`                | Navigate the spatial viewer to a **1-based** page number                                                 |
| `nextPage(documentFamilyId?)`                            | `documentFamilyId?: string`                                      | `void`                | Step the spatial viewer forward one page. Clamped to the last page -- a no-op at the end of the document |
| `previousPage(documentFamilyId?)`                        | `documentFamilyId?: string`                                      | `void`                | Step the spatial viewer back one page. Clamped to the first page -- a no-op at the start of the document |
| `rotatePage(direction, documentFamilyId?)`               | `direction: "left" \| "right", documentFamilyId?: string`        | `void`                | Rotate the spatial viewer's **current** page by 90° in the given direction                               |
| `getCurrentPage(documentFamilyId?)`                      | `documentFamilyId?: string`                                      | `number \| undefined` | Current 1-based page of the spatial viewer                                                               |
| `getPageCount(documentFamilyId?)`                        | `documentFamilyId?: string`                                      | `number \| undefined` | Total page count of the spatial viewer's document                                                        |
| `nextSection()`                                          | --                                                               | `boolean`             | Scroll the form to the next visible section and focus its first input                                    |
| `nextException(documentFamilyId?)`                       | `documentFamilyId?: string`                                      | `boolean`             | Scroll the form to the next open validation exception                                                    |
| `scrollToNode(ref)`                                      | `ref: string`                                                    | `void`                | Scroll the document viewer to a content node *(planned)*                                                 |
| `switchView(viewName)`                                   | `viewName: string`                                               | `void`                | Switch to a different form view *(planned)*                                                              |

Note that `focusAttribute` takes `dataObjectUuid` as the first parameter and `attributePath` as the second.

`rotatePage` behaves differently from the absolute `setPage`:

* **Rotation is relative.** Each call rotates by ±90° from the page's *current* rotation -- it is not an absolute angle. Calling `rotatePage("right")` twice leaves the page at 180°; a following `rotatePage("left")` returns it to 90°.
* **It affects only the current page.** Rotating every page of the document remains a separate viewer menu action; the bridge method touches just the page the viewer is currently showing.
* **It requires the `navigation` permission** and accepts an optional `documentFamilyId` to target a specific open document when the form is bound to more than one. A per-document rotation does not bleed across documents.

An invalid `direction` (anything other than `"left"` or `"right"`) or a missing viewer is a no-op -- a warning is logged to the browser console and no error is thrown.

```javascript theme={null}
// Jump to page 3 of the form's document
kodexa.navigation.setPage(3);

// Step one page at a time -- these clamp at the document edges,
// so calling nextPage() on the last page (or previousPage() on
// the first) is a silent no-op rather than an error.
kodexa.navigation.nextPage();
kodexa.navigation.previousPage();

// Read the current page after a navigation
const current = kodexa.navigation.getCurrentPage();
kodexa.log.debug("Now on page " + current + " of " + kodexa.navigation.getPageCount());

// Target a specific document when the form is bound to multiple
kodexa.navigation.setPage(1, "doc-family-uuid");

// Rotate the current page of the form's document clockwise
kodexa.navigation.rotatePage("right");

// Rotate a specific open document's current page counter-clockwise
kodexa.navigation.rotatePage("left", "doc-family-uuid");
```

These rotate actions are typically wired to keyboard shortcuts rather than called directly. The `alt+R` / `alt+shift+R` bindings are not hard-coded -- they are authored declaratively as form `shortcuts` entries whose script calls `kodexa.navigation.rotatePage(...)`:

```yaml theme={null}
shortcuts:
  - key: "alt+r"
    description: "Rotate the current page right"
    scriptRef: rotate-page-right
  - key: "alt+shift+r"
    description: "Rotate the current page left"
    scriptRef: rotate-page-left
```

The `rotate-page-right` script calls `kodexa.navigation.rotatePage("right")` and `rotate-page-left` calls `kodexa.navigation.rotatePage("left")`, so a `shortcuts:` block round-trips through the data form schema.

### Section and exception jumps

`nextSection()` and `nextException()` walk a reviewer through the form rather than the document:

* **`nextSection()` scrolls the form to the next visible section and focuses its first input.** Sections are the form's visible `v2:panel` components; each call advances one section, and after the last section the jump wraps back to the top of the form.
* **`nextException(documentFamilyId?)` scrolls the form to the next open validation exception.** The anchor is the field's attribute editor when one is rendered; for an exception on a grid row, it is the grid containing the row. Like `nextSection`, it cycles back to the top after the last anchor. Pass the optional `documentFamilyId` to scope the exception scan to a specific open document when the form is bound to more than one.
* **Both return `false` when the window has nothing to act on** -- no form pane in the current window (for example a popped-out viewer tab), or, for `nextException`, no open exceptions. A warning is logged to the browser console and no error is thrown. Both require the `navigation` permission.

Bind them as form `shortcuts` entries to give reviewers keyboard-driven section-by-section or exception-by-exception review:

```yaml theme={null}
shortcuts:
  - key: "alt+shift+n"
    description: "Jump to the next section"
    scriptRef: next-section
  - key: "alt+shift+e"
    description: "Jump to the next exception"
    scriptRef: next-exception
```

The `next-section` script calls `kodexa.navigation.nextSection()` and `next-exception` calls `kodexa.navigation.nextException()`.

## kodexa.viewer

Requires `viewer`. The `viewer` surface manipulates the spatial document viewport -- its scroll and pan position, its zoom level, and whether the viewer is docked in the workspace or popped out into its own browser tab -- rather than the page state. That separation is why it is a distinct permission from `navigation`.

| Method                                  | Parameters                                                                  | Returns | Description                                                                                              |
| --------------------------------------- | --------------------------------------------------------------------------- | ------- | -------------------------------------------------------------------------------------------------------- |
| `scroll(direction, documentFamilyId?)`  | `direction: "up" \| "down" \| "left" \| "right", documentFamilyId?: string` | `void`  | Nudge the spatial viewer \~¼ of its visible size in the given direction                                  |
| `zoom(direction, documentFamilyId?)`    | `direction: "in" \| "out", documentFamilyId?: string`                       | `void`  | Zoom the spatial viewer in or out one step, matching the toolbar zoom buttons                            |
| `showRegion(region, documentFamilyId?)` | `region: "top" \| "middle" \| "bottom", documentFamilyId?: string`          | `void`  | Reset zoom and scroll the named region of the current page into view                                     |
| `fit(direction, documentFamilyId?)`     | `direction: "width" \| "height", documentFamilyId?: string`                 | `void`  | Zoom so the page fills the viewport's width or height                                                    |
| `copySelection(documentFamilyId?)`      | `documentFamilyId?: string`                                                 | `void`  | Copy the current document selection to the clipboard                                                     |
| `detach()`                              | --                                                                          | `void`  | Pop the viewer out into a new browser tab, mirroring the viewer's "Open in new tab" button               |
| `dock()`                                | --                                                                          | `void`  | Dock a popped-out viewer back into the workspace, mirroring the viewer's "Dock back to workspace" button |

* **`scroll` direction is one of `"up"`, `"down"`, `"left"`, `"right"`.** Any other value is a no-op -- a warning is logged to the browser console and no error is thrown. `"up"` / `"down"` scroll the viewer vertically; `"left"` / `"right"` pan the zoom transform (the spatial image is CSS-transformed, so left/right movement pans rather than scrolls). Both feel like a small "arrow key" nudge, not a full-screen jump.
* **`zoom` steps one increment in or out.** `"in"` and `"out"` map to the toolbar's zoom-in / zoom-out buttons -- one call is one step. Any other value is a no-op with a browser-console warning and no error thrown.
* **`showRegion` resets zoom and brings a region of the current page into view.** The region is one of `"top"`, `"middle"`, or `"bottom"`; any other value is a no-op with a browser-console warning. The region names are looser than they sound -- `"middle"` scrolls to roughly a quarter down the page, matching long-standing viewer behavior.
* **`fit` zooms so the page fills the viewport.** `"width"` fits the page to the viewport's width, `"height"` to its height; any other value is a no-op with a browser-console warning.
* **`copySelection` copies the current document selection to the clipboard.** It deliberately stays out of the way while you are typing in a field -- when focus is inside an input, the method does nothing, so a copy shortcut bound to it coexists with native copy in form fields.
* **`detach` pops the viewer out into a new browser tab.** This is the same action as the viewer's "Open in new tab" button. It is only meaningful from the main window -- calling it from inside the popped-out tab is a no-op (a warning is logged). If the pop-out tab is already open, `detach` focuses that tab rather than opening a second one. It takes no arguments.
* **`dock` returns a popped-out viewer to the workspace.** This mirrors the viewer's "Dock back to workspace" button and works from either the main window or the popped-out tab. When no viewer is currently detached it is a no-op with a warning. It takes no arguments.
* **`scroll`, `zoom`, `showRegion`, `fit`, and `copySelection` can target one document.** With no argument they act on the form's default (first) document family; pass an optional `documentFamilyId` to target a specific open document. All five apply to the viewer whether it is docked in the workspace or popped out into its own tab. `detach` and `dock` always act on the workspace's pop-out viewer and take no target.
* **Requires the `viewer` permission.** Calling any `kodexa.viewer` method without `"viewer"` in `bridge.permissions` throws a `Permission denied: viewer` error.

```javascript theme={null}
// Nudge the viewer down, then pan right
kodexa.viewer.scroll("down");
kodexa.viewer.scroll("right");

// Zoom in one step, then back out
kodexa.viewer.zoom("in");
kodexa.viewer.zoom("out");

// Pop the viewer out into its own tab, then dock it back
kodexa.viewer.detach();
kodexa.viewer.dock();

// scroll and zoom accept a documentFamilyId to target a specific open document
kodexa.viewer.scroll("up", "doc-family-uuid");
kodexa.viewer.zoom("in", "doc-family-uuid");
```

Like the rest of the Bridge API, these actions are usually wired to keyboard shortcuts rather than called directly. Bindings are authored declaratively as form `shortcuts` entries (see [Keyboard Shortcuts](/guides/data-forms/shortcuts)); zoom, detach, and dock fit the `"zoom"` help-dialog group. The keys below are only examples chosen by the form author -- there are no fixed hotkeys for these actions:

```yaml theme={null}
shortcuts:
  - key: "control+="
    description: "Zoom the document in"
    group: "zoom"
    scriptRef: zoom-in
  - key: "control+-"
    description: "Zoom the document out"
    group: "zoom"
    scriptRef: zoom-out
  - key: "control+9"
    description: "Open the viewer in a new tab"
    group: "zoom"
    scriptRef: detach-viewer
  - key: "control+w"
    description: "Dock the viewer back to the workspace"
    group: "zoom"
    scriptRef: dock-viewer
```

Each `scriptRef` points at a script that calls the matching method -- for example `detach-viewer` calls `kodexa.viewer.detach()` and `dock-viewer` calls `kodexa.viewer.dock()`.

## kodexa.form

Requires `formState`.

| Method            | Parameters                | Returns               | Description                                  |
| ----------------- | ------------------------- | --------------------- | -------------------------------------------- |
| `get(key)`        | `key: string`             | `any`                 | Read a form state value                      |
| `set(key, value)` | `key: string, value: any` | `void`                | Write a form state value                     |
| `getNodeRef(ref)` | `ref: string`             | `{ setProps(props) }` | Get a UINode by ref for dynamic prop updates |

Form state is ephemeral -- it persists for the lifetime of the form session but is not saved to the server. Use it for UI-only concerns like toggling visibility, tracking selection state, or passing values between scripts.

```javascript theme={null}
// Toggle a detail panel
const expanded = kodexa.form.get("detailExpanded") || false;
kodexa.form.set("detailExpanded", !expanded);

// Dynamically update a node's props
const node = kodexa.form.getNodeRef("statusLabel");
node.setProps({ text: "Validated", variant: "success" });
```

## kodexa.document

Requires `document:read`. The writable snapshot also requires `data:write`.

| Method               | Returns               | Description                                                  |
| -------------------- | --------------------- | ------------------------------------------------------------ |
| `snapshot()`         | `ScriptDocumentProxy` | Read-only proxy over the current document data               |
| `writableSnapshot()` | `ScriptDocumentProxy` | Writable proxy -- mutations flow through the workspace store |

The `ScriptDocumentProxy` exposes `getAllDataObjects()`, `getDataObjectByUUID(uuid)`, and `getDataObjectsByPath(path)`. Each returns `ScriptDataObjectProxy` instances with methods like `getAttributes()`, `getAttribute(label)`, `getChildren()`, and `getPath()`. Writable proxies additionally support `addAttribute()`, `addChild()`, and `setValue()` on attributes.

This API is separate from `loadDocument()` available in inline or named scripts. `kodexa.document` provides Bridge API context tied to the current workspace session; `loadDocument()` is for standalone script execution.

## kodexa.http

Requires `http:get` for GET requests, `http:post` for POST requests. Both are async.

| Method              | Parameters                 | Returns        |
| ------------------- | -------------------------- | -------------- |
| `get(path)`         | `path: string`             | `Promise<any>` |
| `post(path, body?)` | `path: string, body?: any` | `Promise<any>` |

Requests are sent to `apiBaseUrl + path`. The base URL defaults to `"/api"` if not configured.

```javascript theme={null}
// Call an external validation endpoint
const result = await kodexa.http.post("/validate", {
  invoiceNumber: invoiceNum,
  vendorId: vendorId
});

if (!result.valid) {
  kodexa.log.warn("Validation failed: " + result.reason);
}
```

## kodexa.serviceBridge

Requires `http:post`.

| Method                       | Parameters                                  | Returns        |
| ---------------------------- | ------------------------------------------- | -------------- |
| `call(ref, endpoint, body?)` | `ref: string, endpoint: string, body?: any` | `Promise<any>` |

Service bridges are named proxy endpoints that connect the platform to external APIs with centralized authentication. The `ref` is the bridge slug (e.g., `"acme-logistics/carrier-lookup"`), and `endpoint` is the endpoint name defined in the bridge YAML.

The bridge manages an `X-Bridge-Context` header for session caching. On the first call, no context header is sent; the server runs any configured `initScript` and returns context in the response header. Subsequent calls attach the cached context, skipping re-initialization. Context expires after a configurable TTL (default 3600 seconds).

```javascript theme={null}
const carriers = await kodexa.serviceBridge.call(
  "acme-logistics/carrier-lookup",
  "lookup-carrier",
  { scac: scacCode }
);
```

## kodexa.log

No permission required.

| Method           | Parameters        |
| ---------------- | ----------------- |
| `debug(message)` | `message: string` |
| `warn(message)`  | `message: string` |
| `error(message)` | `message: string` |

All log output is prefixed with `[DataFormV2]` and routed to the browser console.

## Service Bridges on Panels

In addition to imperative `kodexa.serviceBridge.call()` from scripts, panels support declarative service bridge integration through the `serviceBridge` prop on `v2:panel`. This allows a component to declare an external API dependency without writing script code.

### ServiceBridgeConfig

| Property          | Type                           | Description                                                |
| ----------------- | ------------------------------ | ---------------------------------------------------------- |
| `ref`             | `string`                       | Bridge reference (e.g., `"acme-logistics/carrier-lookup"`) |
| `endpoint`        | `string`                       | Endpoint name from the bridge YAML                         |
| `method`          | `"GET" \| "POST"`              | HTTP method, defaults to `POST`                            |
| `requestMapping`  | `Record<string, string>`       | Maps data attribute paths to request fields                |
| `responseMapping` | `ServiceBridgeResponseMapping` | Controls how the response maps back to UI or data          |
| `triggerOn`       | `string[]`                     | Attribute paths that trigger a re-call when values change  |

### ServiceBridgeResponseMapping

| Property      | Type                  | Description                                                                                          |
| ------------- | --------------------- | ---------------------------------------------------------------------------------------------------- |
| `value`       | `string`              | Path to extract option value from each response item                                                 |
| `label`       | `string`              | Path or expression for the option label (supports concatenation like `"code + ' - ' + description"`) |
| `description` | `string`              | Path or expression for hint text shown below the label in dropdowns                                  |
| `autoSelect`  | `string`              | Auto-select a single best-match field from the response                                              |
| `attributes`  | `Array<{ from, to }>` | Map response fields to data attributes (`from`: response path, `to`: attribute path)                 |

### How It Works

When any attribute listed in `triggerOn` changes, the panel reads the current values from `requestMapping`, calls the service bridge endpoint, and maps the response back using `responseMapping`. This creates a reactive loop: user edits a field, the bridge fetches updated data, and dependent fields populate automatically.

```json theme={null}
{
  "component": "v2:panel",
  "props": {
    "title": "Carrier Details",
    "serviceBridge": {
      "ref": "acme-logistics/carrier-lookup",
      "endpoint": "lookup-carrier",
      "requestMapping": {
        "scac": "Shipment/CarrierSCAC"
      },
      "responseMapping": {
        "value": "carrierId",
        "label": "scac + ' - ' + carrierName",
        "attributes": [
          { "from": "carrierName", "to": "Shipment/CarrierName" },
          { "from": "dotNumber", "to": "Shipment/DOTNumber" }
        ]
      },
      "triggerOn": ["Shipment/CarrierSCAC"]
    }
  }
}
```

In this example, when the user enters a SCAC code, the panel calls the carrier lookup endpoint and auto-populates the carrier name and DOT number fields from the response.

## v2:serviceBridgeView

A container component that calls a service bridge endpoint and makes the response available to its children through the data context. Unlike the panel `serviceBridge` prop (which maps responses back to attributes), `v2:serviceBridgeView` is designed for **read-only display** -- rendering bridge responses as tables, markdown, labels, or any combination of child components.

### How It Works

1. The component resolves the bridge slug to an ID via the platform's `/api/resolve` endpoint.
2. It POSTs the `params` to the bridge proxy endpoint (`/api/service-bridges/{id}/proxy/{endpoint}`).
3. If a `transform` expression is provided, the response is reshaped using [JSONata](https://jsonata.org/).
4. The result is injected into a scoped `DataContextV2` as `ctx.$bridgeResult`, along with `ctx.$bridgeLoading` and `ctx.$bridgeError`.
5. Children render using bindings that reference these context variables.

The component handles loading and error states automatically -- children are only rendered once data is available.

### Props

| Prop      | Type   | Default  | Description                                                                              |
| --------- | ------ | -------- | ---------------------------------------------------------------------------------------- |
| bridgeRef | string | required | Service bridge URI (e.g., `"service-bridge://acme/rate-lookup"` or `"acme/rate-lookup"`) |
| endpoint  | string | required | Endpoint name defined in the bridge configuration                                        |
| params    | Record | --       | Request body sent to the bridge (typically provided via `bindings`)                      |
| transform | string | --       | JSONata expression applied to the response before injecting into context                 |

### Context Variables

Children of `v2:serviceBridgeView` receive these additional context variables:

| Variable             | Type      | Description                                            |
| -------------------- | --------- | ------------------------------------------------------ |
| `ctx.$bridgeResult`  | `any`     | The bridge response (after optional JSONata transform) |
| `ctx.$bridgeLoading` | `boolean` | `true` while the request is in flight                  |
| `ctx.$bridgeError`   | `string`  | Error message if the request failed                    |

All existing context variables (`ctx.dataObjects`, `ctx.tagMetadataMap`, etc.) remain available -- the bridge context is additive.

### Reactive Parameters

When `params` is provided via `bindings`, the component re-calls the bridge whenever the bound values change. This creates a reactive chain: the user edits an attribute, the binding expression re-evaluates, new params are sent to the bridge, and children re-render with fresh data.

<Note>
  When referencing attributes in binding expressions, the `tag` property is the **leaf name** (e.g., `originZip`), not the full taxonomy path (`shipment/originZip`). The full path is available as `path` on the attribute. Use `a.tag === 'originZip'` or `a.path === 'shipment/originZip'` depending on which you need.
</Note>

### Example: Table from Bridge Response

Call a rate lookup endpoint, transform the response with JSONata, and render the results as a filterable AG Grid table:

```yaml theme={null}
component: v2:serviceBridgeView
props:
  bridgeRef: "service-bridge://acme/rate-lookup"
  endpoint: "get-rates"
  transform: |
    results.{
      "Carrier": carrier,
      "Rate": "$" & $string(rate),
      "Transit": transit & " days"
    }
bindings:
  params: |
    {
      originZip: ctx.dataObjects[0]?.attributes?.find(
        a => a.tag === 'originZip'
      )?.stringValue,
      destZip: ctx.dataObjects[0]?.attributes?.find(
        a => a.tag === 'destZip'
      )?.stringValue,
      weight: ctx.dataObjects[0]?.attributes?.find(
        a => a.tag === 'weight'
      )?.numericValue
    }
children:
  - component: v2:dataTable
    bindings:
      rows: "ctx.$bridgeResult"
    props:
      filterable: true
      columns:
        - field: Carrier
          title: Carrier Name
        - field: Rate
          title: Rate
          width: 120
        - field: Transit
          title: Transit Time
          width: 120
```

### Example: Markdown Summary

Fetch a report from a bridge and render it as markdown:

```yaml theme={null}
component: v2:serviceBridgeView
props:
  bridgeRef: "service-bridge://acme/analysis"
  endpoint: "get-report"
bindings:
  params: |
    {
      invoiceId: ctx.dataObjects[0]?.attributes?.find(
        a => a.tag === 'id'
      )?.stringValue
    }
children:
  - component: v2:markdown
    bindings:
      content: "ctx.$bridgeResult?.report"
```

### Example: Mixed Content

Combine multiple child components to render different parts of the bridge response:

```yaml theme={null}
component: v2:serviceBridgeView
props:
  bridgeRef: "service-bridge://acme/vendor-check"
  endpoint: "validate"
bindings:
  params: |
    {
      vendorCode: ctx.dataObjects[0]?.attributes?.find(
        a => a.tag === 'vendorCode'
      )?.stringValue
    }
children:
  - component: v2:label
    bindings:
      text: "'Vendor: ' + (ctx.$bridgeResult?.vendorName ?? 'Unknown')"
  - component: v2:dataTable
    bindings:
      rows: "ctx.$bridgeResult?.recentOrders"
    props:
      filterable: true
      columns:
        - field: orderNumber
          title: Order #
        - field: date
          title: Date
        - field: amount
          title: Amount
  - component: v2:markdown
    bindings:
      content: "ctx.$bridgeResult?.notes"
```

### JSONata Transform Reference

The `transform` prop accepts any valid [JSONata](https://jsonata.org/) expression. Common patterns:

| Pattern                | Expression                                                    | Description                            |
| ---------------------- | ------------------------------------------------------------- | -------------------------------------- |
| Extract a nested array | `response.data.items`                                         | Drill into the response structure      |
| Reshape objects        | `items.{ "Name": name, "Total": "$" & $string(price * qty) }` | Create new fields from existing ones   |
| Filter rows            | `items[status = "active"]`                                    | Only include rows matching a condition |
| Aggregate              | `$sum(items.amount)`                                          | Compute totals or other aggregations   |
| Sort                   | `items^(>amount)`                                             | Sort results by a field                |
| String formatting      | `items.{ "Display": firstName & " " & lastName }`             | Concatenate fields                     |

If the transform is omitted, the raw response is passed through as `ctx.$bridgeResult`.

<Note>
  The JSONata transform runs client-side after the response is received. For large responses, consider using the bridge's `postReplyScript` to filter server-side before the data reaches the browser.
</Note>

### Comparison: Panel serviceBridge vs serviceBridgeView

| Feature           | Panel `serviceBridge` prop              | `v2:serviceBridgeView`                         |
| ----------------- | --------------------------------------- | ---------------------------------------------- |
| Purpose           | Map bridge response to data attributes  | Display bridge response as read-only content   |
| Trigger           | Attribute path changes (`triggerOn`)    | Binding expression changes (reactive params)   |
| Response handling | `responseMapping` writes to attributes  | Children render from `ctx.$bridgeResult`       |
| Rendering         | No built-in display                     | Children render tables, markdown, labels, etc. |
| Script required   | No                                      | No                                             |
| Use case          | Auto-populate fields from external data | Show reference data, reports, lookup tables    |
