fix: render markdown artifacts authored under the default HTML type - #720
Merged
Conversation
create_artifact's content_type defaults to text/html, so a request like "create a markdown recipe artifact" produced raw Markdown stored verbatim as HTML — the iframe then showed run-together `#`/`**` source and the card badge read "HTML" instead of rendering the document. Add a server-side safety net: HTML-typed content that lacks a standalone HTML document shell (`<!doctype html>` / `<html>`) is reclassified as text/markdown before storage, so the writer wraps it into a proper render document and the DynamoDB row (badge + render-Lambda mapping) stays truthful. Real HTML documents, Markdown, and other MIME types pass through untouched. Applied in both create_artifact_record and update_artifact_record (the update path covers the inherited-type case where there is no argument for the model to get right). Also steer the model up front: the create_artifact docstring now says to author prose (reports, docs, recipes, mostly-text) as Markdown and to honor an explicit "markdown" request, reducing how often the backstop fires. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This was referenced Jul 24, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Asking the agent to "create a banana bread recipe markdown artifact" produced an artifact that rendered as run-together raw Markdown source (
#,##,**…) with a card badge reading HTML — not a formatted document. Repro conversation (prod):33cb39eb-4d3c-4e72-94a9-904400243d4f.Root cause
The Markdown pipeline works end-to-end (the writer wraps Markdown in an HTML render document via
marked, the badge shows "MD", the render Lambda maps the type). The failure is at authoring time:create_artifact'scontent_typedefaults totext/html. The model wrote raw Markdown ascontentbut never setcontent_type="text/markdown", soservice._put_objectstored the Markdown verbatim as HTML — which the sandboxed iframe renders as plain text, and the DynamoDB row recordstext/html(hence the "HTML" badge).Fix
Server-side safety net (the guarantee). HTML-typed content that lacks a standalone HTML document shell (
<!doctype html>/<html>) is reclassified totext/markdownbefore storage, so it's wrapped into a proper render document and the DynamoDB row (badge + render-Lambda mapping) stays truthful. Real HTML documents, Markdown, and other MIME types pass through untouched. Applied in bothcreate_artifact_recordandupdate_artifact_record— the update path covers the inherited-type case, where there's no argument for the model to get right at all.Docstring nudge (reduces how often the backstop fires).
create_artifact's description now steers the model to author prose (reports, docs, recipes, mostly-text) as Markdown and to honor an explicit "markdown" request.Why a content-based net rather than making
content_typerequiredThe bug was a type/content mismatch (HTML type declared over Markdown body), not an omitted value. A required argument forces the model to state a value, not a correct one, removes a genuinely good default for the majority-HTML case, and still can't help the
update_artifactinherited-type path. The coercion validates the invariant we actually care about — declared type matches content shape — deterministically.Cost note
The docstring adds ~60 tokens to the
create_artifactdescription, which lives in the cacheabletoolConfigprefix — only when the artifact tool is enabled, and it's a one-time-per-session prefix cost, not per-turn growth.Tests
Added 4 regression tests; full artifact suite green (26 artifact-tool + 29 app-api artifact tests):
text/html→ reclassifiedNotes
update_artifacton the banana-bread artifact — now renders correctly.🤖 Generated with Claude Code