Hero image for Catching Design Token Drift in CI at the Figma-to-DTCG Boundary

Catching Design Token Drift in CI at the Figma-to-DTCG Boundary

The seam where drift actually happens

Design tokens pass through at least three representations on the way from a design decision to code: Figma’s internal Variables model, an export or bridge’s JSON, and the DTCG token file committed to a repository. These are related representations, not automatically identical ones.

The DTCG format report describes a familiar integration problem: teams carry the burden of writing and maintaining their own bespoke glue code and workflows to keep tools in sync. That glue is the seam where drift can enter. Figma’s native API exposes its internal representation; the cited zeroheight guide says it is not DTCG-compliant and has no transformation layer to platform outputs. In this guide’s framing, the export bridge or custom transformation is where that boundary gets made explicit.

The useful CI thesis is deliberately bounded: a CI job cannot prove that someone exported the latest Figma decisions, or that a design decision is right. It can deterministically check the artifact that was committed—its identity comparisons, alias references, names, types, and any resolver document’s structural invariants.

That distinction matters. A clean build can validate the checked-in file without certifying that the file reflects the current design.

What the Figma API hands you

Figma documents three Variables API methods: GET local variables, GET published variables, and POST variables. The GET endpoints return variable data; POST supports bulk creation, updates, and deletion.

For local variables, the response includes meta.variables and meta.variableCollections. A variable has a resolvedType and valuesByMode, with the latter keyed by mode ID. Crucially, the published endpoint omits modes. A check that needs to inspect per-mode values must use the local endpoint or an exported artifact that includes the relevant mode data.

Published-variable identity also affects diff quality. Figma documents id and key as stable over a variable’s lifetime, while subscribed_id changes each time the variable is modified and published. A collection’s updatedAt changes when any variable in that collection changes, so it signals activity but does not identify which member changed. A naïve diff on changing IDs or timestamps can therefore create noise instead of a useful comparison.

Direct API access has operational prerequisites, too. The GET endpoints are Tier 2 and require file_variables:read; POST is Tier 3 and requires file_variables:write. Figma states that the API is available to full members of Enterprise organizations. In this guide’s framing, a CI design that queries Figma directly inherits those access conditions, while a committed export lets validation run against the artifact rather than a live request.

The determinism test

A property qualifies as a CI check here only when it can be evaluated from a committed artifact or an API response. If a machine can inspect the input and apply a documented rule, the check can produce a reproducible pass or failure. If the question is whether an export reflects the latest design decision, whether the design decision is correct, or whether two systems mean the same thing, the evidence in the artifact is not enough. Those remain human or separately specified workflow concerns.

This test keeps a CI contract honest. It also separates “the file is internally valid” from “the file is the right design.” The rules below concern the first claim.

The computable rules

These rules describe checks that can be implemented around the Figma-to-DTCG boundary. They do not define an export transformation or assert that one exists.

R1 — Compare published identity using stable keys

Input: The current GET /v1/files/:file_key/variables/published response and a committed baseline.

Check: Index variables and collections by stable id or key. Treat changes to subscribed_id and updatedAt as non-failures by themselves. Compare additions, removals, and published values using stable identity. Since updatedAt on a collection changes when any member changes, do not use it to attribute a change to a particular variable.

Pass condition: No unexpected additions, removals, or value changes keyed by stable IDs. “Unexpected” needs a repository policy or human review; CI can report a delta, but cannot decide whether a design change was intended.

R2 — Check mode completeness using local data

Input: GET /v1/files/:file_key/variables/local, or committed bridge JSON that retains the mode data needed for the check.

Check: For each collection, verify that every modeId in a variable’s valuesByMode exists in that collection’s modes[]. Check the limits documented on the POST variables endpoint: no more than 40 modes per collection and no mode name longer than 40 characters. The limit statements come from the API documentation; this check does not assume they describe every constraint in Figma’s authoring interface.

Pass condition: No orphan mode IDs and no documented limit breaches. A published-only response cannot satisfy the mode-value part of this rule because it omits modes.

R3 — Validate the Figma alias graph

Input: The local Variables response, including VARIABLE_ALIAS and composed-color values.

Check: Resolve each alias’s target id to an existing variable. Reject self-aliases, and walk the graph to detect cycles. For composed colors, verify that the color and opacity fields include at least one variable alias, as required by Figma’s documented rule.

