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.
agent.artifact(type="table", title="Coverage", key="coverage", data=v1) # version 1
agent.artifact(type="table", title="Coverage", key="coverage", data=v2) # version 2Typed artifacts are limited to 1 MB of JSON. HTML pages can be up to 5 MB.
#table
{
"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
{
"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
{ "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.
{
"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:
{ "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:
{ "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>, anddata: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.
<!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>