Published OnApril 17, 2026April 16, 2026

We Built a Browser Robot Because Webflow's API Can't Handle a Code Block

We Built a Browser Robot Because Webflow's API Can't Handle a Code Block

We introduced a new brand identity in 2025 alongside Ditto.com and with that came hosting the site on Webflow. Previously, we ran our own site infrastructure and this lead to brittle code management by a shadow engineering team. Standardizing on Webflow seemed like a reasonable choice and we didn't have complex needs.

Fast forward to 2026 and I wanted to author some content myself. As a result, I immediately started developing a fully automated pipeline - create the posts in markdown, then use an AI-powered publishing pipeline to push content from local files to the CMS via API. It works great for paragraphs, headings, lists, and links. But when I tried to publish an article with code blocks, the API returned empty strings. When I tried tables, they simply don't exist in the API!? These CMS API limitations aren't unique to Webflow, but they aren't ok in today's agentic world.

So to work around these gaps, I figured if I can't use an API, let me just create one in the wonkiest way possible. I built a Playwright agent that connects to Chrome, navigates the Webflow Designer, clicks tiny "+" buttons in the Rich Text editor, and types code into modal dialogs. A robot clicking buttons because the API wouldn't let us type!

This post is about that experience. But it's also about something bigger: in the age of AI agents, your API isn't a developer convenience. It's your product.

More Background

