/n8n-tutorials

How to fix broken markdown formatting in Claude messages from n8n?

Learn how to fix broken markdown formatting in Claude messages from n8n by properly escaping or converting markdown syntax before sending messages.

Matt Graham, CEO of Rapid Developers

Book a call with an Expert

Starting a new venture? Need to upgrade your web app? RapidDev builds application with your growth in mind.

Book a free consultation

How to fix broken markdown formatting in Claude messages from n8n?

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:

  • Special characters like asterisks (\*), underscores (\_), and backticks (\`) have special meaning in markdown
  • n8n might be passing these characters without proper escaping
  • Claude might interpret these characters as markdown formatting instructions when they're meant to be literal

 

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:

  1. Create a new workflow named "Process Markdown for Claude"
  2. Add a Start node that expects an input with a "text" field
  3. Add your Function node with the markdown processing code
  4. Add a Return node that outputs the processed text
  5. Save this workflow
  6. In your main workflow, add an Execute Workflow node that calls your 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;

Want to explore opportunities to work with us?

Connect with our team to unlock the full potential of no-code solutions with a no-commitment consultation!

Book a Free Consultation

Client trust and success are our top priorities

When it comes to serving you, we sweat the little things. That’s why our work makes a big impact.

Rapid Dev was an exceptional project management organization and the best development collaborators I've had the pleasure of working with. They do complex work on extremely fast timelines and effectively manage the testing and pre-launch process to deliver the best possible product. I'm extremely impressed with their execution ability.

CPO, Praction - Arkady Sokolov

May 2, 2023

Working with Matt was comparable to having another co-founder on the team, but without the commitment or cost. He has a strategic mindset and willing to change the scope of the project in real time based on the needs of the client. A true strategic thought partner!

Co-Founder, Arc - Donald Muir

Dec 27, 2022

Rapid Dev are 10/10, excellent communicators - the best I've ever encountered in the tech dev space. They always go the extra mile, they genuinely care, they respond quickly, they're flexible, adaptable and their enthusiasm is amazing.

Co-CEO, Grantify - Mat Westergreen-Thorne

Oct 15, 2022

Rapid Dev is an excellent developer for no-code and low-code solutions.
We’ve had great success since launching the platform in November 2023. In a few months, we’ve gained over 1,000 new active users. We’ve also secured several dozen bookings on the platform and seen about 70% new user month-over-month growth since the launch.

Co-Founder, Church Real Estate Marketplace - Emmanuel Brown

May 1, 2024 

Matt’s dedication to executing our vision and his commitment to the project deadline were impressive. 
This was such a specific project, and Matt really delivered. We worked with a really fast turnaround, and he always delivered. The site was a perfect prop for us!

Production Manager, Media Production Company - Samantha Fekete

Sep 23, 2022