Skip to content

Proposal Flow

Agents don’t just ask permission — they propose data. An enrichment skill reads Apollo and drafts a Salesforce account; a sync job reconciles two systems and proposes the merged record. A bare approve/reject is too coarse for that: the human needs to see where each value came from, fix the ones the agent got wrong, and apply only the fields they trust.

That’s what elicit_proposal is for: a diff grid — one or more read-only Source columns, a read-only Current column, and an editable Proposed column with a per-field Include toggle. The reviewer corrects, includes, and submits; the result carries only the fields they included.

Write the proposal skill once; the server adapts. Your skill calls elicit_proposal, then reuses the same elicit_awaitelicit_result loop as the Approval Flow. Underneath, the server renders the grid 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 decision comes back as action plus the included-fields-only values.

Diagram

The caller supplies three things (full shapes on the elicit_proposal reference):

  • columns — names the read-only columns: one or more Sources (e.g. “Apollo”, “Crunchbase”) and the Current system (e.g. “Salesforce”). For a brand-new record the Current column is simply blank.
  • requestedSchema — the authority for the editable Proposed column, the same MCP elicitation subset as elicit_approval’s form mode. Each property’s default is its initial Proposed value — the caller computes it (typically “take Current if present, else Source”).
  • fields — each property’s Source and Current cell values.

The review page derives the rest: Source cells that differ from Current get a highlight outline, double-clicking a Source or Current cell copies it into Proposed, rows whose Proposed value differs from Current start included, and a required property with no Current value locks its Include on so the reviewer can’t build an incomplete record. Header actions — Accept All, Reject All, Restore Proposed — handle the whole grid at once. See Derived behavior for the exact rules.

Two outcomes, both first-class:

  • Submit — the request resolves approved; elicit_result returns action: "submit" and values holding only the included fields with their final (possibly edited) Proposed values, validated against the schema subset for those keys. Unincluded fields are omitted entirely — not present, not null — so the skill applies exactly what the human blessed and nothing more.
  • Cancel — the request resolves rejected, and elicit_result returns action: "cancel" (fetchable like any other decided request, not an error). The skill reads it as “don’t touch the record.”

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

  • elicit_proposal — record the proposed create-or-update 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: action plus values on submit (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.

context (object type, record id, external keys) is echoed back verbatim in the result so the skill knows which record to apply values to — but it is not shown to the reviewer by default. The grid is the focus; pass a LiquidJS displayTemplate to render a header above it (see Templates).

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 submission. Ask the agent to resend if that happens.