Figma Fix Variable Bindings logo

Figma Fix Variable Bindings

Community
schudarin
figma-fix-variable-bindings

Fix detached or missing variable bindings in Figma — typography (fontFamily, fontStyle, fontSize, fontWeight), padding/spacing (paddingTop/Right/Bottom/Left, itemSpacing, counterAxisSpacing), corner radii (cornerRadius, topLeftRadius, topRightRadius, bottomLeftRadius, bottomRightRadius), and colors (solid fills and strokes). Trigger when the user mentions detached styles, deleted variables, broken bindings, "Variable was deleted" warnings, wrong fonts, manual/hardcoded padding, unbound spacing, hardcoded corner radius / rounding, detached/hardcoded colors, unbound fills or strokes, broken color variables, or asks to audit/clean up/fix variables across a file or page. Covers the full scan → report → confirm → rebind workflow; read-only until the user approves the report. Trigger even if only one of typography/padding/roundings/colors is mentioned.

Overview

Publisherschudarin
Repositorychudarin-design-skills
Skill namefigma-fix-variable-bindings
Stars
4
Forks
0
Bundled files
1
LicenseMIT
Links
  • Markdown instructions

    A SKILL.md file the model loads on demand, so it only costs tokens when a request actually matches.

  • Works with any LLM

    AI skills are plain Markdown, not provider-specific code, so this works with GPT, Claude, Gemini, Grok, or a local model.

  • 1 bundled files

    Scripts, templates, and references the model can read while it works. Files are read-only and never executed.

  • Open source

    Published by schudarin on GitHub. Read the source before you install it.

Installation

Install the Figma Fix Variable Bindings AI skill in TypingMind to use it with any LLM, or drop it into another agent that reads SKILL.md.

1

Install in TypingMind

TypingMind installs a skill straight from its GitHub folder — it reads SKILL.md, bundles the resource files, and stores the result locally.

  1. Open the app and go to Plugins → Skills.
  2. Choose "Install from GitHub".
  3. Paste the skill folder URL below and confirm.
    https://github.com/schudarin/chudarin-design-skills/tree/main/skills/figma-fix-variable-bindings
  4. Enable the skill in any chat where you want it available.
Plugins → Skills → Add skill → From GitHub URL, then paste the folder URL and press Continue.
2

Install in another agent

Any agent that reads the Agent Skills format can use this skill — copy the folder into that agent's skills directory.

Claude Code — .claude/skills
git clone --depth 1 https://github.com/schudarin/chudarin-design-skills.git /tmp/chudarin-design-skills
mkdir -p .claude/skills
cp -r /tmp/chudarin-design-skills/skills/figma-fix-variable-bindings .claude/skills/figma-fix-variable-bindings
Restart Claude Code after copying so it picks up the new skill.

Use it in TypingMind

Enable Figma Fix Variable Bindings in any TypingMind chat and the model takes it from there. Its name and description sit in the system prompt, and the moment a request matches, the model loads the full instructions itself — you never invoke it by hand, and it costs no tokens until it is actually used.

The model loads Figma Fix Variable Bindings on its own as soon as a request matches it.

Works with any AI model

AI skills are plain Markdown instructions rather than provider-specific code, so Figma Fix Variable Bindings is not tied to the model it was written for. Install it once in TypingMind and use it with GPT-5, Claude, Gemini, Grok, DeepSeek, Mistral, Llama, or a local model you run yourself — all on your own API keys.

  • Loaded only when it is needed

    The system prompt carries just the name and description. The instructions are fetched on the first matching request, so an idle skill costs nothing.

  • Switch models mid-chat

    Because the skill is instructions rather than code, changing model does not break it — the next model reads the same SKILL.md.

Skill instructions

This is the SKILL.md content the model loads. Read it before installing — a skill is instructions your model will follow.

Figma Fix Variable Bindings

Required companions: this skill writes through use_figma, so your tool's own Figma API instructions must be loaded first — the figma-use plugin skill in Claude Code, the MCP resource skill://figma/figma-use/SKILL.md elsewhere — plus figma-plugin-api-rules for the silent-failure patterns this workflow depends on (per-paint colour bindings, per-corner radii, read-back after every write).

Figma nodes lose variable bindings when collections get orphaned, libraries update, or components migrate. This skill scans for broken/missing bindings on typography (text nodes), padding/spacing (auto-layout frames), corner radii (any node with radii), and colors (solid fills/strokes), then rebinds them to the correct library variables.