I created a content library in an Obsidian vault: markdown files with YAML frontmatter, organized by topic (a topic for another post as this was inspired by Andrej Karpathy's LLM-Wiki). When a post is ready, a Claude Code skill converts the markdown to HTML, maps fields to Webflow's CMS schema, and pushes a draft via the Webflow Data API.

This works for most content, but our engineering tutorials have Swift code blocks and comparison tables. Here's what Webflow's own documentation says about code blocks:

"The API doesn't currently support code blocks in Rich Text fields. Passing code blocks will result in an empty string."

Tables are worse - the <table> element isn't in the supported tag list at all! You have to create tables via custom code embeds. The community has been requesting this since 2016. The wishlist item for tables has 76 votes and has been in "Reviewed" status for four years.

For a CMS used by thousands of developer-facing companies to publish technical content, this is... not great.

The Workaround

To "solve" this, I built an agentic pipeline. Here's what it actually does:

  1. Push the article via Webflow's API with placeholder text where code blocks and tables should go ("Placeholder for code block zero here").
  2. Connect to the user's Chrome via CDP (Chrome DevTools Protocol) on a debug port.
  3. Navigate the Webflow Designer CMS interface: find the Blog collection, click on the draft article, scroll to the Rich Text field.
  4. For each placeholder, click on the paragraph, press Enter to create an empty line, hover the left margin to trigger the native "+" add button, and select "Code block" or "HTML embed" from the menu.
  5. Type the code content into Webflow's CodeMirror editor. Click "Save & Close."
  6. Clean up the placeholder text.

Here's a simplified version of the core insertion logic that inserted this code block into this article:

// Connect to the user's real Chrome session via CDP
const browser = await chromium.connectOverCDP('http://127.0.0.1:9222');
const page = browser.contexts()[0].pages().find(p => p.url().includes('webflow'));

// Find the placeholder paragraph in the Rich Text editor
const richText = await page.$('div.bem-RichTextInput_Content[contenteditable="true"]');
const para = await richText.$('p:has-text("Placeholder for code block")');

// Click on it, press Enter to create an empty line
await para.click({ force: true });
await page.keyboard.press('End');
await page.keyboard.press('Enter');

// Hover the left margin to trigger the "+" add button
const btn = await page.$('[data-automation-id="content-editor-toolbar"]');
await page.mouse.click(btnBox.x + btnBox.width / 2, btnBox.y + btnBox.height / 2);

// Select "Code block" from the menu, type into CodeMirror, save
const item = await page.$('[data-automation-id="content-editor-add-menu-item-code-block"]');
await item.evaluate(el => el.click());

const editor = await page.$('.cm-content[contenteditable="true"]');
await page.mouse.click(editorBox.x + 50, editorBox.y + 10);
await page.keyboard.type(codeContent, { delay: 1 });

await page.locator('button:has-text("Save & Close")').click({ force: true });

It's Playwright automating a browser - my own robot author. It's brittle as it depends on CSS selectors and DOM positions that Webflow can change at any time. I also had to work around Cloudflare's bot detection by connecting to a real Chrome session. Finally, I discovered that Webflow's Rich Text editor mangles placeholder text with underscores (it strips certain character patterns), so I had to use natural English sentences as markers too.

The fact that it needs to exist at all is wild. This isn't what we think of "legacy" software but in the world of AI, it is acting like it.

The API Parity Gap Is a CMS-Wide Problem

This isn't just a Webflow problem. It's a pattern across SaaS: the GUI ships first, the API ships second, and the API never quite catches up. Features that work when a human clicks buttons don't work when a machine sends HTTP requests.

In 2020, that was an inconvenience. In 2026, it's failure and evidence of legacy software.

AI agents are now first-class consumers of SaaS products. Claude Code calls your API, GitHub Copilot agent mode opens pull requests, etc. These tools don't look at your UI. they read your API spec, your documentation, and your endpoint responses. When they hit a gap, they either fail or they route around it.

Crossplane put it well in their case for API-first infrastructure: "AI needs APIs, not UIs, and most platforms still aren't built that way." The agent hits a wall not because it lacks capability, but because the platform wasn't built for programmatic access.

Entire companies have been built to workaround this problem. Browserbase raised $40M at a $300M valuation in 2025, processing 50 million browser automation sessions for AI agents. Plaid's entire premise existed on solving this for the banking industry. Howecer, these are bandaids and the real fix is creating first class APIs.

Every SaaS Company is Now an Infrastructure Company

If your product has an API, you're infrastructure. AI agents consume your API the way developers consume cloud services. They evaluate your documentation, test your endpoints, and choose the tool that lets them complete the task with the fewest obstacles.

The Model Context Protocol (MCP) makes this concrete. Over 12,000 MCP servers already exist.

MCP is a standard that lets AI agents discover what your product can do and then do it. If your API has gaps, the MCP server inherits those gaps. If your API can't create a code block, neither can any AI agent that integrates with your product.

The question isn't whether to support AI agents, but rather it's whether your product will be usable by them or whether they'll need a browser robot to work around you.

What Local-First Gets Right

At Ditto, we think about this through the lens of local-first software. Local-first doesn't just mean your app works offline. It means the user controls their data and their workflow.

Our blog content lives in markdown files on a local machine. We can read it, edit it, grep it, version it with git, and push it to any CMS we choose. If Webflow's API has gaps, we can build workarounds. If we switch CMS(es) tomorrow, the content comes with us. The CMS is a distribution channel, not a prison.

This is the same philosophy behind Ditto's sync platform. Every device is a first-class participant in the mesh network. The data lives where the work happens. The server is optional, not required. When you build this way, a broken API is an annoyance, not a showstopper. Your system is resilient because you never gave up control of the source of truth.

The opposite model is writing content directly in a CMS editor, with no local copy, no version history outside the platform, no way to automate publishing except through whatever API the vendor decided to ship. When that API has gaps, you're stuck.

The Fix for CMS API Limitations is Straightforward

This isn't a hard problem. It's a prioritization problem.

API parity. If it works in the GUI, it should work in the API. Code blocks, tables, embeds, every element your visual editor supports should be creatable via API. This is the baseline.

Ship a CLI. A command-line interface forces you to think about your product as a programmable system, not just a visual one. webflow cms push --collection blog --file article.html should be a real command.

Build an MCP server. This is becoming table stakes. Credit where it's due: Webflow already has one. We used it to build our publishing pipeline. The MCP server wraps the Data API, which means AI agents can create CMS items, update fields, and publish pages. That's genuinely useful, but an MCP server can only expose what the underlying API supports. If the API can't create a code block, neither can the MCP. The foundation has to be complete (subtly too, MCP is less efficient than a CLI in terms of context usage, so ideally these are just higher abstractions: API --> CLI --> MCP).

Treat agents as first-class users. Your API documentation is your onboarding flow for AI. Your error messages are your support team. Your rate limits and authentication flows are your checkout process; design for these.

Writing this we have work to do at Ditto on this as well. Our portal lacks administrative APIs to automate certain tasks. We are going to fill those gaps soon!

So, yes, Webflow builds an excellent visual design tool. Offloading the site management to them is still helpful, but issues like this make that decision harder when you can also reach for Claude Code or other tools to self-manage your own infrastructure. We are contemplating this decision now as a result, however, moving the site is not the most valuable use of our team's time.

For now that means a goofy Playwright workaround, but hopefully they close the API parity gap!

Read more
Tutorial
April 13, 2026
Build a Multiplayer Emoji Game in SwiftUI with Peer-to-Peer Sync
by
Adam Fish
Build a multiplayer emoji game in SwiftUI that syncs over Bluetooth with no server. Step-by-step tutorial using Ditto's peer-to-peer SDK for iOS.
Stories
February 17, 2026
The State of React Native in 2026
by
Aaron LaBeau
New Architecture, React Compiler, and What Developers Need to Know