I counted every change an AI agent made to a real Webflow site. There was no middle ground.

aiwebflowmcpdesign-systems

There’s an app I look after called TomoToki. It’s a shared calendar — you make plans with friends, chat inside each event, see who’s free before picking a time. It’s been sitting untouched for seven months, and its marketing site needed a proper pass: the design tidied up, the SEO written, the whole thing brought back to life.

That’s a real job with a real scope. So instead of building something impressive with an AI agent and filming it, I used the job to answer a question I actually wanted answered:

How much of this can an agent do on its own, and where exactly does it stop?

What I was actually using

Webflow has something called an MCP — a connection that lets an AI assistant talk directly to your Webflow project. Not “generate some code you paste in”. The agent reads your real pages, your real classes, your real CMS, and it can change them.

I connected Claude to it, pointed it at TomoToki, and got to work.

The difference from the usual AI-and-Webflow story matters: nothing here is a suggestion I then implement by hand. When I say the agent created a variable, it created a variable, in the project, and I went and checked.

How I decided to count, before I started

This is the part that makes the rest mean anything, so it goes first.

I broke the work into units. One unit is one small, checkable change:

  • one variable created
  • one property set on one class
  • one field added to a CMS collection
  • one page’s SEO title written

Not “the header” and not “the design system”. Those are containers — and if you count containers, you hide exactly the thing you’re trying to measure. “The agent built the header” tells you nothing about whether it fought you for an hour doing it.

Then I gave myself five possible outcomes for each unit, and I fixed them before I opened the project:

  1. Clean first try — the agent got it right on the first attempt, no help.
  2. Right after re-asking — I had to rephrase or retry, but it got there.
  3. Half-done — the agent did part of it and I finished it by hand.
  4. Done by me — the agent couldn’t, so I did it manually.
  5. Blocked — nobody could; it wasn’t possible.

Two rules I wrote down at the same time, because both are very easy to bend once you can see how the results are going:

Blocked units stay in the total. Quietly dropping the things that didn’t work is the easiest way to end up with a nicer number.

I write the failures before I decide what to publish. A number without its failures is a marketing claim in a lab coat.

And I published nothing during the run. The live site was never touched, and the whole project went back to a backup when I finished.

The result

106 units. Here’s how they landed.

What I was doingClean first tryBlockedTotal
CMS — a collection, 5 typed fields, 10 items16016
Structured data — JSON-LD on 6 pages606
Custom code — one animation, built two different ways9110
Pointing existing styles at design tokens9110
Renaming classes11213
Building a Material Design token set25429
SEO and social metadata on 6 pages16622
Total9214106

92 out of 106 on the first try. 87%, if you want the percentage.

Now here’s why that percentage is close to useless on its own.

Nothing landed in between

Remember those five possible outcomes? I only ever used two of them.

Outcomes 2, 3 and 4 — right after re-asking, half-done, done by me — are empty. Not rare. Empty. Across 106 changes, nothing needed a second attempt, nothing came back partly finished, and I never once had to step in and complete something the agent had started.

Every single unit was either outcome 1 or outcome 5. Clean, or a wall.

That’s the opposite of how these tools get talked about. “The AI does most of the work” makes you picture a gradient — it gets you 80% there and you polish the last bit. I didn’t find a gradient. I found a cliff.

And that’s genuinely useful, because it changes what you have to worry about. You don’t need to review whether the agent sort of got it right. You need to know, before you start, whether the thing you’re asking for is on the possible side of the cliff or the impossible side.

Which brings me to the pattern in that table.

Great at content. Unreliable at design systems.

Look at where the zeros are.

Everything that’s content-shaped went through cleanly, and fast. I wrote SEO titles and descriptions for six pages across three languages in a single request. I created a CMS collection, added five different field types, and filled ten complete entries in three requests. Structured data on six pages: one request, no complaints.

Everything that failed was design-system work.

If you’re wondering where an agent earns a place in your workflow today, that’s the line. It isn’t a percentage. It’s a shape — and knowing the shape is worth more than knowing the number.


A short detour: what I was trying to build, and why

The next few failures won’t make sense without this, so let me back up.

A design system, in the sense I mean here, is a set of named values that everything else refers to. Instead of typing #2168ca into forty different places, you define it once as “primary colour” and point forty things at that name. Change the name’s value, and all forty update.

In Webflow these named values are called variables. In the wider design world they’re often called tokens. Same idea.

Material Design 3 is Google’s open design system, and it’s the one I use as a starting point. It’s a good default because it isn’t just a colour palette — it defines roles. Not “blue”, but “the colour of a primary button” and “the colour text must be when it sits on top of that button”. That distinction is what makes a design system survive contact with a real project, and it’s why I wanted it here rather than a hand-rolled set of colours.

It also specifies two things that matter for what follows:

  • A light and a dark mode, as a first-class part of the system rather than an afterthought.
  • A fluid type scale. Text sizes that grow smoothly with the screen instead of jumping at fixed breakpoints. In CSS this is normally written with a function called clamp() — you give it a minimum, a preferred size that scales, and a maximum, and the browser works out the rest.

