
# Artifacts

An artifact is a result your agent shows instead of describing it: a table, a chart, a document, a form to fill in, or a small HTML page. Cestra renders the typed ones (`table`, `chart`, `markdown`, `form`) natively, so they look right on a phone. `html` runs your own page in a locked-down sandbox.

Every artifact has a `title` (≤ 200 chars) and an optional `summary` (≤ 300 chars) that's shown on the card in the thread. It can also have a `key`.

## Versions

If you send an artifact with the same `key` (letters, digits, `. _ -`, ≤ 64 chars) again, it becomes **version 2, 3, …** of the same artifact rather than a new one. People can flip between versions in the app. Use this for anything that evolves, like a report you regenerate or a checklist you update.

```python
agent.artifact(type="table", title="Coverage", key="coverage", data=v1)   # version 1
agent.artifact(type="table", title="Coverage", key="coverage", data=v2)   # version 2
```

Typed artifacts are limited to 1 MB of JSON. HTML pages can be up to 5 MB.

## table

```json
{
  "type": "table",
  "title": "Coverage matrix",
  "key": "matrix",
  "data": {
    "columns": ["Requirement", { "label": "Tests", "align": "right" }, "Status"],
    "rows": [
      ["Login", 12, { "value": "covered", "tone": "ok" }],
      ["Export", 0, { "value": "missing", "tone": "bad" }],
      ["Search", 3, "partial"]
    ],
    "row_tones": [null, "bad", "warn"],
    "stats": [{ "label": "Covered", "value": "12 / 14", "tone": "ok" }],
    "note": "Generated from spec v3."
  }
}
```

| Field | |
| --- | --- |
| `columns` | 1–30 labels, or `{label, align}` with align `left`, `right` or `center` |
| `rows` | ≤ 2 000 rows. A cell is a string (≤ 2 000), number, boolean, `null`, or `{value, tone}` |
| `row_tones` | Optional per-row `ok`, `bad`, `warn` or `null` |
| `stats` | Up to 6 summary tiles: `{label, value, tone?}` |
| `note` | Optional footnote, ≤ 5 000 chars |

## chart

```json
{
  "type": "chart",
  "title": "Weekly signups",
  "data": {
    "kind": "line",
    "labels": ["W40", "W41", "W42", "W43"],
    "series": [
      { "name": "Signups", "values": [120, 140, 135, 180] },
      { "name": "Activated", "values": [80, 96, null, 130] }
    ],
    "unit": "users",
    "headline": { "label": "This week", "value": 180 },
    "note": "W42 activation data missing."
  }
}
```

| Field | |
| --- | --- |
| `kind` | `bar` (default) or `line` |
| `labels` | 1–500 x-axis labels |
| `series` | 1–8 series of `{name, values}`. `values` line up with `labels`, and `null` leaves a gap |
| `unit` | Optional, ≤ 20 chars |
| `headline` | Optional big number: `{label, value}` |
| `note` | Optional |

## markdown

```json
{ "type": "markdown", "title": "Release notes", "data": { "text": "# 2.4.0\n\n- Faster sync\n- Fixed export" } }
```

`text` can be up to 200 000 characters of GitHub-flavoured Markdown.

## form

A form collects structured input. When someone submits it, your agent gets a `form.submitted` event with the values.

```json
{
  "type": "form",
  "title": "Deploy settings",
  "key": "deploy",
  "data": {
    "intro": "Pick where this build goes.",
    "fields": [
      { "name": "env", "label": "Environment", "type": "select", "options": ["staging", "production"], "required": true },
      { "name": "replicas", "label": "Replicas", "type": "number", "min": 1, "max": 10, "default": 2 },
      { "name": "notes", "label": "Notes", "type": "textarea", "placeholder": "Anything I should know?" },
      { "name": "notify_team", "label": "Tell the team", "type": "checkbox", "default": true },
      { "name": "date", "label": "Go-live date", "type": "date" }
    ],
    "submit_label": "Deploy"
  }
}
```

| Field property | |
| --- | --- |
| `name` | An identifier (`[A-Za-z_][A-Za-z0-9_]*`), unique within the form |
| `label` | ≤ 120 chars |
| `type` | `text` (default), `textarea`, `number`, `select`, `checkbox` or `date` |
| `required` | Default `false` |
| `options` | Required for `select`, up to 50 |
| `placeholder`, `default`, `min`, `max` | Optional |

A form has 1–40 fields. `submit_label` defaults to "Submit". The submission is validated against the fields before it reaches you:

```json
{ "type": "form.submitted", "data": { "artifact_id": "…", "key": "deploy", "version": 1, "title": "Deploy settings",
  "values": { "env": "production", "replicas": 3, "notes": "", "notify_team": true, "date": "2026-11-20" },
  "by": { "id": "u_…", "name": "Ada" } } }
```

## html

For anything the typed artifacts can't show, send a complete HTML page, up to 5 MB:

```json
{ "type": "html", "title": "Seat picker", "html": "<!doctype html><html>…</html>" }
```

The page runs in a sandbox:

- It's served from a separate origin, inside `<iframe sandbox="allow-scripts">`.
- It has **no network**. `fetch`, XHR, WebSockets, external scripts, images, fonts and form posts are all blocked. Inline everything (`<style>`, `<script>`, and `data:` URIs for images and fonts).
- It has no cookies, no storage shared with Cestra, and no navigation of the top window.

To send data back, call `window.cestra.submit(values)` with a JSON object (≤ 100 KB). Your agent receives it as a `form.submitted` event, just like a form. `window.cestra.resize()` asks the frame to fit the content. It also runs automatically when the content changes size.

```html
<!doctype html>
<html>
<body style="font-family: system-ui; margin: 16px">
  <p>Which seats?</p>
  <label><input type="checkbox" value="A1"> A1</label>
  <label><input type="checkbox" value="A2"> A2</label>
  <button id="go">Book</button>
  <script>
    document.getElementById('go').addEventListener('click', function () {
      var seats = [].slice.call(document.querySelectorAll('input:checked')).map(function (i) { return i.value; });
      window.cestra.submit({ seats: seats });
    });
  </script>
</body>
</html>
```
