Skip to content

Product design, leadership and the Harness Design series

Harness Design seriesArticle no. 05

The first result: how we defined token governance for the Harness Design System

John Morais, .

Two grids of tiles, blue on the left and orange on the right, linked tile to tile by thin threads, with one loose orange tile, no thread, in the bottom corner

In those three days (the start of the project's two weeks of dedicated focus, which we kicked off on a Wednesday) we defined a governance process and a way to create, edit, and update tokens, both in Figma and in code.

We made a point of settling governance right away, because the goal was never just to make it work once. It was to deliver a scalable solution that other people on the team can maintain afterward.

Day one: getting everyone on the same page

The kickoff was about alignment. We talked about the current state of the design system and the next steps, and we put designers and developers on the same page before any line of code. And we already started the week's central experiment: applying harness documentation to the tokens using Claude Code.

In parallel, the development team started studying how to have a GitHub repository as the source of truth for the tokens: a single place, updated whenever needed, that React and Flutter will consume from.

And we built an important POC: the old-to-new mapping for the token change. The review we did touched a lot of things, so we created a map that shows each current token in the system and what its new equivalent will be. The idea is simple: with that map, the AI can update the tokens in code much faster than we could by hand.

The bind: the problem with no clean solution

Before the problem, I need to explain a term I'll use a lot: bind. Doing the bind means "tying" a property of an element to a token (a Design System variable) instead of leaving a loose value typed by hand. In our case, doing (or fixing) the bind means making sure every color, border, and corner radius of a button is tied to the right token: not to a hand-typed value, not to the wrong token, and not pointing to a variable from another library. That is what keeps the component consistent and easy to maintain, and, down the road, it is what reduces time and cost.

And it was exactly at this stage that an issue came up that took a good chunk of time: we needed to bind the components to the new tokens in Figma. It sounds simple. It wasn't.

First I tested Claude Code via MCP. It didn't work out. The next solution was to ask Claude to generate scripts and use the Scripter plugin to run the bind. That didn't work well either. On the second day I got an update from Figma Agents and, after iterating, I arrived at a bind prompt that worked better. Not perfect, but it genuinely helped.

The prompt ended up like this:

Faça o bind das variáveis semânticas nas propriedades visuais do componente selecionado, seguindo estas regras:

Corner radius → bind cada canto individualmente (topLeftRadius, topRightRadius, bottomLeftRadius, bottomRightRadius) à variável correspondente da coleção Radius (ex: valor 4 → sm, 8 → md, 12 → lg, 16 → xl). Aplica em todos os nós com cornerRadius > 0 sem binding: COMPONENTs, FRAMEs, INSTANCEs, RECTANGLEs.

Stroke weight → bind cada lado individualmente (strokeTopWeight, strokeBottomWeight, strokeLeftWeight, strokeRightWeight) à variável correspondente da coleção Stroke Width (ex: valor 0.5 → sm, 1 → md, 2 → lg, 4 → xl). Aplica em qualquer nó com strokes e sem strokeWeight binding. Para VECTORs de ícone, usa setBoundVariable('strokeWeight', ...) direto.

Cores de stroke de ícones → ícones que renderizam via stroke (VECTORs sem fill, apenas strokes) devem ter o stroke color bound a variável da coleção Colour Usage > Icon/*. Identifica a variável correta pelo contexto da variante (cor do botão/estado). Não tocar nos fills de INSTANCE que envolvem ícones — se o ícone é stroke-based, limpar qualquer fill residual da instância wrapper em vez de bindá-lo.

Importante: Antes de executar, escaneia todas as variantes do component set para mapear o que está sem bind. Divide as operações em scripts separados por tipo de propriedade para evitar timeout (corner radius primeiro, depois stroke weight nos components, depois nos VECTORs internos). Verifica o resultado exportando uma imagem e checando node.boundVariables (atenção: cornerRadius é armazenado como topLeftRadius etc., e strokeWeight como strokeTopWeight etc. — checar esses campos, não cornerRadius diretamente).

But even that prompt wasn't the final solution. It came later, and in a way that reinforces one of the things I defend most. We built our own Figma plugin, in a multidisciplinary way, designers and developers together. That collaboration is what got us to a real solution: the plugin delivered much better results, tailored exactly to what we needed, instead of depending on a generic script or on a prompt that sometimes gets it right and sometimes doesn't. And it didn't just solve the bind, it also started exporting the tokens the way we wanted.

This is the kind of thing that doesn't show up in the pretty slide. You try one path, it fails, you try another, it fails again, and what really unblocks you, in the end, is the team sitting down together and building the tool that was missing. It comes with the territory.

In that same period we also found structural problems in the semantics of the token names and fixed them. Token naming matters more than it seems when the reader is an LLM.

Day three: the schema, the moment that unblocked everything

The third day was the most important. It was when we stopped to structure how the harness would actually be built: where this information would live, how it would be updated, who touches it. And that is where we defined our Schema standard for the token description, and finally applied the Harness Design layer.

This was the example schema we created:

{
  "purpose": "Fill sólido de alta ênfase de marca — identidade de marca e ação primária. Passo Bold base: superfície protagonista (ação primária, item selecionado, badge sólido).",
  "useWhen": ["Ação primária ou elemento de máxima proeminência da marca."],
  "doNotUseWhen": [
    { "scenario": "Sinalizar status (sucesso, erro, atenção, processo)", "reason": "marca não comunica estado; usá-la para status gera leitura ambígua de severidade", "alternative": "Background|Text|Icon/Status/*" },
    { "scenario": "Conteúdo apenas informativo, que não é a ação principal", "alternative": "Status/Info/* (mesma cor, papel semântico correto)" }
  ],
  "pairsWith": { "text": "Text/Brand/Inverse", "icon": "Icon/Brand/Inverse" },
  "note": null
}

Creating this schema was only possible because of a strategy decision: at this first stage, we put the documentation directly in the description of each variable, inside Figma itself. And we validated something that was the missing piece: Claude Code can pull this information along with the variables JSON. In other words, both the tokens and the harness schema will live in Figma.

I want to say one thing: this structure is fully functional for our context, with the tools and the team we have. It is not the definitive architecture of the universe. It is the one that makes sense for us right now.

The flow we settled on

With the schema defined, the flow for applying harness to the tokens ended up like this:

  1. Define the schema first. Before anything else, the documentation contract that goes in the description field, using the W3C DTCG as a base (which already guarantees name, title, and value as static fields) and adding the harness properties on top. This schema is accessible to Claude in Prolog Claude, our repository with the Prolog information that Claude consumes, in the CLAUDE.md file.

  2. Initial load via Claude Code: the experiment we ran. With the schema ready, we asked Claude Code to enter Figma via MCP and analyze how each token is being used. From that context it gets what it needs to write the schema for each of the ~600 tokens. After writing the schemas, it goes back to Figma, also via MCP, and inserts each one into the description of the corresponding variable, instead of someone filling in 600 tokens by hand. We don't expect it to come out perfect the first time; the idea is to test how effective this path is and improve it afterward.

  3. Maintenance when a token changes. The designer creates or edits the variable in Figma and writes the description following the schema. A skill in Claude Code makes sure they don't forget to apply the schema, don't erase anything that already exists, and edit the right way.

  4. Export and commit, the truly manual part. A parser in Claude Code reads the descriptions via Figma MCP, breaks down the harness, downloads the current JSON from the repository, compares the diff, and commits to Prolog Claude. That JSON is what React and Flutter consume afterward.

Why manual, and not the "ideal" version

The obvious question is: why not automate everything? We did design more automated versions, including one with a tool of our own centralizing everything. But we reached a conclusion worth recording: updating a token is rare. It happens less than once a month, and it's occasional.

Building all the automation now wasn't worth it. Every automatic step opens the door to an edge case, the process breaks halfway, the AI edits wrong, someone forgets to export, and the cost of designing this properly, dealing with each of those failures, is high. For a task that happens once a month, the manual path is cheaper and more reliable.

This ties directly to what I wrote in "Before AI-first, human-first". The ideal solution on paper is beautiful. The real solution is the one the team can operate today, without drowning in exception cases. We chose the real one and spent a lot of discussion time in a single brainstorm to reach this seemingly simple choice. It was worth every minute.

Harness for a token is not harness for a component

One thing that became clear in this process: harness for a token is different from harness for a component. The normal case is that the LLM builds interfaces using the components that already exist; it doesn't keep creating new components all the time. The need for it to understand how to use a token shows up more in the scenario where it has to make decisions while creating something from scratch, without a ready-made component to reuse.

That scenario tends to get rarer as we apply harness to the components and expand the number of usable components. That is also why the token schema ended up smaller than I imagined: it doesn't need to cover everything, because most interface decisions will come from the components, not from loose tokens.

What's next: refinement and tests for harness on tokens

What we'll do next is test how the LLM is understanding the design tokens and validate the whole flow we created for updating tokens. After that, we finally get to the tests with components.