Skip to content

Push Vars

A Figma file holds at most one Systema theme at a time (see Single-theme-in-Figma policy). The push flow enforces this and survives structure changes, theme renames, and re-pushes.

Identity: how the plugin knows what's in the file

  • figma.root.getPluginData("systema_pushed") — a JSON record { themeId, themeName, pushedAt, schemaHash } written at the end of every successful push. This is the source of truth for "which theme currently lives in this file".
  • collection.getPluginData("systema_managed") === "true" — Systema's own collections. Anything in the file without this tag is foreign user content and is never touched.
  • collection.getPluginData("systema_theme") === {themeId} — secondary per-collection tag, written alongside systema_managed. Used as a fallback when systema_pushed is empty (legacy tester files).
  • variable.getPluginData("systema_tid") — stable semantic identity computed by the token engine (buildTid([...path-parts])). Lets the plugin track a variable across path renames and structure changes without creating a duplicate.
  • collection.getPluginData("systema_hash") — FNV-1a hash of all tokens in that collection (paths + per-mode values + each token's aliasOfTokenId). Including the alias state in the hash means flipping the Alias duplicate tokens setting actually triggers a re-push instead of being skipped — without that bit, raw-COLOR ↔ VARIABLE_ALIAS swaps left every collection on disk untouched because the per-mode values themselves were unchanged.

Step-by-step: what happens when you click Push Vars

  1. Save configfigma.root.setPluginData("systema_config", JSON.stringify(config)). Shared across all users of the file.
  2. Generate tokensgenerateAllTokensWithStats() produces the flat token array.
  3. Check for theme overwrite — read systema_pushed. If the stored themeId differs from the theme being pushed, open the Replace theme in Figma? dialog:
    • Same structure (schemaHash matches) → three outcomes: Cancel / Change values only / Replace (unbind). Change values only rewrites values in the existing variables so node bindings survive; Replace (unbind) sweeps every Systema collection up front and recreates from scratch (bindings become unbound).
    • Different structure or no recorded schemaHash (legacy tester case) → Cancel / Replace (unbind) only. There's no reliable way to reuse existing variables when paths might not match.
    • Cancel → backend posts push-complete { success: false } and the UI discards the optimistic push snapshot so Push Vars stays active.
  4. Legacy-prefix migration — collections named {prefix}/color/... or {prefix}/meta (produced by old builds) are renamed to flat color/... / meta. Variable ids survive, so existing node bindings keep resolving.
  5. Index variables — only collections tagged with systema_managed AND systema_theme === {activeThemeId} are indexed. Every other Systema collection's variables are skipped (OOM-safe on files that still carry legacy multi-theme collections). Foreign user variables are always skipped.
  6. Hash-skip collections — for each target collection, compare the computed hash against systema_hash on the existing collection. If equal, the whole collection is skipped with no Figma API writes at all.
  7. Sync modes — rename mode 0 to the first theme mode, add missing modes, remove stale modes the user deleted. Every theme mode has a stable id; the engine uses modeIdMap to translate.
  8. Apply tokens — for every token:
    • If a variable with the same path exists in the target collection → reuse.
    • Else if a variable in any managed collection carries the same systema_tid → rename in place.
    • Else create a new variable via figma.variables.createVariable.
    • Set scopes (derived from entityType via scopesForEntityType()), description, hiddenFromPublishing, and systema_tid plugin data. New variables skip the read-before-write diff for these properties to halve the proxy boundary traffic on large first pushes.
    • For each mode, set the value. Existing variables skip the write when the new value is within ε of the current one (fast diff). Duplicate-color tokens are written as VARIABLE_ALIAS references to the canonical variable instead of literal hex — totalAliased in the summary.
  9. Cross-collection rebind — when collectionStructure changes (e.g. balanced → granular), the Figma API forbids moving variables between collections, so the plugin creates the new variables and then walks file nodes rebinding VARIABLE_ALIAS references from old → new. Old variables are converted to legacy aliases (zero-value + hiddenFromPublishing) so externally-published library consumers continue to resolve until they can rebind themselves.
  10. Orphan resolution — variables that exist in a managed collection but aren't in the incoming token set. Pure-migration orphans (cross-collection tid matches) and orphans during a replace-theme flow are auto-removed without prompting. Other orphans open a single dialog asking to keep or remove.
  11. Stale collection cleanup — remove any managed collection that isn't in byCollection.keys(). This is also how foreign-theme collections get swept on the way out after the user picked Change values only.
  12. Write systema_pushed — save { themeId, themeName, pushedAt: Date.now(), schemaHash } on figma.root. Post push-complete { success: true, message } to the UI, which promotes the optimistic push snapshot into lastPushedJsons[themeId] and flips isPushNeeded to false.

STRING variables

The meta collection includes a theme-name variable with resolvedType: "STRING" so tools consuming the published library know which theme is in the file. All other variables are resolvedType: "COLOR".

Last-pushed marker

The UI subscribes to last-pushed messages from the backend (emitted on startup and after every successful push) and stores the record in useConfigStore.lastPushed. The theme-selector dropdown renders the Figma logo on the row whose id matches; hovering it shows a tooltip. No indicator is shown when the active theme and the last-pushed theme coincide — the selector trigger already names it.


All rights reserved.