Broken markdown in Claude responses within n8n happens because special characters like asterisks, backticks, and brackets get escaped or stripped during data transfer between nodes. Fix this by using a Code node to unescape double-escaped characters, configuring the Respond to Webhook node with the correct content type, and ensuring downstream nodes treat the response as raw text rather than HTML.
Preserving Markdown Formatting in Claude Responses Within n8n Workflows
Claude's responses often include rich markdown formatting: headers, bold text, code blocks, and bullet lists. When these responses pass through n8n nodes, the markdown can break in several ways. Asterisks get doubled or stripped, backticks disappear, newlines collapse into single lines, and code blocks lose their indentation. This tutorial identifies where markdown corruption occurs in the n8n data pipeline and provides fixes for each scenario.
Prerequisites
- An n8n workflow with an Anthropic Chat Model or AI Agent node connected to Claude
- A downstream node that consumes the Claude response (Respond to Webhook, Email, Slack, etc.)
- Basic understanding of markdown syntax and JSON string escaping
- Familiarity with the n8n Code node
Step-by-step guide
Identify where markdown breaks by inspecting each node's output
Identify where markdown breaks by inspecting each node's output
Run your workflow manually and click on each node in sequence to inspect the output. Start from the Claude or AI Agent node and trace the response through every downstream node. In the output panel, switch between Table view and JSON view to see how the markdown text is stored. In JSON view, look for double-escaped characters: \\n instead of \n for newlines, \\* instead of * for asterisks, or missing backticks. The node where the formatting first breaks is where you need to add a fix. Common culprits include Set nodes that re-assign the text value, Function/Code nodes that call JSON.stringify without handling special characters, and HTTP Request or Webhook Response nodes that encode the output.
Expected result: You can identify the exact node where markdown formatting breaks in the pipeline.
Add a Code node to fix double-escaped characters
Add a Code node to fix double-escaped characters
Insert a Code node immediately after the node where formatting breaks. This node unescapes double-escaped characters that commonly corrupt markdown. Set it to Run Once for All Items. The code processes the Claude response text and replaces double-escaped newlines, tabs, asterisks, and backticks with their single-escaped equivalents. It also normalizes line endings and preserves code block indentation. Place this node before any output node like Respond to Webhook, Slack, or Email.
1// Code node: Fix Markdown Escaping2// Mode: Run Once for All Items34const items = $input.all();56for (const item of items) {7 let text = item.json.output || item.json.text || item.json.message || '';89 // Fix double-escaped newlines10 text = text.replace(/\\n/g, '\n');1112 // Fix double-escaped tabs13 text = text.replace(/\\t/g, '\t');1415 // Fix escaped asterisks (broken bold/italic)16 text = text.replace(/\\\*/g, '*');1718 // Fix escaped backticks (broken code blocks)19 text = text.replace(/\\`/g, '`');2021 // Fix escaped brackets (broken links)22 text = text.replace(/\\\[/g, '[');23 text = text.replace(/\\\]/g, ']');2425 // Normalize line endings26 text = text.replace(/\r\n/g, '\n');2728 item.json.formattedOutput = text;29}3031return items;Expected result: The markdown text has proper single-escaped characters, with newlines, bold, italic, and code blocks rendering correctly.
Configure Respond to Webhook with the correct content type
Configure Respond to Webhook with the correct content type
If your workflow returns the Claude response via Respond to Webhook, incorrect content type settings cause markdown to render as plain text or get HTML-encoded. In the Respond to Webhook node, set the Response Content Type to text/plain if you want the raw markdown, or text/html if you want to render it. For API consumers that parse markdown (like chat UIs), use application/json and wrap the markdown in a JSON field. If using text/html, you need to convert markdown to HTML first using a Code node with a simple markdown-to-HTML converter. Do NOT use application/json content type if you are putting raw text directly in the response body, as this will add extra JSON escaping.
1// For Respond to Webhook sending raw markdown:2// Response Content Type: text/plain3// Response Body: {{ $json.formattedOutput }}45// For API consumers expecting JSON:6// Response Content Type: application/json7// Response Body:8{9 "response": "{{ $json.formattedOutput }}",10 "status": "success"11}Expected result: The webhook response contains properly formatted markdown text with the correct content type header.
Preserve code blocks through Set and Merge nodes
Preserve code blocks through Set and Merge nodes
Set nodes and Merge nodes can strip whitespace from text values, destroying code block indentation. When you need to pass Claude's response through a Set node, use the expression {{ $json.output }} directly rather than assigning it to a new field and then referencing it. If you must use a Set node, set the value type to String and use the expression editor rather than the fixed value input. For Merge nodes, use the Combine mode and ensure the text field from the Claude branch is the one preserved, not overwritten by the other branch. Add a test Code node after the Merge that validates the code blocks still have their original indentation.
1// Code node: Validate code block preservation2const items = $input.all();34for (const item of items) {5 const text = item.json.formattedOutput || item.json.output || '';67 // Check if code blocks still have indentation8 const codeBlockRegex = /```[\s\S]*?```/g;9 const codeBlocks = text.match(codeBlockRegex) || [];1011 for (const block of codeBlocks) {12 const lines = block.split('\n');13 const hasIndentation = lines.some(line => line.startsWith(' ') || line.startsWith('\t'));14 if (!hasIndentation && lines.length > 2) {15 console.log('WARNING: Code block may have lost indentation');16 }17 }18}1920return items;Expected result: Code blocks retain their original indentation and formatting through Set and Merge nodes.
Handle markdown in Slack and Email output nodes
Handle markdown in Slack and Email output nodes
Slack and Email nodes have their own markdown interpretation. Slack uses mrkdwn format which differs from standard markdown: bold is *text* not **text**, and code blocks use triple backticks without a language specifier working differently. Email nodes may strip markdown entirely or render it as plain text. For Slack, add a Code node that converts standard markdown to Slack mrkdwn before the Slack node. For Email, convert markdown to HTML. If you need both, branch the workflow and apply different formatting transformations for each destination.
1// Code node: Convert standard markdown to Slack mrkdwn2const items = $input.all();34for (const item of items) {5 let text = item.json.formattedOutput || '';67 // Convert standard bold **text** to Slack bold *text*8 text = text.replace(/\*\*(.+?)\*\*/g, '*$1*');910 // Convert headers ## to bold11 text = text.replace(/^#{1,6}\s+(.+)$/gm, '*$1*');1213 // Convert standard italic _text_ to Slack italic _text_ (same)14 // Convert links [text](url) to Slack <url|text>15 text = text.replace(/\[(.+?)\]\((.+?)\)/g, '<$2|$1>');1617 item.json.slackMessage = text;18}1920return items;Expected result: Claude's markdown response is correctly formatted for the specific output channel, whether Slack, Email, or webhook.
Complete working example
1// Code node: Comprehensive Markdown Fixer for Claude Responses2// Mode: Run Once for All Items3// Place between Claude/AI Agent output and your destination node45const items = $input.all();67function fixMarkdown(text) {8 if (!text) return '';910 // Step 1: Fix double-escaped characters11 let fixed = text;12 let prev = '';13 while (fixed !== prev) {14 prev = fixed;15 fixed = fixed.replace(/\\n/g, '\n');16 fixed = fixed.replace(/\\t/g, '\t');17 fixed = fixed.replace(/\\\*/g, '*');18 fixed = fixed.replace(/\\`/g, '`');19 fixed = fixed.replace(/\\\[/g, '[');20 fixed = fixed.replace(/\\\]/g, ']');21 }2223 // Step 2: Normalize line endings24 fixed = fixed.replace(/\r\n/g, '\n');25 fixed = fixed.replace(/\r/g, '\n');2627 // Step 3: Ensure blank lines before headers28 fixed = fixed.replace(/([^\n])\n(#{1,6}\s)/g, '$1\n\n$2');2930 // Step 4: Ensure blank lines around code blocks31 fixed = fixed.replace(/([^\n])\n```/g, '$1\n\n```');32 fixed = fixed.replace(/```\n([^\n])/g, '```\n\n$1');3334 return fixed;35}3637function toSlackMrkdwn(text) {38 let slack = text;39 slack = slack.replace(/\*\*(.+?)\*\*/g, '*$1*');40 slack = slack.replace(/^#{1,6}\s+(.+)$/gm, '*$1*');41 slack = slack.replace(/\[(.+?)\]\((.+?)\)/g, '<$2|$1>');42 return slack;43}4445function toPlainHtml(text) {46 let html = text;47 html = html.replace(/&/g, '&');48 html = html.replace(/</g, '<');49 html = html.replace(/>/g, '>');50 html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');51 html = html.replace(/\*(.+?)\*/g, '<em>$1</em>');52 html = html.replace(/`([^`]+)`/g, '<code>$1</code>');53 html = html.replace(/^#{3}\s+(.+)$/gm, '<h3>$1</h3>');54 html = html.replace(/^#{2}\s+(.+)$/gm, '<h2>$1</h2>');55 html = html.replace(/^#{1}\s+(.+)$/gm, '<h1>$1</h1>');56 html = html.replace(/\n/g, '<br>');57 return html;58}5960const results = [];6162for (const item of items) {63 const raw = item.json.output || item.json.text || '';64 const fixed = fixMarkdown(raw);6566 results.push({67 json: {68 ...item.json,69 markdown: fixed,70 slackMrkdwn: toSlackMrkdwn(fixed),71 html: toPlainHtml(fixed)72 }73 });74}7576return results;Common mistakes when fixing Broken Markdown Formatting in Claude Messages from n8n
Why it's a problem: Using JSON.stringify on the Claude response in a Code node, which double-escapes all special characters
How to avoid: Pass the text directly as a string property on the JSON object. Do not wrap it in JSON.stringify unless you are building a JSON response body.
Why it's a problem: Setting Respond to Webhook content type to application/json when sending raw markdown text
How to avoid: Use text/plain for raw markdown or properly structure the response as a JSON object with the markdown in a named field.
Why it's a problem: Using Slack's message field with standard markdown syntax instead of Slack's mrkdwn format
How to avoid: Add a conversion Code node before the Slack node that transforms **bold** to *bold* and converts links to Slack's <url|text> format.
Why it's a problem: Assuming Table view in n8n shows the exact text that will be sent downstream
How to avoid: Always check JSON view for the actual string content. Table view may render or truncate markdown differently.
Why it's a problem: Not testing with code blocks that contain special characters like backticks inside backticks
How to avoid: Test with edge cases including nested code blocks, inline code with special chars, and multi-language code blocks.
Best practices
- Always inspect node output in JSON view, not Table view, to see the actual character escaping
- Minimize the number of nodes between Claude output and the final destination to reduce formatting corruption opportunities
- Use text/plain content type for webhook responses that return raw markdown
- Convert markdown to the target format (Slack mrkdwn, HTML) as the last step before the output node
- Test with responses that contain all markdown elements: headers, bold, italic, code blocks, lists, and links
- Avoid JSON.stringify on text that contains markdown unless you intend to double-escape it
- Use the expression editor's preview to verify markdown characters are preserved before running the workflow
- Store Claude's raw response in a separate field and transform copies for different output channels
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
My n8n workflow sends Claude responses to Slack but the markdown formatting is broken. Bold text shows as **text** instead of rendering, and code blocks lose their formatting. How do I convert standard markdown to Slack's mrkdwn format in a Code node?
Add a Code node after my AI Agent node that fixes double-escaped markdown characters in Claude's response and outputs three versions: raw markdown, Slack mrkdwn, and HTML. The response needs to preserve code block indentation.
Frequently asked questions
Why does Claude's markdown break when passing through n8n nodes?
n8n stores data as JSON objects. When text containing markdown special characters (*, `, [, ]) is serialized to JSON and deserialized between nodes, these characters can be double-escaped. Each node transition is a potential point where escaping can add extra backslashes.
How do I send Claude's markdown response as formatted HTML in an email?
Add a Code node before the Email node that converts markdown to HTML. Replace **bold** with <strong> tags, # headers with <h> tags, and newlines with <br> tags. Set the Email node to send HTML content, not plain text.
Does n8n have a built-in markdown renderer?
No, n8n does not include a built-in markdown-to-HTML converter. You need to handle the conversion in a Code node. For complex markdown, consider calling a markdown parsing library through an external API.
Why do code blocks in Claude responses lose their indentation?
Set nodes and some transformation operations trim or normalize whitespace. Preserve code blocks by avoiding Set nodes between the Claude output and your destination, or by extracting and reattaching code blocks after other transformations.
Can I preserve markdown formatting when storing Claude responses in a database?
Yes, store the response in a TEXT or JSONB column. Use a Code node to ensure the text is not double-escaped before inserting. When reading it back, the text should retain its original formatting.
How do I handle markdown in Claude responses sent to a React frontend?
Return the raw markdown via the Respond to Webhook node with text/plain content type. In your React frontend, use a library like react-markdown to render the markdown as HTML components.
Why does my Slack message show raw asterisks instead of bold text?
Slack uses mrkdwn format, not standard markdown. Standard markdown bold is **text** but Slack bold is *text*. Add a Code node before the Slack node that converts **text** to *text* and adjusts other syntax differences.
Can RapidDev help build n8n workflows with proper formatting pipelines?
Yes, RapidDev specializes in n8n workflow development and can build formatting pipelines that correctly transform LLM responses for any output channel including Slack, Email, webhooks, and databases.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation