Agent API
window.figpea is Figpea's curated, versioned, JSON-serializable command surface for agents and other external tools. It wraps the editor's design primitives (layer creation, mutation, history, export) behind a small, stable API, and it is available only when the ?agent=1 query parameter is present on the editor URL -- open https://editor.figpea.com/?agent=1 (or your local dev build with the same flag) and the API attaches shortly after the app's initial render.
Every method returns a structured result -- { ok: true, value } on success, or { ok: false, code, message } on failure -- and never throws across the boundary, so always check ok before touching value. Layers are addressed by string id; there are no live proxy references, so every returned value is plain, JSON-serializable data you can log, diff, or hand back to a model.
Documents window.figpea contract version 1.6.0.
Tier-1 recipe
The end-to-end loop for driving Figpea programmatically: open the editor with ?agent=1, call figpea.describe() to discover the live surface, author and refine layers, take a screenshot with your browser-automation MCP (Playwright MCP, Chrome DevTools MCP, or similar) and read figpea.session.layerTree() back to see what actually changed, then iterate.
The block below is the canonical, executable Tier-1 recipe. Wait for window.figpea to exist, then run await figpeaTier1Recipe(window.figpea):
// Figpea agent Tier-1 recipe — runs in a live editor tab opened with ?agent=1.
// Each call returns { ok, value } | { ok:false, code, message }; we throw on
// failure so any drift between these docs and the live API surfaces loudly.
async function figpeaTier1Recipe(figpea) {
const must = (r, label) => {
if (!r || r.ok !== true) {
throw new Error(`${label} failed: ${r && r.code ? r.code : "no result"}`);
}
return r.value;
};
// 1. Discover the surface — an agent's first call.
const manifest = figpea.describe();
if (!manifest || !manifest.session || !manifest.layer) {
throw new Error("describe() did not return the expected surface groups");
}
// 2. Author: a fresh project with a page, a rounded rectangle, and a caption.
await figpea.session.newProject();
const page = must(await figpea.layer.create("page", {
name: "Recipe Page", pageWidth: 400, pageHeight: 300,
}), "create(page)");
const rect = must(await figpea.layer.create("rect", {
parentId: page.id, rwidth: 160, rheight: 90,
style: { fill: "#4f46e5", cornerRadius: 8 },
}), "create(rect)");
must(await figpea.layer.setPosition(rect.id, { x: 40, y: 40 }), "setPosition(rect)");
const label = must(await figpea.layer.create("text", {
parentId: page.id, transform: [1, 0, 0, 1, 40, 160], text: "Made by an agent",
}), "create(text)");
// 3. Refine.
must(await figpea.layer.move(rect.id, 20, 0), "move(rect)");
must(await figpea.layer.stylePatch(label.id, { fontSize: 24, fill: "#111827" }), "stylePatch(label)");
// 4. Read the tree back — this is what you'd screenshot with your MCP, then iterate on.
const tree = must(await figpea.session.layerTree(), "layerTree()");
return { pageId: page.id, rectId: rect.id, labelId: label.id, tree };
}
The recipe returns { pageId, rectId, labelId, tree }. tree is the same shape as figpea.session.layerTree()'s result -- walk it to confirm the three created layers are present, then continue the screenshot-and-mutate loop from there. For an isolated visual check without leaving your browser-automation MCP's own screenshot step, figpea.canvas.screenshot() is the in-contract feedback-loop alternative documented under figpea.canvas below.
API reference
The full surface: figpea.version and figpea.describe(), then the eight groups -- figpea.session, figpea.layer, figpea.history, figpea.export, figpea.canvas, figpea.report, figpea.interaction, and figpea.component.
figpea.version
Type: string
Current API contract version (semver), independent of the product version. Value: 1.6.0.
Breaking changes by contract version
- 0.16.0 (AGENT_API_CONTRACT_VERSION):
figpea.describe()delivery shape change — bare call returns compact index; dotted selector drills into full schemas. - 0.31.0 (AGENT_API_CONTRACT_VERSION):
setPositionandrotatemechanics realigned from raw matrix translate to visibleboundstop-left and center pivot. - 1.0.0 (AGENT_API_CONTRACT_VERSION): MAJOR:
layer.createshape-origin parameters (x,yfor rects;cx,cyfor ellipses) removed from creation schemas. Callers must pass position viasetPosition(id, {x, y})ortransform. - 1.2.0 (AGENT_API_CONTRACT_VERSION): Declared mask model verbs (
setMask,clearMask) and property read-backs (mask,maskedBy). - 1.3.0 (AGENT_API_CONTRACT_VERSION): Parameter validation for
openFile(non-hanging fast failure), error-code catalog alignment, and asset harvest discovery sync. - 1.5.0 (AGENT_API_CONTRACT_VERSION): Added PSD layer effect support for gradient overlay, pattern overlay, outer glow, and inner glow via style patch keys.
- 1.6.0 (AGENT_API_CONTRACT_VERSION): Widened the
blendModeenum from 16 to 27 members (PSD blend-mode-engine compositor operators) — see theBlend mode:bullet understylePatchbelow for the full accepted list.
figpea.describe()
Signature: describe(): DescribeIndex and describe(selector: string): GroupDescriptor | MethodDescriptor | Record<string, string> | undefined
Works like a CLI's --help — call it bare to see what exists (a compact index of every group, each method's name and one-line doc), then drill with a selector to see how one thing works (its full parameter schema).
Bare describe(): returns a compact index — every group name, each method's name and one-line doc, plus version and the enumerable errorCodes keys; it deliberately omits params/byKind/result schema trees so the mandated first call stays small as the surface grows.
Drill shapes: describe("layer.create") returns one method's full { doc, params, result }; describe("layer") returns every method in a group; describe("errorCodes") returns the complete catalog. Unknown/malformed/non-string selector returns undefined — never throws.
The honesty beat: each drilled schema is byte-for-byte identical to the machine-readable schema it always was — a valid call is still constructible from describe() alone, no external docs; you just fetch the detail when you need it. A consumer that wants the whole surface (e.g. an MCP bridge building tool definitions) simply drills every group at startup, paying the full cost once, intentionally.
Prefer reading describe() over this page when you need the authoritative, up-to-the-minute shape.
// Discover what methods exist
const index = figpea.describe();
console.log(index.layer.create); // "Creates a new layer"
// Drill the full schema for one method
const method = figpea.describe("layer.create");
console.log(method.params, method.result);
figpea.session -- Project and Layer-Tree Operations
newProject()
Signature: newProject(): Promise<{ ok: true, value: { id: string } } | { ok: false, code: string, message: string }>
Creates a new, empty design project and makes it the active session. Result: the new project's id.
openFile(options)
Signature: openFile(options: { url: string, type: string, fileName?: string } | { bytes: ArrayBuffer | number[], fileName: string, type?: string }): Promise<{ ok: true } | { ok: false, code: string, message: string }>
Opens a design file (PSD, XD, Figma, SVG, or PDF) into the active session, either by URL ({ url, type, fileName? }) or by raw bytes ({ bytes, fileName, type? }) -- provide one variant, not a mix of both. After opening, the resulting layer tree is available via layerTree() or getSelection().
layerTree()
Signature: layerTree(): Promise<{ ok: true, value: FigpeaLayerNode } | { ok: false, code: string, message: string }>
Returns the curated, JSON-serializable layer tree from the active project's root, including hidden layers. Every node has this shape:
{
id: string;
name?: string;
layerType: string; // e.g. "page", "folder", "rect", "text", "image"
shape?: string; // present only for shape layers
visible: boolean;
transform: [a, b, c, d, e, f]; // 2D affine matrix
bounds: { // calculated bounding box
left: number;
top: number;
right: number;
bottom: number;
};
style?: {
fill?: string | { ... };
gradient?: { type, stops, geometry, transform };
fillType?: string;
strokeEnabled?: boolean;
strokeColor?: string;
strokeWidth?: number;
opacity?: number;
cornerRadius?: number | [number, number, number, number];
fontFamily?: string; // text layers only
fontSize?: number; // text layers only
fontWeight?: string; // text layers only
fontItalic?: boolean; // text layers only
align?: string; // text layers only
case?: string; // text layers only
patternScaleType?: string; // image/pattern fills only
patternRepeat?: string; // image/pattern fills only
};
constraints?: { horizontal?: string, vertical?: string };
resizeMode?: string;
effectiveResizeMode?: string;
mask?: { type: "alpha" | "vector" }; // present only on declared mask layers
maskedBy?: string; // present only on layers covered by a mask
rawText?: string; // present only for text layers
children?: FigpeaLayerNode[]; // present only for container layers
}
Geometry is always matrix-first (transform + bounds), never rect-based, and style only ever carries the whitelisted keys documented under stylePatch below -- the full 35-key round-trip, read from the same internal fields the write side writes to (a key can't be settable without also being readable). Only keys actually set on the layer are present in a given node. All nodes are plain JSON -- no live proxy references. Honesty boundaries: for gradient-filled layers, style contains the entire gradient object (type, stops, geometry, transform) because gradients are small pure JSON with nothing held back — the deliberate opposite of the image-fill raster boundary stated below. For image/pattern-filled layers, style reports only the current fit/tile mode (patternScaleType/patternRepeat) -- the underlying image bytes (and the "free"-mode matrix) are deliberately never serialized back.
layerById(id)
Signature: layerById(id: string): Promise<{ ok: true, value: FigpeaLayerNode } | { ok: false, code: string, message: string }>
Same node shape as layerTree(), but for a single layer id (with its subtree if it's a container). Returns { ok: false, code: "not_found" } if the id doesn't exist.
getSelection() / setSelection(ids)
Signatures: getSelection(): Promise<{ ok: true, value: string[] } | { ok: false, code: string, message: string }> and setSelection(ids: string[]): Promise<{ ok: true } | { ok: false, code: string, message: string }>
Read or set the ids of the layer(s) currently selected in the active session. Pass [] to setSelection to clear the selection; unknown ids are silently ignored.
find(selector?)
Signature: find(selector?: object): Promise<{ ok: true, value: FigpeaLayerNode[] } | { ok: false, code: string, message: string }>
Queries the layer tree by attribute, returning all matching nodes in document order (depth-first pre-order). A pure read that does not mutate the current selection. All selector fields are optional and AND-combined; empty/omitted selector matches every node (returns the entire tree as a flat array). Selector fields: type?, name?, nameRegex? + nameRegexFlags?, text? (text layers only), textRegex? + textRegexFlags?, visible?, hasChildren?. Returns { ok: false, code: "invalid_selector" } if the selector contains an unknown key or an uncompilable regex pattern/flags.
// Find all text layers
const textLayers = await figpea.session.find({ type: "text" });
// Find layers matching a regex name, case-insensitive
const headers = await figpea.session.find({ nameRegex: "^header", nameRegexFlags: "i" });
waitForIdle(options?)
Signature: waitForIdle(options?: { timeout?: number }): Promise<{ ok: true } | { ok: false, code: string, message: string }>
Waits for the active session to become idle -- the layer model populated and the canvas has drawn a settled frame -- the render-settle signal for an agent's mutate -> settle -> observe loop. Bounded by a timeout (default 10 seconds); resolves promptly if already idle, is safe to call repeatedly, and never rejects or hangs. Result: void on success; { ok: false, code: "timeout" } if the deadline elapses before a settled frame is drawn.
await figpea.layer.create("rect", { rwidth: 100, rheight: 50 });
const settled = await figpea.session.waitForIdle();
if (settled.ok) {
const shot = await figpea.canvas.screenshot(); // canvas has finished drawing
}
figpea.layer -- Layer Authoring Operations
All layer operations apply to the active session and share the same undo/redo history as the UI -- each call is recorded as exactly one undo step, regardless of wall-clock timing.
create(kind, props)
Signature: create(kind: string, props: object): Promise<{ ok: true, value: { id: string } } | { ok: false, code: string, message: string }>
Creates a new layer of the given kind -- one of "rect", "ellipse", "line", "polygon", "path", "text", "folder", "page", "image" -- and attaches it under props.parentId (or the project root if omitted). Common props fields: parentId?, index?, name?, transform?: [a,b,c,d,e,f], style? (whitelisted keys only, see stylePatch). Every kind has its own real geometry fields -- there is no generic width/height:
"rect":rwidth?(default100),rheight?(default50)"ellipse":rx?(default50),ry?(default50)"line":x2?(default100),y2?(default0) -- runs from the local origin to(x2, y2)"polygon":points?: { x: number, y: number }[]"path":path?: string(SVG path data, default"M0 0")"text":text?: string(default"") -- position text viatransformorsetPositionafter creation"folder": no kind-specific fields"page":pageWidth?(default300),pageHeight?(default150);namesets the page title. Pages are auto-placed clear of existing pages when no explicit transform is given."image":url?orbytes?(exactly one), optionalmimeType?, plusrwidth?/rheight?like"rect", plus optionalpatternScaleType?: "fit" | "cover" | "fill" | "free"(default"cover") andpatternRepeat?: "no-repeat" | "repeat" | "repeat-x" | "repeat-y"(default"no-repeat") to control image fit/tile mode
Result: the newly created layer's id. Note: create() specifies extent only; use setPosition(id, { x, y }) after creation to place the layer precisely.
duplicate(id)
Signature: duplicate(id: string): Promise<{ ok: true, value: { id: string } } | { ok: false, code: string, message: string }>
Duplicates an existing layer and attaches the clone as a sibling immediately after id. Returns the new layer's id.
move(id, dx, dy) / resize(id, size) / rotate(id, deg) / flipX(id) / flipY(id)
Pan a layer by a local delta (move), resize it to an absolute local target { width?, height? } (resize), rotate it by degrees around its visible box center (rotate), or flip it in place (flipX/flipY). All return Promise<{ ok: true } | { ok: false, code: string, message: string }>.
setPosition(id, pos) / setTransform(id, matrix)
Signatures: setPosition(id: string, pos: { x: number, y: number }): Promise<{ ok: true } | { ok: false, code: string, message: string }> and setTransform(id: string, matrix: [a, b, c, d, e, f]): Promise<{ ok: true } | { ok: false, code: string, message: string }>
setPosition sets a layer's absolute local position — placing its visible bounding box (bounds) top-left corner at (x, y) — leaving rotation/scale untouched; repeating the same call is idempotent. setTransform sets the full affine matrix verbatim (no decompose/recompose), so a read immediately afterward deep-equals exactly what was set. Result: void on success; setPosition returns { ok: false, code: "invalid_position" } if x/y aren't both finite numbers, setTransform returns { ok: false, code: "invalid_transform" } if matrix isn't an array of exactly 6 finite numbers.
await figpea.layer.setPosition(layerId, { x: 100, y: 100 });
await figpea.layer.setTransform(layerId, [5, 0, 1.5, 1, 40, 30]); // scale x5, shear, translate
setMask(id, type) / clearMask(id)
Signatures: setMask(id: string, type: "alpha" | "vector"): Promise<{ ok: true } | { ok: false, code: string, message: string }> and clearMask(id: string): Promise<{ ok: true } | { ok: false, code: string, message: string }>
setMask declares a layer to be a mask over its preceding sibling / container, specifying whether masking uses "alpha" or "vector" mode. clearMask un-declares mask behavior. Returns { ok: false, code: "invalid_mask_type" } if type is invalid.
setConstraints(id, patch) / setResizeMode(id, mode)
Signatures: setConstraints(id: string, patch: { horizontal?: "left" | "right" | "leftAndRight" | "center" | "scale", vertical?: "top" | "bottom" | "topAndBottom" | "center" | "scale" }): Promise<{ ok: true } | { ok: false, code: string, message: string }> and setResizeMode(id: string, mode: "content" | "viewport" | "constraint"): Promise<{ ok: true } | { ok: false, code: string, message: string }>
setConstraints sets a layer's per-child constraint axes (used when its parent is in constraint mode); the patch merges against the layer's current/default constraints, so a partial {horizontal?}- or {vertical?}-only call preserves the other axis. setResizeMode sets a container's (page or group/folder) resize mode: "content" hugs to its children, "viewport"/"constraint" give it a fixed frame that clips overflow; a page cannot use "content" mode. Returns { ok: false, code: "invalid_constraint" } if an axis/mode value is outside its enum, if "content" mode is applied to a page, or if the target is neither a page nor a folder.
placeProjectImage(hash)
Signature: placeProjectImage(hash: string): Promise<{ ok: true, value: { id: string } } | { ok: false, code: string, message: string }>
Places an image already in the project -- identified by the sha256 content hash figpea.report.projectImages() reports -- onto the canvas as a new rect layer, sized to the image's intrinsic pixel dimensions. Placement is fully automatic (there is no parentId/x/y param): it parents into the selected page/layer's page, or the project root at the viewport centre when nothing is selected, and positions at the viewport centre when that point lies on the target page (otherwise the centre of the page's on-screen region, or the page's own centre if off-screen). The camera never moves. Selected on creation. One undo step. Reuses the library's existing image instance rather than decoding a second copy. Returns { ok: false, code: "not_found" } for an unknown hash or when no session is active.
stylePatch(id, patch)
Signature: stylePatch(id: string, patch: object): Promise<{ ok: true } | { ok: false, code: string, message: string }>
Patches a layer's style with an explicit whitelist of public keys -- any key not on the list is rejected with { ok: false, code: "unsupported_style_key" }:
- Fill & stroke:
fill,fillType,strokeEnabled,strokeColor,strokeWidth - Gradient fill:
gradient—type: "linear" | "radial" | "conic", required non-emptystops: [{ offset, color }], optional per-type geometry (linearX1/Y1/X2/Y2for linear;innerCircleX/Y/R+outterCircleX/Y/Rfor radial;startAngle/centerX/centerYfor conic) and optionaltransformmatrix (a 6-number affine applied in the gradient's paint space; its translate components are in normalized layer-local units, 0..1 across the layer, the same space as the geometry fields); bothtypeandstopsmust be supplied in the same patch asfillType: "gradient"(settingfillTypealone is not valid). Malformed gradient →invalid_style_value, with the layer's prior fill left completely unchanged. - Stroke controls:
strokePosition("inside" | "center" | "outside"),lineCap("butt" | "round" | "square"),lineJoin("bevel" | "miter" | "round"),dashArray(number array, e.g.[4, 2]),dashOffset(number),miterLimit(number) - Effects:
dropShadow/innerShadow({ enabled, dx, dy, r, color }),objectBlur({ enabled, r }),backgroundBlur({ enabled, r, brightness, opacity }),overlay({ enabled, color }),gradientOverlay({ enabled: boolean, gradient?: { type: "linear" | "radial" | "conic", stops: Array<{ offset: number, color: string }> }, blendMode?: string, opacity?: number, reverse?: boolean }—reverseflips the gradient's stop order at render time),patternOverlay({ enabled: boolean, pattern?: object, scale?: number, origin?: { x: number, y: number }, blendMode?: string, opacity?: number }—patternis the decoded PSD pattern bitmap set by the file's own decoder; it is not independently authorable through this call, so apatternOverlaypatch is effectively limited to togglingenabled/scale/origin/blendMode/opacityon an already-decoded pattern),outerGlow({ enabled: boolean, color: string, size: number, spread?: number, blendMode?: string, opacity?: number }—spread(0-100) narrows the effective blur as it increases),innerGlow({ enabled: boolean, color: string, size: number, choke?: number, blendMode?: string, opacity?: number, source?: "edge" | "center" }—choke(0-100) is inner glow's equivalent ofspread;sourcepicks whether the glow hugs the shape's silhouette ("edge", default) or radiates from its local-bounds center ("center")) - Blend mode:
blendMode(accepted:"normal" | "dissolve" | "darken" | "multiply" | "color-burn" | "linear-burn" | "darker-color" | "lighten" | "screen" | "color-dodge" | "linear-dodge" | "lighter-color" | "overlay" | "soft-light" | "hard-light" | "vivid-light" | "linear-light" | "pin-light" | "hard-mix" | "difference" | "exclusion" | "subtract" | "divide" | "hue" | "saturation" | "color" | "luminosity") - Transparency:
opacity - Geometry:
cornerRadius(number, or a[tl, tr, br, bl]tuple) - Text style (text layers only):
fontFamily,fontSize,fontWeight,fontItalic,align,case("upper" | "lower" | "title"— layer-level default only, no ranged authoring; out-of-enum →invalid_style_value) - Text-frame sizing:
fixWidth/fixHeight(numbers; omit for auto-width / auto-height).valign("top" | "center" | "bottom") — only visible whenfixHeightis set. - Image fill mode (image/pattern layers only):
patternScaleType("fit" | "cover" | "fill" | "free"),patternRepeat("no-repeat" | "repeat" | "repeat-x" | "repeat-y")
This is the full 35-key round-trip -- the same keys layerTree()/layerById() read back under style above.
await figpea.layer.stylePatch(layerId, {
fill: "#ff0000",
opacity: 0.8,
strokeWidth: 2,
cornerRadius: [4, 4, 8, 8],
blendMode: "luminosity",
});
setImageFill(id, source)
Signature: setImageFill(id: string, source: { url: string, mimeType?: string, patternScaleType?: "fit" | "cover" | "fill" | "free", patternRepeat?: "no-repeat" | "repeat" | "repeat-x" | "repeat-y" } | { bytes: ArrayBuffer | Uint8Array | number[], mimeType?: string, patternScaleType?: "fit" | "cover" | "fill" | "free", patternRepeat?: "no-repeat" | "repeat" | "repeat-x" | "repeat-y" }): Promise<{ ok: true } | { ok: false, code: string, message: string }>
Attaches an image fill to an existing layer (any kind), choosing the fit/tile mode in the same call. Validate-before-mutate: any supplied patternScaleType/patternRepeat is checked first (out-of-enum -- invalid_style_value, nothing touched), then the source is resolved to a loaded image (fetch/decode failure -- invalid_image_source, nothing touched).
Parameters:
id(string) — Layer id.source(object) — Image source (exactly one ofurl/bytesis required):url?: string— HTTP(S) URL to an image.bytes?: ArrayBuffer | Uint8Array | number[]— Raw image bytes.mimeType?: string— used when constructing frombytes.patternScaleType?: "fit" | "cover" | "fill" | "free"(default"cover") andpatternRepeat?: "no-repeat" | "repeat" | "repeat-x" | "repeat-y"(default"no-repeat") — same enums asstylePatch/create("image"), to control image fit/tile mode.
Result: Void on success; { ok: false, code: "not_found" } for an unknown layer id; { ok: false, code: "invalid_style_value" } if patternScaleType/patternRepeat is outside its enum; { ok: false, code: "invalid_image_source" } if the source (url fetch or bytes decode) failed to resolve to a usable image.
await figpea.layer.setImageFill(layerId, { url: "https://example.com/image.png" });
// Read it back:
const node = await figpea.session.layerById(layerId);
if (node.ok && node.value.style?.fillType === "pattern") {
console.log(`Fit mode: ${node.value.style.patternScaleType}`);
}
setText(id, text) / setVisible(id, visible) / delete(id)
Replace a text layer's content (setText), toggle visibility (setVisible), or remove a layer from its parent (delete). All return Promise<{ ok: true } | { ok: false, code: string, message: string }>.
group(ids) / ungroup(id)
Signatures: group(ids: string[]): Promise<{ ok: true, value: { id: string } } | { ok: false, code: string, message: string }> and ungroup(id: string): Promise<{ ok: true } | { ok: false, code: string, message: string }>
Group the given layer ids into a new folder layer at the first id's position, or ungroup a folder layer, promoting its children to its own parent.
reorder(id, targetId, pos) / reparent(id, parentId, index?)
Move a layer to a sibling position relative to targetId (pos is "before", "on", or "after"), or move it into a new parent's children at an optional index (defaults to appending at the end). Both return Promise<{ ok: true } | { ok: false, code: string, message: string }>.
clip(id) / unclip(id)
Remove a layer from its parent's children and turn it into the clip mask for the sibling immediately behind it (the previous entry in the parent's children array) (clip), or restore a clip mask as a visible sibling again (unclip). Both legacy clip helpers execute as exactly one undo step each when called via batching.
batch(ops)
Signature: batch(ops: { method: string, args: unknown[] }[]): Promise<{ ok: true, value: { results: OpResult[] } } | { ok: false, code: string, message: string, failedIndex: number, results: OpResult[] }>
Applies a sequence of other figpea.layer.* ops as one undo step -- a single figpea.history.undo() reverses the whole batch. All-or-nothing: if any op fails, the whole batch rolls back (no partial application, no undo step created) and the result reports the failing op's own code/message plus its failedIndex. method: "batch" (recursion) or any unrecognized method name is rejected per-op with { ok: false, code: "unsupported_op" }.
// Build a 2-rect layout as ONE undo step.
const result = await figpea.layer.batch([
{ method: "create", args: ["rect", { parentId: pageId, x: 0, y: 0, rwidth: 50, rheight: 50 }] },
{ method: "create", args: ["rect", { parentId: pageId, x: 60, y: 0, rwidth: 50, rheight: 50 }] },
]);
if (result.ok) {
const ids = result.value.results.map((r) => r.value.id);
await figpea.history.undo(); // removes both rects
} else {
console.log(`batch failed at op ${result.failedIndex}: ${result.code}`);
}
select(target)
Signature: select(target: string | string[]): Promise<{ ok: true } | { ok: false, code: string, message: string }>
Sets the active session's current selection to the given layer id(s), rendering the transform widget for them -- the ergonomic authoring-surface wrapper over the same selection path figpea.session.setSelection uses, accepting either a single id or an array for multi-select. Result: void on success; { ok: false, code: "not_found" } if any given id doesn't resolve to a layer.
// Select one layer, then read where its transform widget rendered:
await figpea.layer.select(rectId);
const geometry = await figpea.canvas.widgetGeometry();
figpea.history -- Undo/Redo
All figpea.layer calls are recorded in a shared history with the UI's undo/redo -- one API call, one undo step.
undo() / redo()
Signatures: undo(): Promise<{ ok: true } | { ok: false, code: string, message: string }> and redo(): Promise<{ ok: true } | { ok: false, code: string, message: string }>
Revert or re-apply the most recent step on the active session's history. Prefer undo() over hand-computing an inverse mutation while iterating -- it's cheaper and safer, and it shares the same stack as the UI.
figpea.interaction -- Interaction / Prototype Authoring
Author and read back prototype interactions (tap, time, keys-gamepad, hover triggers; navigate, artboard-transition, auto-animate, overlay-transition, previous-artboard, scroll-transition, hyperlink actions). Each operation is one undo step and free model mutation; authoring does not enable Pro-gated Present-mode playback.
create(layerId, input)
Signature: create(layerId: string, input: { trigger: "tap" | "time" | "keys-gamepad" | "hover", action: "navigate" | "artboard-transition" | "auto-animate" | "overlay-transition" | "previous-artboard" | "scroll-transition" | "hyperlink", targetId?: string, properties?: object }): Promise<{ ok: true, value: { id: string } } | { ok: false, code: string, message: string }>
Authors an interaction on a layer — a trigger that navigates or transitions to a target. Validation runs before any mutation: unsupported trigger/action, missing/invalid targetId, or non-page target all reject with invalid_interaction.
Result: the newly created interaction's id on success, not_found if the layer or target page don't exist, or invalid_interaction on validation failure.
const result = await figpea.interaction.create(rectId, {
trigger: "tap",
action: "navigate",
targetId: pageId,
properties: { duration: 0.3 }
});
if (result.ok) {
console.log("Interaction created:", result.value.id);
}
list(layerId)
Signature: list(layerId: string): { ok: true, value: { id: string, trigger: string, action: string, targetId?: string, properties?: object }[] } | { ok: false, code: string, message: string } (synchronous -- no Promise)
Lists all interactions on a layer. Returns an empty array for layers with no interactions.
const interactions = figpea.interaction.list(rectId);
if (interactions.ok) {
for (const interaction of interactions.value) {
console.log(`${interaction.trigger} → ${interaction.action} to ${interaction.targetId}`);
}
}
remove(interactionId)
Signature: remove(interactionId: string): Promise<{ ok: true } | { ok: false, code: string, message: string }>
Removes an interaction by id from the active project.
figpea.component -- Component State Authoring, Instance Creation & Overrides
Author component instances, manage component states, convert layers to reusable components, and inspect/reset instance overrides. All methods are free -- no entitlement gating.
The eleven supported methods in this group are:
component.create(layerId)component.placeInstance(componentId)component.addState(componentId, input?)component.setActiveState(componentId, input?)component.getStates(componentId)component.getChildOverrides(childId)component.getOverrides(instanceId)component.resetOverride(childId, input)component.resetAllOverrides(instanceId)component.removeChild(childId)component.restoreChild(instanceId, input)
Every signature and undo claim below is copied verbatim from v3's live component.descriptor.ts -- never assume undo behavior by analogy between methods, each one is independently measured.
create(layerId)
Signature: create(layerId: string): Promise<{ ok: true, value: { id: string } } | { ok: false, code: string, message: string }>
Converts a plain layer to a component and registers it in the project's component library. The layer stays in its current page position; selects the new component. One undo step: a single figpea.history.undo() fully reverses the whole conversion -- com/states are cleared and the layer is deregistered from the component library; figpea.history.redo() re-applies it.
placeInstance(componentId)
Signature: placeInstance(componentId: string): Promise<{ ok: true, value: { id: string } } | { ok: false, code: string, message: string }>
Creates an instance of a component and adds it to the master's own page, offset from the master so the two do not overlap. Selects the new instance. NOT undoable via the app's undo stack -- adding the instance is a repo-level layer mutation, the same deliberate history exclusion applied to any repo-level add.
addState(componentId, input?)
Signature: addState(componentId: string, input?: { name?: string }): Promise<{ ok: true, value: { id: string, name: string } } | { ok: false, code: string, message: string }>
Adds a new state to a component and makes it the active state. Omit input.name (or the whole input object) for a default "State {n}" name. NOT undoable via the app's undo stack -- creating the state is a repo-level layer mutation, the same deliberate history exclusion as placeInstance's.
setActiveState(componentId, input?)
Signature: setActiveState(componentId: string, input?: { stateId?: string }): Promise<{ ok: true } | { ok: false, code: string, message: string }>
Switches a component's active state by id, or clears it to Base (omit input.stateId, or the whole input object). One undo step for ANY state transition -- named-to-named or to/from Base alike: a single figpea.history.undo() reverses the switch either way.
getStates(componentId)
Signature: getStates(componentId: string): { ok: true, value: { states: [{ id: string, name: string, active: boolean }], activeStateId?: string } } | { ok: false, code: string, message: string } (synchronous -- no Promise)
Returns the list of available states on a component. Imported component states (from Adobe XD or Figma files) are fully surfaced by getStates() and retain their original source file state names.
getChildOverrides(childId)
Signature: getChildOverrides(childId: string): { ok: true, value: { instanceId?: string, groups: ("transform" | "fill" | "text" | "visibility")[] } } | { ok: false, code: string, message: string } (synchronous -- no Promise)
Reconcile-and-read: recomputes which override groups childId currently diverges from its enclosing instance's master, syncing the instance's record as a side effect. A layer with no enclosing instance (plain layer, master child, or the instance root itself) is not an error -- returns { instanceId: undefined, groups: [] }.
getOverrides(instanceId)
Signature: getOverrides(instanceId: string): { ok: true, value: { changedProps: { childId: string, groups: string[] }[], removedChildren: { baseChildId: string, name: string, icon: string }[] } } | { ok: false, code: string, message: string } (synchronous -- no Promise)
Full read of one instance's Overrides block: every child with a currently-recorded overridden-groups entry, plus every removed child resolved via its recorded base id.
resetOverride(childId, input)
Signature: resetOverride(childId: string, input: { group: "transform" | "fill" | "text" | "visibility" }): Promise<{ ok: true } | { ok: false, code: string, message: string }>
Clears one overridden group on a child -- its own value for that group's keys is removed (falls back to tracking the master live) or copied from the master's current value. Other overridden groups on the same child are left untouched. A childId with no enclosing instance, or a group outside the four recognized names, is a safe no-op (ok(undefined), no mutation), never an error. One undo step, faithfully reversible in both directions.
resetAllOverrides(instanceId)
Signature: resetAllOverrides(instanceId: string): Promise<{ ok: true } | { ok: false, code: string, message: string }>
"Reset all changes" on an instance: clears every overridden group plus the other inspector-editable style keys on every descendant with a resolvable master child, and restores every removed child -- bounded to one instance's subtree. One undo step whenever the instance has at least one overridden group to clear or at least one removed child to restore; zero undo steps only when there is genuinely nothing to reset.
removeChild(childId)
Signature: removeChild(childId: string): Promise<{ ok: true } | { ok: false, code: string, message: string }>
Removes a child from its enclosing instance only (the master and every sibling instance are untouched) -- records the child's base id so it can be restored later via restoreChild. A no-op if childId has no enclosing instance. One undo step.
restoreChild(instanceId, input)
Signature: restoreChild(instanceId: string, input: { baseChildId: string }): Promise<{ ok: true, value: { id: string } } | { ok: false, code: string, message: string }>
Restores a previously-removed child on an instance by its recorded base id -- re-instances it from the master child at the master's child-order index (restore always re-instances; it never re-attaches the original detached child, which would not survive a project serialize/restore round-trip). One undo step.
const states = figpea.component.getStates(componentId);
if (states.ok) {
console.log(`Active: ${states.value.activeStateId || "Base"}`);
}
figpea.export -- Layer and Project Export
UI soft graces do not apply to the Agent API. The interactive editor UI includes session-based soft graces (such as 6 free scaled exports per session) for human trial sampling. The Agent API does not grant these soft graces — all programmatic export calls (
scale ≠ 1, PDF, ZIP, spec bundles, tokens, originals, asset harvest, Figma Migration Kit, contact sheet, flow poster) strictly enforce claim checks (hasClaim(...)) and require an active Pro plan or Rescue Pass.
All export operations honor the same entitlement gating as the UI (via hasClaim(...)), returning { ok: false, code: "entitlement_required" } instead of popping an upgrade modal when access is denied. Authoring under figpea.session, figpea.layer, and figpea.history is always free; export is the one boundary that can require a plan.
layer(id, input) / artboard(id, input)
Signature: layer(id: string, input: { format: "png" | "jpeg" | "webp" | "svg", scale?: number, suffix?: string }): Promise<{ ok: true, value: { bytes: string, mime: string, filename: string } } | { ok: false, code: string, message: string }>
Exports a single layer as a raster image or SVG. scale ≠ 1 requires hasClaim("extract.scale"); format === "svg" requires hasClaim("extract.svg"). artboard(id, input) has the identical signature and gating -- an "artboard" is just a page/folder layer addressed like any other layer id.
Result: the exported bytes as a base64 string, a MIME type, and a suggested filename. Decode the bytes with Uint8Array.from(atob(bytes), c => c.charCodeAt(0)) -- not new Blob([atob(bytes)], ...), which corrupts anything outside the Latin-1 range because atob produces a binary string, not raw bytes:
const result = await figpea.export.layer(layerId, { format: "png", scale: 2 });
if (result.ok) {
const { bytes, mime, filename } = result.value;
const byteArray = Uint8Array.from(atob(bytes), (c) => c.charCodeAt(0));
const blob = new Blob([byteArray], { type: mime });
// download or process blob...
} else if (result.code === "entitlement_required") {
console.log("User must upgrade to export at this scale");
}
project(input)
Signature: project(input: { format: "pdf" | "zip" | "figpea", size?: number, imageFormat?: "png" | "jpeg" | "webp" }): Promise<{ ok: true } | { ok: false, code: string, message: string }>
Exports the whole active project as PDF (requires hasClaim("export.pdf")), ZIP of per-layer rasters (requires hasClaim("export.zip")), or the native Figpea format (ungated). size/imageFormat apply to the ZIP format's raster output.
const result = await figpea.export.project({ format: "pdf" });
if (!result.ok && result.code === "entitlement_required") {
console.log("User must upgrade to export PDF");
}
specBundle() / tokens()
Signatures: specBundle(): Promise<{ ok: true, value: { html: string } } | { ok: false, code: string, message: string }> and tokens(): Promise<{ ok: true, value: { css: string, json: string } } | { ok: false, code: string, message: string }>
Developer-handoff exports. specBundle() returns a self-contained, offline HTML string (zero external references) covering every artboard's inspectable elements -- position, size, fills, typography, copyable CSS. tokens() returns the active project's distinct fill colors and text styles as CSS custom properties (css) plus a documented JSON subset (json), with deterministic naming across consecutive exports. Both take no arguments and return their text directly rather than triggering a download. Both require hasClaim("export.zip") -- the same claim project({ format: "zip" }) gates on. They are not free, the same "authoring free, export gated" boundary above applies to them too.
const bundle = await figpea.export.specBundle();
const tokens = await figpea.export.tokens();
if (bundle.ok && tokens.ok) {
console.log(bundle.value.html.length, tokens.value.css);
} else if (!bundle.ok && bundle.code === "entitlement_required") {
console.log("User must upgrade to export the spec bundle / tokens");
}
originals()
Signature: originals(): Promise<{ ok: true, value: { bytes: string, mime: "application/zip", filename: string } } | { ok: false, code: string, message: string }>
Recovers every distinct (by content hash) original embedded image in the active project as a ZIP of the verbatim original bytes -- no re-encode, byte-identical to the source -- the agent-API mirror of "Recover Original Images." Takes no arguments and returns the ZIP as base64 bytes rather than triggering a download.
Result: ZIP bytes as a base64 string, mime: "application/zip", and a suggested filename.
const result = await figpea.export.originals();
if (result.ok) {
const { bytes, filename } = result.value;
const blob = new Blob([Uint8Array.from(atob(bytes), (c) => c.charCodeAt(0))], { type: "application/zip" });
// save or process blob...
} else if (result.code === "entitlement_required") {
console.log("User must upgrade to recover original images");
}
assetHarvest(options?)
Signature: assetHarvest(options?: { preset?: "web" | "ios" | "android" }): Promise<{ ok: true, value: { bytes: string, mime: "application/zip", filename: string } } | { ok: false, code: string, message: string }>
Harvests every detected asset (embedded images and icon-sized vector groups) from the active project as a preset-formatted ZIP with platform-appropriate scales, formats, and folder layout -- the agent-API mirror of the "Harvest Assets" gallery modal. preset defaults to "web"; the agent-API call exports all detected assets (selection is a UI-only affordance). Browsing the gallery is free -- only the export call itself is gated.
Result: ZIP bytes as a base64 string, mime: "application/zip", and a suggested filename.
const result = await figpea.export.assetHarvest({ preset: "ios" });
if (result.ok) {
const { bytes, filename } = result.value;
// save or process the ZIP bytes...
} else if (result.code === "entitlement_required") {
console.log("User must upgrade to export assets");
}
// Harvest with the default (web) preset:
const webAssets = await figpea.export.assetHarvest();
figmaKit()
Signature: figmaKit(): Promise<{ ok: true, value: { bytes: string, mime: "application/zip", filename: string } } | { ok: false, code: string, message: string }>
Assembles the Figma Migration Kit: a Figma-tuned SVG for every top-level artboard (text stays editable, named layers/groups keep their names) plus an assets/ folder, fonts.md, copy-deck.md, and README.md, all in one ZIP -- the agent-API mirror of the "Figma Migration Kit" export option. This emits new Figma-tuned output; it does not save back into an existing Figma file. Takes no arguments.
Result: ZIP bytes as a base64 string, mime: "application/zip", and a suggested filename.
const result = await figpea.export.figmaKit();
if (result.ok) {
const { bytes, filename } = result.value;
// save or process the ZIP bytes...
} else if (result.code === "entitlement_required") {
console.log("User must upgrade to export the Figma Migration Kit");
}
Entitlement note: like specBundle()/tokens() above, originals(), assetHarvest(), and figmaKit() all require hasClaim("export.zip") -- they are not free -- the same claim project({ format: "zip" }) gates on. The "authoring free, export gated" boundary applies to every developer-handoff export in this section, with no exceptions.
contactSheet()
Signature: contactSheet(): Promise<{ ok: true, value: { bytes: string, mime: "application/pdf", filename: string } } | { ok: false, code: string, message: string }>
Exports the whole active project's contact-sheet PDF — a cover page (project name, stats, font manifest) plus one titled page per artboard in document order. Takes no arguments.
Gating: Requires hasClaim("export.pdf") -- the same claim project({ format: "pdf" }) above gates on (the contact sheet is a PDF; no new claim key).
Result: PDF bytes as a base64 string, mime: "application/pdf", and a suggested filename.
const result = await figpea.export.contactSheet();
if (result.ok) {
const { bytes, filename } = result.value;
const blob = new Blob([Uint8Array.from(atob(bytes), (c) => c.charCodeAt(0))], { type: "application/pdf" });
// save or process blob...
} else if (result.code === "entitlement_required") {
console.log("User must upgrade to export the contact sheet");
}
flowPoster(input?)
Signature: flowPoster(input?: { renderFormat?: "svg" | "png" | "pdf" }): Promise<{ ok: true, value: { bytes: string, mime: string, filename: string } } | { ok: false, code: string, message: string }>
Exports the project's prototype flow poster — one node per artboard (thumbnail + name), one edge per parsed prototype transition — as SVG (default) or a PNG/PDF render of that same SVG. GATED on hasClaim("export.pdf").
Result: Bytes as a base64 string, MIME type (matching renderFormat), and a suggested filename.
const result = await figpea.export.flowPoster({ renderFormat: "png" });
if (result.ok) {
const { bytes, mime, filename } = result.value;
const blob = new Blob([Uint8Array.from(atob(bytes), (c) => c.charCodeAt(0))], { type: mime });
// save or process blob...
} else if (result.code === "entitlement_required") {
console.log("User must upgrade to export the flow poster");
}
figpea.canvas -- Camera Control, Live Screenshot, Layer Geometry & Widget Geometry
A visual-feedback aid for driving the editor programmatically: camera (viewport) control, a live capture of what's actually on the canvas right now, and a geometry read-out of the currently-rendered transform widget. All of figpea.canvas is ungated -- none of it routes through the entitled export encoder path, because none of it is an export artifact; it's the agent's own equivalent of looking at the screen (and moving its own eyes to the right place first).
Recommended feedback loop: mutate (figpea.layer.*) -> figpea.canvas.fit(pageId) (bring the new/changed content into frame, mirroring the UI's zoom-to-fit) -> your browser-automation MCP's own native screenshot -> iterate. figpea.canvas.screenshot() below is the explicit no-camera-move fallback, for an isolated single-layer or off-screen check that doesn't disturb whatever the camera is currently framing.
fit(id?)
Signature: fit(id?: string): Promise<{ ok: true, value: FigpeaViewport } | { ok: false, code: string, message: string }>
Pans/zooms the editor camera so the given layer/page/artboard (or the whole design when id is omitted) is framed within the visible viewport, centered with ~10% padding. A degenerate/zero-area target (e.g. an empty new project) is a no-op that returns the current viewport unchanged (ok, not an error).
const rect = await figpea.layer.create("rect", { rwidth: 100, rheight: 50, style: { fill: "#ff0000" } });
await figpea.canvas.fit(rect.value.id); // frame the new shape
getViewport() / setViewport(viewport)
Signatures: getViewport(): Promise<{ ok: true, value: FigpeaViewport } | { ok: false, code: string, message: string }> and setViewport(viewport: { x: number, y: number, zoom: number }): Promise<{ ok: true, value: FigpeaViewport } | { ok: false, code: string, message: string }>
getViewport reads the current editor camera as a world-space visible rectangle -- FigpeaViewport is { x, y, width, height, zoom }, where { x, y } is the world coordinate at the DOM viewport's top-left corner, zoom is the stage scale factor, and width/height are derived, read-only world extent. This is the same world basis figpea.session.layerTree()/layerById()'s bounds use, so a layer's bounds can be compared directly against the viewport. setViewport is the exact inverse: it accepts only { x, y, zoom } (width/height are outputs of getViewport/fit, never accepted here), and setViewport(v) followed by getViewport() round-trips v (within floating-point tolerance).
await figpea.canvas.setViewport({ x: 0, y: 0, zoom: 1 });
const viewport = await figpea.canvas.getViewport();
screenshot(options?)
Signature: screenshot(options?: { id?: string, pixelRatio?: number }): Promise<{ ok: true, value: { bytes: string, mime: "image/png", width: number, height: number } } | { ok: false, code: string, message: string }>
Captures the live canvas as a PNG -- the whole visible stage by default, or (via options.id) a single layer/page/artboard's own on-canvas region; options.pixelRatio controls the capture's pixel density. bytes is base64-encoded PNG data with the identical decode gotcha as figpea.export.layer's bytes above -- see that section if you need raw bytes rather than a data:image/png;base64,... URL.
const shot = await figpea.canvas.screenshot();
if (shot.ok) {
const img = new Image();
img.src = `data:image/png;base64,${shot.value.bytes}`;
}
widgetGeometry() / gradientWidgetGeometry()
Signature: widgetGeometry(): { ok: true, value: { handles: { leftTop, top, rightTop, left, right, leftBottom, bottom, rightBottom, rotate, body }, corners: { leftTop, rightTop, rightBottom, leftBottom } } } | { ok: false, code: string, message: string } (each point is { x: number, y: number }; synchronous -- no Promise)
Returns the current on-screen (client/viewport CSS-pixel) coordinates of the selected layer's transform-widget interaction points -- the 8 resize handles, the rotate handle, and the body-move region -- plus the selected layer's on-canvas rect corners. gradientWidgetGeometry() returns the active gradient handle points when editing gradient fills. Reads the widget's own rendered DOM, so every point matches exactly where that handle renders.
await figpea.layer.select(rectId);
const geometry = await figpea.canvas.widgetGeometry();
if (geometry.ok) {
const { x, y } = geometry.value.handles.rightBottom;
// drive a real pointer drag on the bottom-right resize handle...
}
layerRect(id)
Signature: layerRect(id: string): { ok: true, value: { x: number, y: number, width: number, height: number } } | { ok: false, code: string, message: string } (synchronous -- no Promise)
Returns the world-space bounding box (axis-aligned) of any layer, selected or not -- a pure model-level geometry read, the same bounds/calcBounds() basis layerTree()/layerById()/fit() use. Not a rendered measurement -- it excludes rendered stroke/shadow overflow; use figpea.canvas.screenshot() if you need actual pixel extent. Selection-preserving: a read-only call that never touches the current selection.
Result: { x, y, width, height } in world px (x/y are the left/top edge). Hidden or zero-area layers resolve ok with their (possibly zero-area) rect -- visibility is a separate axis, never a throw; only an unknown layer id returns not_found.
// Get the bounding box of a layer without selecting it.
const rect = await figpea.canvas.layerRect(layerId);
if (rect.ok) {
console.log(`${rect.value.width}x${rect.value.height} at (${rect.value.x}, ${rect.value.y})`);
}
figpea.report -- Project Health Report, Copy Deck, Font Manifest & Soft Diagnostics
All methods are FREE -- ungated -- inspection, like figpea.canvas above. Each mirrors the editor's Project Report modal and its "Export Copy Deck" button.
summary()
Signature: summary(): Promise<{ ok: true, value: ProjectReportData } | { ok: false, code: string, message: string }>
Builds the full project health report for the active session: artboard names, component count, font manifest, interaction count, distinct-by-content-hash image count/total bytes, and text stats. Takes no arguments.
{
projectName: string;
artboards: { name: string }[];
componentCount: number;
fonts: { name: string, missingLocally: boolean, googleFontsMatch?: string }[];
interactionCount: number;
images: { count: number, totalBytes: number };
text: { layerCount: number, totalCharacters: number, wordCount: number };
}
const result = await figpea.report.summary();
if (result.ok) {
console.log(`${result.value.artboards.length} artboards, ${result.value.images.count} distinct images`);
}
copyDeck()
Signature: copyDeck(): Promise<{ ok: true, value: { markdown: string, csv: string } } | { ok: false, code: string, message: string }>
Builds the copy deck: every non-empty text-layer string, grouped by top-level artboard, as both Markdown and RFC-4180-quoted CSV. Same data source as the UI's "Export Copy Deck" button, returned inline instead of downloaded. Takes no arguments.
const result = await figpea.report.copyDeck();
if (result.ok) {
console.log(result.value.markdown);
}
fonts()
Signature: fonts(): Promise<{ ok: true, value: { name: string, missingLocally: boolean, googleFontsMatch?: string }[] } | { ok: false, code: string, message: string }>
Returns just the font manifest -- a convenience projection of summary()'s fonts field, for agents that only need font data. Takes no arguments; the result is sorted by name.
const result = await figpea.report.fonts();
if (result.ok) {
const missing = result.value.filter((f) => f.missingLocally);
console.log(`${missing.length} fonts missing locally`);
}
projectImages()
Signature: projectImages(): Promise<{ ok: true, value: { hash: string, name: string, mime: string, size: number, width: number, height: number, usedBy: string[] }[] } | { ok: false, code: string, message: string }>
Returns the active project's derived image library: one entry per distinct image (content-hash deduplicated) that the project uses as a layer fill, each carrying its sha256 content hash, name, mime type, byte size, intrinsic pixel width/height, and the ids of every layer using it (usedBy). Ordering is by content hash ascending. Pass a returned hash to figpea.layer.placeProjectImage(hash) to put that image on the canvas. PSD raster layers and masks are not listed -- they decode to ImageData, not an image blob.
diagnostics()
Signature: diagnostics(): Promise<{ ok: true, value: { severity: 'info' | 'warning' | 'error', code: string, message: string, layerId?: string, count?: number }[] } | { ok: false, code: string, message: string }>
Returns soft decode/render diagnostics accumulated for the active project since its last open (reset on openFile/newProject). An empty result means no detected soft issues by the checks this version runs, never a fidelity guarantee. Takes no arguments.
Diagnostic codes (all formats unless noted):
'font_missing_local'— a font family used in the project is not available locally (fallback to Google Fonts may be available).'unsupported_node'— SVG, XD, and Figma. A source node/reference Figpea could not fully realize on open — a<use>whose target is missing, or an<image>whosehrefis unsupported (SVG), or an unsupported shape/element/gradient/fill (XD) — and dropped or emptied instead.'rasterized_fallback'— SVG and XD. A vector construct Figpea could not reproduce faithfully, replaced with a simpler approximation — currently an unresolvable<pattern>falling back to a solid fill.'layer_decode_failed'— PSD and .fig, severityerror. A layer whose decode threw during import — dropped entirely on PSD (the document still opens, but that layer's content is missing), kept as an empty placeholder group on .fig.'interaction_lost'— XD error. Unreadable prototype interaction.
Calibration Framing: Diagnostic coverage breadth reflects wiring order across decoders, not overall parser fidelity — it grows per format over time, and a format surfacing more codes today is not inspected more faithfully than one surfacing fewer. Figma (.fig) decoder now wires ctx.warn at converter sites; PDF maintains pure channel-based zero-coverage framing.
const result = await figpea.report.diagnostics();
if (result.ok && result.value.length === 0) {
console.log("File opened clean — no detected soft issues");
} else if (result.ok) {
for (const diag of result.value) {
console.warn(`[${diag.code}] ${diag.message}`);
}
}
Error codes
The complete set of codes returned across the surface:
| Code | Meaning |
|---|---|
not_found | Resource (layer, project, active session, component) does not exist -- including an unselectable id (layer.select), no rendered transform widget (canvas.widgetGeometry), an unknown layer id (canvas.layerRect), no active session (report.summary / copyDeck / fonts / diagnostics), or unknown component/layer id in figpea.component.* lookups |
unsupported_style_key | Style patch includes a key not in the whitelist |
entitlement_required | User does not have the required claim for an export (including export.specBundle / export.tokens / export.originals / export.assetHarvest / export.figmaKit / export.contactSheet / export.flowPoster) |
unsupported_kind | layer.create's kind argument isn't a recognized layer kind |
not_flippable | The target layer's mutator doesn't implement flipping |
not_a_text_layer | The target layer isn't a text layer (setText) |
not_a_group | The target layer isn't a folder/group layer (ungroup) |
group_failed | Grouping the given selection produced no group |
open_failed | The file failed to decode/open (session.openFile) |
export_failed | An unexpected error was thrown during export encoding |
invalid_format | export.project's format isn't "pdf" / "zip" / "figpea" |
invalid_position | setPosition's { x, y } isn't both finite numbers |
invalid_transform | setTransform's matrix isn't an array of exactly 6 finite numbers |
unsupported_op | batch's op method is "batch" itself (recursion) or not a recognized layer.* method name |
timeout | The operation did not complete before the given deadline (session.waitForIdle) |
invalid_style_value | A whitelisted style key is present but its value is outside the enum it allows (patternScaleType/patternRepeat, malformed gradient with bad type/empty/out-of-range stops/non-string/empty color, out-of-enum case) |
invalid_image_source | The image source (url fetch or bytes decode) failed to resolve to a usable image (layer.setImageFill, layer.create) |
invalid_selector | The selector object includes an unknown key or an uncompilable regex pattern/flags (session.find) |
invalid_interaction | interaction.create validation failure: unsupported trigger/action, missing/invalid targetId, or non-page target |
invalid_mask_type | Invalid declared mask type (layer.setMask) |
invalid_constraint | Invalid responsive constraint or resize mode (layer.setConstraints, layer.setResizeMode) |
not_duplicable | The target layer has no parent and cannot be duplicated (layer.duplicate) |
invalid_params | Method parameter validation failure -- missing required fields, invalid types, or unknown keys (session.openFile) |
Download the Skill
The Tier-1 recipe above, plus the full authoring loop, a reference-recreation method (real-shape authoring and a target-comparison loop), undo etiquette, and entitlement boundaries, are packaged as a Claude Agent Skill you can drop straight into an agent's skills directory:
Download the Skill (SKILL.md)