Topic: claude artifacts export

How to Export Your Claude Artifacts (and Why They're the Decision Layer)

Claude doesn't ship a separate Artifacts API. Every schema, plan, code block, and diagram you've built lives inline in the conversation export, wrapped in an <antartifact> tag. Here's how to pull every one of them out — and why they're usually the most decision-bearing part of the chat.

TL;DR

There's no /artifacts endpoint. Artifacts ride inside chat_messages[].text in your Claude export, wrapped in <antartifact identifier="…" type="…" title="…" language="…">…</antartifact>. The 12-line jq recipe below scans every assistant turn, parses the wrapper attributes, writes one file per Artifact with the right extension, and groups versions (create + update blocks) by identifier. For most use cases — auditing what you actually shipped, archiving design work, surfacing decisions — the Artifacts alone are more useful than the full transcript.

The wrapper schema

Every Artifact is a single tag with four attributes that matter, around a body of arbitrary length:

<antartifact
    identifier="user-onboarding-schema"
    type="application/vnd.ant.code"
    language="sql"
    title="users + sessions schema (Postgres)">
CREATE TABLE users (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  email TEXT NOT NULL UNIQUE,
  ...
);
</antartifact>

Attribute meanings: identifier is the stable key — the same Artifact iterated 7 times keeps one identifier across all revisions. type is the MIME type, one of five (see below). language is set only for code Artifacts and follows the GitHub linguist conventions (python, typescript, sql, etc.). title is the human-readable label that appears in the right-hand panel of the Claude UI. There's also a command attribute on update blocks (see versioning below) and occasionally old_str / new_str for partial edits.

The five MIME types

typeWhat it isFile extension
application/vnd.ant.codeSource code in any language (the language attr disambiguates).py / .ts / .sql / .go / etc., from language
text/htmlSelf-contained HTML page or component.html
text/markdownLong-form document, design spec, plan, ADR draft.md
application/vnd.ant.reactReact component (TSX) Claude can preview.tsx
application/vnd.ant.mermaidMermaid diagram source.mmd

Older exports may also include application/vnd.ant.svg; treat that as .svg. The extractor below applies a small switch statement to pick the extension from type and falls through to .txt for any unknown MIME.

Versioning: how Claude tracks edits

Claude doesn't update an Artifact in place — it emits a new <antartifact> block carrying the same identifier and a command attribute. The first occurrence has command="create" (often omitted, treat absence as create). Each subsequent edit is a separate block in a later assistant turn:

# in turn 3:
<antartifact identifier="users-schema" command="create" type="application/vnd.ant.code" language="sql" title="users schema">
CREATE TABLE users (id UUID PRIMARY KEY, email TEXT);
</antartifact>

# in turn 5:
<antartifact identifier="users-schema" command="update"
             old_str="email TEXT" new_str="email TEXT NOT NULL UNIQUE"
             type="application/vnd.ant.code" language="sql" title="users schema">
</antartifact>

# in turn 9 (full rewrite, no old_str/new_str):
<antartifact identifier="users-schema" command="update" type="application/vnd.ant.code" language="sql" title="users + sessions schema (Postgres)">
CREATE TABLE users (id UUID PRIMARY KEY DEFAULT gen_random_uuid(), email TEXT NOT NULL UNIQUE);
CREATE TABLE sessions (...);
</antartifact>

Two reconstruction strategies. (1) Last-wins — group by identifier, sort by chat_messages[].created_at, take the last full body. Right answer for "what's the final state?" — and what most archives want. (2) Replay — start from the create block, apply each old_str / new_str diff in order, fall back to full-body replacement on rewrites. Right answer for "show me the version history" or "diff turn 5 against turn 9." The script below uses last-wins by default; toggle the REPLAY=1 flag at the top to switch.

The 12-line jq recipe (last-wins)

Save as extract-artifacts.sh. Reads conversations.json, writes one file per Artifact identifier into ./artifacts/:

#!/usr/bin/env bash
set -euo pipefail
mkdir -p artifacts

jq -r '
  .[] | .chat_messages[] | select(.sender=="assistant") |
  .text | scan("<antartifact[^>]*>[\\s\\S]*?</antartifact>")[]
' conversations.json |
while IFS= read -r block; do
  id=$(  echo "$block" | grep -oE 'identifier="[^"]+"' | head -1 | cut -d'"' -f2)
  type=$(echo "$block" | grep -oE 'type="[^"]+"'       | head -1 | cut -d'"' -f2)
  lang=$(echo "$block" | grep -oE 'language="[^"]+"'   | head -1 | cut -d'"' -f2 || true)
  ext=$(case "$type" in
    application/vnd.ant.code)    echo "${lang:-txt}";;
    text/html)                    echo "html";;
    text/markdown)                echo "md";;
    application/vnd.ant.react)    echo "tsx";;
    application/vnd.ant.mermaid)  echo "mmd";;
    application/vnd.ant.svg)      echo "svg";;
    *) echo "txt";;
  esac)
  body=$(echo "$block" | sed -E 's|^<antartifact[^>]*>||' | sed -E 's|</antartifact>$||')
  [[ -z "$body" ]] && continue   # skip update-blocks with only old_str/new_str
  echo "$body" > "artifacts/${id}.${ext}"
