HTML Tags Showing in Notes After a Sync
Formatting breaks after a sync because text carries a representation (HTML, Markdown, Slack mrkdwn, or plain) that no field declares, so the string moves and the meaning does not. There are two mechanisms: a representation mismatch, where markup lands in a field that reads a different one and shows literally as <p> or **bold**, and escape drift, where an API that returns & as & is copied into a field that stores it as text, so it comes back as &amp; on the next pass. The fix is to declare each field's representation, convert exactly once at the hop, and never let a converted field sync back.
A note that arrives full of <p> tags, a Slack alert that says Tom & Jerry, and an AI summary that lands in the CRM as one long line with asterisks in it are the same class of bug wearing three costumes. Text carries a formatting representation, HTML or Markdown or Slack's mrkdwn or nothing at all, and no field anywhere declares which one it holds. A sync moves the string and leaves the representation behind. There are exactly two ways that goes wrong: the markup is read by a system that speaks a different one and shows it literally, or a system escapes on output, the next system stores the escape as text, and the entity grows by one layer per pass. The fix is the same for both. Write down what each field holds, convert once at the hop, and never let a converted field sync back.
Which representation does each system actually store?
Nobody labels this in a field settings screen, so the table below is what the vendors' own documentation says each system stores and returns, checked September 2026. The third column is the one that predicts the bug.
| System and field | What the API stores or returns | What happens to the wrong markup |
|---|---|---|
| Salesforce Rich Text Area | HTML from an approved tag list; 131,072 characters including the tags | Unsupported tags are removed and the text inside them is kept as plain text. The API returns & as &, < as <, " as ", ' as '. |
| Salesforce Text, Text Area, Long Text Area | Plain text | Anything else is characters. Salesforce documents that the rich text editor "treats HTML code as text," and a plain field does too. |
HubSpot note body (hs_note_body) | HTML, up to 65,536 characters | The timeline renders tags. A raw newline is whitespace in HTML, so a Markdown or plain-text paragraph break collapses to a space. |
| Slack message text | mrkdwn, Slack's own syntax: *bold* with one asterisk, _italic_, ~strike~, links as <url|text> | &, <, > are control characters and must be sent as entities when literal; nothing else should be encoded. **double asterisks** and ## headers are not mrkdwn and show as typed. |
| Airtable long text, rich formatting on | A variation of Markdown that Airtable says is not fully compliant with GitHub Flavored Markdown; a trailing newline on every read | HTML is not supported outside the send-email automation action, so tags show literally. |
| Notion rich text | An array of rich text objects with annotation flags for bold, italic, code | A Markdown or HTML string written into the text is stored as characters, not as formatting. |
| SMS body, Google Sheets cell, most single-line CRM fields | Plain text | Everything shows literally. |
| A language model's output | Markdown by default | Whatever field receives it inherits the asterisks and pound signs. Anthropic's guidance notes that removing Markdown from the prompt reduces Markdown in the output. |
Two things fall out of the table. First, "rich text" is not one format. HubSpot and Salesforce store HTML, Airtable stores Markdown, Slack stores something that looks like Markdown and is not, and Notion stores structured objects, so a sync between any two rich-text fields is a translation, not a copy. Second, the Salesforce row changes the text on the way out. A literal ampersand in a Rich Text Area comes back through the API as &, which is correct for HTML and wrong for every other destination in the table, and that one documented behavior is behind most of the escape drift we get asked to fix.
Two mechanisms, two different symptoms
Look at what the reader actually sees before touching the workflow, because the two failures need different fixes.
If the garbage is tags or Markdown punctuation, <p>, <br>, **, ##, - at the start of lines, the mechanism is a representation mismatch. Some hop wrote representation A into a field that reads representation B, and B rendered A's control characters as ordinary text. Nothing was corrupted. The full note is there, wearing the wrong clothes, and one conversion at the right hop fixes every record.
If the garbage is entities, &, ', ", , the mechanism is escape drift, and there is a second question to ask: is it one layer or several? One layer means an escaping read was copied into a storing write once. A & sitting in a plain CRM field sourced from a Salesforce rich text read is exactly this. Slack is the odd case: it decodes exactly those three sequences for display, so a visible & in a Slack message means the string arrived as &amp;, escaped once by the source's API on read and once more by whichever step prepared it for Slack, which puts it in the second category already. Several layers means a loop. The escaped value went into a plain field, the plain field synced back to the rich field, which stored the literal & and escapes its leading & on the next read to produce &amp;. Each pass adds four characters. The note is being rewritten by your own integration, the same shape as a two-way sync that overwrites data, except the conflicting edit was manufactured by the encoding step.
This is also where the fix that everyone reaches for first fails. Stripping tags does not decode entities. Zapier's Remove HTML Tags transform "removes all HTML tags from your input, leaving only plain, unformatted text," and Make's stripHTML "removes all html tags from text." Neither says anything about entities, and the Make community thread asking why stripHTML left – behind gets the honest answer that an entity is not a tag. They are two operations, strip and decode, in that order, and a pipeline that does only the first ships clean paragraphs with & in them.
One thing to rule out: if the garbage is é or ’ rather than é or ', it is not markup. That is a byte-level encoding mismatch, and no amount of HTML handling will touch it.
Why converting twice destroys the note
Once you know the representations, the temptation is to convert freely, HTML to Markdown here and Markdown back to HTML there. Do that inside a two-way sync and the note degrades on every pass, because none of these conversions is reversible.
The losses are concrete. Salesforce's rich text supports colored spans and pasted tables; Markdown has no color, and Airtable's documented Markdown has no table syntax at all, so a round trip through Markdown comes back with the color gone and the table flattened to lines. Slack's mrkdwn has no headers, so a converted ## Next steps arrives as plain text or literal pound signs, and it never goes back to a header. Salesforce documents the same thing inside its own product: converting a Rich Text Area field to a Long Text Area hides the markup rather than removing it, so you can restore it, which is an admission that the plain version cannot be turned back into the rich one.
So the rule is convert exactly once, at the hop, in the direction of the receiver, and treat the converted copy as write-only. If the CRM note is the record of truth, the Airtable version, the Slack alert, and the billing system's plain field are derived views, and the sync never reads any of them back into the note. It is the same rule that keeps a clipped text field from syncing back over the original: a lossy transformation on one side of a bidirectional sync is a conflict you scheduled.
The AI step is where the stray asterisks come from now
The fastest-growing source of formatting bugs is an AI summarization step, because it introduces a representation nobody chose. Models write Markdown unless told otherwise, so a summary comes back with **Key points**, a bulleted list, and blank-line paragraph breaks, and then gets written into a field from the table above.
Into a HubSpot note, the double asterisks show literally and the blank lines vanish, because the body is HTML and HTML collapses whitespace. The June 2025 HubSpot community thread about a ChatGPT summary posted through Make that arrived as "a single line looking mess" is this exact case, and the accepted answer is to replace \n with <br> before the write. Into an SMS, the asterisks and pound signs go straight to the customer's phone. Into Slack, **bold** does not render as bold, because mrkdwn uses a single asterisk, and a ## header is two pound signs and a space.
There are two correct fixes and they must not be combined. The first is to get the model to emit the receiver's representation. Anthropic's prompting guidance is specific: tell the model what to produce rather than what to avoid ("your response should be composed of smoothly flowing prose paragraphs" beats "do not use markdown"), and match the prompt's own style to the output you want, since a prompt written in Markdown pulls Markdown back out. For an HTML destination, ask for HTML with <p> paragraphs and nothing else; for SMS, ask for plain sentences. The second fix is to leave the model alone and convert: Zapier's Formatter has a Convert Markdown to HTML transform for the HubSpot and Salesforce case and a Remove HTML Tags transform for the plain case. Doing both, prompting for HTML and then feeding it to a converter that expects Markdown, produces escaped tags, which is the mismatch bug with an extra step.
Whichever you pick, the AI step's output representation is now a fact about the workflow, and the next person to edit the prompt will not know that the <br> replace step three nodes later depends on it. It belongs in the prompt's version record next to the model and the schema.
Declare the representation per field, then convert at the hop
This is the procedure we use when a client's notes arrive with tags in them, whether the pipeline is form notes feeding a sales system or a field app pushing job notes to billing.
Inventory first. For every text field that crosses a system boundary, write down its representation using the table above, and for API reads note whether the source escapes on output. Then pick one of four moves at each hop, based on the pair.
Same representation on both sides: pass the string through untouched. No strip, no escape, no converter. The most common self-inflicted wound is an HTML-to-HTML copy with a "clean up" step in the middle that escapes the tags it was supposed to preserve.
Rich to plain: strip the tags, then decode the entities, in that order, and mark the destination write-only. A stripped note cannot become a rich note again.
Plain to rich: escape &, <, and > first, then add the receiver's line breaks, <br> or <p> for HTML, a real newline for Airtable Markdown and Slack. Escaping after adding tags escapes your own tags. For Slack, escape only those three characters, since Slack decodes only those.
Rich to a different rich: convert once with a converter you have tested on a note that contains a list, a link, and an ampersand, accept the loss, and mark the destination write-only.
Finally, run one write per receiver with a sentinel note before going live. Ours is a two-line value: Tom & Jerry <Sales> said "it's **ready**" on the first line and - next step on the second. That string exercises all three Slack control characters, both quote entities, Markdown bold, a list marker, and a line break. Read it back through the API, not the UI, and compare. Anything that came back as &, ', **, or a single line tells you which mechanism you have and which hop owns it, in one run. Log the representation you sent and the length before and after each conversion; a run record that carries that turns "the notes look weird" into a one-minute diagnosis.
Where to start
Pull the ten most recent notes people complained about and sort them into two piles: tags and punctuation, or entities. If the entity pile has anything with &amp; in it, that sync has a loop, and it gets fixed first by making the converted side write-only, before you clean a single record, because cleaning records inside a running loop is how you get &amp;amp;.
Then do the inventory. If it comes back with a Salesforce rich text read, an AI summary, and a Slack alert in one pipeline, which is common, three representations are meeting at one workflow with no owner for the conversion layer, the kind of shared data contract that workflow automation systems are built around. If you want a second pair of eyes on which hop should own the conversion, tell us what the notes look like and where they were read from.
Frequently Asked Questions
SOURCES & CITATIONS
- Rich Text Area Field Considerations — Salesforcehttps://help.salesforce.com/s/articleView?id=platform.fields_rich_text_area_limitations.htm&language=en_US&type=5
- Formatting message text — Slackhttps://docs.slack.dev/messaging/formatting-message-text
- Change your Zap data to HTML, Markdown, ASCII, or plain text — Zapierhttps://help.zapier.com/hc/en-us/articles/8496308580365-Change-your-Zap-data-to-HTML-Markdown-ASCII-or-plain-text
- Prompting best practices: Control the format of responses — Anthropichttps://platform.claude.com/docs/en/build-with-claude/prompt-engineering/claude-prompting-best-practices
About Alexey Yushkin
Alexey is the founder of GENERAL INFORMATICS LLC. He designs and ships AI and automation systems for businesses and operators across the US.
Related reading
Want this kind of system in your business?
We build practical AI and automation systems for operators. Send us your current workflow and we will show you what to automate first.