Pass condition: Every alias target resolves, there are no self-aliases or cycles, and composed-color values meet the alias-field requirement. This is a check of Figma’s representation; it does not make Figma alias IDs directly comparable to DTCG name-path references.

R4 — Enforce DTCG token-name and object rules

Input: Committed .tokens or .tokens.json files.

Check: Under the DTCG Format Module 2025.10, token and group names must not begin with $ and must not contain {, }, or .. Identify tokens as objects with $value; their name and value are required. Names are case-sensitive, so case-only collisions are not necessarily invalid, but a validator can report them as warnings.

Pass condition: No forbidden names and no token objects missing $value. Case-only collisions are reported as warnings, which the format report says tools may emit.

Figma’s API also restricts variable names, describing certain characters such as .{} as disallowed. The supplied documentation does not establish that Figma’s character ban is identical to DTCG’s. A cross-format validator should enforce each documented rule at its own boundary, not pretend the sets are equivalent.

R5 — Resolve $type without guessing

Input: DTCG token files.

Check: Apply the format report’s three-step rule: use the referenced token’s resolved type when the token value is a reference; otherwise use the nearest parent group with $type; otherwise mark the token invalid. Do not infer a type by inspecting the value’s contents. The report explicitly says tools must not guess.

Pass condition: Every token’s type can be determined by those rules; otherwise report it as invalid rather than repairing it.

The supplied material does not establish a mapping from Figma’s BOOLEAN, FLOAT, STRING, or COLOR resolvedType values to DTCG $type values. It also does not provide the full normative DTCG type vocabulary. A conversion table would therefore be an invented contract, not a sourced one.

R6 — Validate resolver documents when committed

Input: Any committed .resolver.json document.

Check: Validate that version is 2025.10 and resolutionOrder is present. If a modifier has a default, check that it matches a key in that modifier’s contexts. Resolve $ref references, reject circular references, and verify that aliases point to a token with the correct $type. The report provides a Resolver Module 2025.10 and a resolver JSON Schema.

Pass condition: All listed invariants hold. The Resolver Module describes multiple contexts and their resolution, but it does not specify how to translate Figma modes into resolver sets, modifiers, or contexts. We treat any such mapping as a proposed team convention, not a normative bridge.

Where the gate sits

The export step is still human-triggered in the workflow described by the zeroheight guide: a designer uses a plugin sync or someone makes a manual API call to get changes from Figma into the repository. A CI job that begins after a commit cannot detect an export that never happened.

A practical pattern is two-stage. A designer triggers an export that opens a pull request with token changes; a reviewer evaluates the design change; after merge, CI checks the committed artifact and runs downstream work. This keeps review in the loop for a design decision while automating the checks that can be performed on repository contents. It is a workflow pattern described by the guide, not evidence that every plugin independently opens pull requests.

Direct API checks add another boundary: permissions and availability. The Figma documentation identifies Enterprise membership, file access, and API scopes as prerequisites. It also distinguishes the read and write tiers. For a validation-only job, avoid assuming that access to a repository artifact implies permission to query or modify a Figma file.

The three export paths compared

The zeroheight guide titled “Exporting design tokens from Figma: formats, tools and workflows” is the source for the broad comparison below. Its canonical article URL is not established in the supplied material; related Learn pages include the design-token pipeline, using design tokens in a codebase, doing it in zeroheight, and setting up design tokens in zeroheight.

Export path Artifact location Format at the boundary Modes/themes data What CI sees as input
Figma native REST export Figma file; a custom workflow can commit an export Figma’s internal representation; aliases use internal variable IDs; not DTCG-compliant Published endpoint omits modes; local endpoint includes mode data API response or a committed custom-export artifact
Tokens Studio Git sync GitHub folder or single JSON file Default proprietary JSON; optional DTCG-compatible output Folder storage can include additional data files such as themes; single-file storage combines Token Sets Token JSON files in the repository
zeroheight token manager Token manager, with exports available via URL CSS, SCSS, JavaScript, JSON, Android, iOS, or Style Dictionary format Export is provided through the token-manager workflow; exact mode mapping is not established here Export payload available via URL, if used as a build input

