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.
When to use
Section titled “When to use”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 60–86400 — 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)" } ]}Options
Section titled “Options”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).
Row and column templates
Section titled “Row and column templates”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 callercss. Rendered per option with{ option }in context, same asrowTemplate.
Example 1: zero-config (id + label)
Section titled “Example 1: zero-config (id + label)”{ "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.
Safety model
Section titled “Safety model”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 evenallow-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 maliciousrowTemplatecan make a row look however it wants, but it cannot forge whichidgets 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/maxSelectionsbefore 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.
Output
Section titled “Output”{ "elicitationId": "…", "status": "pending", "reviewUrl": "https://…/review?elicitationId=…", "expiresAt": "2026-07-30T12:00:00.000Z", "elicitationDelivered": true}elicitationDeliveredistrueonly when the host advertised theelicitation: { url: {} }capability. The prompt is non-blocking;reviewUrlis always returned so any host can reach the review page.- For a completed selection,
elicit_resultreturns aselectedIdsstring array (the chosen optionids) alongside the usualdecision.
Related
Section titled “Related”elicit_await/elicit_status— wait for the decisionelicit_result— fetchselectedIds- Selection Flow — the full lifecycle
- Templates — the shared LiquidJS engine rules