Skip to content

Render to Figma

Every other command in Specs reads Figma and produces a spec. render runs the pipeline in reverse: it takes a spec and creates or updates the matching component in a real, open Figma file.

render itself is a thin client. Almost everything that determines whether a render succeeds happens before you type it — the data you fetched, the lookup cache built from it, the bridge process, and the plugin connection. This guide walks the whole chain in order, with what each stage contributes, how to verify it before moving on, and what it looks like when it’s wrong.

The Chain

specs init config: sources, dataDirectory, outputDirectory
specs fetch Figma payloads → data/{alias}.file.json, .variables.json
↓ (and builds the cache as its last step)
specs cache lookup tables → data/cache/*.yaml
specs bridge start background server on localhost
Figma plugin Specs 2 open, CLI Bridge toggled on → connects to the bridge
specs render spec → bridge → plugin → component in Figma
specs generate --from-bridge: read back what actually landed
--from-bridge

The first three stages are about meaning — a spec names components, styles, tokens, and glyphs, and those names are resolved against your fetched library. The next two are about reach — a Node process cannot touch a Figma document, so the bridge relays to a plugin that can. The last is how you check the result, and it is always an explicit second call, never a side effect of render.

Stage 1 — Config

A render workspace is an ordinary Specs workspace. If you don’t have one:

Terminal window
specs init

Then fill in sources with your Figma file keys. Three config values matter to the render path:

SettingWhy render cares
sourcesEach declared source must be fetched and cached. render fails naming any that aren’t.
dataDirectoryWhere fetched payloads and cache/ live. --refresh-cache requires it.
outputDirectoryWhat specs render with no path argument renders as a batch.

See sources config for the full shape.

Stage 2 — Fetch the Library

Terminal window
export FIGMA_TOKEN=figd_...
specs fetch

This downloads the raw Figma payloads and builds the render cache as its final step, which is why a normal workflow never runs specs cache by hand.

Re-run fetch whenever the library moves underneath you — components added or renamed, styles or variables changed. A spec binds by name, so rendering against data that predates a rename silently binds to the wrong thing or drops content.

Verify the last lines of the output name your sources and non-zero entry counts:

Cache rebuilt: library
Entries: 3567 components, 97 styles, 1458 variables, 469 icons

See fetch for flags, and note that variables and styles data requires a Figma Enterprise plan — file and icons work on any plan.

Stage 3 — The Cache

The cache is the set of lookup tables that give a spec’s names meaning. Four files under {dataDirectory}/cache/:

FileResolvesWhat breaks without it
components.yamlinstance elements → published component keysInstances can’t be placed; content is missing
styles.yamlstyle names → style keysStyles aren’t applied
variables.yamltoken names → variable keysToken bindings fall back to raw values
icons.yamlglyph names → icon node idsGlyph elements can’t be placed

You run specs cache by hand only when the cache is missing, when something outside fetch changed your data directory, or when render tells you to:

Terminal window
specs cache # rebuild whatever is out of date
specs cache --force # rebuild every source from scratch

The bridge checks the cache against your data files before every render, and a mismatch is a hard failure rather than an automatic rebuild:

Error: Render failed: Render cache is not usable:
- variables.yaml: "library" is stale
Run `specs cache` to rebuild it.

Rebuilding on every render would reintroduce exactly the cost the cache exists to remove, and rendering against data that has moved on produces a failure that surfaces far from its cause. When you’d rather not run a separate command, render --refresh-cache rebuilds first and then renders.

Note that render reads nothing produced by scan. A scan manifest is generated and then hand-authored, which makes it a poor thing to resolve against; scan serves generate, not render.

Stage 4 — The Bridge

Making a spec real in Figma means placing frames, applying styles, wiring variants, and building instances inside a running Figma session — work that can only happen inside the plugin runtime, not in a Node process. The bridge is what connects the two.

Terminal window
specs bridge start
Bridge started (pid 12345).
WebSocket : ws://localhost:9001 (plugin)
HTTP : http://localhost:9002/render (control)
Logs : /Users/you/.specs/bridge.log
Enable the CLI Bridge in the Specs 2 plugin to connect.

It runs in the background until you stop it, so this is once per session, not once per render. specs render never starts it for you — if it isn’t running, the command fails immediately rather than hanging:

Error: bridge is not running.
Start it with: specs bridge start

Pin a workspace for the whole session with specs bridge start --workspace ~/design-system when every render targets the same one; otherwise it’s derived per request from each spec’s path.

Stage 5 — Connect the Plugin

Starting the bridge connects it to nothing by itself. In Figma:

  1. Open the target file — the one you want components rendered into.
  2. Run the Specs 2 plugin.
  3. Toggle CLI Bridge on. If the plugin was already open before you started the server, toggle it off and back on.
  4. Navigate to the page you want rendered onto, unless you plan to pass --page.

Verify from the terminal:

Terminal window
specs bridge status
Bridge running (pid 12345). 1 plugin connected:
abc123XYZ (Design System Library) — connected

bridge status exits non-zero when the bridge isn’t running, so it’s safe in a conditional (if specs bridge status; then ...).

More than one file can be connected at once. When two or more plugins are connected, render asks which file to use in an interactive terminal, and reports the ambiguity as an error in a non-interactive one rather than hanging on stdin. Scripted runs should always pass --file <fileKey> — the key is the first column of bridge status.

Stage 6 — Render

One component

Terminal window
specs render specs/deButton.yaml # a spec file
specs render specs/deButton/ # a component folder (api.* + variants.*)

A spinner holds the line while Figma works, then the outcome replaces it:

✓ Rendered: deButton (2.4s, nodeId: 1234:5678)

The node id is the component that was created or updated — open it directly at figma.com/file/.../?node-id=1234-5678.

A batch

Point render at a directory of component folders, or omit the path entirely to use outputDirectory:

Terminal window
specs render specs/ # every component beneath specs/
specs render specs/forms/ # one group
specs render # resolved from config
Found 6 components in specs:
- deButton
- deCard
...
✓ Rendered: deButton (2.4s, nodeId: 1234:5678)
✓ Rendered: deCard (3.1s, nodeId: 1234:5690)
...
Done: 6 rendered in Figma, 0 failed.

Batch scanning looks at most two levels deep and never descends into a component folder. Components render one at a time, in path order — order isn’t configurable, so when one component’s spec references another, render them individually in the order you need. A single failure doesn’t abort the sweep; the exit code reflects the total.

Reading the outcome

OutputMeans
✓ Rendered: name (…)The component was created or updated
⚠ INCOMPLETE: n instance element(s) …The component exists but is missing content — an instance couldn’t be resolved
✗ name: Render failed: …Nothing was produced for that component
⚠ n rendered with missing content: …Batch summary of the incomplete ones

An incomplete render is a success, not a failure — the component is in Figma, minus what couldn’t be placed. Missing instances almost always mean the cache doesn’t know the referenced component: re-run specs fetch. Pass --strict to turn incompleteness into a failure, which is what you want in CI where a silently partial component is worse than a red build.

Stage 7 — Verify What Landed

render reports success and a node id — nothing about fidelity. To read back what actually exists in Figma, select the rendered component and run generation through the same bridge:

Terminal window
specs generate --from-bridge -o specs/roundtrip/deButton.yaml
diff specs/deButton.yaml specs/roundtrip/deButton.yaml

That round trip is the real check. Because render is under active development, treat every render as something to look at in Figma too, rather than a guaranteed match to the source spec.

Stage 8 — Iterate

While working on one spec, let the watcher re-render on every save:

Terminal window
specs render specs/deButton/ --watch

--watch implies --overwrite, since each pass replaces the component the last one produced. It requires an explicit path, debounces changes (300ms), defers rather than overlaps when a render is in flight, and keeps going after a failed render — the next save may fix it. Ctrl+C to stop.

Without --watch, a title collision is an error; --overwrite deletes the existing same-titled component first. That’s destructive, so an interactive batch of more than one component asks for confirmation.

When a render is slow, --timing attributes the wall clock to bridge lookup work and each plugin write phase.

Stage 9 — Scripted Runs

Everything ambiguous in an interactive session must be pinned in a script:

Terminal window
specs render specs/ --file abc123XYZ --page 12:345 --strict
  • --file — never rely on the prompt; a non-interactive run errors instead of asking.
  • --page — without it the target is whatever page the user has open, which can drift mid-run if someone navigates in Figma.
  • --strict — fail on incomplete renders instead of shipping partial components.

Note that a render session still needs a human-driven Figma app open with the plugin connected, so render is not a fit for headless CI.

Stage 10 — Tear Down

Terminal window
specs bridge stop

Graceful first, forced if it doesn’t exit within a few seconds. Safe to run when nothing is running.

Troubleshooting

SymptomStageFix
Error: bridge is not running.4specs bridge start, then retry
Bridge running … No plugin connected.5Enable CLI Bridge in the Specs 2 plugin; toggle off/on if it was already open
Prompted to choose a file, or an ambiguity error5Two plugins are connected — pass --file <fileKey> from bridge status
Nothing happens / times out5specs bridge status to confirm the connection; check you’re on the intended page; tail -f ~/.specs/bridge.log
Render cache is not usable3specs cache, or render with --refresh-cache
Not fetched, skipped: <alias> then a render failure2render needs every declared source fetched — run specs fetch
⚠ INCOMPLETE: n instance element(s)2–3The cache can’t resolve a referenced component — specs fetch to refresh it
Missing glyphs, styles, or variable bindings2–3specs fetch, then render again
Rendered onto the wrong page5Pass --page <id>; without it the target is the plugin’s current page
Error: provide a spec path.6No path given and no outputDirectory in config to fall back to
no component folders found6A component folder holds api.(yaml|json) and variants.(yaml|json), at most two levels deep

Limits

  • One render at a time. The bridge holds a single render slot; concurrent calls queue.
  • No automatic round trip. Reading a rendered component back is always an explicit generate --from-bridge.
  • Render order isn’t configurable. Batches go in path order.
  • This is a young feature. Expect rough edges on more elaborate components.

Further Reading

  • render command reference — every flag, argument shape, and exit code
  • bridge command reference — start/stop/status, logs, pid file
  • cache — what the lookup tables hold, and when they go stale
  • fetch — downloads the library data and builds the cache
  • generate — produces the specs that render consumes, and reads them back with --from-bridge
  • sources config — how workspace aliases and data files are resolved