Tokens Studio documents two-way GitHub sync: users can push JSON to GitHub and pull tokens from GitHub into Figma. Its Git Sync Provider documentation describes folder storage as one JSON file per Token Set with additional data files, or file storage as a single tokens.json. The zeroheight guide says the default Tokens Studio format is proprietary and that DTCG-compatible output is also supported; proprietary output needs pre-processing before DTCG-aware tools can use it. The exact proprietary schema and the settings that produce DTCG-compatible output are not established here.

The zeroheight guide describes a token manager that imports through its Figma plugin and exports through a URL. It says that route is less suited to teams wanting full pipeline control through a Git-native workflow. The table describes where an artifact may be available, not a guarantee that every route provides a repository commit or the same data.

What no CI check can verify

A repository check cannot prove that a designer performed an export after a Figma change. It can only evaluate the artifact it receives. This is why the human export step and review should remain visible in the workflow instead of being silently conflated with validation.

Nor can these rules certify design intent. They can report a changed value or an invalid alias graph; they cannot establish that the new value is the right choice. The supplied sources frame token changes as design decisions, not just code changes.

The relationship between Figma modes and the DTCG resolver model is also not established. Figma describes modes[] and valuesByMode; the resolver report describes sets, modifiers, contexts, defaults, and resolution order. A team may define a mapping, but that mapping is a proposed convention requiring its own documentation and tests.

Finally, none of the supplied sources guarantees a lossless round trip from Figma through a bridge and DTCG back to Figma. The evidence supports a more careful statement: native Figma output is not structured as DTCG-compliant token JSON, and a transformation layer is needed for that boundary. Which individual fields survive a particular route is not established.

DTCG’s status should also be clear in the CI contract: the 2025.10 Format Module is a Final Community Group Report, described as stable, but it is “not a W3C Standard” and is not on the W3C Standards Track. A conformance check is therefore checking against that report, not claiming compliance with a W3C Standard.

FAQ

Can CI catch every drift between Figma and code?

No. It can check committed data and, where permitted, API responses. It cannot prove that the latest Figma state was exported, or that a reviewed design decision is correct.

Why does my diff show every variable changed after each publish?

Check whether the comparison uses subscribed_id. Figma says that ID changes when a variable is modified and published, while id and key are stable over the variable’s lifetime. Prefer stable identity for comparisons; treat updatedAt as a timestamp, not a per-variable change explanation.

Can CI verify per-mode values like light and dark?

Yes, if its input contains the mode data. The published endpoint omits modes; use the local endpoint or a committed export that retains the necessary information. How Figma modes map to DTCG resolver contexts is not established.

Can CI infer a missing $type from the value?

No. The DTCG format report says tools must not guess from value contents. A token with no resolvable type should be reported as invalid.

Does Figma’s character ban match DTCG’s exactly?

That is not established. The Figma documentation says variable names cannot contain certain special characters “such as” .{}; DTCG specifies that token and group names must not contain {, }, or .. Do not treat those statements as proof of identical rules.

The Bottom Line

The useful promise of token CI is narrower—and more dependable—than “Figma and code are in sync.” A gate can validate stable identities, mode references, alias graphs, DTCG names and type resolution, and resolver-document invariants. It cannot establish that an export happened, that a design decision was correct, or that two different mode models have a normative correspondence.

Keep the artifact under review in the repository, make the human export step explicit, and let CI assert only what the documented input and rules allow. For the downstream DTCG-to-Style-Dictionary-to-CSS boundary, the sibling post, “Six DTCG-to-Style-Dictionary-to-CSS Rules,” covers that separate contract; its rules are not re-derived here.

How This Guide Was Built

This guide draws on six dossier sources: the Figma REST API Variables documentation, the DTCG Format Module 2025.10 report, the DTCG Resolver Module 2025.10 report, Tokens Studio’s GitHub-sync documentation, zeroheight’s “Exporting design tokens from Figma: formats, tools and workflows” guide and its internal Learn links cited above, and Style Dictionary. Related resources named in the dossier include the Tokens Studio GitHub-sync issue 2320 and the SD-Transforms README.

The author did not run Figma’s API, Tokens Studio, zeroheight, or Style Dictionary hands-on. No tool was run hands-on for this post; its claims are based on the dossier documents. The DTCG format excerpt supplied for this guide does not include the full type-definition sections, so this post does not enumerate a complete type vocabulary or invent a Figma-to-DTCG type mapping.