How Bindings Work

Typography (TEXT nodes): fontFamily (STRING), fontStyle (STRING, e.g. "Bold"), fontSize (FLOAT), fontWeight (FLOAT, e.g. 700), lineHeight, letterSpacing.

Padding (auto-layout frames): paddingTop, paddingRight, paddingBottom, paddingLeft, itemSpacing, counterAxisSpacing — all FLOAT.

Roundings (any node with corner radii): cornerRadius, topLeftRadius, topRightRadius, bottomLeftRadius, bottomRightRadius — all FLOAT. cornerRadius returns figma.mixed when corners differ; don't treat this as a skip — just bypass the aggregate property and let the four per-corner props be processed individually in the same walk. Mixed nodes still get fully bound, one corner at a time.

Colors (any node with fills/strokes): each is an array of Paint (or figma.mixed). A SOLID paint carries .color ({r,g,b}, floats 0–1) and .opacity. The color variable binding lives per-paint at paint.boundVariables.color (a VARIABLE_ALIAS) — not on node.boundVariables. Bind with figma.variables.setBoundVariableForPaint(paint, 'color', variable), which returns a new paint object — you must clone the array, replace the entry, and reassign node.fills / node.strokes wholesale (mutating a paint in place silently no-ops). Match RGB only, normalized to #RRGGBB (round each channel ×255) so float-rounding noise doesn't cause misses. Only SOLID paints are handled; GRADIENT/IMAGE/VIDEO are reported, never bound.

A node can have dual bindings (remote library + local) on the same property. The local one wins at render time. setBoundVariable(field, null) only removes the remote binding — the local persists. variable.remove() silently no-ops if referenced. These are platform limitations.

Workflow

This skill writes to the user's file. Nothing is bound, changed or deleted before the user has seen the scan and said "apply". Phase 1 and 2 are read-only; Phase 3 runs only after an explicit go-ahead on the report; Phase 4 (deleting variables and collections) needs its own separate confirmation — never bundle it into the "apply" answer.

Phase 1: Unified Scan (1 call)

Single walk collecting all issues. Every node op in try/catch — one bad node must not abort the scan. Skip the root PAGE node's own property access (PAGE has no boundVariables, padding, or radius props — only walk its children).

javascript
const root = await figma.getNodeByIdAsync("TARGET_NODE_ID");
const PAD_PROPS = ['paddingTop','paddingRight','paddingBottom','paddingLeft','itemSpacing','counterAxisSpacing'];
const RADIUS_PROPS = ['cornerRadius','topLeftRadius','topRightRadius','bottomLeftRadius','bottomRightRadius'];
const TYPO_PROPS = ['fontFamily','fontSize','fontStyle','fontWeight'];
const issues = { typo: [], padding: [], radius: [], color: [] };
const localVarIds = new Set();
const remoteVarMap = {};
const errors = [];

// Normalize a Figma {r,g,b} (floats 0–1) to "#RRGGBB" so float-rounding noise doesn't break exact matching.
const toHex = (c) => "#" + ["r","g","b"].map(k => Math.round((c[k] ?? 0) * 255).toString(16).padStart(2,"0")).join("").toUpperCase();

// FigJam-style nodes that lack boundVariables — skip but still descend into children.
const SKIP_TYPES = new Set(["CONNECTOR","STAMP","WIDGET","STICKY","SHAPE_WITH_TEXT","CODE_BLOCK","TABLE"]);

