Skip to content

Selection Flow

People speak imprecisely. “Build a presentation for Guggenheim” — but which Guggenheim? The museum in New York, the one in Bilbao, Guggenheim Partners, the foundation? When an agent can’t safely guess, it should show the candidates and let a human pick — then continue with the chosen one.

That’s a different shape from approval. Approve/Reject is a yes/no on a fixed payload; disambiguation is pick one of N. elicit_selection is the tool for it.

Write the selection skill once; the server adapts. Your skill calls elicit_selection, then reuses the same elicit_awaitelicit_result loop as the Approval Flow. Underneath, the server renders the options as a review page and (best-effort) pushes a URL-mode elicitation prompt to hosts that advertise support; every host reaches the same outcome through the same long-poll. The reviewer’s choice comes back as selectedIds.

Diagram

elicit_selection takes a list of options (each with a unique id) and two optional bounds:

  • Single-select (the default, minSelections/maxSelections both 1) — the disambiguation case. The review page renders radio buttons; the reviewer picks exactly one.
  • Multi-select — widen the range (e.g. maxSelections: 3) and the page renders checkboxes. Use it for “pick the services to deploy” or “choose up to three reviewers.”

Either way the result is the same shape: elicit_result returns selectedIds — a string array of the chosen ids (one element in the single-select case). The server validates every submitted id against the option set and enforces the min/max bounds, so a malformed submission never resolves the request.

By default the page shows a plain id + label table — zero configuration. When you want the richer presentation the disambiguation case deserves (logos, domains, a change table), supply optional LiquidJS templates — rowTemplate, headingTemplate, footerTemplate, hoverTemplate — plus stock css. Each option’s data is available to rowTemplate as {{ option.* }}. See the elicit_selection reference for the full field list and examples.

The rendering is safe by construction, and it’s why selection is trustworthy:

  • The table renders inside a null-origin sandboxed iframe with no scripting — template markup is inert, and it can’t read the reviewer’s session.
  • The radio/checkbox controls are native, injected by the server — a template can’t spoof or intercept which option the human actually picks.
  • Interpolated option data is HTML-escaped, and the submission is authorized by a signed, per-render token (not cookies) that the sandbox can’t forge.

Selection reuses the approval polling tools — only the first call is new. Each has its own reference page with exact input/output shapes:

  • elicit_selection — record the options and (best-effort) push a URL-mode prompt; always returns reviewUrl.
  • elicit_await — bounded long-poll until the reviewer decides or the wait bound elapses.
  • elicit_status — non-blocking status read.
  • elicit_result — fetch the outcome; for a completed selection it returns selectedIds (idempotent, every fetch recorded).
Diagram

The skill’s column never changes from the Approval Flow — the only new step is the first call. Both flows converge on the same elicit_await wake.

The review page also carries a Reject — refine search control. Selection isn’t always “one of these is right”: sometimes the list is wrong and the human wants to narrow the query. Rejecting resolves the request as rejected (no selectedIds), which the skill reads as “none of these — gather a better candidate set and ask again.” It’s the selection analog of a rejected approval.

elicit_await behaves exactly as it does for approvals — bounded so it always returns before the host’s tool-call timeout:

  • A decision landed — the reviewer picked or refined; the call wakes immediately.
  • The bound elapsed — it returns "pending"; call elicit_await again to keep waiting.

On a URL-capable host the reviewer likely acts on the pushed prompt; on any other host they open reviewUrl. Either way the same wake resolves the same loop.

Every request carries a TTL (timeoutSeconds, default 1 hour, clamped 60 seconds to 24 hours). Once it passes, the request is expired — a terminal state. elicit_await and elicit_status both report "expired" at the deadline, and the review page refuses a late selection. Ask the agent to resend if that happens.