# Show results to customers

> Use caplo.yaml presentation to show progress, confirmation cards, receipts, result layouts and files, and let read tools ask the visitor for missing details.

By default a tool’s result goes only to the agent, which answers in text. A `presentation` in the tool’s `caplo.yaml` policy also shows the customer selected parts of it: a confirmation card for a write, a receipt afterwards, a list of products as cards, a delivery timeline, a PDF invoice. The agent’s text reply goes out as usual alongside.

Presentation is display metadata only. It selects and labels properties of the tool’s schemas; nothing else of the arguments or result is shown, and it grants no permission to run anything. The build checks every path and role against your input and output schemas and fails with a clear message if something does not fit.

Result layouts, confirmation cards and forms appear in the [website widget](/channels/website-widget/install/). Where a channel cannot show them, the customer gets the agent’s text reply. Design your tools so the text result is enough on its own.

Note

Paths are JSON pointers to one direct property: `/orderId`, `/products`. Labels and titles are plain text up to 80 characters. Every `presentation` needs `schemaVersion: 1`, a `title` and at least one entry in `fields`.

## Show progress

`progress` shows a status line while the tool runs. It is fixed text: it cannot include values from the call.

```yaml
tools:
  - name: search_products
    effect: read
    timeoutSeconds: 10
    progress:
      label: Searching the catalog
      spoken: Let me look that up.
```

* `label`: shown in the widget. 3 to 40 characters, no trailing punctuation, no links.
* `spoken`: optional, said in voice conversations. One sentence of 3 to 90 characters ending with a full stop.

## Confirmation and receipt

For a [write tool](/tools/write-tools/), `fields` chooses which input arguments the review card shows, and `receipt` chooses which properties of the result to show once the write succeeds.

```yaml
tools:
  - name: cancel_order
    effect: write
    timeoutSeconds: 15
    confirmation: required
    presentation:
      schemaVersion: 1
      title: Cancel order
      fields:
        - path: /orderId
          label: Order
          display: plain
        - path: /reason
          label: Reason
          display: plain
          editable: true
          input: select
          options:
            - value: changed_mind
              label: Changed my mind
            - value: ordered_by_mistake
              label: Ordered by mistake
            - value: found_cheaper
              label: Found it cheaper
        - path: /note
          label: Note
          display: plain
          editable: true
          input: text
      receipt:
        fields:
          - path: /orderId
            label: Order
            display: plain
          - path: /status
            label: Status
            display: plain
```

* Up to 12 fields, each a direct property of the input schema with one primitive type (string, number, integer or boolean).
* `display: last4` shows only the last four characters of a string, for example a card or account number. A masked field cannot be editable.
* `editable: true` lets the customer change a string argument before confirming. With `input: text`, the property needs a `maxLength` of 1 to 1000. With `input: select`, the property needs a string `enum` of 1 to 20 values, and `options` must label every value exactly once.
* `writeOnly` properties, `format: password` properties, names that match a declared secret and the [subject selector](/tools/customer-aware-tools/) cannot be shown. `readOnly` properties can be shown but not edited.
* `receipt.fields` (up to 12) come from the structured result, so the tool needs an `outputSchema`. A receipt only displays values; it never decides whether the write succeeded.

## Show a result

`result` shows items from the tool’s structured result with the agent’s reply. The tool needs an `outputSchema`, and `path` names a direct array property of it (or, for a detail, an object).

| Layout          | Shows                                                              | Columns                                                             | Item actions      |
| --------------- | ------------------------------------------------------------------ | ------------------------------------------------------------------- | ----------------- |
| `list`, `table` | Up to 20 items                                                     | Up to 6, no roles                                                   | None              |
| `cards`         | Up to 20 items as cards                                            | Up to 6; `title` role required                                      | Up to 2           |
| `choices`       | Up to 10 items the customer taps to reply                          | Up to 6; `title` role required                                      | Exactly 1 `reply` |
| `detail`        | One object: the result itself, or the object property `path` names | Up to 12; `title` role required                                     | Up to 2           |
| `comparison`    | Up to 4 items side by side                                         | Up to 12; `title` role required; columns without a role become rows | Up to 2           |
| `timeline`      | Up to 12 steps                                                     | Up to 4, every column needs a role; `title` and `status` required   | None              |