done

Behavior: every create block writes a file; every full-rewrite update block overwrites it (last-wins); partial-edit blocks (with old_str / new_str but no body) are skipped because their body is empty after the regex strip — fine for last-wins, broken for replay. For replay, you'd want a real XML-tolerant parser (node -e "const x=require('xmldoc')…" or Python's lxml) plus an in-memory state map keyed on identifier; our open-source extractor ships that path in JS.

What the output looks like

For a typical 200-conversation export from a senior engineer using Claude regularly, expect 30–80 distinct Artifact identifiers — far fewer than the total number of <antartifact> blocks (those will run 200–400 because of update churn). The directory after extraction looks like:

artifacts/
├── auth-middleware-design.md
├── invoice-service-architecture.md
├── job-queue-comparison.md
├── pricing-page-component.tsx
├── prisma-schema-iteration.sql
├── retention-spec.md
├── stripe-webhook-handler.ts
├── system-architecture-diagram.mmd
└── ...

That list, on its own, is a quarter of work as a flat directory of artifacts. Skim it and you can name every durable thing you built with Claude — which is the audit-trail outcome ADR practices try (and usually fail) to deliver. The transcript around them is supporting material; the artifacts themselves are the deliverable.

Why Artifacts ARE the decision layer

A working hypothesis after extracting a few thousand of these: when a Claude conversation produced an Artifact, that's almost always where the decision crystallized. The user asked for help with a schema; Claude wrote five candidate schemas in prose; one made it into an Artifact; the user iterated; Claude updated the Artifact; the iteration converged. The Artifact is the chosen option; the prose is the trade-off discussion that selected it. This pattern holds for code, designs, plans, ADR drafts, and pricing tables. It does not hold for clarification questions, brainstorming, or one-shot answers — none of those produce Artifacts.

That's why an extractor optimized for decisions (not for messages) treats Artifacts as first-class. Each Artifact is a candidate decision record; the prose in the surrounding turns is the rationale; the create + update chain is the iteration history. WhyChose's open-source extractor implements exactly this — every Artifact in your export becomes a row in the output, with the chosen option (Artifact body) and the rationale (prose around it) split, and the model + conversation_uuid + Artifact identifier carried through for back-linking. Decision extraction on the ChatGPT side has to lean on heuristic patterns ("we'll go with…", "decided to…") because OpenAI doesn't ship an Artifact equivalent; on the Claude side the structured signal is already there in the wrapper.

How WhyChose helps

Manually running the script above gets you a folder of Artifacts; that's a useful archive. WhyChose goes one layer further — it extracts the Artifact body, identifies the surrounding rationale, links back to the source conversation, and emits a structured decision log you can share with a teammate or import into your ADR repo. Same wrapper-parser under the hood as the script above; the extractor source is in bin/extractor.js, MIT-licensed. The hosted product adds the share-by-link UX, Notion / Linear export, and version-history replay (the REPLAY=1 branch the script above stubs out).

Get early access

Related questions

Where do Claude Artifacts live in the export?

Inline, inside assistant messages. Every Artifact is wrapped in an <antartifact> pseudo-XML tag inside the assistant turn's text field. There's no separate file, no separate API, no /artifacts endpoint — they ride along with the conversation and you parse them out with regex or an XML-tolerant parser.

How does Claude track Artifact versions?

Through the command attribute on the wrapper. First occurrence is command="create"; every edit is a separate <antartifact> block with command="update" and the same identifier — sometimes carrying old_str / new_str for partial edits, sometimes a full rewrite. Group by identifier and walk in document order to reconstruct the version history; for most use cases the last block is the canonical body.

Which MIME types do Artifacts use?

Five common ones: application/vnd.ant.code (a language attribute carries python/typescript/etc.), text/html, text/markdown, application/vnd.ant.react (TSX with preview), and application/vnd.ant.mermaid. Older exports also include application/vnd.ant.svg. The extractor maps type → file extension during write.

Why do you call Artifacts 'the decision layer'?

Because that's where durable choices crystallize. Schemas, plans, designs, code, ADR drafts — when someone is iterating with Claude toward a chosen output, the Artifact is the chosen output and the surrounding prose is the rationale. Treating Artifacts as first-class is what an extractor optimized for decisions (rather than for messages) does.

Further reading