function walk(node) {
  try {
    const isPage = node.type === "PAGE";
    const isSkipType = SKIP_TYPES.has(node.type);
    if (isSkipType) { if ("children" in node) for (const c of node.children) walk(c); return; }
    const bv = isPage ? {} : (node.boundVariables || {});

    if (!isPage && node.type === "TEXT" && node.fontName !== figma.mixed) {
      const styled = typeof node.textStyleId === "string" && node.textStyleId.length > 0;
      const info = { id: node.id, name: node.name, font: `${node.fontName.family} ${node.fontName.style} ${node.fontSize}`, styled, boundVars: {} };
      for (const [prop, bindings] of Object.entries(bv)) {
        const arr = Array.isArray(bindings) ? bindings : [bindings];
        for (const b of arr) {
          if (!b.id.includes("/")) localVarIds.add(b.id);
          else if (styled) remoteVarMap[`${prop}:${b.id}`] = { prop, id: b.id };
        }
        if (TYPO_PROPS.includes(prop)) info.boundVars[prop] = arr.map(b => ({ id: b.id, isLocal: !b.id.includes("/") }));
      }
      if (!styled) issues.typo.push(info);
    }

    if (!isPage && ("paddingTop" in node || "layoutMode" in node)) {
      const padIssues = [];
      for (const prop of PAD_PROPS) {
        if (!(prop in node)) continue;
        const val = node[prop], bound = bv[prop];
        if (bound) {
          const arr = Array.isArray(bound) ? bound : [bound];
          for (const b of arr) { if (!b.id.includes("/")) { localVarIds.add(b.id); padIssues.push({ prop, issue: "orphaned", varId: b.id }); } }
        } else if (typeof val === "number") {
          padIssues.push({ prop, issue: "hardcoded", value: val });   // include 0 — it should bind to None
        }
      }
      if (padIssues.length) issues.padding.push({ id: node.id, name: node.name, type: node.type, issues: padIssues });
    }

    if (!isPage && ("cornerRadius" in node || "topLeftRadius" in node)) {
      const radIssues = [];
      for (const prop of RADIUS_PROPS) {
        if (!(prop in node)) continue;
        let val; try { val = node[prop]; } catch(_) { continue; }
        if (val === figma.mixed) continue;   // skip mixed cornerRadius (per-corner will be handled separately)
        const bound = bv[prop];
        if (bound) {
          const arr = Array.isArray(bound) ? bound : [bound];
          for (const b of arr) { if (!b.id.includes("/")) { localVarIds.add(b.id); radIssues.push({ prop, issue: "orphaned", varId: b.id }); } }
        } else if (typeof val === "number") {
          radIssues.push({ prop, issue: "hardcoded", value: val });
        }
      }
      if (radIssues.length) issues.radius.push({ id: node.id, name: node.name, type: node.type, issues: radIssues });
    }

    if (!isPage) {
      for (const surface of ["fills","strokes"]) {
        if (!(surface in node)) continue;
        const paints = node[surface];
        if (paints === figma.mixed) { issues.color.push({ id: node.id, name: node.name, type: node.type, surface, issue: "mixed" }); continue; }
        if (!Array.isArray(paints)) continue;
        paints.forEach((p, i) => {
          if (p.type !== "SOLID") { issues.color.push({ id: node.id, name: node.name, type: node.type, surface, index: i, issue: "non-solid", paintType: p.type }); return; }
          const bound = p.boundVariables && p.boundVariables.color;  // per-paint, single alias (not an array)
          if (bound) {
            if (!bound.id.includes("/")) { localVarIds.add(bound.id); issues.color.push({ id: node.id, name: node.name, type: node.type, surface, index: i, issue: "orphaned", varId: bound.id, hex: toHex(p.color), opacity: p.opacity }); }
          } else {
            issues.color.push({ id: node.id, name: node.name, type: node.type, surface, index: i, issue: "hardcoded", hex: toHex(p.color), opacity: p.opacity });
          }
        });
      }
    }

    if ("children" in node) for (const c of node.children) walk(c);
  } catch (e) { errors.push({ nodeId: node?.id, error: e.message }); }
}
walk(root);

Phase 2: Resolve Library Variables (1 call)

Don't rely on search_design_system alone — its index frequently misses spacing/rounding tokens. Enumerate the library directly via figma.teamLibrary, which always returns the full set:

javascript
const cols = await figma.teamLibrary.getAvailableLibraryVariableCollectionsAsync();
// Match by name: Paddings | Spacing | Gaps | Roundings | Radii | Typography | Colors | Palette | Brand | Semantic
const wanted = cols.filter(c => /padding|spacing|gap|round|radius|radii|typography|colou?r|palette|brand|semantic|fill/i.test(c.name));
const libTokens = {}; // collectionName -> [{ name, key, id, type, value }]
for (const c of wanted) {
  const vars = await figma.teamLibrary.getVariablesInLibraryCollectionAsync(c.key);
  const items = [];
  for (const v of vars) {
    const imp = await figma.variables.importVariableByKeyAsync(v.key);
    const firstVal = Object.values(imp.valuesByMode)[0];
    items.push({ name: imp.name, key: v.key, id: imp.id, type: imp.resolvedType, value: firstVal });
  }
  libTokens[c.name] = items;
}

Then resolve any orphaned local var IDs (follow alias chains) so you know what value each one represents:

