Session 1: Pipeline + Color Token Architecture
Series: What If a Design System Could Self QA?
Author: C. Maldonado
Date: February 2026
The Starting Point
Our design system was in that awkward middle stage — we had Figma variables, dev had implemented their own version of our color tokens, and the two didn't quite match. The variable names were borrowed from Material Design 3 (primary, secondary, tertiary, neutral, neutral variant) which made sense to the developer who set them up, but meant nothing to a designer picking colors in Figma. "What's the difference between content.base and content.elevated?" was a question that shouldn't require a Slack thread to answer.
I wanted three things:
A single source of truth for design tokens that both design and dev could trust
An automated pipeline so token changes flow from Figma to code without manual handoffs
Naming that communicates intent — not Material Design jargon
This is the story of building all three in one session, including every failure along the way.
Part 1: The Pipeline
Setting Up the Repo
The foundation is a GitHub repo (kwi-design-tokens) with a simple structure:
kwi-design-tokens/
├── .github/workflows/token-pipeline.yml
├── style-dictionary.config.js
├── package.json
├── tokens/
│ └── colors.json ← hand-written clean tokens
├── tokens.json ← auto-generated by Tokens Studio
└── build/ ← generated output (CSS, SCSS, JS, JSON)
The pipeline uses Style Dictionary (by Amazon) to transform token JSON files into platform-specific outputs — CSS custom properties, SCSS variables, JS modules, and a flat JSON file. A GitHub Action watches for changes and auto-builds.
Four Runs to Green
Getting the pipeline to actually work was a journey. Here's the real timeline:
Run 1 — Config not found. Style Dictionary v4 doesn't auto-detect style-dictionary.config.js. The CLI just says "unable to find config file" and dies. Fix: add --config ./style-dictionary.config.js explicitly to the build command. Lesson learned — don't assume tools find your config by convention.
Run 2 — 215 broken references. I'd added tokens.json (the Tokens Studio sync file) to the Style Dictionary source array. Bad idea. The Figma Variables were messy — references like fontFamily.Base that pointed to nothing. 215 broken references. Fix: exclude tokens.json from the build source for now, keep only the clean hand-written tokens. Added a TODO comment to re-add it once variables are cleaned up.
Run 3 — Build passed, push failed. The Style Dictionary build actually worked (green check on that step!) but the GitHub Actions bot couldn't push the generated files back to the repo. "Write access to repository not granted." Exit code 128. Fix: repo Settings → Actions → General → Workflow permissions → "Read and write permissions." A one-checkbox fix that took longer to find than to apply.
Run 4 — Green. Everything passed. The full loop worked: push triggers workflow → Style Dictionary builds → bot commits generated files back to the repo. 17 seconds total.
The takeaway: Pipeline debugging is mostly about permissions, paths, and config — not the actual transformation logic. Style Dictionary itself worked perfectly once it could find its inputs and the bot could write its outputs.
Connecting Tokens Studio
Tokens Studio is a Figma plugin that lets you manage design tokens and sync them to a Git repo. The connection itself was straightforward — authenticate with GitHub, point it at the repo and branch, done. The key decision was making Tokens Studio the single source of truth, not Figma's native Variables panel. Why?
Tokens Studio supports the full DTCG token format with references, descriptions, and extensions
It pushes directly to Git, which triggers the pipeline automatically
Figma's native Variables panel is great for consumption but awkward for authoring complex token structures
The workflow becomes: edit in Tokens Studio → push to GitHub → pipeline builds → dev consumes output files. No Slack messages, no manual exports, no "hey can you update the colors" tickets.
Part 2: The Naming Problem
With the pipeline working, I turned to the actual token names. Our system had two layers:
Primitives — the raw color palettes with 0-100 brightness scales:
primitive.primary → deep blues (brand color)
primitive.secondary → blue-grays
primitive.tertiary → pure grays
primitive.neutral → pure grays (identical to tertiary!)
primitive.neutral variant → warm grays/tans
primitive.error/success/warning/info → feedback colors
Semantics — intent-based tokens that reference primitives:
semantic.surface.base → {primitive.neutral.98} (page backgrounds)
semantic.surface.primary → {primitive.primary.60} (brand surfaces)
semantic.content.base → {primitive.neutral.10} (primary text)
semantic.content.elevated → {primitive.neutral.10} (same as base??)
semantic.content.dim → {primitive.neutral.10} (also same as base???)
See the problems?
Problem 1: "Primary" means two different things
At the primitive level, primary means "the brand color family." At the semantic level, surface.primary means "a brand-colored surface" — but it reads like "the most important surface" (which is actually surface.base). The word was doing double duty and confusing everyone.
Problem 2: Content tokens mirrored surface names for no reason
content.base, content.elevated, and content.dim were all mapped to the exact same color — neutral/10 (near-black). Dev had mirrored the surface naming thinking "content ON base surface, content ON elevated surface, content ON dim surface." But in light mode, dark text on any light surface is... the same dark text. Three tokens, one color, maximum confusion.
Problem 3: Tertiary and neutral were identical
Both primitive.tertiary and primitive.neutral were pure gray ramps with identical hex values at every stop. Two names for one palette.
Problem 4: M3 vocabulary isn't designer-friendly
"Neutral variant" is Material Design 3 terminology. A designer browsing the token panel shouldn't need to know M3 to understand that these are warm-toned grays.
Part 3: Intent-Based Naming
The fix was guided by one principle: names should communicate purpose, not implementation. And they should survive white-labeling — if we spin up a POS product with a different brand color, the token names should still work.
Primitive Renames
| Before | After | Why |
| primitive.primary | primitive.brand | "Brand" communicates role. If we white-label, brand maps to whatever the new color is. |
| primitive.secondary | primitive.accent | Supporting color family. "Accent" is universally understood. |
| primitive.tertiary | removed | Identical to neutral. One gray ramp is enough. |
| primitive.neutral | primitive.neutral | Already clear. No change. |
| primitive.neutral variant | primitive.neutral-warm | Communicates the warm undertone without being hue-specific. |
| primitive.error/success/warning/info | no change | Already intent-based. "Error" works whether it's red, orange, or purple. |
Semantic Renames
Surface (3 renames, no structural changes): surface.primary → surface.brand, surface.secondary → surface.accent, surface.tertiary → surface.muted
Content (restructured):
content.base→content.default(clearer — "the default text color")content.elevated→ deleted (identical to base, redundant)content.dim→ deleted (identical to base, redundant)content.primary→content.brand(brand-colored interactive text)content.secondaryandcontent.tertiary→ kept (these ARE hierarchy — 2nd and 3rd level text)
Border (1 rename): border.primary → border.brand (focus rings, active input borders)
The content hierarchy became a clean intensity ladder: default → secondary → tertiary → disabled. Any text or icon just picks its rung. No more guessing whether "elevated" is darker or lighter than "base."
State Tokens
We also cleaned up the state tokens and connected them to primitives:
| Token | Before | After |
| state.hover | hardcoded to primary/99 | {primitive.brand.99} |
| state.pressed | hardcoded #1A1A1A at 12% | #1A1A1A at 12% alpha (neutral/10 equivalent) |
| state.selected | hardcoded #004499 at 20% | #004499 at 20% alpha (brand/40 equivalent) |
| state.focus | hardcoded #004499 at 20% | #004499 at 20% alpha (brand/40 equivalent) |
The pressed/selected/focus tokens stayed hardcoded because Tokens Studio's color modifier (reference + alpha) requires a Pro license. Added TODO descriptions to swap these to primitive references when we upgrade.
Part 4: The POC Moment
After making all the renames in Tokens Studio, I pushed to GitHub. The pipeline fired. 15 seconds later: green check.
Run #5 — "updated color tokens based on - intent not implementation"
That commit message captured the whole philosophy in one line. The pipeline took the renamed tokens, Style Dictionary transformed them into CSS/SCSS/JS/JSON, and the bot committed the output files. Zero visual changes in the product. Zero broken builds. Just better names that everyone on the team can understand.
What I Learned
Start with the pipeline, not the tokens. Getting the automation working first — even with dummy tokens — meant I could iterate on naming with confidence. Every change gets validated by the build. If I'd spent weeks perfecting token names first, I'd still be manually exporting JSON files.
Debug in public. Four failed pipeline runs sounds bad. But each failure taught something specific: config detection, reference validation, file permissions. Documenting these failures (and their fixes) in the migration guide means the next person doesn't hit the same walls.
Naming is a design decision, not a dev decision. Material Design 3 naming conventions (primary, secondary, tertiary, neutral variant) are designed for Google's ecosystem. They're not wrong — they're just not ours. Renaming tokens to match how our team thinks about color (brand, accent, muted, warm-gray) reduces cognitive overhead for every designer and developer who touches the system.
Redundancy is the enemy of clarity. Three content tokens with the same value. Two primitive palettes with identical grays. Every duplicate is a decision someone has to make that shouldn't exist. Removing them made the system simpler and the remaining tokens more meaningful.
Intent survives rebranding. Hue doesn't. The temptation was to rename primary to blue. But "blue" breaks the moment you white-label. "Brand" works forever because it describes the role, not the color. This distinction matters for any design system that might serve more than one product.
Artifacts
KWI-Token-Migration-Guide.docx— full migration doc with rename tables, CSS variable mapping, risk assessment, and timeline for dev coordinationLight.tokens.updated.json— programmatically generated token file with all renames applied (used as reference; actual renames done manually in Tokens Studio)GitHub Actions pipeline — 5 runs total (4 debugging, 1 POC success)
Pipeline proven. Color tokens renamed and pushed. Migration guide ready for dev handoff.
Built with: Figma, Tokens Studio, GitHub Actions, Style Dictionary v4, Claude Code, and an unreasonable amount of patience for YAML indentation.