---
title: Render agent artifacts
description: "Emit charts, summaries, tables, and option prompts from your agent so Agent Analytics shows what your users actually saw, instead of raw tool output."
product: general
token_estimate: 3769
---
# Render agent artifacts

> For AI agents: a documentation index is available at [/docs/llms.txt](/docs/llms.txt). Append `.md` to any page URL for markdown, or send `Accept: text/markdown`.

An artifact is a small, versioned JSON envelope your agent attaches to a tool call to describe a visual it showed a user. Agent Analytics validates the envelope and redraws that visual as a card in the session view, so a reviewer sees what the user saw instead of the JSON behind it. Amplitude draws the card with built-in renderers that follow the viewer's theme, so you never ship UI code and Amplitude never executes yours.

Emit artifacts when the visual _was_ the answer and the raw payload doesn't tell you whether the answer was any good, such as a chart your agent generated or a set of choices it offered. If you only need the underlying numbers for analysis, you don't need artifacts: tool content already lands as event properties you can query in charts and cohorts.

> **Note:**
>
> Artifact rendering is off by default. Contact your Amplitude representative to enable it for your organization.

## How it works

1. Your agent produces a visual for its user, as it does today.
2. Alongside it, your code attaches an artifact envelope to the tool call's `input` or `output`.
3. Agent Analytics recognizes the envelope, validates it, and renders a card in both the conversation view and the trace panel.
4. The raw tool payload stays visible next to the card. Amplitude never replaces or hides it.

If a payload doesn't validate, Agent Analytics shows a short reason next to the raw output instead of a card. It never guesses at your data or renders a partial result.

## The envelope

Every artifact is a JSON object under a single reserved key. The key is deliberately vendor-neutral and nothing in the payload grammar is Amplitude-specific, so the same builder can emit to another platform that adopts this shape — you change the key, not your code.

```json
{
  "$agent_artifact": {
    "v": 1,
    "spec": "https://amplitude.com/docs/amplitude-ai/agent-analytics/render-artifacts",
    "type": "chart",
    "title": "Monthly payouts",
    "caption": "August was the highest month.",
    "scope": { "label": "Aug 2026" },
    "locale": "en-IN",
    "payload": {}
  }
}
```