javascript
const localVars = {};
const queue = [...localVarIds], seen = new Set();
while (queue.length) {
  const vid = queue.pop();
  if (seen.has(vid)) continue; seen.add(vid);
  try {
    const v = await figma.variables.getVariableByIdAsync(vid);
    if (!v) continue;
    const col = await figma.variables.getVariableCollectionByIdAsync(v.variableCollectionId);
    localVars[vid] = { name: v.name, type: v.resolvedType, values: v.valuesByMode, collectionId: v.variableCollectionId, collectionName: col?.name };
    for (const val of Object.values(v.valuesByMode))
      if (typeof val === "object" && val.type === "VARIABLE_ALIAS" && !val.id.includes("/")) queue.push(val.id);
  } catch (e) { localVars[vid] = { error: e.message }; }
}

Phase 2½: Report and stop — the gate

Before any write, show the user what Phase 1 + 2 found and what would change:

  • per family (typography / padding / radius / colour): how many nodes, how many will bind to which token, how many have no exact match and stay untouched
  • the list of unmatched values and hexes, so the designer can decide
  • how many instance nodes are skipped (their master is the fix surface)
  • whether any orphaned local variables/collections would be deletion candidates in Phase 4

Then ask, in one line, whether to apply — and wait. If the scope is a whole page or file, offer to apply to one frame first as a proof, then the rest. No "apply" → the run ends here with the report; that is a complete, useful outcome, not a failure.

Phase 3: Build Replacement Maps & Apply Fixes (1–2 calls) — only after the user said "apply"

