---
updatedAt: 2026-08-25T01:46:33.000Z
agentTools:
  projectIndex: https://docs.oneschema.co/llms.txt
---

# Offering match options in a review step

Write a script that tells a review step which values a reviewer may choose from for a cell, so they pick from real candidates instead of typing a value by hand.

A review step exists because a value could not be resolved automatically. Filtering on every key you have still leaves more than one possibility, so a person has to decide which one is right.

The **Match options** script is how you tell the review step what those possibilities are. It runs once when a run pauses for review, and every cell it narrows becomes a dropdown of exactly those values. Cells the script says nothing about stay ordinary text fields.

## Writing the script

Open the review step's settings and find the **Match options** section. Your script receives two things — the rows of the data under review, and the step's reference file — and calls `setOptions` for each cell it can narrow:

```typescript
import { Reference, Row } from "oneschema/reviewoptions"

export default function (rows: Row[], reference: Reference) {
  rows.forEach((row) => {
    // Already resolved upstream — leave it alone.
    if (row.values["Tax Code"]) return

    const candidates = reference.findRows("Locality", row.values["Locality"])

    row.setOptions(
      "Tax Code",
      candidates.map((match) => ({
        value: match.values["Code"],
        label: match.values["Name"],
        description: match.values["County"],
        referenceRowId: match.id,
      })),
    )
  })
}
```

A reviewer working the `Tax Code` column now sees the two Pocono codes on the `POCONO T` row, and nothing at all on the row that was already resolved. Because each option says which reference row it came from, they can also jump to that row in the reference pane before accepting it.

## The `Row` object

| Property                       | Description                                             |
| :----------------------------- | :------------------------------------------------------ |
| `row.id`                       | Identifier for the row. Stable while the run is paused. |
| `row.values`                   | The row's cells, keyed by column name.                  |
| `row.setOptions(column, opts)` | Offer `opts` to the reviewer for this row's `column`.   |

Say nothing about a cell and it stays a text field, so a script only has to speak up where it can genuinely narrow things down. Calling `setOptions` twice for the same cell replaces the earlier list.

## The `Option` object

| Field            | Required | Description                                                                                                                                            |
| :--------------- | :------- | :----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `value`          | Yes      | Written into the cell when the reviewer picks it, so it must be the value your later steps expect. A number is accepted and stored as its string form. |
| `label`          | No       | Shown beside the value. When omitted, the value is shown on its own.                                                                                   |
| `description`    | No       | Secondary text, for telling near-identical candidates apart.                                                                                           |
| `referenceRowId` | No       | The `id` of the reference row you read this option out of. Lets the reviewer jump to that row.                                                         |

`label` and `description` exist for the situation this feature is for: when two candidates differ by something a bare code does not show. A reviewer recognizes "Pocono Twp SD" long before they recognize `340603`.

`referenceRowId` must be the `id` of a row of this step's reference file. An id that names no such row is reported as an error rather than stored, so a "show me where this came from" never quietly goes nowhere.

## Reading the reference file

The reference file configured on the step is handed to your script as its second argument, so you can compare the data under review against it instead of copying it into the script.

| Property                            | Description                                                                    |
| :---------------------------------- | :----------------------------------------------------------------------------- |
| `reference.rows`                    | Every row of the reference file. Read-only — only the review data is editable. |
| `reference.columns`                 | Its columns, in the order they appear in the file.                             |
| `reference.findColumn(key)`         | One reference column by key, or `undefined`.                                   |
| `reference.findRows(column, value)` | Every reference row whose `column` holds exactly `value`.                      |

`findRows` matches exactly and is case-sensitive, and it indexes the column the first time you ask for it — so looking a value up once per review row costs one pass over the file, not one per row. For anything looser, filter `reference.rows` yourself.

The reference is read-only, and typed that way: `findRows` answers from an index built once from these rows, so reordering or editing them in place would change what later lookups return. Copy first if you need to sort — `[...reference.rows].sort(…)`.

A reference row has an `id` and a `values` object keyed by the reference file's own column names, the same shape as a review row:

```typescript
import { getReference, Row } from "oneschema/reviewoptions"

function codesFor(locality: string) {
  // The same reference the default export is given, for a helper that isn't it.
  return getReference().findRows("Locality", locality)
}
```

With no reference file configured on the step, `reference` is present but empty — a script that consults it offers nothing rather than failing.

## Fetching data from elsewhere

For a lookup that lives outside both the review data and the reference file, your script can make a request:

```typescript
import { fetch } from "oneschema"
import { Row } from "oneschema/reviewoptions"

export default async function (rows: Row[]) {
  const response = await fetch("https://example.com/tax-codes")
  const codes = await response.json()
  // …
}
```

Network access is off unless outbound requests are enabled for your organization — the same setting that enables validation webhooks. Contact support if your script needs it.

## Working with the columns

`findColumn` and `getColumns` describe the data under review, which is useful when column names vary between files:

```typescript
import { Row, getColumns } from "oneschema/reviewoptions"

export default function (rows: Row[]) {
  const codeColumns = getColumns().filter((c) => c.key.endsWith(" Code"))
  // …
}
```

| Function          | Returns                                                             |
| :---------------- | :------------------------------------------------------------------ |
| `getColumns()`    | Every column, in the order the reviewer sees them.                  |
| `findColumn(key)` | One column by key, or `undefined` when the data has no such column. |

A column's `key` here is its name in the data under review — the same string you use to read a cell from `row.values` and to name a column in `setOptions`. That differs from a validation code hook, where the key is the template's target attribute: a review step need not have a template at all, so it works from the column names the data actually carries.

## What a reviewer sees

* **Cells you narrowed** open a dropdown of your candidates, searchable by value or label.
* **Cells you said nothing about** stay ordinary text fields.
* **Candidates that name a reference row** point at it: hovering one scrolls the reference pane to that row and tints it, **Show row** pins it without accepting the value, and picking the value leaves its row marked.
* **A reviewer can always type a value you did not offer.** The review step exists because the decision needed a person, so when your narrowing is wrong they are never boxed in by it.

## When options do not appear

The script is a convenience, so a failure never blocks a review — the run still pauses and the affected cells stay text fields. Options are missing when:

* No script is configured on the step.
* The script ran without narrowing any cell.
* The script raised an error. The failure is recorded on the step for support to look into; there is no self-serve view of it yet.
* The script offered options for an implausible number of cells. A review is human-scale; a script narrowing thousands of cells is usually doing something other than helping a person decide.

## Limits

| Limit                       | Value                                              |
| :-------------------------- | :------------------------------------------------- |
| Options per cell            | 1,000                                              |
| Cells with options per file | 5,000                                              |
| Script run time             | Shares the code-hook timeout for your organization |

Exceeding either count is reported as an error rather than silently truncated.