Columns are direct primitive properties of an item. A result that does not fit the layout’s bounds, for example 25 items for `cards`, shows no component; the agent’s reply still goes out. Return at most as many items as the layout shows.

### Cards

```yaml
    presentation:
      schemaVersion: 1
      title: Product search
      fields:
        - path: /query
          label: Search
          display: plain
      result:
        path: /products
        layout: cards
        title: Matching products
        columns:
          - path: /imageUrl
            label: Photo
            display: plain
            role: image
          - path: /name
            label: Product
            display: plain
            role: title
          - path: /price
            label: Price
            display: plain
            role: money
            currency:
              path: /currency
          - path: /url
            label: Details
            display: plain
            role: link
        actions:
          - kind: reply
            label: Add to cart
            template: Add {name} to my cart
          - kind: link
            label: View
            path: /url
```

Here the output schema has a `products` array whose items have `imageUrl`, `name`, `price`, `currency` and `url`.

### Column roles

| Role                      | Property type                             | Layouts                                                            | Shows                                                                                                                        |
| ------------------------- | ----------------------------------------- | ------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------- |
| `title`                   | string                                    | all with roles                                                     | The item’s heading.                                                                                                          |
| `subtitle`, `description` | string                                    | cards, detail, comparison; `description` also choices and timeline | Secondary text.                                                                                                              |
| `image`                   | string                                    | cards, detail, comparison                                          | An image. The value must be an `https` URL.                                                                                  |
| `link`                    | string                                    | cards, detail, comparison                                          | A link. The value must be an `https` URL.                                                                                    |
| `money`                   | number or integer                         | cards, detail, comparison                                          | An amount, with `currency` set to a fixed `code` such as `USD` or the `path` of a string property holding the ISO 4217 code. |
| `badge`                   | string, number or integer                 | cards, detail, comparison                                          | A short highlighted value, such as a status.                                                                                 |
| `status`                  | string with an `enum`                     | timeline                                                           | The step’s state. `statuses` maps every enum value to `done`, `current` or `upcoming`.                                       |
| `time`                    | string with `format: date` or `date-time` | timeline                                                           | When the step happened or is due.                                                                                            |

Each role can be used by one column only, and a column with a role uses `display: plain`. Values that are not `https` URLs are left off the card.

### Actions

* `reply` sends a message as the customer, built from `template`. `{name}` inserts the item’s value of the listed column `name`; image, link, status and time columns cannot be inserted.
* `link` opens the URL in the listed string column `path`.
* Labels are up to 20 characters. Neither kind calls a tool; the agent handles the reply like any message.

### Choices

```yaml
      result:
        path: /slots
        layout: choices
        title: Pick a time
        columns:
          - path: /label
            label: Time
            display: plain
            role: title
        actions:
          - kind: reply
            label: Book
            template: Book the {label} slot
```

Each choice sends its filled-in reply. Columns without a role must appear in the template.

### Detail

```yaml
      result:
        layout: detail
        title: Your order
        columns:
          - path: /orderId
            label: Order
            display: plain
            role: title
          - path: /status
            label: Status
            display: plain
            role: badge
          - path: /total
            label: Total
            display: plain
            role: money
            currency:
              path: /currency
          - path: /trackingUrl
            label: Track parcel
            display: plain
            role: link
```

Without `path`, a detail shows the result object itself.

### Comparison

```yaml
      result:
        path: /plans
        layout: comparison
        title: Compare plans
        columns:
          - path: /name
            label: Plan
            display: plain
            role: title
          - path: /monthly
            label: Per month
            display: plain
            role: money
            currency:
              code: USD
          - path: /data
            label: Data
            display: plain
          - path: /contract
            label: Contract
            display: plain
```