For each property family build value → libraryVariable maps from libTokens:

  • Paddings/Spacing/Gaps → bind padding & itemSpacing properties.
  • Roundings/Radii → bind cornerRadius & per-corner properties.
  • Typography → bind fontFamily/fontStyle/fontSize (only when font matches the library's Family/Font Type value).
  • Colors (COLOR-type tokens) → build a #RRGGBB → libraryVariable map (normalize each token's {r,g,b} via the same toHex). Bind solid fill/stroke paints whose hex is in the map.

Matching rules:

  • Exact value matching only for paddings/spacings/typography. Hardcoded 16 → token whose value is 16. No nearest-match.
  • Include zeros. 0 should bind to the collection's None token (almost every spacing/radius library has one). This is a desired explicit binding, not noise.
  • Ignore negatives and sub-pixel values silently. Negative paddings (e.g. -16) are deliberate overlap layouts; sub-pixel values (e.g. 4.5999…, 1.9605…, 27.220…) are scale-transform artifacts. Neither should be auto-bound or surfaced as no-match in the report — skip them quietly. Detect sub-pixel via !Number.isInteger(val), negative via val < 0.
  • Roundings: large radii are Circle, with one reserved exception. Any radius value >= 100 binds to the Circle token (typically value 999). Designers use 100, 500, 1000, sometimes the shape's half-dimension (e.g. 296, 1123.875) as sentinels for fully-rounded corners — semantically they're all Circle. Values < 100 need exact-value matching.
    • Reserved values. A workspace may keep specific radius or spacing values deliberately unbound (a sentinel the team uses on purpose). Read them from <project>/.claude/design.md, or ask once before the first run; skip them and report as "intentionally preserved", not under "no match".
  • Typography (dual-bound nodes): set the local var value to match the library value so both resolve identically. Local-only or unbound → setBoundVariable to the remote var.
  • Colors: exact #RRGGBB match only (RGB), no nearest-color guessing. Opacity/alpha is not part of the match — if the paint's opacity or color alpha differs from the token, bind the color anyway and report the opacity mismatch separately. Non-SOLID paints (gradient/image/video) are never bound — report their counts for manual review.

Critical guards:

  • "Auto" spacing: NEVER touch. Detect via node.primaryAxisAlignItems === "SPACE_BETWEEN" (itemSpacing) or node.counterAxisAlignItems === "SPACE_BETWEEN" (counterAxisSpacing).
  • Mixed cornerRadius: bypass the aggregate property, but always process the four per-corner radii. Don't report this as a skip in the summary — those corners DO get fixed.
  • FigJam-style nodes: ALWAYS skip and descend. CONNECTOR, STAMP, WIDGET, STICKY, SHAPE_WITH_TEXT, CODE_BLOCK, TABLE show up in design files (leftover from imports / FigJam blending) and throw on boundVariables access. Skip the node itself but continue walking its children. The SKIP_TYPES set in Phase 1 must always be applied.
  • Instance nodes (ID contains ;): skip — fix the main component instead. Report the instance count separately so the user knows the residual surface.
  • Pre-import variables once. Cache figma.variables.importVariableByKeyAsync results outside the walk; calling it per-node is slow on large pages.
  • Color: clone → replace → reassign. setBoundVariableForPaint returns a new paint; mutating the paint in place silently no-ops. Always rebuild the array and reassign node.fills / node.strokes once per node.
  • Color: only orphaned-local or hardcoded paints are candidates. A paint already bound to a remote (library) color variable is correct — leave it alone.
  • Mixed fills/strokes (=== figma.mixed): skip the node's paints and report; don't attempt per-segment binding.
javascript
const node = await figma.getNodeByIdAsync(targetId);
const isInstance = node.id.includes(";");
if (isInstance) { /* skip */ } else {
  if (prop === "itemSpacing" && node.primaryAxisAlignItems === "SPACE_BETWEEN") continue;
  if (prop === "counterAxisSpacing" && node.counterAxisAlignItems === "SPACE_BETWEEN") continue;
  const libVar = valueMap[targetValue];
  if (libVar) node.setBoundVariable(prop, libVar);
}

Colors — bind per paint, then reassign the whole array (one write per surface):

javascript
// colorMap: "#RRGGBB" -> imported library COLOR variable
for (const surface of ["fills","strokes"]) {
  const paints = node[surface];
  if (!Array.isArray(paints)) continue;       // skip figma.mixed / absent
  let changed = false;
  const next = paints.map(p => {
    if (p.type !== "SOLID") return p;          // gradient/image/video — never bind
    if (p.boundVariables?.color && !p.boundVariables.color.id.includes("/")) {
      // orphaned local binding — fall through to rebind by hex
    } else if (p.boundVariables?.color) {
      return p;                                // already remote-bound — leave alone
    }
    const libVar = colorMap[toHex(p.color)];
    if (!libVar) return p;                     // no exact match — report, don't guess
    changed = true;
    return figma.variables.setBoundVariableForPaint(p, "color", libVar);  // returns a NEW paint
  });
  if (changed) node[surface] = next;           // single write; in-place mutation no-ops
}

Phase 4: Cleanup — separate, explicit confirmation required

Deleting orphaned local variables and collections is destructive and not reversible from the API. Do it only if the user confirmed this step by name after seeing the Phase 3 report ("also delete the N orphaned local variables?"). Never fold it into the earlier "apply". Silent failures expected — variable.remove() no-ops if anything still references it.

Pitfalls

  1. Team-licensed fonts absent from the plugin sandbox can't be loaded via figma.loadFontAsync, but you can still bind variables to them with setBoundVariable.
  2. fontStyle = STRING weight name ("Bold"); fontWeight = FLOAT (700). Libraries typically use fontStyle.
  3. PAGE nodes have no boundVariables — accessing it throws. Walk children, skip property reads on the page itself.
  4. search_design_system is incomplete for spacing/rounding tokens. Always enumerate via figma.teamLibrary.getAvailableLibraryVariableCollectionsAsync() + getVariablesInLibraryCollectionAsync() (Phase 2).
  5. Bind 0 → None deliberately. Don't filter out zero values during the scan — explicit None bindings are a goal, not a bug.
  6. cornerRadius === figma.mixed when corners differ. Don't bind the aggregate; iterate per-corner instead. Mixed is not a skip — per-corner radii still get fully fixed in the same walk.
  7. All modes: when calling setValueForMode, iterate all collection.modes.
  8. FigJam-style nodes leak into design files. CONNECTOR, STAMP, WIDGET, STICKY, SHAPE_WITH_TEXT, CODE_BLOCK, TABLE have no boundVariables and throw on access. Skip them in the walk but keep descending into children.
  9. Color bindings are per-paint, not per-node. Look at paint.boundVariables.color (a single VARIABLE_ALIAS, not an array), never node.boundVariables. setBoundVariableForPaint returns a new paint — clone the array, replace the entry, reassign node.fills/node.strokes. Mutating a paint object in place silently no-ops.
  10. Match colors by quantized hex, not raw float. Two visually identical colors can differ in the 7th decimal from rounding. Normalize {r,g,b} to #RRGGBB (round each channel ×255) before comparing — strict float equality under-binds. Match RGB only; opacity/alpha is reported, not matched.
  11. Only SOLID paints bind. GRADIENT_LINEAR/RADIAL/ANGULAR/DIAMOND, IMAGE, VIDEO are never auto-bound — report them for manual review. fills/strokes can also be figma.mixed (multi-segment text) — skip and report.

Reporting

Brief summary after the run:

  • ✅ X typography bindings fixed, Y padding bindings fixed, Z radii bindings fixed, C color bindings fixed (break out fills vs strokes, and zero → None separately, if meaningful)
  • ⏭️ Skipped: auto spacing, no exact match (list the unmatched values/hexes with counts), mixed radii, mixed fills/strokes, gradient/image/video paints (count for manual review), instance occurrences (give an instance total so the user knows the follow-up surface)
  • ⚠️ Color matched but opacity/alpha differs — list these (the color bind succeeded; opacity is the designer's call)
  • ⚠️ N errors (list node IDs)
  • 🧹 Orphaned vars removed: A succeeded, B need manual cleanup
  • One-liner for manual cleanup: "Select layer → Design panel → click ⚠️ variable icon → detach and rebind."

Call out unmatched values that look semantically equivalent to a library token but differ in value (e.g. radii of 100 or 500 that are clearly meant to be Circle/999, itemSpacing of 10 when the system uses 8/12, or a hex that's one channel off from a token color). These need a designer decision, not an automated fix.

If any issues came up during execution that were resolved on the fly (e.g. a variable resolved unexpectedly, an alias chain led somewhere surprising, a collection had unusual mode configurations, or a workaround was needed for an API quirk), briefly mention what happened and how it was handled at the end of the report. This helps the user understand if the file has unusual patterns that might recur, and feeds back into improving this skill for next time.

Bundled files

The model reads these on demand while the skill is loaded. They are exposed as readable files and are never executed.

Frequently asked questions

What does the Figma Fix Variable Bindings AI skill do?

Fix detached or missing variable bindings in Figma — typography (fontFamily, fontStyle, fontSize, fontWeight), padding/spacing (paddingTop/Right/Bottom/Left, itemSpacing, counterAxisSpacing), corner radii (cornerRadius, topLeftRadius, topRightRadius, bottomLeftRadius, bottomRightRadius), and colors (solid fills and strokes). Trigger when the user mentions detached styles, deleted variables, broken bindings, "Variable was deleted" warnings, wrong fonts, manual/hardcoded padding, unbound spacing, hardcoded corner radius / rounding, detached/hardcoded colors, unbound fills or strokes, broken c...

Why use Figma Fix Variable Bindings on TypingMind?

Because you install it once and use it with any model. Figma Fix Variable Bindings is plain Markdown rather than provider-specific code, so the same skill runs on GPT-5, Claude, Gemini, Grok, or a local model — and you can switch model mid-chat without it breaking. TypingMind runs on your own API keys, so you pay providers directly instead of a per-seat subscription, and your skills and chats stay in your own storage.

How do I install Figma Fix Variable Bindings in TypingMind?

Open Plugins → Skills → Install from GitHub in TypingMind and paste https://github.com/schudarin/chudarin-design-skills/tree/main/skills/figma-fix-variable-bindings. TypingMind reads its SKILL.md and bundles its files and installs it as a skill you can enable per chat.

Which AI models can use Figma Fix Variable Bindings?

Any model you connect in TypingMind. AI skills are plain Markdown instructions rather than provider-specific code, so GPT, Claude, Gemini, Grok, and local models can all load this skill when a request matches it.

How many AI models can I use with Figma Fix Variable Bindings?

As many as you like. As long as a model supports skills, you can use Figma Fix Variable Bindings with it — GPT, Claude, Gemini, Grok, DeepSeek, Mistral, Llama and more — all on TypingMind with your own API keys.

Is the Figma Fix Variable Bindings AI skill free?

Yes. It is published on GitHub by schudarin under the MIT license. You only pay your own AI provider for the tokens you use.

What are AI skills?

An AI skill is a reusable instruction bundle that teaches an AI model how to do one specific task. It follows the open Agent Skills format: a SKILL.md file with a name and description, plus any scripts, templates or reference files the model may need. The model reads the instructions only when your request matches the skill, so an installed skill costs nothing until it is used.

How are AI skills different from plugins or MCP servers?

A plugin or MCP server gives a model new tools to call — code that runs somewhere and returns a result. An AI skill gives the model knowledge and process instead: how to approach a task, which steps to follow, what good output looks like. Skills are plain Markdown, so they need no server, no API key and no runtime, and they work with any model.

View all

Set up your own AI workspace now

Get notified about new features and future giveaways by subscribing to our newsletter 👇