Scripting
openpencil eval runs JavaScript against an 4B Pen document with a Figma-compatible figma global. Use it for headless batch edits, inspection, fixture setup, and automation without opening the editor UI.
Basic usage
openpencil eval design.fig -c "return figma.currentPage.children.length"The -c flag accepts JavaScript. Its last expression is the result, so return is optional; await works at the top level. A script whose last statement is not an expression, such as a declaration, has no result and prints nothing.
openpencil eval design.fig -c "
const frame = figma.createFrame()
frame.name = 'Card'
frame.resize(300, 200)
frame.layoutMode = 'VERTICAL'
frame.itemSpacing = 12
return { id: frame.id, name: frame.name }
"Query nodes
openpencil eval design.fig -c "
return figma.currentPage
.findAll((node) => node.type === 'FRAME' && node.name.includes('Button'))
.map((button) => ({
id: button.id,
name: button.name,
width: button.width,
height: button.height
}))
"Modify and save
Use --write / -w to write changes back to the input file:
openpencil eval design.fig -c "
figma.currentPage.children.forEach((node) => {
node.opacity = 0.5
})
" --writeUse --output / -o to write to a new file:
openpencil eval design.fig -c "figma.currentPage.name = 'Updated'" -o updated.figRead scripts from stdin
cat transform.js | openpencil eval design.fig --stdin --writeLive app mode
Omit the file path to run against the currently open document in the desktop app:
openpencil eval -c "return figma.currentPage.name"The desktop app must be running with a document open.
Output
By default, non-TTY output is JSON. Use --json to force JSON output:
openpencil eval design.fig -c "return figma.currentPage.children.map((n) => n.name)" --jsonUse --quiet / -q to suppress output when only writing a file.
The openpencil API
Next to figma, scripts get openpencil: what 4B Pen adds to the Figma Plugin API, in the same style. The same global is available to the eval tool and the running app's automation.
A main component or component set can behave as a Reka UI control. Bind it by its own property and slot names:
openpencil eval design.fig -w -c '
const set = figma.currentPage.findOne((n) => n.type === "COMPONENT_SET" && n.name === "Switch")
const behaviour = openpencil.setBehaviour(set, "switch")
.bindValue("value", "State") // On and Off guessed from the variants
.bindPart("thumb", set.findOne((n) => n.name === "Thumb")) // the frame becomes a slot
behaviour.states = "Interaction"
behaviour.missing
'openpencil.setBehaviour(node, kindOrSpec)replaces a component's behaviour: a kind alone, or a spec such as{ kind: "slider", parts: { track: "Track", thumb: "Thumb" }, numbers: { value: { min: 0, max: 10 } } }. It returns the behaviour.openpencil.getBehaviour(node)reads one, ornull; a variant reads its set's.- A behaviour has
kind,spec(by name),missing(required values and parts still unbound), andstates, andbindValue(valueId, property, { on, off }),bindPart(partId, slotNameOrFrame),setNumber(valueId, { min, max, step, default }), andremove(). openpencil.behaviourKindslists every kind with the values and parts it binds.openpencil.createSlot(frame)makes a frame of a main component a slot and returns its name.
Names the component does not have fail with an error that lists the ones it has.
Comments people leave with the Comment tool are part of the document, so scripts and agents can read the feedback on a file, act on it, and answer:
openpencil eval design.fig -w -c '
for (const comment of openpencil.getComments({ resolved: false })) {
console.log(comment.author, comment.node?.name, comment.text)
comment.reply("Bumped the title to 24px", { author: "Claude" }).resolve()
}
'openpencil.getComments(filter)lists threads oldest first: all of them, or{ resolved: false }for open ones,{ page }for one page's,{ author }for those a person started or replied to.openpencil.addComment(text, { node, page, x, y, author })pins a comment on a layer, which it follows, or at a point of a page; the current page by default.authoris"Agent"when left out.- A comment has
id,text(Markdown),author,createdAt,resolved,page,node(the top-level layer it is pinned to),position, andreplies, andreply(text, { author }),resolve(),reopen(), andremove().openpencil.getComment(id)finds one.
Supported API surface
The API is intentionally close to Figma's Plugin API, but it maps to 4B Pen's scene graph and file format.
Document and pages
figma.rootfigma.currentPagefigma.currentPage.selectionfigma.getNodeById(id)figma.createPage()
Node creation
figma.createFrame()figma.createRectangle()figma.createEllipse()figma.createText()figma.createLine()figma.createPolygon()figma.createStar()figma.createVector()figma.createNodeFromSvg(svg)figma.createComponent()figma.createSection()
Tree operations
node.childrennode.parentnode.appendChild(child)node.insertChild(index, child)node.clone()node.remove()node.findAll(callback?)node.findOne(callback)node.findChild(callback)node.findChildren(callback?)figma.group(nodes, parent)figma.ungroup(node)
Components
figma.createComponentFromNode(node)figma.combineAsVariants(components, parent)— properties come from names such asState=On, Size=Largecomponent.createInstance()instance.mainComponent
Variables
figma.getLocalVariables(type?)figma.getVariableById(id)figma.getLocalVariableCollections()figma.getVariableCollectionById(id)figma.createVariable(name, type, collectionId, value?)figma.setVariableValue(variableId, modeId, value)figma.deleteVariable(id)figma.createVariableCollection(name)figma.deleteVariableCollection(id)figma.bindVariable(nodeId, field, variableId)figma.unbindVariable(nodeId, field)
Properties
Common node properties are readable/writable through the proxy, including:
- Geometry:
x,y,width,height,rotation,resize(width, height) - Appearance:
fills,strokes,effects,opacity,visible,locked,blendMode,clipsContent - Radius:
cornerRadius,topLeftRadius,topRightRadius,bottomRightRadius,bottomLeftRadius - Text:
characters,fontSize,fontName,fontWeight,fills,letterSpacing,lineHeight, andtextDecoration, which readfigma.mixedwhile characters differ and restyle every character when set, and alignment - Text ranges:
getStyledTextSegments(fields, start?, end?), andgetRange…/setRange…forFontSize,FontName,Fills,LetterSpacing,LineHeight,TextDecorationwith itsStyle,Offset,Thickness,Color, andSkipInk,TextCase,Hyperlink(a{ type: 'URL' | 'NODE', value }link, ornull),TextStyleIdandFillStyleId(set withsetRangeTextStyleIdAsyncandsetRangeFillStyleIdAsync, which give the range the style's fields), andgetRangeBoundVariable/setRangeBoundVariablefor the text fields Figma binds, plusgetRangeFontWeight,getRangeAllFontNames, andgetRangeOpenTypeFeatures. Lengths read in pixels; a percent given to a setter is of the font size - Text lists and paragraphs:
getRangeListOptions/setRangeListOptionsfor bulleted (UNORDERED) and numbered (ORDERED) items,getRangeIndentation/setRangeIndentationfor nesting (1–5 for list items),listSpacing,paragraphSpacing, andparagraphIndentfor the whole text (figma.mixedwhen paragraphs differ) or per paragraph withgetRangeListSpacing/setRangeListSpacing,getRangeParagraphSpacing/setRangeParagraphSpacing, andgetRangeParagraphIndent/setRangeParagraphIndent, andhangingList - Auto-layout:
layoutMode,primaryAxisAlignItems,counterAxisAlignItems,itemSpacing, padding, sizing, and layout positioning fields - Stroke helpers:
strokeWeight,strokeAlign,dashPattern
Utilities
figma.mixedfigma.createImage(data)figma.loadFontAsync(fontName)no-ops because 4B Pen does not gate text edits on plugin font loadingfigma.listAvailableFontsAsync()returns host-provided fonts when availablefigma.getNodeByIdAsync(id)andinstance.getMainComponentAsync()resolve to the same nodes asfigma.getNodeById(id)andinstance.mainComponent, for scripts written for Figma's dynamic-page modefigma.notify(message)logs a warning in headless modeinstance.swapComponent(component)points an instance at another component- A layer inside an instance refuses
x,y,rotation,relativeTransform,constraints,isMask, andmaskTypewith Figma's error, such asin set_x: This property cannot be overridden in an instance: relative-transform; those always follow the component. Slot content keeps them, and setting one on a slot's default content makes the instance own the slot component.createSlot()adds a slot frame and itsSLOTproperty; slot frames readtype: 'SLOT',resetSlot()brings back an instance slot's component content, andlimitViolationslists the limits an instance slot breaks.addComponentPropertyandeditComponentPropertytake adescriptionand, for slots,slotSettingsfigma.viewport
Not yet Figma-compatible
These Figma APIs are not exposed as compatible helpers yet:
node.exportAsync()node.setBoundVariable(field, variable)figma.combineAsVariants(components, parent)- Figma style APIs such as
figma.createPaintStyle()/figma.createTextStyle() - Full vector boolean operation parity
Use 4B Pen CLI export commands, core tools, or direct scene-graph helpers where available.