Orchestrator guide for delegating Figma MCP phases to specialized sub-agents. Use when a Figma task is large enough to risk context overflow — component sets with 8+ variants, unknown tree depth, or sessions expected to exceed 100 tool calls. Supports both serial discovery and parallel build/style phases.
Large Figma sessions hit three problems in a single-agent context: context pressure (large node tree responses), attention drift (losing track of which nodes are done after 30+ sequential calls), and error pollution (9 retries of a failing tool consuming planning context). Sub-agents solve this by giving each phase its own clean context window.
The primary tool for reading nodes is get, which returns structured YAML (FSGN format) with deduplicated variable/style/component defs and a tokenEstimate in the meta. It accepts nodeId (single) or nodeIds (multiple, fetched in parallel).
Sub-agents also enable parallel execution: multiple agents can modify different parts of the Figma document simultaneously, with plugin-level concurrency control ensuring safety.
Available sub-agents:
figma-discovery agent) — read-only exploration, always runs serialget response has tokenEstimate > 8000 even at detail=structureSkip it when you already have the node IDs and structure, the target has < 20 children, or you only need one piece of info (just call the tool directly).
join_channel (no args) before spawning any sub-agent.status first in every sub-agent result — if "blocked", surface the error to the user and stop.Discovery → Planning → Building → Styling → Verification
Discovery always runs alone. Planning happens in the orchestrator. Verification happens in the orchestrator.
The Figma plugin has concurrency control that makes parallel agent execution safe:
create, delete_multiple_nodes, etc.)Rules for parallel sub-agents:
run_in_background: true on the Agent tool to launch parallel agents. You will be notified when each completes.get(nodeId, detail="structure") on the parent to confirm the expected structure.By variant (most common): Each agent handles a disjoint set of variants within a component set.
Agent A: Build/style variants for State=Loading (nodes A1-A5)
Agent B: Build/style variants for State=Empty (nodes B1-B5)
Agent C: Build/style variants for State=Selection (nodes C1-C5)
By section: Each agent handles a different top-level section of the page.
Agent A: Build Header component set
Agent B: Build Sidebar component set
Agent C: Build Footer component set
By operation type (for styling): Each agent handles a different type of binding.
Agent A: Bind all color variables
Agent B: Apply all text styles
Agent C: Bind all spacing/radius variables
Note: this only works if each node gets only ONE type of binding per agent. If a node needs both a color variable and a text style, assign that node to ONE agent that does both.
The agent definition lives at .claude/agents/figma-discovery.md. It has:
Tools available to the agent: join_channel, get, find, scan_text_nodes, get_local_variables, get_styles, get_local_components, get_design_system. All tools are declared in the agent definition and loaded automatically — no ToolSearch needed. Note: get_main_component is no longer needed — get includes component metadata in defs.components.
Use the Agent tool with subagent_type: "figma-discovery". The prompt only needs task-specific parameters — no system prompt needed.
Agent(
subagent_type: "figma-discovery",
description: "Discover <component name> structure",
prompt: JSON.stringify({
channelName: "<from your join_channel call>",
nodeId: "<target component set or frame ID>",
description: "Map DataViews component set",
include: ["text_nodes", "variables", "text_styles"],
nameFilter: "DataRow" // omit if not filtering components
})
)
Valid include values: text_nodes, variables, text_styles, components.
The agent's final message is JSON. Parse it immediately:
const discovery = JSON.parse(agentResult);
if (discovery.status === "blocked") {
// Surface to user: discovery.error + discovery.recommendation
// Do NOT proceed to build/style phases
} else {
// discovery.component_set.variants[].id → parentId values for create/clone calls
// discovery.component_sets_in_frame → all component sets when target is a FRAME (pick one to deep-map)
// discovery.text_nodes[] → input for apply (variables, textStyleId)
// discovery.unbound_nodes → if >= 20, a Styler phase is needed; null = unknown
// discovery.variables → sanity-check tokens are loaded
// discovery.summary → user-facing status message
//
// Variant children now include:
// layoutMode → auto-layout direction (if active)
// boundVariables → list of bound field names (e.g. ["fill", "cornerRadius"])
// componentName/Id → resolved for INSTANCE nodes via defs.components in FSGN
}
Success:
{
"status": "success",
"component_sets_in_frame": [
{ "id": "...", "name": "DataViews", "type": "COMPONENT_SET", "variantCount": 16 },
{ "id": "...", "name": "DataForm", "type": "COMPONENT_SET", "variantCount": 2 }
],
"component_set": {
"id": "...",
"name": "...",
"variant_properties": ["Layout", "State"],
"variants": [
{
"id": "...",
"name": "Layout=List, State=Default",
"children": [
{ "id": "...", "name": "Header", "type": "FRAME", "layoutMode": "HORIZONTAL", "boundVariables": ["fill", "cornerRadius"] },
{ "id": "...", "name": "Row 1", "type": "INSTANCE", "componentName": "_Dataviews/Table/Row", "componentId": "2254:11156", "boundVariables": [] }
]
}
]
},
"text_nodes": [
{ "id": "...", "name": "Title", "parentVariantId": "16547:36681", "content": "Activity", "style": "Heading MD", "fills_variable": null }
],
"variables": {
"collections": ["Primitives", "Semantic"],
"total_count": 84,
"by_collection": {
"Semantic": [
{ "id": "VariableID:15613:5786", "name": "gray-700", "type": "COLOR" },
{ "id": "VariableID:15613:5784", "name": "surface-primary", "type": "COLOR" }
]
}
},
"text_styles": [
{ "name": "Heading MD", "id": "S:5a04abc..." },
{ "name": "Body SM", "id": "S:7b12def..." }
],
"unbound_nodes": 47,
"summary": "4 variants exist. 47 nodes have no variable bindings. 12 text nodes have no text style."
}
Blocked:
{
"status": "blocked",
"error": "get timed out twice",
"last_tool": "get",
"recommendation": "Call join_channel again — connection may have dropped"
}
Creates or clones node structures. Uses general-purpose agent type (not a custom agent definition — the prompt contains all instructions).
Agent(
description: "Build [description] variants",
run_in_background: true, // for parallel execution
prompt: `You are a Figma Builder agent. The WebSocket channel is already joined —
call join_channel with channelName "${channelName}" as your first action.
YOUR ASSIGNED NODES (do NOT touch anything outside this list):
${JSON.stringify(assignedNodes)}
WHAT TO BUILD:
${buildSpec}
RULES:
- Use create for complex structures (reduces many calls to 1)
- Use clone_node + clone_and_modify when duplicating existing patterns
- Use create with type="INSTANCE" and componentId for reusing library parts
- After creating nodes, verify with get(nodeId, detail="structure") that structure matches spec
- Return JSON: {"status": "success", "created_nodes": [...ids], "summary": "..."}
- If any tool fails twice on the same call, stop and return: {"status": "blocked", "error": "...", "last_tool": "...", "recommendation": "..."}
`
)
create specs that don't share parent nodes{
"status": "success",
"created_nodes": ["16547:36700", "16547:36701", "16547:36702"],
"summary": "Created 3 Loading state variants with 4 children each"
}
Applies variable bindings and text styles. Uses general-purpose agent type.
Before spawning Styler agents, the orchestrator can call get(nodeId, detail="full") on the built subtree. The FSGN defs section lists all variables and styles already present; variableBindings on each node shows what's already bound. This makes the binding plan explicit — pass it directly to the Styler via VARIABLE BINDINGS TO APPLY.
Agent(
description: "Style [description] variants",
run_in_background: true, // for parallel execution
prompt: `You are a Figma Styler agent. The WebSocket channel is already joined —
call join_channel with channelName "${channelName}" as your first action.
YOUR ASSIGNED NODES (do NOT touch anything outside this list):
${JSON.stringify(assignedNodes)}
VARIABLE BINDINGS TO APPLY:
${JSON.stringify(bindings)}
TEXT STYLE ASSIGNMENTS:
${JSON.stringify(textStyles)}
RULES:
- Use apply() with variables field to bind design tokens to node properties (supports flat list or nested tree)
- Use apply() with textStyleId to apply text styles (deduplicates font loading automatically)
- Process in order: variable bindings first, then text styles
- After applying, verify a sample node with get(nodeId, detail="full") to confirm bindings took
- Return JSON: {"status": "success", "bindings_applied": N, "styles_applied": N, "summary": "..."}
- If any tool fails twice on the same call, stop and return blocked status
`
)
The orchestrator prepares a binding plan from Discovery output and passes it to each Styler:
{
"bindings": [
{ "nodeId": "16547:36700", "field": "fill", "variableId": "VariableID:15613:5786" },
{ "nodeId": "16547:36701", "field": "cornerRadius", "variableId": "VariableID:15613:5800" }
],
"textStyles": [
{ "nodeId": "16547:36710", "styleId": "S:5a04abc..." },
{ "nodeId": "16547:36711", "styleId": "S:7b12def..." }
]
}
{
"status": "success",
"bindings_applied": 22,
"styles_applied": 8,
"summary": "Applied 22 variable bindings and 8 text styles to Loading variants"
}
TIME ORCHESTRATOR BUILDER-A BUILDER-B
───── ──────────── ───────── ─────────