So: build the token set, add the modes, make the type fluid, then point the site’s existing classes at it. That was the plan.

The agent did most of it. One specific thing stopped it dead.


What broke, and how I know it wasn’t just me

Four failures. For each one I did the same thing: instead of reporting it from a single call that went wrong, I tried a version that should definitely have worked. If that succeeds and the original still fails, you’ve found the real edge. If it fails too, your first diagnosis was wrong.

That’s what I mean below when I say I checked it against something simpler.

1. The fluid type scale couldn’t be built at all

First, what is not broken: the agent created 24 variables on the first attempt. Twelve colour roles, eight spacing sizes, two text sizes, two font families — plus a second mode for dark theming. Building a token set works.

Every one of those used a plain, fixed value. 16px. #2168ca. Straightforward.

The failure is in one specific place. When you create a variable through the MCP, you can supply its value in two different ways:

  • A fixed value — you hand over a number and a unit, like 16 and px, in separate fields.
  • A custom expression — you hand over a piece of CSS as text, for anything that isn’t a plain number. This is the field you need for clamp(), calc(), and anything else with a function in it. It’s the only way in.

The fixed-value route worked perfectly, 24 times. The custom-expression route failed every single time, with the same unhelpful message: “An internal error occurred.”

And because a Material Design type scale is fluid by definition, that one field blocked the entire scale.

Here’s what I tried, all through that same custom-expression field:

What I put in the expression fieldResult
clamp(2.25rem, 5vw, 3.5625rem)internal error
exactly the same thing againinternal error
calc(1rem + 1vw) — a simpler functioninternal error
2rem — not a function at all, just a plain sizeinternal error
color-mix(in srgb, red 50%, blue)the example from Webflow’s own documentationinternal error

And, for comparison, the fixed-value route on the very same variables: worked every time.

The 2rem line is the one that settles it, and it’s worth being precise about why. 2rem and a fixed value of 2 + rem describe the identical size. The only difference is which field I put it in. Through the fixed-value fields: fine. Through the expression field: internal error.

So it isn’t that Webflow rejects clamp(). It isn’t that the variable is the wrong type for the value. The expression field itself doesn’t work — it fails on the simplest possible input, and on the example in Webflow’s own docs.

One more thing, because it changes what you can actually do about it: clamp() writes perfectly well as a normal CSS property on a class. It only fails inside a variable. So you can have fluid sizes in your classes, but you can’t have them in your tokens — which quietly breaks the one-place-to-change-it idea that a token system exists for.

2. A script you can create, then can’t use, then can’t delete

Webflow gives you two ways to add your own JavaScript to a page, and they’re very different in spirit.

The tidy one is a registered script. You upload your code once, it gets a name and a version number, Webflow hosts it, and then you attach it to whichever pages you want. Change the code once, every page gets the update. It’s the approach you’d choose if you were building something properly.

The blunt one is the custom code box in page settings. You paste your script tag in and that’s it. No versioning, no reuse — just code in a box.

I used the tidy one, because that’s what a careful integration should prefer.

Uploading the script worked. Everything after that failed:

  • Attaching it to a page — failed.
  • Attaching it to the whole site — failed.
  • Listing which scripts are attached — failed.
  • Detaching it — failed.
  • Deleting it — failed.
  • Restoring the entire project from a backup — it was still there.

Meanwhile, pasting the identical code into the blunt custom-code box worked on the first try, and undoing it was a matter of clearing the box.

So the careful, structured, reusable mechanism left me with something I can’t use and can’t remove, while the crude one behaved perfectly. Out of 106 changes, the only irreversible thing I did was the one that looked safest. That script is still sitting in the project. I couldn’t find a way to remove it in the API or in Webflow’s documentation.

3. Six classes I couldn’t touch, and it took three attempts to work out why

This one started as a small annoyance and turned into my favourite finding.

I was pointing existing classes at the new design tokens — telling the class called H Paragraph Header to take its text colour from the token rather than a hard-coded hex. Nine of them worked immediately. One came back with “style not found”.

Which was strange, because I hadn’t typed that name. I’d copied it straight out of the project moments earlier.

First guess: maybe there are two classes with that name. I searched, and there were — one ordinary class and one combined class, both called H Paragraph Header. That would explain it: the agent asks for a name, two things answer, nothing gets picked.

Good theory. So I tested it properly, by finding a class with a trailing space in its name that is completely unique — only one match, nothing to confuse it with. If my theory was right, that one should work.

It failed identically. So it wasn’t the duplicate.

What was left was the thing I’d been looking straight past. Every one of these class names ends in a space. H Paragraph Header — with an invisible character on the end. Searching finds it, because searching matches loosely. Renaming and updating both need the name exactly, and something in that path trims the space before comparing, so the name never matches itself.

Six of the 334 classes in this project have that problem. One of them is called F2 — the entire name is a two-character prefix and a space.

And here’s why it’s my favourite: a stray space on the end of a class name is precisely the kind of mess you’d rename the class to clean up. The classes that most need tidying are the exact ones the tidying tool can’t reach.

4. The error messages don’t agree with each other

