Skip to content

elicit_selection

Ask a human to choose one or more options from a caller-supplied list (disambiguation), on the hosted review page. The chosen ids come back via elicit_result.

The question is “which of these did you mean?” — the agent already has the candidates and needs the human to pick one (or several). The reviewer can also reject the whole list (“none of these”), telling the agent to refine its search. For approve/reject of a single payload, or free-form field input, see Choosing a tool.

Parameter Type Required Description
message string yes Human-readable question shown to the reviewer.
options array yes 1–500 candidate objects — see Options below.
minSelections / maxSelections number no Default 1 / 1 (single choice). maxSelections is clamped down to the number of options. Widen the range (e.g. maxSelections: 3) to turn on multi-select (checkboxes instead of radio buttons).
rowTemplate / headingTemplate / footerTemplate / hoverTemplate string no LiquidJS templates that control how the option table renders. Max 20 KB each. See Row and column templates.
css string no Stock CSS injected into the sandboxed document’s <head>, so your templates can use classes, @media queries, and :hover rules. (hoverTemplate’s reveal-on-hover is built in and works without it.) Tailwind is not available here — only plain CSS you supply yourself.
labels object no { ok?, cancel? }, each 1–40 characters; renames the action buttons: ok is the submit button (default Choose, or Submit selection when maxSelections > 1), cancel the reject button (default Reject — refine search). Labels only rename — button placement and styling stay fixed.
timeoutSeconds number no Default 3600, clamped 6086400 — how long the request stays decidable.
{
"message": "Which Guggenheim did you mean?",
"options": [
{ "id": "bilbao", "label": "Guggenheim Bilbao" },
{ "id": "nyc", "label": "Solomon R. Guggenheim Museum (NYC)" }
]
}

Each entry in options is an object with:

  • id (string, required) — must be unique and non-empty within the request. This is the only value that ever comes back.
  • label (string, optional) — used by the zero-config fallback rendering (see below).
  • any other fields you like — arbitrary extra data available to your templates as {{ option.<field> }} (e.g. domain, city, updatedAt).

Without any templates, elicit_selection renders a minimal two-column table: a selector cell plus one cell showing option.label (falling back to option.id if there’s no label). That zero-config path is enough for a simple pick-list.

For anything richer — an extra column, styling, a heading row — supply your own LiquidJS templates. See Templates for the shared engine rules — JavaScript-style truthiness, escaping, and limits. The framework always injects the selector cell itself (the radio button or checkbox); your templates supply everything else:

  • rowTemplate — the <td> cells for one option row. Rendered once per option with { option } in context (so {{ option.label }}, {{ option.domain }}, etc.).
  • headingTemplate — the <th> cells for the table header. Rendered once, with no option context (there’s no single option to bind to a header).
  • footerTemplate — a <tfoot> row, e.g. for a count or a note. Also rendered with no option context.
  • hoverTemplate — extra detail shown on hover over a row. The reveal-on-hover behavior is built into the base stylesheet the framework always injects, so it works with zero caller css. Rendered per option with { option } in context, same as rowTemplate.
{
"message": "Which environment?",
"options": [
{ "id": "staging", "label": "Staging" },
{ "id": "prod", "label": "Production" }
]
}

No templates needed — renders a simple radio-button list of labels.

Example 2: template table (the “which Guggenheim?” case)

Section titled “Example 2: template table (the “which Guggenheim?” case)”
{
"message": "Which Guggenheim did you mean?",
"options": [
{ "id": "bilbao", "name": "Guggenheim Bilbao", "domain": "guggenheim-bilbao.eus" },
{ "id": "nyc", "name": "Solomon R. Guggenheim Museum", "domain": "guggenheim.org" },
{ "id": "venice", "name": "Peggy Guggenheim Collection", "domain": "guggenheim-venice.it" }
],
"headingTemplate": "<th>Name</th><th>Domain</th>",
"rowTemplate": "<td>{{ option.name }}</td><td class=\"domain\">{{ option.domain }}</td>",
"css": "td.domain{color:#666;font-family:monospace}tr:hover{background:rgba(0,0,0,.05)}"
}

Renders a name + domain table so the reviewer can tell the candidates apart at a glance, instead of guessing from a bare id.

Example 3: multi-select (maxSelections: 3)

Section titled “Example 3: multi-select (maxSelections: 3)”
{
"message": "Pick up to 3 launch candidates to promote",
"options": [
{ "id": "v1", "label": "v1.2.0" },
{ "id": "v2", "label": "v1.3.0-rc1" },
{ "id": "v3", "label": "v1.3.0-rc2" },
{ "id": "v4", "label": "v1.4.0-beta" }
],
"minSelections": 1,
"maxSelections": 3
}

Because maxSelections (3) is greater than minSelections (1), the review page renders checkboxes instead of radio buttons and accepts 1–3 selections.

The option table is attacker-adjacent input — a caller can put almost anything into options, rowTemplate, css, etc. The review page is designed so none of that can forge a decision or exfiltrate data:

  • The table is rendered server-side and displayed inside a sandboxed, null-origin iframe with sandbox="allow-forms"no scripts allowed, not even allow-scripts. Anything a template renders (including any stray <script> tag) is inert markup; it cannot run.
  • The actual selection is a native <input type="radio"> / <input type="checkbox"> control that the framework injects, not something the template can draw or fake. A malicious rowTemplate can make a row look however it wants, but it cannot forge which id gets submitted.
  • The submit is authorized by a signed, time-limited token minted for that request (not cookies or ambient session state), so the sandboxed (null-origin) form can still submit a valid decision despite having no access to the parent page’s session. A submission can only take effect once because the underlying request resolves a single time, not because the token itself is stateful or single-use.
  • The server re-validates every submitted id against the request’s own option set and enforces minSelections/maxSelections before recording a decision; anything out of range or referencing an unknown id is rejected, and the reviewer sees the table again with a fresh token.
  • Interpolated {{ option.* }} values are HTML-escaped, so option data can’t break the layout or inject markup — your template’s own tags render as-is, the data does not.
{
"elicitationId": "",
"status": "pending",
"reviewUrl": "https://…/review?elicitationId=…",
"expiresAt": "2026-07-30T12:00:00.000Z",
"elicitationDelivered": true
}
  • elicitationDelivered is true only when the host advertised the elicitation: { url: {} } capability. The prompt is non-blocking; reviewUrl is always returned so any host can reach the review page.
  • For a completed selection, elicit_result returns a selectedIds string array (the chosen option ids) alongside the usual decision.