| Field | Required | Notes |
| --- | --- | --- |
| `v` | yes | Contract version. Always `1`. |
| `spec` | no | URL of the contract this payload follows. The key is vendor-neutral, so `spec` is how a payload says whose shape it is. Amplitude ignores it when rendering. |
| `type` | yes | One of `chart`, `summary`, `options`, `table`, `html`. |
| `title` | by type | Required for `chart`, `table`, and `html`; optional for `summary` and `options`. |
| `caption` | no | One line of narrative under the card. |
| `scope` | no | `{ label, dateRange? }`, the period or population the data covers. Amplitude renders it beside the title, so a month's numbers never read as all-time. |
| `locale` | no | BCP-47 tag. Drives number formatting. |
| `generatedAt`, `id` | no | Provenance. Amplitude stores these but doesn't render them. |
| `payload` | yes | Per-type body. Refer to [Artifact types](#artifact-types). |

Two conventions apply everywhere:

- **`unit` is a display string, not a concept.** Send `"INR"`, `"%"`, `"ms"`, `"req/s"`, or anything else. Amplitude appends it to values as-is and never interprets it. Number formatting comes from `locale`.
- **Colors are semantic, never literal.** Send `primary`, `success`, or `danger` and Amplitude maps them to theme colors that work in light and dark mode, and that stay distinguishable for color-blind readers. Hex values, fonts, and sizes aren't part of the contract.

## Attach the envelope to a tool call

Artifacts travel on the tool call that produced them. Pass the envelope as the tool's `output`, or as `input` when the artifact _is_ the input, as with a question you asked the user.

#### TypeScript

```typescript
const artifact = {
  $agent_artifact: {
    v: 1,
    type: "chart",
    title: "Monthly payouts",
    payload: {
      shape: "categorical",
      kind: "bar",
      unit: "INR",
      items: [
        { label: "Aug 2026", value: 37279.7, color: "danger" },
        { label: "Jul 2026", value: 23925, color: "primary" },
      ],
    },
  },
};

session.trackToolCall("render_payouts_chart", latencyMs, true, {
  input: JSON.stringify({ range: "last-3-months" }),
  output: JSON.stringify(artifact),
});
```

#### Python

```python
artifact = {
    "$agent_artifact": {
        "v": 1,
        "type": "chart",
        "title": "Monthly payouts",
        "payload": {
            "shape": "categorical",
            "kind": "bar",
            "unit": "INR",
            "items": [
                {"label": "Aug 2026", "value": 37279.7, "color": "danger"},
                {"label": "Jul 2026", "value": 23925, "color": "primary"},
            ],
        },
    }
}

session.track_tool_call(
    "render_payouts_chart",
    latency_ms,
    True,
    input=json.dumps({"range": "last-3-months"}),
    output=json.dumps(artifact),
)
```

> **Warning:**
>
> Tool content comes from the `input` and `output` options. Amplitude ignores other spellings without an error, so your artifact never arrives. That includes `toolInput` and `toolOutput`, which are the names of the resulting event properties. If you rely on `patch()` for zero-code instrumentation, note that auto-extracted tool calls carry no input or output, so artifacts need an explicit `trackToolCall`. Refer to [Track tool calls](https://amplitude.com/docs/sdks/agent-analytics/sdk) in the SDK reference.

## Emit from any ingest path

The envelope is the same on every path. What changes is only where you put it: artifacts ride the tool call's existing content fields, so any path that carries tool content carries artifacts.

### AI SDK

Pass the envelope as the `output` (or `input`) option, as in the examples above. Amplitude serializes it into the `[Agent] Tool Output` property.

### HTTP API or a standard Amplitude SDK

Set the `[Agent] Tool Output` property directly on an `[Agent] Tool Call` event. Refer to [Set up Agent Analytics](https://amplitude.com/docs/amplitude-ai/agent-analytics/setup) for the full event contract and the ID fields every event needs.

```bash
curl -X POST https://api2.amplitude.com/2/httpapi \
  -H "Content-Type: application/json" \
  -d '{
    "api_key": "YOUR_API_KEY",
    "events": [{
      "event_type": "[Agent] Tool Call",
      "user_id": "user-123",
      "event_properties": {
        "[Agent] Session ID": "ticket-42",
        "[Agent] Turn ID": 2,
        "[Agent] Trace ID": "trace-abc",
        "[Agent] Agent ID": "support-bot",
        "[Agent] Tool Name": "render_refund_summary",
        "[Agent] Tool Success": true,
        "[Agent] Tool Input": "{\"order_id\":\"A-1\"}",
        "[Agent] Tool Output": "{\"$agent_artifact\":{\"v\":1,\"type\":\"summary\",\"title\":\"Refund total\",\"payload\":{\"headline\":{\"value\":42.5,\"label\":\"Refunded\",\"unit\":\"USD\"}}}}"
      }
    }]
  }'
```

Both properties take a JSON **string**, so serialize the envelope before assigning it.

### OTLP

Put the envelope in whichever result attribute your instrumentation already uses on the tool span. Amplitude reads the first one present, in this order:

| Attribute | Convention |
| --- | --- |
| `gen_ai.output.messages` | OpenTelemetry GenAI |
| `gen_ai.tool.call.result` | OpenTelemetry GenAI |
| `output.value` | OpenInference |

Tool input follows the same pattern, from `gen_ai.tool.call.arguments` or `input.value`. The tool name comes from `gen_ai.tool.name` or `tool.name`. Refer to [Set up Agent Analytics](https://amplitude.com/docs/amplitude-ai/agent-analytics/setup) for the OTLP endpoint and headers.

## Artifact types

### chart

Describe a chart by its **data shape**, which Amplitude validates, plus an optional **`kind`** that hints at the picture you drew.

| `shape` | Payload | Use for |
| --- | --- | --- |
| `categorical` | `items: [{ label, value, color? }]` | Bars, columns, pies, donuts |
| `series` | `series: [{ label, color?, points: [{ x, y }] }]` | Lines and areas over an ordered x |
| `steps` | `steps: [{ label, value }]` | Funnels, waterfalls |
| `points` | `points: [{ x, y, size? }]`, where `x` and `y` are numbers | Scatter, bubble |
| `matrix` | `xLabels`, `yLabels`, `values: number[][]` | Heatmaps, grids |

`kind` is a free string: `bar`, `column`, `pie`, `donut`, `line`, `area`, or anything else you call your chart. Amplitude draws a kind that has its own renderer that way. Anything else renders as the shape's default presentation **with a visible note** saying so, so a reviewer always knows they're looking at a substitute. Shapes that have no dedicated renderer yet (`steps`, `points`, `matrix`) render as a labeled table and carry the same note.

Optional chart fields: `unit`, `xLabel`, `yLabel`, and `highlightedIndex` (for `categorical`, to emphasize one item).

```json
{
  "$agent_artifact": {
    "v": 1,
    "type": "chart",
    "title": "Monthly cash flow",
    "locale": "en-IN",
    "payload": {
      "shape": "series",
      "kind": "line",
      "unit": "INR",
      "xLabel": "Month",
      "yLabel": "Amount",
      "series": [
        {
          "label": "Cash-in",
          "color": "success",
          "points": [
            { "x": "Apr 2026", "y": 222389 },
            { "x": "May 2026", "y": 42810 }
          ]
        },
        {
          "label": "Cash-out",
          "color": "danger",
          "points": [
            { "x": "Apr 2026", "y": 61453 },
            { "x": "May 2026", "y": 32580 }
          ]
        }
      ]
    }
  }
}
```

### summary

One number card at whatever density you need: a bare headline, a headline with a comparison, or a full overview with supporting metrics and facts. Send a `headline`; every other field is optional.

```json
{
  "$agent_artifact": {
    "v": 1,
    "type": "summary",
    "title": "Net cash flow",
    "scope": { "label": "Sep 2026" },
    "payload": {
      "headline": {
        "value": 25008,
        "label": "Net cash flow",
        "unit": "SAR",
        "tone": "positive"
      },
      "delta": { "value": 80192, "direction": "up", "comparedTo": "vs August" },
      "metrics": [{ "label": "Entries", "value": 41, "tone": "neutral" }],
      "facts": [
        { "label": "All-time balance", "value": 918220, "tone": "positive" }
      ],
      "suggestions": ["Show entries for this month"]
    }
  }
}
```

`tone` is `positive`, `negative`, or `neutral`. It carries direction, not judgment. `suggestions` are the follow-ups your agent offered; they render as labels, because the session view is a record of what happened, not a place to continue the conversation.

### options

A choice you presented to the user. Because the user has already answered by the time anyone reviews the session, the choices render as labels.

```json
{
  "$agent_artifact": {
    "v": 1,
    "type": "options",
    "payload": {
      "question": "Which quarter should I show?",
      "options": [
        { "id": "2026-q1", "label": "Jan – Mar 2026" },
        { "id": "2026-q2", "label": "Apr – Jun 2026" }
      ],
      "reason": "too_many_results"
    }
  }
}
```

Attach this one to the tool's `input`, since the question is what your tool received. `reason` is your own short code for why the agent asked; it's stored for analysis and not rendered.

### table

```json
{
  "$agent_artifact": {
    "v": 1,
    "type": "table",
    "title": "Spend by region",
    "payload": {
      "columns": ["Region", "Sessions", "Spend"],
      "rows": [
        ["EU", 1200, 4517.5],
        ["US", 880, 91000]
      ]
    }
  }
}
```

Cells are strings or numbers; numbers format by `locale` and align right. Every row needs exactly one cell per column. Amplitude rejects a row of any other length instead of padding it, because padding would misstate your data.

### html

For a visual the other types genuinely can't express, send self-contained markup.

```json
{
  "$agent_artifact": {
    "v": 1,
    "type": "html",
    "title": "Custom SLA gauge",
    "payload": {
      "html": "<html><head><style>…</style></head><body>…</body></html>"
    }
  }
}
```

Amplitude enforces all of these:

- **Static markup only**: Amplitude rejects script tags, and the rendering frame can't execute scripts at all. Interactivity belongs in the declarative types.
- **Fully self-contained**: inline your CSS and data. Amplitude blocks external requests for images, fonts, stylesheets, and APIs, so a document that assumes the network renders broken. Use `data:` URIs for images.
- **Bring your own styling**: the frame isolates your document, so Amplitude's theme doesn't reach inside it.
- **100,000 characters maximum**.

Amplitude shows an `html` artifact collapsed, with a button to render it, then draws it in a sealed frame that can't reach the surrounding page.

Prefer a declarative type wherever one fits: those cards follow the viewer's theme, work with screen readers, and appear in exports. `html` trades all of that for freedom of layout.

## Limits

Amplitude rejects oversized payloads instead of truncating them, because a shortened chart would show the wrong picture.

| Limit | Value |
| --- | --- |
| Chart items (`categorical`) | 12 |
| Chart series | 4, up to 100 points each |
| Steps | 12 |
| Table | 8 columns × 50 rows |
| Summary metrics / facts / suggestions | 12 / 12 / 8 |
| Options | 12 |
| `html` | 100,000 characters |
| Whole payload | 256 KB |

## Privacy

Artifact payloads travel on your tool content, so they follow the same content settings as the rest of your instrumentation, including your redaction configuration. Two things to know:

- **Redact before you serialize**: redaction that rewrites text can corrupt JSON mid-payload, which makes the artifact unparseable and the card disappear. Remove or mask values as you build the envelope.
- **Markup carries data in more places than text**: for `html`, check attributes and inline styles, not only visible strings.

If your organization uses `metadata_only` content mode, Amplitude doesn't capture tool content, so artifacts don't render.

## Troubleshooting

| Symptom | Cause |
| --- | --- |
| Raw JSON, no card | The envelope key is missing or misspelled, or artifact rendering isn't enabled for your organization. |
| Nothing at all, no card and no raw content | Tool content isn't reaching Amplitude. Confirm you're passing `input`/`output` (not `toolInput`/`toolOutput`) and that your content mode captures tool content. |
| "Artifact not rendered" with a reason | Amplitude recognized the envelope, but the payload failed validation. The reason names the field. Fix it and re-emit. |
| A note about a substituted presentation | Your `kind` or `shape` has no dedicated renderer yet, so Amplitude drew the closest honest equivalent. The data is intact. |
| An `html` card that looks broken | The document loads something external, or relies on scripts. Inline everything; remove scripts. |

