elicit_approval
Record an approval request and (best-effort) push a URL-mode elicitation prompt to capable hosts. The first step of the Approval Flow.
When to use
Section titled “When to use”A decision that must be durable: it survives disconnects, reaches any reviewer on any device via the hosted review page, and leaves an audit trail. JSON mode shows a read-only payload the reviewer approves or rejects as-is; form mode has the reviewer fill in fields that become the approved data. For an in-host dialog answered by the person at the keyboard right now, see Choosing a tool.
At least one of context / requestedSchema is required.
| Parameter | Type | Required | Description |
|---|---|---|---|
message |
string | yes | Human-readable summary shown to the reviewer. |
context |
any | one of these two | A read-only payload displayed to the reviewer verbatim and handed back unchanged from elicit_result, so the human approves exactly what runs. |
requestedSchema |
object | one of these two | A JSON Schema restricted to the MCP elicitation requestedSchema subset: a flat object whose properties are strings, numbers/integers, booleans, or single/multi-select enums — no nested objects, arrays of objects, $ref, or allOf. The reviewer fills these fields on the review page; their validated values come back as formSubmission from elicit_result. |
displayTemplate |
string | no | A LiquidJS template that customizes how context is rendered on the review page, replacing the default pretty-printed JSON. The payload is exposed as context (e.g. {{ context.field }}). Max 20 KB. Combinable with requestedSchema — the template renders first, the form below it. See Display templates. |
labels |
object | no | { ok?, cancel? }, each 1–40 characters; renames the decision buttons in both modes: ok is the approve button (default Approve), cancel the reject button (default Reject). Labels only rename — button placement and styling stay fixed. |
timeoutSeconds |
number | no | Default 3600, clamped 60–86400 — how long the request stays decidable. |
JSON mode — a read-only payload the reviewer approves or rejects as-is:
{ "message": "Deploy to production", "context": { "target": "prod" }, "timeoutSeconds": 3600 }Form mode — a schema the reviewer fills in; their submission becomes the approved data:
{ "message": "Approve the deploy", "requestedSchema": { "type": "object", "properties": { "approver": { "type": "string", "title": "Your name", "minLength": 2 }, "confidence": { "type": "integer", "title": "Confidence %", "minimum": 0, "maximum": 100 } }, "required": ["approver", "confidence"] }}Both may be passed together: context is then shown as read-only context
alongside the form — it is not merged into or used to pre-fill the fields
(those are seeded only from requestedSchema defaults).
Display templates
Section titled “Display templates”By default, context is shown as pretty-printed JSON in a code block.
A displayTemplate replaces that with your own LiquidJS
markup — a table, a diff, a labeled summary — so the reviewer sees the decision,
not a blob. The payload is available as the context variable. See
Templates for the shared engine rules — JavaScript-style
truthiness, escaping, and limits.
How it’s rendered (and why it’s safe):
- Rendered server-side; the output is displayed inside a sandboxed,
null-origin iframe (
sandbox="allow-scripts", no same-origin access). Template markup and any scripts it contains are fully isolated — they cannot read the reviewer’s session, cookies, or the surrounding page. - The Approve / Reject controls live outside the iframe, so a template can never spoof or intercept the decision.
- Interpolated values (
{{ context.… }}) are HTML-escaped, so payload content can’t break the layout or inject markup — your template’s own tags render as-is, the data does not. - If the template fails to parse or render, the review page falls back to the pretty-printed JSON — a bad template never blocks an approval.
- Templates run against limits (size ≤ 20 KB, parse/render/memory caps) and have no
filesystem or
include/renderaccess.
Example: field/value summary
Section titled “Example: field/value summary”{ "message": "Approve refund", "context": { "customer": "acme@co", "amount": "$1,240.00", "reason": "duplicate charge" }, "displayTemplate": "<dl><dt>Customer</dt><dd>{{ context.customer }}</dd><dt>Amount</dt><dd>{{ context.amount }}</dd><dt>Reason</dt><dd>{{ context.reason }}</dd></dl>"}Renders the payload as a clean definition list instead of raw JSON.
Example: change table (loops)
Section titled “Example: change table (loops)”{ "message": "Approve production deploy", "context": { "service": "elicitly-api", "version": "v2.4.0", "changes": [ { "file": "src/auth.ts", "added": 12, "removed": 3 }, { "file": "src/db.ts", "added": 5, "removed": 40 } ] }, "displayTemplate": "<strong>Deploy {{ context.service }} {{ context.version }}</strong><table><tr><th>File</th><th>+</th><th>−</th></tr>{% for c in context.changes %}<tr><td>{{ c.file }}</td><td>+{{ c.added }}</td><td>−{{ c.removed }}</td></tr>{% endfor %}</table>"}{% for %} iterates the array; each row shows the file and its added/removed counts.
Example: before → after comparison
Section titled “Example: before → after comparison”{ "message": "Approve plan change", "context": { "field": "Plan", "current": "Starter", "proposed": "Enterprise" }, "displayTemplate": "<p><strong>{{ context.field }}</strong></p><p><s>{{ context.current }}</s> → <mark>{{ context.proposed }}</mark></p>"}Highlights what changes — the old value struck through, the new value marked.
With a form
Section titled “With a form”Pass displayTemplate and requestedSchema together to show a custom
rendering of the immutable context first, with the reviewer’s form directly below
it (the template does not pre-fill the form; fields are seeded only from
requestedSchema defaults).
Output
Section titled “Output”{ "elicitationId": "…", "status": "pending", "reviewUrl": "https://…/review?elicitationId=…", "expiresAt": "2026-07-16T12: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.
Related
Section titled “Related”elicit_await(long-poll) /elicit_status(non-blocking read) — wait for the decisionelicit_result— fetch the outcome- Approval Flow — the full lifecycle
- Templates — the shared LiquidJS engine rules