Three real messages, from the same integration, in the same afternoon:

  • “Cannot have duplicate style names.” Perfect. Tells me exactly what’s wrong and what to do.
  • “Style not found.” For a style that demonstrably exists. Worse than useless — it sent me hunting for a missing class instead of an invisible space.
  • “An internal error occurred.” No field, no reason, nothing.

You recover from the first. You get misdirected by the second. On the third you can only guess — which is why diagnosing the variable problem took me five throwaway attempts. The message gave me nothing to aim at.


Three things the agent simply can’t see

These aren’t bugs. They’re absences, and together they answer a question worth asking before you let an agent near a live site unsupervised.

It can’t reach the safety net. The agent has no way to make a backup, no way to see whether one exists, and no way to roll anything back. Every change it makes is permanent from where it’s standing. I protected this whole experiment with a backup — which I made by hand, in the Webflow interface, because that’s the only way it can be done. The safety net exists. The agent just can’t touch it.

It can’t see what your plan allows. It can build an entire CMS structure and only discover, at the moment you try to publish, that your plan doesn’t include it.

It can’t see what it just did. This one is the least obvious and the most important. The agent reads the instructions — this class has this property at this screen size — but it never sees the rendered page. It can recite every style rule in your project and have no idea what a visitor actually looks at.

Which is exactly how I got something wrong.


The two things I got wrong

Both of these are mine, not Webflow’s, and I’m including them because leaving them out would make the rest less trustworthy, not more.

I reported a limitation that didn’t exist

Partway through, I needed to read the contents of an embedded code block — one of those boxes where someone has pasted a script into the page.

I tried the obvious route: ask for the page’s elements. It told me the block existed but not what was in it. I tried reading its attributes: empty. I tried the page’s custom-code settings: also empty, because that’s a different thing entirely. I tried the tool that builds HTML: it only writes, it doesn’t read.

Four routes, all exhausted. So I wrote it down as a real limitation: the agent cannot read embedded code. I had more evidence for that conclusion than for most of my correct ones.

It was wrong.

There’s a tool in the toolkit whose entire job is to hand you the guidance for using everything else. Its description says, twice, in capital letters, to call it before you do anything else. Nothing in the system surfaces it. I found it by listing every available tool, very late in the process, out of thoroughness rather than instruction.

I read it. It says, plainly, which tool reads the contents of an embedded code block. I tried that tool. It returned the script immediately, in full.

Being thorough in the wrong direction looks exactly like being thorough. Four exhausted routes felt like proof. All four were the wrong door, and I never checked whether there was a fifth.

I said something was visible when nobody could see it

While going through the site I found leftover blocks from an old template — including a button, in Russian, that had nothing to do with this app.

I looked up how those blocks were styled at each screen size, worked through which rules would apply where, and concluded the button showed up on phones. I wrote that down as a finding.

Then someone asked me a simple question: are you sure? I can’t see it on my phone.

So I opened the site on a phone-sized screen and looked. The button doesn’t appear at any width. It’s completely invisible. My reasoning had missed how these rules cascade downward — one screen size inherits from another unless it explicitly overrides it, and I’d read a rule as replacing something when it only added to it.

The style information I had was correct. My conclusion from it wasn’t, and one glance at the actual page would have caught it.

What both have in common

I reasoned over data instead of looking at the result.

No tool caught either mistake. What caught them was writing everything down as it happened, so the reasoning was still visible days later — and someone asking a plain question about a claim I’d made too confidently.


So: should you let an agent do this work?

Partly, and the useful answer isn’t a percentage. It’s about which side of that cliff your task falls on.

If your work is content-shaped — building CMS structures, filling them, writing page metadata and structured data — an agent earns its place today, and the speed is not a marginal gain. Six pages of metadata in one request is a different kind of afternoon.

If your work is design-system-shaped, expect walls, and expect them somewhere you didn’t predict. I could build a full token set but not make the type fluid. I could rename most classes but not the ones that most needed it. Nothing about that was guessable in advance — I found it by trying and counting.

And whatever you’re doing, don’t leave it running unsupervised. Not because it’s careless — it was startlingly precise, 92 times out of 106 with no second attempts — but because of those three blind spots. It cannot verify its own backup, it cannot see what it’s allowed to do, and it cannot look at what it just built. Precision without any of those is a good reason to stay in the room.

The thing I keep coming back to is the cliff. We’ve all absorbed a story where AI gets you most of the way and you finish the job. On a real project, over 106 changes, that middle ground never appeared once. It either worked completely or it didn’t work at all — and if that holds beyond this one project, it changes what you should be checking. Stop reviewing whether the output is roughly right. Start working out, before you begin, which side you’re on.

One last thing about how this is written. Every claim I’ve made here has its evidence sitting next to it — the counts, the exact inputs I sent and the exact errors that came back, the theory I got wrong about duplicate class names and the test that killed it, both retractions. None of it is behind a link to a document I promise to publish later.

That’s on purpose. If a claim like “eight failures across four different attempts” is worth making, the thing that backs it up should be in the same place as the claim — not somewhere I might get round to.