Claude API conversation history breaks after 2-3 messages - JSON escaping issue


I’m building a chat interface that connects to Claude’s API via API Connector. The chat works for 2-3 messages, then breaks. Claude loses all conversation history and starts fresh.

The Problem: After 2-3 exchanges, I get either:

  • “messages.X: all messages must have non-empty content” error

  • Blank screen with no response

  • Claude repeats its opening greeting (forgets entire conversation)

My Setup:

  • Storing conversation in a custom state (list of texts) on the page

  • Each message formatted as: {“role”: “user”, “content”: “message text”}

  • Passing to API as: chat’s Conversation join with “,”

  • Using :formatted as JSON-safe on content field

What I’ve Tried (all failed):

  1. Format as text with JSON-safe

  2. Manual find & replace for special characters

  3. Toolbox JavaScript for escaping

  4. Different quote/escape combinations

  5. Storing in database instead of custom state

Root Cause (I believe): Claude’s responses contain newlines, quotes, and special characters. Bubble’s text manipulation can’t properly escape these for valid JSON, especially inside :format as text.

What I Need: Multi-turn conversation with system prompt (I’m building a guided interview that asks ~10 questions). Need Claude to remember the full conversation history.

Questions:

  1. Has anyone successfully implemented multi-turn Claude chat with conversation history in Bubble?

  2. Is there a plugin that properly handles the messages array with JSON escaping?

  3. Any workarounds for the :format as text newline issue?

Thanks for any help!

Thank you! I’ll check out this plugin. Does it handle multi-turn conversation history automatically?

Hi @gregoryjameslessard

The easiest way to debug this is to copy your API call in the workflow, and prepend a link to a https://webhook.site / request catcher URL to the API call URL (you can do this by making the URL an optional parameter in the API Call).

That will forward your raw request to the request catcher, so you can inspect the payload. It will tell you what part of your JSON is invalid and hence why it’s not working.

Additionally, I’d recommend storing messages as a data type rather than a list of texts. It gives you metadata like when the message was sent, which role etc and will give you more flexibility in the long time. Your current solution won’t solve page refreshes, for example.

You can then provide your messages as Do a search for Messages where Conversation = (The Conversation):format as text.

Thanks for this — really helpful perspective.

The webhook.site debugging approach makes sense. I’d like to see exactly what’s breaking in the payload rather than guessing.

Quick context: I’m storing conversation history in a custom state (list of texts) formatted as JSON for the Claude API. Works fine for the first message, but breaks on the second — my guess has been that Claude’s response contains line breaks or special characters that corrupt the JSON when I try to append it back to the list.

A few questions if you don’t mind:

  1. For the webhook.site approach — do I just prepend the webhook URL to my actual API endpoint URL? So it would be something like https://webhook.site/my-unique-id/https://api.anthropic.com/v1/messages?

  2. On storing messages as a Data Type — would that be something like a “Messages” table with fields for role (user/assistant), content (text), timestamp, and a link to a parent Conversation? Then the :format as text would build the JSON array from those records?

  3. Does :format as text handle escaping automatically, or would I still need to deal with line breaks in Claude’s responses somehow?

Appreciate the guidance — I’m relatively new to Bubble and trying to get this working for a Valentine’s Day launch.

Yeah I mean this example is for OpenAI schema but principle applies:

If webhookURL is empty, it’s ignored.

Yeah. You never need to store JSON inline. Some here on the forum will beg you to though :upside_down_face:

Your message will be something like:

{"role": This Message's Role's id:formatted as JSON safe, "content": This Message's content:formatted as JSON safe} with a delimiter of a comma ,.

This is guaranteed to be always valid JSON and will output:

{"role": "user", "content": "Tell me a joke"}, {"role": "assistant", "content": "No."}

lmfao

1 Like

This is exactly what I needed — thank you!

So to confirm my understanding: I create a Messages data type with role and content fields, then when building the API call I use:

Do a search for Messages (where Conversation = current conversation):format as text

With each item formatted as:

{"role": This Message's Role:formatted as JSON safe, "content": This Message's content:formatted as JSON safe}

And delimiter set to ,

That :formatted as JSON safe is the key piece I was missing — I was trying to manually escape everything and it kept breaking.

Going to implement this today. Really appreciate you taking the time to explain it clearly. Marking as solution.

Yes.

Role should be an option set. You’ll have System, Assistant, and User probably. Their IDs can be system, assistant, and user respectively (essentially what you’re passing to the AI). Could you just lowercase the display and use that? Possibly, but the display is a display intended to be readable, not the ID. So create an ID attribute and use that :slight_smile:

Good luck!

Perfect — option set for roles makes total sense. I’ll set it up as:

Option Set: MessageRole

  • System (ID: system)

  • Assistant (ID: assistant)

  • User (ID: user)

Then reference the ID attribute in the :formatted as JSON safe call instead of the display.

This has been incredibly helpful — you’ve saved me hours of debugging and maybe a plugin I was about to buy. Marked as solution.

Thanks again! :folded_hands:

On a tangentially related note, you might consider using https://openrouter.ai instead of Anthropic directly, because you can use any provider’s models with one API call.

1 Like

Of course