Generate API overview specifications documenting component properties, values, defaults, and configuration examples. Use when the user mentions "api", "api spec", "props", "properties", "component api", or wants to document a component's configurable properties.
81
77%
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Advisory
Suggest reviewing before use
Optimize this skill with Tessl
npx tessl skill review --optimize ./.cursor/skills/create-api/SKILL.mdGenerate an API overview directly in Figma — property tables with values, defaults, required status, sub-component tables, and configuration examples.
Read uspecs.config.json → mcpProvider. Follow the matching column for every MCP call in this skill.
| Operation | figma-console | figma-mcp |
|---|---|---|
| Verify connection | figma_get_status | Skip — implicit. If first use_figma call fails, guide user to check MCP setup. |
| Navigate to file | figma_navigate with URL | Extract fileKey from URL (figma.com/design/:fileKey/...). No navigate needed. |
| Take screenshot | figma_take_screenshot | get_screenshot with fileKey + nodeId |
| Execute Plugin JS | figma_execute with code | use_figma with fileKey, code, description. JS code is identical — no wrapper changes. |
| Search components | figma_search_components | search_design_system with query + fileKey + includeComponents: true |
| Get file/component data | figma_get_file_data / figma_get_component | get_metadata or get_design_context with fileKey + nodeId |
| Get variables (file-wide) | figma_get_variables | use_figma script: return await figma.variables.getLocalVariableCollectionsAsync(); |
| Get token values | figma_get_token_values | use_figma script reading variable values per mode/collection |
| Get styles | figma_get_styles | search_design_system with includeStyles: true, or use_figma: return figma.getLocalPaintStyles(); |
| Get selection | figma_get_selection | use_figma script: return figma.currentPage.selection.map(n => ({id: n.id, name: n.name, type: n.type})); |
figma-mcp requires fileKey on every call. Extract it once from the user's Figma URL at the start of the workflow. For branch URLs (figma.com/design/:fileKey/branch/:branchKey/:fileName), use :branchKey as the fileKey.
figma-mcp page context: use_figma resets figma.currentPage to the first page on every call. When a script accesses a node from a previous step via getNodeByIdAsync(ID), the page content may not be loaded — findAll, findOne, and characters will fail with TypeError until the page is activated. Insert this page-loading block immediately after getNodeByIdAsync:
let _p = node; while (_p.parent && _p.parent.type !== 'DOCUMENT') _p = _p.parent;
if (_p.type === 'PAGE') await figma.setCurrentPageAsync(_p);This walks up to the PAGE ancestor and loads its content. Console MCP does not need this — figma_execute inherits the Desktop page context.
Copy this checklist and update as you progress:
Task Progress:
- [ ] Step 1: Read instruction file
- [ ] Step 2: Verify MCP connection (if Figma link provided)
- [ ] Step 3: Read template key from uspecs.config.json
- [ ] Step 4: Gather context (MCP tools + user-provided input)
- [ ] Step 4b: Run extraction script for deterministic property identification
- [ ] Step 5: Identify properties and sub-components
- [ ] Step 6: Generate structured data (main table, sub-component tables, config examples)
- [ ] Step 7: Re-read instruction file (Pre-Output Validation Checklist, Common Mistakes) and audit
- [ ] Step 8: Import and detach the API template
- [ ] Step 9: Fill header fields
- [ ] Step 10: Fill main API table
- [ ] Step 11: Fill sub-component tables (if any)
- [ ] Step 12: Fill configuration examples
- [ ] Step 13: Visual validationIf a Figma link is provided, read mcpProvider from uspecs.config.json and verify the connection:
If figma-console:
figma_get_status — Confirm Desktop Bridge plugin is activeIf figma-mcp:
use_figma call. No explicit check needed.Read the file uspecs.config.json and extract the apiOverview value from the templateKeys object.
Save this key as API_TEMPLATE_KEY. If the key is empty, tell the user:
The API overview template key is not configured. Run
@firstrunwith your Figma template library link first.
Use ALL available sources to maximize context:
From user:
From MCP tools (when Figma link provided):
figma_navigate — Open the component URLfigma_take_screenshot — Capture the component and its variantsfigma_get_file_data — Get component set structure with variant axesfigma_get_component — Get detailed component data for specific instancefigma_get_component_for_development — Get component data with visual referencefigma_get_variables — Check for variable mode-controlled properties (shape, density)figma_search_components — Find component by name if neededWhen a Figma link is provided, run this extraction script via figma_execute to programmatically extract all component properties. Replace __NODE_ID__ with the component set node ID extracted from the URL (node-id=123-456 → 123:456):
const TARGET_NODE_ID = '__NODE_ID__';
const node = await figma.getNodeByIdAsync(TARGET_NODE_ID);
if (!node || (node.type !== 'COMPONENT_SET' && node.type !== 'COMPONENT')) {
return { error: 'Node is not a component set or component. Type: ' + (node ? node.type : 'null') };
}
const isComponentSet = node.type === 'COMPONENT_SET';
const propDefs = node.componentPropertyDefinitions;
const variantAxes = [];
const booleanProps = [];
const instanceSwapProps = [];
const slotProps = [];
for (const [rawKey, def] of Object.entries(propDefs)) {
const cleanKey = rawKey.split('#')[0];
if (def.type === 'VARIANT') {
variantAxes.push({
name: cleanKey,
options: def.variantOptions || [],
defaultValue: def.defaultValue
});
} else if (def.type === 'BOOLEAN') {
let associatedLayer = null;
const defaultVariant = isComponentSet ? (node.defaultVariant || node.children[0]) : node;
const props = defaultVariant.componentProperties;
if (props) {
for (const [k, v] of Object.entries(props)) {
if (k.split('#')[0] === cleanKey && v.type === 'BOOLEAN') {
const nodeId = k.split('#')[1];
if (nodeId) {
try {
const layerNode = await figma.getNodeByIdAsync(defaultVariant.id.split(';')[0] + ';' + nodeId);
if (layerNode) associatedLayer = layerNode.name;
} catch {}
}
}
}
}
booleanProps.push({
name: cleanKey,
defaultValue: def.defaultValue,
associatedLayer,
rawKey
});
} else if (def.type === 'INSTANCE_SWAP') {
let swapTargetName = null;
if (def.defaultValue) {
try {
const targetNode = await figma.getNodeByIdAsync(def.defaultValue);
if (targetNode) swapTargetName = targetNode.name;
} catch {}
}
instanceSwapProps.push({
name: cleanKey,
defaultValue: swapTargetName || def.defaultValue,
rawKey
});
} else if (def.type === 'SLOT') {
const preferred = [];
if (def.preferredValues && def.preferredValues.length > 0) {
for (const pv of def.preferredValues) {
if (pv.type === 'COMPONENT') {
let compName = null;
try {
const comp = await figma.getNodeByIdAsync(pv.key);
if (comp) compName = comp.name;
} catch {}
preferred.push({ componentKey: pv.key, componentName: compName || pv.key });
}
}
}
slotProps.push({
name: cleanKey,
description: def.description || '',
preferredInstances: preferred,
rawKey,
defaultChildren: []
});
}
}
const defaultVariant = isComponentSet ? (node.defaultVariant || node.children[0]) : node;
const defaultProps = { ...(defaultVariant.variantProperties || {}) };
// Read default children of SLOT nodes for contextual overrides
if (slotProps.length > 0) {
const allSlotNodes = defaultVariant.findAll ? defaultVariant.findAll(n => n.type === 'SLOT') : [];
for (const slotNode of allSlotNodes) {
const cpRefs = slotNode.componentPropertyReferences || {};
const matchingSlot = slotProps.find(sp => {
const refKey = Object.values(cpRefs)[0];
if (refKey && refKey.split('#')[0] === sp.name) return true;
return sp.name === slotNode.name;
});
if (matchingSlot && slotNode.children) {
for (const child of slotNode.children) {
if (child.type === 'INSTANCE') {
const mainComp = await child.getMainComponentAsync();
const overrides = {};
if (child.componentProperties) {
for (const [k, v] of Object.entries(child.componentProperties)) {
overrides[k.split('#')[0]] = v.value;
}
}
matchingSlot.defaultChildren.push({
componentName: mainComp ? mainComp.name : child.name,
componentKey: mainComp ? mainComp.key : '',
contextualOverrides: overrides
});
}
}
}
}
}
// Read composable children for legacy (non-SLOT) components
const composableChildren = [];
if (slotProps.length === 0 && defaultVariant.children) {
for (const child of defaultVariant.children) {
if (child.type === 'INSTANCE') {
const mainComp = await child.getMainComponentAsync();
const overrides = {};
if (child.componentProperties) {
for (const [k, v] of Object.entries(child.componentProperties)) {
overrides[k.split('#')[0]] = v.value;
}
}
composableChildren.push({
componentName: mainComp ? mainComp.name : child.name,
componentKey: mainComp ? mainComp.key : '',
contextualOverrides: overrides
});
} else if (child.children) {
for (const grandchild of child.children) {
if (grandchild.type === 'INSTANCE') {
const mainComp = await grandchild.getMainComponentAsync();
const overrides = {};
if (grandchild.componentProperties) {
for (const [k, v] of Object.entries(grandchild.componentProperties)) {
overrides[k.split('#')[0]] = v.value;
}
}
composableChildren.push({
componentName: mainComp ? mainComp.name : grandchild.name,
componentKey: mainComp ? mainComp.key : '',
contextualOverrides: overrides,
parentLayer: child.name
});
}
}
}
}
}
const variantAxesObj = {};
if (isComponentSet && node.variantGroupProperties) {
for (const [key, val] of Object.entries(node.variantGroupProperties)) {
variantAxesObj[key] = val.values;
}
}
const textNodeMap = [];
const allTextNodes = defaultVariant.findAll ? defaultVariant.findAll(n => n.type === 'TEXT') : [];
for (const tn of allTextNodes) {
textNodeMap.push({
name: tn.name,
characters: tn.characters,
parentName: tn.parent ? tn.parent.name : null
});
}
return {
componentName: node.name,
compSetNodeId: TARGET_NODE_ID,
isComponentSet,
variantAxes,
booleanProps,
instanceSwapProps,
slotProps,
composableChildren,
variantAxesObj,
defaultProps,
defaultVariantName: defaultVariant.name,
textNodeMap
};Save the returned JSON. This provides:
compSetNodeId — needed for creating live preview instances in configuration examples (Step 12)variantAxes — each axis with name, options, and defaultValue for populating the main property tablebooleanProps — each boolean with name, defaultValue, associatedLayer, and rawKey (the exact Figma key including #nodeId suffix for setProperties())instanceSwapProps — each instance swap with name, defaultValue, and rawKeyslotProps — each native SLOT property with name, description, preferredInstances (approved components for the slot), and defaultChildren (instances found in the slot with their contextualOverrides — property values set by the designer that may differ from the component's standalone defaults)composableChildren — for legacy components without native SLOT nodes: child INSTANCE nodes found in the default variant, each with componentName, componentKey, contextualOverrides, and optional parentLayer (the containing frame name). Empty when slotProps is populated.defaultProps — default variant property values for variant matching in configuration examplesdefaultVariantName — for fallback identificationtextNodeMap — array of { name, characters, parentName } for every TEXT node in the default variant. Use the name field (not parentName) as the key in textOverrides and slotInsertions[].textOverrides. This eliminates guessing layer names from frame structure or design context output. Layer names are case-sensitive.Use this structured data in Step 5 to identify properties deterministically rather than relying solely on MCP tool interpretation. When building sub-component tables (Pattern A or B), use slotProps.defaultChildren.contextualOverrides or composableChildren.contextualOverrides to populate the default column with context-specific values rather than the component's global defaults.
When building configuration examples (Step 12), use slotProps to populate slotInsertions: the slot name comes from slotProps[].name (e.g., "trailing content slot"), and the componentNodeId comes from the preferred instance node IDs discovered during Step 4 context gathering (e.g., the node IDs returned for trailing preferred instances). Use textOverrides for any text values shown in the example table that differ from the component's default text — look up the exact TEXT node layer name from textNodeMap (e.g., if textNodeMap shows { name: "section heading", characters: "Section heading", parentName: "title" }, use "section heading" as the key, not "title").
Using gathered context and the extraction data from Step 4b, identify:
A. Variant properties
B. Boolean toggles
C. Variable mode properties
D. Sub-component configurations (Pattern A: slot content types; Pattern B: fixed sub-components — see instruction file for decision criteria)
Follow the ApiOverviewData schema defined in the instruction file. Build the data as a structured object matching those interfaces.
Re-read the instruction file, focusing on:
Check your output against each rule. Fix any violations.
Run via figma_execute (replace __API_TEMPLATE_KEY__, __COMPONENT_NAME__, and __COMPONENT_NODE_ID__ with the node ID extracted from the component URL):
const TEMPLATE_KEY = '__API_TEMPLATE_KEY__';
const COMP_NODE_ID = '__COMPONENT_NODE_ID__';
const compNode = await figma.getNodeByIdAsync(COMP_NODE_ID);
let _p = compNode;
while (_p.parent && _p.parent.type !== 'DOCUMENT') _p = _p.parent;
if (_p.type === 'PAGE') await figma.setCurrentPageAsync(_p);
const templateComponent = await figma.importComponentByKeyAsync(TEMPLATE_KEY);
const instance = templateComponent.createInstance();
const frame = instance.detachInstance();
const GAP = 200;
frame.x = compNode.x + compNode.width + GAP;
frame.y = compNode.y;
frame.name = '__COMPONENT_NAME__ API';
figma.currentPage.selection = [frame];
figma.viewport.scrollAndZoomIntoView([frame]);
return { frameId: frame.id, pageId: _p.id, pageName: _p.name };Save the returned frameId — you need it for all subsequent steps.
Run via figma_execute (replace __FRAME_ID__, __COMPONENT_NAME__, and __GENERAL_NOTES__):
const frame = await figma.getNodeByIdAsync('__FRAME_ID__');
const textNodes = frame.findAll(n => n.type === 'TEXT');
const fontSet = new Set();
const fontsToLoad = [];
for (const tn of textNodes) {
try {
const fn = tn.fontName;
if (fn && fn !== figma.mixed && fn.family) {
const key = fn.family + '|' + fn.style;
if (!fontSet.has(key)) { fontSet.add(key); fontsToLoad.push(fn); }
}
} catch {}
}
await Promise.all(fontsToLoad.map(f => figma.loadFontAsync(f).catch(() => {})));
const compNameFrame = frame.findOne(n => n.name === '#compName');
if (compNameFrame) {
const t = compNameFrame.findOne(n => n.type === 'TEXT');
if (t) t.characters = '__COMPONENT_NAME__';
}
const notesFrame = frame.findOne(n => n.name === '#general-api-notes');
if (notesFrame) {
const hasNotes = __HAS_GENERAL_NOTES__;
if (!hasNotes) {
notesFrame.visible = false;
} else {
const t = notesFrame.findOne(n => n.type === 'TEXT');
if (t) t.characters = '__GENERAL_NOTES__';
}
}
return { success: true };Run via figma_execute. Replace __FRAME_ID__ and __PROPERTIES_JSON__ with the main table properties array.
const FRAME_ID = '__FRAME_ID__';
const PROPERTIES = __PROPERTIES_JSON__;
const frame = await figma.getNodeByIdAsync(FRAME_ID);
const mainTable = frame.findOne(n => n.name === '#main-api-table');
const rowTemplate = mainTable.findOne(n => n.name === '#api-row-template');
const textNodes = mainTable.findAll(n => n.type === 'TEXT');
const fontSet = new Set();
const fontsToLoad = [];
for (const tn of textNodes) {
try {
const fn = tn.fontName;
if (fn && fn !== figma.mixed && fn.family) {
const key = fn.family + '|' + fn.style;
if (!fontSet.has(key)) { fontSet.add(key); fontsToLoad.push(fn); }
}
} catch {}
}
await Promise.all(fontsToLoad.map(f => figma.loadFontAsync(f).catch(() => {})));
for (const prop of PROPERTIES) {
const row = rowTemplate.clone();
mainTable.appendChild(row);
row.name = 'Row ' + prop.property;
const nameFrame = row.findOne(n => n.name === '#property-name');
if (nameFrame) {
const t = nameFrame.findOne(n => n.type === 'TEXT');
if (t) t.characters = prop.property;
}
const valuesFrame = row.findOne(n => n.name === '#property-values');
if (valuesFrame) {
const t = valuesFrame.findOne(n => n.type === 'TEXT');
if (t) t.characters = prop.values;
}
const requiredFrame = row.findOne(n => n.name === '#property-required');
if (requiredFrame) {
const t = requiredFrame.findOne(n => n.type === 'TEXT');
if (t) t.characters = prop.required ? 'Yes' : 'No';
}
const defaultFrame = row.findOne(n => n.name === '#property-default');
if (defaultFrame) {
const t = defaultFrame.findOne(n => n.type === 'TEXT');
if (t) t.characters = prop.default;
}
const notesFrame = row.findOne(n => n.name === '#property-notes');
if (notesFrame) {
const t = notesFrame.findOne(n => n.type === 'TEXT');
if (t) t.characters = prop.notes;
}
// Handle hierarchy indicator for sub-properties
const hierarchyIndicator = row.findOne(n => n.name === '#hierarchy-indicator');
if (hierarchyIndicator) {
hierarchyIndicator.visible = !!prop.isSubProperty;
}
}
rowTemplate.remove();
return { success: true };If there are sub-component tables, run one figma_execute call per sub-component to avoid timeouts. If there are NO sub-component tables, run a single call to hide the template.
For each sub-component table, run:
const FRAME_ID = '__FRAME_ID__';
const SUB_NAME = '__SUBCOMPONENT_NAME__';
const SUB_DESCRIPTION = '__SUBCOMPONENT_DESCRIPTION__';
const HAS_DESCRIPTION = __HAS_DESCRIPTION__;
const SUB_PROPERTIES = __SUBCOMPONENT_PROPERTIES_JSON__;
const frame = await figma.getNodeByIdAsync(FRAME_ID);
const subTemplate = frame.findOne(n => n.name === '#subcomponent-chapter-template');
const section = subTemplate.clone();
subTemplate.parent.appendChild(section);
section.name = SUB_NAME;
section.visible = true;
const textNodes = section.findAll(n => n.type === 'TEXT');
const fontSet = new Set();
const fontsToLoad = [];
for (const tn of textNodes) {
try {
const fn = tn.fontName;
if (fn && fn !== figma.mixed && fn.family) {
const key = fn.family + '|' + fn.style;
if (!fontSet.has(key)) { fontSet.add(key); fontsToLoad.push(fn); }
}
} catch {}
}
await Promise.all(fontsToLoad.map(f => figma.loadFontAsync(f).catch(() => {})));
// Set sub-component title
const titleFrame = section.findOne(n => n.name === '#subcomponent-title');
if (titleFrame) {
const t = titleFrame.findOne(n => n.type === 'TEXT');
if (t) t.characters = SUB_NAME;
}
// Set description (optional)
const descFrame = section.findOne(n => n.name === '#subcomponent-description');
if (descFrame) {
if (!HAS_DESCRIPTION) {
descFrame.visible = false;
} else {
const t = descFrame.findOne(n => n.type === 'TEXT');
if (t) t.characters = SUB_DESCRIPTION;
}
}
// Fill sub-component table
const subTable = section.findOne(n => n.name === '#subcomponent-table');
const rowTemplate = subTable.findOne(n => n.name === '#subcomponent-row-template');
for (const prop of SUB_PROPERTIES) {
const row = rowTemplate.clone();
subTable.appendChild(row);
row.name = 'Row ' + prop.property;
const nameFrame = row.findOne(n => n.name === '#subprop-name');
if (nameFrame) {
const t = nameFrame.findOne(n => n.type === 'TEXT');
if (t) t.characters = prop.property;
}
const valuesFrame = row.findOne(n => n.name === '#subprop-values');
if (valuesFrame) {
const t = valuesFrame.findOne(n => n.type === 'TEXT');
if (t) t.characters = prop.values;
}
const requiredFrame = row.findOne(n => n.name === '#subprop-required');
if (requiredFrame) {
const t = requiredFrame.findOne(n => n.type === 'TEXT');
if (t) t.characters = prop.required ? 'Yes' : 'No';
}
const defaultFrame = row.findOne(n => n.name === '#subprop-default');
if (defaultFrame) {
const t = defaultFrame.findOne(n => n.type === 'TEXT');
if (t) t.characters = prop.default;
}
const notesFrame = row.findOne(n => n.name === '#subprop-notes');
if (notesFrame) {
const t = notesFrame.findOne(n => n.type === 'TEXT');
if (t) t.characters = prop.notes;
}
const hierarchyIndicator = row.findOne(n => n.name === '#subprop-hierarchy-indicator');
if (hierarchyIndicator) {
hierarchyIndicator.visible = !!prop.isSubProperty;
}
}
rowTemplate.remove();
return { success: true, subComponent: SUB_NAME };After all sub-component tables are rendered, hide the original template:
const frame = await figma.getNodeByIdAsync('__FRAME_ID__');
const subTemplate = frame.findOne(n => n.name === '#subcomponent-chapter-template');
if (subTemplate) subTemplate.visible = false;
return { success: true };Hide the template:
const frame = await figma.getNodeByIdAsync('__FRAME_ID__');
const subTemplate = frame.findOne(n => n.name === '#subcomponent-chapter-template');
if (subTemplate) subTemplate.visible = false;
return { success: true };Run one figma_execute call per configuration example to avoid timeouts.
For each example, run (replace __FRAME_ID__, __EXAMPLE_TITLE__, __COMPONENT_SET_NODE_ID__, __VARIANT_PROPERTIES_JSON__, __CHILD_OVERRIDES_JSON__, __TEXT_OVERRIDES_JSON__, __SLOT_INSERTIONS_JSON__, and __EXAMPLE_PROPERTIES_JSON__):
__VARIANT_PROPERTIES_JSON__ is an object mapping Figma property keys (exactly as returned by componentPropertyDefinitions) to values. This is used to instantiate and configure the live component preview. Include variant axes and boolean toggles needed for the example.__CHILD_OVERRIDES_JSON__ is an array of per-child property override objects for composable slot children (index 0 = first child). Use [] when no child overrides are needed. Each entry maps Figma property keys to values, same format as variantProperties.__TEXT_OVERRIDES_JSON__ is an object mapping Figma layer names to new text content (e.g., { "Label": "Submit" }). Applied to TEXT nodes inside the main instance. Use {} when no text overrides are needed.__SLOT_INSERTIONS_JSON__ is an array of slot insertion objects. Each has slotName (SLOT node name), componentNodeId (local component node ID to instantiate), and optional nestedOverrides (component properties for setProperties()) and textOverrides (TEXT node content overrides on the inserted child). All overrides are applied before appendChild into the slot — after adoption, the child's internal nodes get compound IDs and become inaccessible. Use [] when no slot insertions are needed.const FRAME_ID = '__FRAME_ID__';
const EXAMPLE_TITLE = '__EXAMPLE_TITLE__';
const COMPONENT_SET_ID = '__COMPONENT_SET_NODE_ID__';
const VARIANT_PROPS = __VARIANT_PROPERTIES_JSON__;
const CHILD_OVERRIDES = __CHILD_OVERRIDES_JSON__;
const TEXT_OVERRIDES = __TEXT_OVERRIDES_JSON__;
const SLOT_INSERTIONS = __SLOT_INSERTIONS_JSON__;
const EXAMPLE_PROPERTIES = __EXAMPLE_PROPERTIES_JSON__;
async function loadAllFonts(rootNode) {
const textNodes = rootNode.findAll(n => n.type === 'TEXT');
const fontSet = new Set();
const fontsToLoad = [];
for (const tn of textNodes) {
try {
const fn = tn.fontName;
if (fn && fn !== figma.mixed && fn.family) {
const key = fn.family + '|' + fn.style;
if (!fontSet.has(key)) { fontSet.add(key); fontsToLoad.push(fn); }
}
} catch {}
}
await Promise.all(fontsToLoad.map(f => figma.loadFontAsync(f).catch(() => {})));
}
const frame = await figma.getNodeByIdAsync(FRAME_ID);
const exampleTemplate = frame.findOne(n => n.name === '#config-example-chapter-template');
const section = exampleTemplate.clone();
exampleTemplate.parent.appendChild(section);
section.name = EXAMPLE_TITLE;
section.visible = true;
const textNodes = section.findAll(n => n.type === 'TEXT');
const fontSet = new Set();
const fontsToLoad = [];
for (const tn of textNodes) {
try {
const fn = tn.fontName;
if (fn && fn !== figma.mixed && fn.family) {
const key = fn.family + '|' + fn.style;
if (!fontSet.has(key)) { fontSet.add(key); fontsToLoad.push(fn); }
}
} catch {}
}
await Promise.all(fontsToLoad.map(f => figma.loadFontAsync(f).catch(() => {})));
// Set example title
const titleFrame = section.findOne(n => n.name === '#example-title');
if (titleFrame) {
const t = titleFrame.findOne(n => n.type === 'TEXT');
if (t) t.characters = EXAMPLE_TITLE;
}
// Place live component instance in the Preview frame
const preview = section.findOne(n => n.name === 'Preview');
if (preview) {
// Remove the asset description text placeholder
const assetDesc = preview.findOne(n => n.name === '#example-asset-description');
if (assetDesc) assetDesc.remove();
// Instantiate component and configure variant/boolean properties
const compNode = await figma.getNodeByIdAsync(COMPONENT_SET_ID);
const defaultVariant = compNode.type === 'COMPONENT_SET'
? (compNode.defaultVariant || compNode.children[0])
: compNode;
const instance = defaultVariant.createInstance();
await loadAllFonts(instance);
if (Object.keys(VARIANT_PROPS).length > 0) {
instance.setProperties(VARIANT_PROPS);
await loadAllFonts(instance);
}
// Apply per-child overrides for composable slot children
if (CHILD_OVERRIDES && CHILD_OVERRIDES.length > 0) {
let slot = instance.findOne(n => n.type === 'SLOT');
if (!slot) slot = instance.children[0];
if (slot && slot.children) {
for (let i = 0; i < Math.min(CHILD_OVERRIDES.length, slot.children.length); i++) {
const child = slot.children[i];
if (child.type === 'INSTANCE' && Object.keys(CHILD_OVERRIDES[i]).length > 0) {
try { child.setProperties(CHILD_OVERRIDES[i]); } catch (e) {}
}
}
}
await loadAllFonts(instance);
}
// Apply text overrides to TEXT nodes inside the instance
if (TEXT_OVERRIDES && Object.keys(TEXT_OVERRIDES).length > 0) {
await loadAllFonts(instance);
for (const [layerName, newText] of Object.entries(TEXT_OVERRIDES)) {
const textNode = instance.findOne(n => n.type === 'TEXT' && n.name === layerName);
if (textNode) {
textNode.characters = newText;
}
}
}
// Insert content into named SLOT nodes
if (SLOT_INSERTIONS && SLOT_INSERTIONS.length > 0) {
for (const insertion of SLOT_INSERTIONS) {
const slotNode = instance.findOne(
n => n.type === 'SLOT' && n.name === insertion.slotName
);
if (slotNode) {
const comp = await figma.getNodeByIdAsync(insertion.componentNodeId);
if (comp && comp.type === 'COMPONENT') {
const child = comp.createInstance();
await loadAllFonts(child);
// Apply all overrides BEFORE appendChild — after slot adoption, child nodes get compound IDs and become inaccessible
if (insertion.nestedOverrides && Object.keys(insertion.nestedOverrides).length > 0) {
try {
child.setProperties(insertion.nestedOverrides);
await loadAllFonts(child);
} catch (e) {}
}
if (insertion.textOverrides && Object.keys(insertion.textOverrides).length > 0) {
for (const [layerName, newText] of Object.entries(insertion.textOverrides)) {
const tn = child.findOne(n => n.type === 'TEXT' && n.name === layerName);
if (tn) {
tn.characters = newText;
}
}
}
slotNode.appendChild(child);
await loadAllFonts(instance);
}
}
}
await loadAllFonts(instance);
}
preview.appendChild(instance);
instance.layoutAlign = 'INHERIT';
}
// Fill example table
const exampleTable = section.findOne(n => n.name === '#example-table');
const rowTemplate = exampleTable.findOne(n => n.name === '#example-row-template');
for (const prop of EXAMPLE_PROPERTIES) {
const row = rowTemplate.clone();
exampleTable.appendChild(row);
row.name = 'Row ' + prop.property;
const nameFrame = row.findOne(n => n.name === '#example-prop-name');
if (nameFrame) {
const t = nameFrame.findOne(n => n.type === 'TEXT');
if (t) t.characters = prop.property;
}
const valueFrame = row.findOne(n => n.name === '#example-prop-value');
if (valueFrame) {
const t = valueFrame.findOne(n => n.type === 'TEXT');
if (t) t.characters = prop.value;
}
const notesFrame = row.findOne(n => n.name === '#example-prop-notes');
if (notesFrame) {
const t = notesFrame.findOne(n => n.type === 'TEXT');
if (t) t.characters = prop.notes;
}
}
rowTemplate.remove();
return { success: true, example: EXAMPLE_TITLE };After all examples are rendered, hide the original template:
const frame = await figma.getNodeByIdAsync('__FRAME_ID__');
const exampleTemplate = frame.findOne(n => n.name === '#config-example-chapter-template');
if (exampleTemplate) exampleTemplate.visible = false;
return { success: true };figma_take_screenshot with the frameId — Capture the completed specfigma_execute and re-capture (up to 3 iterations)Print a clickable Figma URL to the completed spec in chat. Construct the URL from the fileKey (extracted from the user's input URL) and the frameId (returned by Step 8), replacing : with - in the node ID:
API spec complete: https://www.figma.com/design/{fileKey}/?node-id={frameId}subComponentTables is empty or absent, the #subcomponent-chapter-template is hidden. If present, each sub-component gets its own cloned section with its own property table.#hierarchy-indicator) and sub-component tables (#subprop-hierarchy-indicator) support isSubProperty for indented child rows.COMPONENT_SET (multi-variant) or a standalone COMPONENT (single variant). The extraction script detects the type and returns isComponentSet accordingly. When the node is a standalone component, there are no variant axes — only boolean, instance swap, and variable mode properties apply. Instance creation in Step 12 uses compNode.createInstance() directly for standalone components.componentPropertyDefinitions from the component set or component, capturing all variant axes (with options and defaults), boolean toggles (with associated layer names and raw keys), and instance swap properties. This structured data makes property identification in Step 5 deterministic rather than relying solely on LLM interpretation of MCP tool output. The rawKey values (including #nodeId suffixes) are needed for setProperties() when creating configuration example previews in Step 12.api/agent-api-instruction.md) contains the JSON schema, examples, and property classification rules. The AI reasoning for property identification is unchanged — only the delivery mechanism has changed.b1213ef
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.