Learn how to fix broken markdown formatting in Claude messages from n8n by properly escaping or converting markdown syntax before sending messages.
Book a call with an Expert
Starting a new venture? Need to upgrade your web app? RapidDev builds application with your growth in mind.
To fix broken markdown formatting in Claude messages from n8n, you need to apply proper text processing to handle markdown syntax before passing the messages to Claude. This involves either converting markdown to plain text or ensuring proper escaping of markdown characters, depending on your specific needs.
Step 1: Understand the Problem
When using n8n to interact with Claude, markdown formatting in messages can break due to several reasons:
Step 2: Add a Function Node for Markdown Processing
Add a Function node in your n8n workflow before sending data to Claude:
// Add this Function node before your Claude API call
const inputText = items[0].json.messageContent; // Adjust according to your data structure
const processedText = escapeMarkdown(inputText);
// Store the processed text for use in the Claude API call
items[0].json.processedMessageContent = processedText;
return items;
// Markdown escaping function
function escapeMarkdown(text) {
if (!text) return '';
// Escape markdown special characters
return text
.replace(/\*/g, '\\*')
.replace(/\_/g, '\\_')
.replace(/\`/g, '\\`')
.replace(/~/g, '\\~')
.replace(/#/g, '\\#')
.replace(/[/g, '\\[')
.replace(/]/g, '\\]')
.replace(/(/g, '\\(')
.replace(/)/g, '\\)')
.replace(/>/g, '\\>')
.replace(/-/g, '\\-')
.replace(/+/g, '\\+')
.replace(/=/g, '\\=')
.replace(/{/g, '\\{')
.replace(/}/g, '\\}')
.replace(/|/g, '\\|');
}
Step 3: Update Your Claude API Call
In your HTTP Request or Claude integration node, update the reference to use the processed content:
// Example using HTTP Request node
{
"url": "https://api.anthropic.com/v1/messages",
"method": "POST",
"authentication": "bearerAuth",
"headers": {
"Content-Type": "application/json",
"anthropic-version": "2023-06-01"
},
"body": {
"model": "claude-3-opus-20240229",
"max\_tokens": 1024,
"messages": [
{
"role": "user",
"content": "{{$node["Function"].json["processedMessageContent"]}}"
}
]
}
}
Step 4: Alternative Method - Strip Markdown Completely
If you prefer to remove markdown formatting entirely instead of escaping it, use this function:
function stripMarkdown(text) {
if (!text) return '';
// Remove common markdown patterns
return text
// Remove headers
.replace(/#+\s+/g, '')
// Remove bold/italic
.replace(/(\*\*|\_\_)(.\*?)\1/g, '$2')
.replace(/(\*|\_)(.\*?)\1/g, '$2')
// Remove code blocks
.replace(/`[\s\S]*?`/g, '')
// Remove inline code
.replace(/`([^`]+)\`/g, '$1')
// Remove blockquotes
.replace(/^\s\*>\s+/gm, '')
// Remove horizontal rules
.replace(/^\s_[-_\_]{3,}\s\*$/gm, '')
// Remove link formatting but keep link text
.replace(/[([^]]+)]([^)]+)/g, '$1')
// Remove image formatting
.replace(/![([^]]+)]([^)]+)/g, '$1')
// Remove list markers
.replace(/^\s_[-_+]\s+/gm, '')
.replace(/^\s\*\d+.\s+/gm, '');
}
Step 5: Implement Message Preprocessing with JSON.parse/stringify
For more complex JSON structures being sent to Claude, use this comprehensive approach:
// For complex JSON structures with messages
const inputData = items[0].json;
// Deep clone the data to avoid modifying the original
const processedData = JSON.parse(JSON.stringify(inputData));
// Process all messages recursively
processMessages(processedData);
items[0].json.processedData = processedData;
return items;
function processMessages(obj) {
if (!obj || typeof obj !== 'object') return;
// If this is an array, process each element
if (Array.isArray(obj)) {
obj.forEach(item => processMessages(item));
return;
}
// Process message content if this looks like a message object
if (obj.role && obj.content && typeof obj.content === 'string') {
obj.content = escapeMarkdown(obj.content);
}
// Process all properties recursively
Object.keys(obj).forEach(key => {
if (typeof obj[key] === 'object') {
processMessages(obj[key]);
}
});
}
function escapeMarkdown(text) {
// Same escaping function as in Step 2
if (!text) return '';
return text
.replace(/\*/g, '\\*')
.replace(/\_/g, '\\_')
.replace(/\`/g, '\\`')
// ... other replacements
.replace(/|/g, '\\|');
}
Step 6: Create a Regex Pattern for Selective Escaping
If you want to be more selective about what markdown to escape, use this targeted approach:
function smartEscapeMarkdown(text) {
if (!text) return '';
// Only escape markdown that seems to be used as formatting
// This is more complex but produces cleaner results
return text
// Escape bold/italic but only when not part of a word
.replace(/(?|-|+|=|{|}||)(?=\S)/g, '\\$1');
}
Step 7: Handle Code Blocks Specially
Since code blocks often contain special characters, handle them separately:
function preserveCodeBlocks(text) {
if (!text) return '';
// Store code blocks temporarily
const codeBlocks = [];
let index = 0;
// Replace code blocks with placeholders
const withoutCode = text.replace(/`[\s\S]*?`/g, match => {
const placeholder = `__CODE_BLOCK_${index}__`;
codeBlocks.push(match);
index++;
return placeholder;
});
// Escape markdown in the text outside code blocks
const escapedText = escapeMarkdown(withoutCode);
// Restore code blocks
const finalText = escapedText.replace(/**CODE_BLOCK_(\d+)**/g, (match, blockIndex) => {
return codeBlocks[parseInt(blockIndex)];
});
return finalText;
}
Step 8: Implement HTML Conversion Option
If Claude supports HTML, convert markdown to HTML instead of escaping it:
// You'll need to install a markdown-to-html converter in n8n
// This example assumes you've added 'markdown-it' as a module
// in n8n's Function node settings
const MarkdownIt = require('markdown-it');
const md = new MarkdownIt();
function convertMarkdownToHtml(text) {
if (!text) return '';
return md.render(text);
}
// Usage in your Function node
const inputText = items[0].json.messageContent;
const htmlContent = convertMarkdownToHtml(inputText);
items[0].json.processedMessageContent = htmlContent;
return items;
Step 9: Create a Complete Preprocessing Workflow
For a full solution, combine multiple techniques:
// Complete preprocessing workflow
function preprocessForClaude(text) {
if (!text) return '';
// 1. First extract and save code blocks
const codeBlocks = [];
let codeBlockIndex = 0;
const withoutCodeBlocks = text.replace(/`[\s\S]*?`/g, match => {
const placeholder = `__CODE_BLOCK_${codeBlockIndex}__`;
codeBlocks.push(match);
codeBlockIndex++;
return placeholder;
});
// 2. Extract and save inline code
const inlineCodes = [];
let inlineCodeIndex = 0;
const withoutCodes = withoutCodeBlocks.replace(/`([^`]+)\`/g, (match, code) => {
const placeholder = `__INLINE_CODE_${inlineCodeIndex}__`;
inlineCodes.push(code);
inlineCodeIndex++;
return placeholder;
});
// 3. Escape markdown in the remaining text
const escapedText = withoutCodes
.replace(/\*/g, '\\*')
.replace(/\_/g, '\\_')
.replace(/#/g, '\\#')
.replace(/[/g, '\\[')
.replace(/]/g, '\\]')
.replace(/(/g, '\\(')
.replace(/)/g, '\\)')
.replace(/>/g, '\\>')
.replace(/-/g, '\\-')
.replace(/+/g, '\\+')
.replace(/=/g, '\\=')
.replace(/{/g, '\\{')
.replace(/}/g, '\\}')
.replace(/|/g, '\\|');
// 4. Restore code blocks
let finalText = escapedText.replace(/**CODE_BLOCK_(\d+)**/g, (match, blockIndex) => {
return codeBlocks[parseInt(blockIndex)];
});
// 5. Restore inline code
finalText = finalText.replace(/**INLINE_CODE_(\d+)**/g, (match, codeIndex) => {
return `\`${inlineCodes[parseInt(codeIndex)]}\`\`;
});
return finalText;
}
// Use in your Function node
items[0].json.processedMessageContent = preprocessForClaude(items[0].json.messageContent);
return items;
Step 10: Test Your Implementation
Add a Debug node to verify your markdown processing works correctly:
// Original and processed content in separate nodes
const original = items[0].json.messageContent;
const processed = items[0].json.processedMessageContent;
// Create a test message with problematic markdown
const testMessage = \`
# This is a header
**Bold text** and _italic text_
\`code\` and \`\`\`
multiline
code block
\`\`\`
- List item 1
- List item 2
[Link](https://example.com)
\`;
const processedTest = preprocessForClaude(testMessage);
// Return for debugging
return [
{
json: {
original: original,
processed: processed,
testOriginal: testMessage,
testProcessed: processedTest
}
}
];
Step 11: Create a Reusable n8n Node
To make this solution reusable, create a dedicated subworkflow:
Step 12: Handle Special Cases for Code Snippets
Code snippets often need special handling in Claude messages:
function handleCodeSnippets(text) {
if (!text) return '';
// Format code blocks for Claude by ensuring proper language tagging
return text.replace(/`([\w]*)\n([\s\S]*?)`/g, (match, language, code) => {
// If no language is specified, add 'text' as default
const lang = language.trim() || 'text';
// Ensure there's a newline after the opening backticks and language
return '`' + lang + '\n' + code.trim() + '\n`';
});
}
// Use in combination with your markdown processing
const inputText = items[0].json.messageContent;
const withFormattedCode = handleCodeSnippets(inputText);
const finalProcessed = preprocessForClaude(withFormattedCode);
items[0].json.processedMessageContent = finalProcessed;
return items;
Step 13: Implement a Solution for Bidirectional Processing
If you're both sending to and receiving from Claude, create bidirectional processing:
// Process outgoing messages to Claude
function preprocessOutgoing(text) {
// Implementation from previous steps for escaping markdown
return escapeMarkdown(text);
}
// Process incoming messages from Claude
function processIncoming(text) {
if (!text) return '';
// Claude may return escaped markdown - unescape it
return text
.replace(/\\*/g, '\*')
.replace(/\\_/g, '\_')
.replace(/\\`/g, '\`')
.replace(/\\~/g, '~')
.replace(/\\#/g, '#')
.replace(/\\[/g, '[')
.replace(/\\]/g, ']')
.replace(/\\(/g, '(')
.replace(/\\)/g, ')')
.replace(/\\>/g, '>')
.replace(/\\-/g, '-')
.replace(/\\+/g, '+')
.replace(/\\=/g, '=')
.replace(/\\{/g, '{')
.replace(/\\}/g, '}')
.replace(/\\|/g, '|');
}
// Usage in your workflow
const direction = items[0].json.direction || 'outgoing';
const content = items[0].json.content;
if (direction === 'outgoing') {
items[0].json.processedContent = preprocessOutgoing(content);
} else {
items[0].json.processedContent = processIncoming(content);
}
return items;
Step 14: Implement Error Handling
Add error handling to ensure your workflow is robust:
// Robust markdown processing with error handling
function safeMarkdownProcess(text) {
try {
if (!text || typeof text !== 'string') {
// Handle non-string input
console.log('Warning: Input is not a string:', typeof text);
return text || '';
}
return escapeMarkdown(text);
} catch (error) {
// Log error but don't break the workflow
console.error('Error processing markdown:', error);
// Return original text as fallback
return text;
}
}
// Use in your workflow with error catching
try {
const inputText = items[0].json.messageContent;
items[0].json.processedMessageContent = safeMarkdownProcess(inputText);
} catch (error) {
// Add error info to output for debugging
items[0].json.error = {
message: error.message,
stack: error.stack
};
// Keep original content as fallback
items[0].json.processedMessageContent = items[0].json.messageContent;
}
return items;
Step 15: Create a Configuration-Based Solution
Build a configurable solution that adapts to different formatting needs:
// Configuration-based markdown processing
function processMarkdown(text, config = {}) {
const options = {
escapeMarkdown: true,
preserveCodeBlocks: true,
handleCodeLanguages: true,
removeMarkdown: false,
...config
};
if (!text || typeof text !== 'string') return '';
let processedText = text;
// Store code blocks if needed
const codeBlocks = [];
if (options.preserveCodeBlocks) {
let codeBlockIndex = 0;
processedText = text.replace(/`[\s\S]*?`/g, match => {
const placeholder = `__CODE_BLOCK_${codeBlockIndex}__`;
codeBlocks.push(match);
codeBlockIndex++;
return placeholder;
});
}
// Process text according to options
if (options.removeMarkdown) {
// Remove all markdown formatting
processedText = processedText
.replace(/#+\s+/g, '')
.replace(/(\*\*|\_\_)(.\*?)\1/g, '$2')
.replace(/(\*|\_)(.\*?)\1/g, '$2')
.replace(/`([^`]+)\`/g, '$1')
.replace(/^\s\*>\s+/gm, '')
.replace(/^\s_[-_\_]{3,}\s\*$/gm, '')
.replace(/[([^]]+)]([^)]+)/g, '$1')
.replace(/![([^]]+)]([^)]+)/g, '$1')
.replace(/^\s_[-_+]\s+/gm, '')
.replace(/^\s\*\d+.\s+/gm, '');
} else if (options.escapeMarkdown) {
// Escape markdown characters
processedText = processedText
.replace(/\*/g, '\\*')
.replace(/\_/g, '\\_')
.replace(/\`/g, '\\`')
.replace(/~/g, '\\~')
.replace(/#/g, '\\#')
.replace(/[/g, '\\[')
.replace(/]/g, '\\]')
.replace(/(/g, '\\(')
.replace(/)/g, '\\)')
.replace(/>/g, '\\>')
.replace(/-/g, '\\-')
.replace(/+/g, '\\+')
.replace(/=/g, '\\=')
.replace(/{/g, '\\{')
.replace(/}/g, '\\}')
.replace(/|/g, '\\|');
}
// Restore code blocks if needed
if (options.preserveCodeBlocks) {
processedText = processedText.replace(/**CODE_BLOCK_(\d+)**/g, (match, blockIndex) => {
let block = codeBlocks[parseInt(blockIndex)];
// Handle code language if configured
if (options.handleCodeLanguages) {
block = block.replace(/\`\`\`([\w]\*)\n/g, (match, language) => {
const lang = language.trim() || 'text';
return '\`\`\`' + lang + '\n';
});
}
return block;
});
}
return processedText;
}
// Example usage with configuration
const inputText = items[0].json.messageContent;
items[0].json.processedMessageContent = processMarkdown(inputText, {
escapeMarkdown: true,
preserveCodeBlocks: true,
handleCodeLanguages: true,
removeMarkdown: false
});
return items;
When it comes to serving you, we sweat the little things. That’s why our work makes a big impact.