### Timeline

```yaml
      result:
        path: /steps
        layout: timeline
        title: Delivery progress
        columns:
          - path: /name
            label: Step
            display: plain
            role: title
          - path: /state
            label: State
            display: plain
            role: status
            statuses:
              complete: done
              in_progress: current
              not_started: upcoming
          - path: /at
            label: When
            display: plain
            role: time
```

The `state` property’s schema is `{ "type": "string", "enum": ["complete", "in_progress", "not_started"] }`, and `at` is a string with `format: date-time`.

## Return files

A read tool can hand the customer up to three files per call, such as an invoice PDF or a floor plan. Declare the types and the largest size:

```yaml
tools:
  - name: get_invoice
    effect: read
    timeoutSeconds: 10
    presentation:
      schemaVersion: 1
      title: Invoice
      fields:
        - path: /orderId
          label: Order
          display: plain
      files:
        accept:
          - application/pdf
        maxBytes: 2097152
```

`accept` lists any of `image/jpeg`, `image/png`, `image/gif`, `image/webp` and `application/pdf`. `maxBytes` is 1024 to 10485760 (10 MiB) per file.

Return each file as an MCP embedded resource with base64 content. The last part of `uri` is the file name the customer sees:

```ts
return {
  content: [
    { type: "text", text: `Invoice ${invoice.number} for order ${orderId}` },
    { type: "resource", resource: { uri: `file:///${invoice.number}.pdf`, mimeType: "application/pdf", blob: pdf.toString("base64") } },
  ],
};
```

Embedded resources are written with the MCP SDK, so files need an MCP package. Caplo takes the files out of the result before the model sees it, checks that each file’s content matches its type, scans it, and shows it with the reply as a download where the channel can. A file of an undeclared type, over `maxBytes`, or a fourth file fails the call. Where the channel cannot deliver files, such as a phone call, the agent is told the file was not delivered, so also return the essentials as text.

## Ask for missing details

A read tool can ask the visitor for details it needs while it runs, with an MCP form. Declare every field the tool may ask for:

```yaml
tools:
  - name: find_store
    effect: read
    timeoutSeconds: 10
    elicitation:
      fields:
        - name: city
```

Then ask from the code. In a function package, use `ctx.canElicit` and `ctx.elicit`:

```ts
async handler(args, ctx) {
  let city = args.city;
  if (!city) {
    if (!ctx.canElicit) return "Ask the customer which city to search in, then call this tool again with it.";
    const answer = await ctx.elicit({
      message: "Which city should I search in?",
      requestedSchema: { type: "object", properties: { city: { type: "string", title: "City", maxLength: 60 } }, required: ["city"] },
    });
    if (answer.action !== "accept") return "The customer did not choose a city.";
    city = String(answer.content?.city);
  }
  // look up stores in city...
}
```

In an MCP package, check `server.server.getClientCapabilities()?.elicitation` and call `server.server.elicitInput({ message, requestedSchema })`.

* Forms are offered only in website widget text conversations. Elsewhere, such as phone calls and evaluations, `canElicit` is false; return a message that lets the agent ask in the conversation instead.
* The answer is `accept` with `content`, `decline`, or `cancel`.
* When the visitor answers, the tool runs again from the start with the same arguments, and the earlier `elicit` call returns the stored answer. Ask before doing any work, and keep everything before the question safe to repeat.
* Up to 12 declared fields and three questions per call. Fields use the MCP form subset: strings (with `minLength`, `maxLength`, `format` `email`, `uri`, `date` or `date-time`, or an `enum`), numbers, integers, booleans and arrays of enum strings. `pattern` is not supported.
* Write tools cannot ask; they collect input through [editable confirmation fields](#confirmation-and-receipt).

Caution

Never ask for a password, card number, one-time code or other credential. Caplo refuses fields and forms whose names, titles or options name one, and the build rejects such field names.
