Static Ads Lab
Concepts

Design scripts

The JavaScript API for editing an ad's layers, modeled on Figma's Plugin API. One script is one atomic edit.

A design script is JavaScript that edits an ad's layers. Send it to POST /v1/image-ads/:id/edits as { "script": "..." }. It is the API our own AI Design Agent writes against, and it is free to call.

The API is Figma's Plugin API, simplified:

  • Atomic. The whole script applies, or (on any error) nothing does.
  • Synchronous. No font loading; await is allowed but never needed.
  • Hex paints. Paints accept "#RRGGBB" strings as well as paint objects.
  • Assignable size. width and height can be set directly.
  • Helpers on doc for finding, arranging and creating layers.

Scripts run in a sandbox against a copy of the ad. If it succeeds, the result is saved as a new tree_revision and the image re-renders in the background.

Find layers

Get the layer ids, names, types, text and boxes from GET /v1/image-ads/:id/outline first, then find layers by id, name, type or text.

doc.root                          // the artboard (a FRAME)
doc.get(id)                       // one layer by id
doc.findAll(criteria?) / doc.findOne(criteria?)
node.findAll(criteria?) / node.findOne / node.findChildren / node.children / node.parent
figma.currentPage.findAll(fn) / findOne(fn) / figma.getNodeById(id)

// criteria: "TEXT" | (node) => boolean | { type, types, name, nameIncludes, text }
doc.findAll("TEXT")
doc.findOne({ name: "Headline" })
doc.findAll({ text: "shop now" })

Every layer

id, type, name, visible, locked, opacity, blendMode, effects, isMask
x, y            // relative to the parent frame, as in Figma (groups don't count)
width, height   // assignable (resizes with the top-left fixed); or node.resize(w, h)
rotation        // degrees, counter-clockwise, about the center
bounds          // { x, y, width, height } on the canvas (read-only)
moveTo(x, y) · moveBy(dx, dy) · scaleBy(factor, { origin?, text? }) · rescale(f)
rotateBy(deg) · flip("horizontal" | "vertical") · clone() · remove()
bringToFront() · sendToBack() · bringForward() · sendBackward()
appendChild(node) · insertChild(index, node)   // keeps the canvas position

Auto-layout children also have layoutSizingHorizontal, layoutSizingVertical, layoutPositioning, layoutGrow, layoutAlign and min/max sizes.

Shapes and frames

fills, strokes      // "#RRGGBB" | paint | paint[]; [] clears
// { type: "SOLID", color: "#FF5500" | { r, g, b } (0..1), opacity? }
// { type: "IMAGE", url, scaleMode: "FILL" | "FIT" | "CROP" | "TILE" }
// { type: "GRADIENT_LINEAR", gradientStops: [{ position, color }], gradientTransform }
strokeWeight, strokeAlign, dashPattern, strokeJoin, strokeCap
cornerRadius, topLeftRadius, topRightRadius, bottomRightRadius, bottomLeftRadius
setImageCrop({ zoom, panX, panY })   // zoom 1 = cover fit; pan 0..1
effects             // DROP_SHADOW, INNER_SHADOW, LAYER_BLUR, BACKGROUND_BLUR

Frames add clipsContent and auto layout (layoutMode, itemSpacing, padding, alignment, wrap). Auto layout re-lays out children after every change, as in Figma.

Text

characters, fontName ({ family, style }), fontSize, lineHeight, letterSpacing,
textCase, textDecoration, textAlignHorizontal, textAlignVertical, textAutoResize,
paragraphSpacing, maxLines, textTruncation, fills

setRangeFontName(start, end, font) · setRangeFontSize · setRangeFills · setRangeStyle(start, end, { ... })
getStyledTextSegments() · insertCharacters(i, text) · deleteCharacters(start, end) · replaceText(find, replacement)
doc.fonts.families() · doc.fonts.styles(family)   // only these fonts exist

Mixed text reads its dominant style; setting a style applies to all of it.

Icons and vectors

A library of about 1,500 Phosphor icons in five weights (thin, light, regular, bold, fill).

icons.search("delivery truck", { limit?, weight? })   // best matches first
node.setIcon("truck", "bold")                         // swap an icon layer's drawing
doc.createIcon("leaf", { size: 48, weight: "bold", x, y, fills: "#000000" })
doc.createFromSvg(svg, { size, x, y, color })         // draw your own when no icon fits

Create and arrange

figma.createRectangle() / createEllipse() / createFrame() / createLine() / createText()
doc.createText(characters, { fontName, fontSize })
figma.group(nodes) · figma.ungroup(group) · doc.frame(nodes) · doc.mask(nodes)
doc.autoLayout(nodesOrFrame, { itemSpacing, paddingTop, ... })

doc.align(nodes, "left" | "center" | "right" | "top" | "middle" | "bottom", { to? })
doc.distribute(nodes, "horizontal" | "vertical", { gap? })
doc.stack(nodes, { axis: "vertical", gap: 24, align?: "start" | "center" | "end" })
doc.center(nodes, { axis?, within? })
doc.rebalance(nodes, { axis: "vertical" })   // re-centers a column between its neighbors
doc.moveBy(nodes, dx, dy) · doc.scale(nodes, factor) · doc.rotate(nodes, deg)
doc.images()   // photos and logos you may place: [{ handle, label, url, width, height }]

Results and errors

  • return a value to see it in the response; layers come back as { id, type, name }.
  • After each script you get what changed, layers created and removed, and warnings it introduced: off canvas, text overflowing its box, font fallback, overlapping text, text too small to read, low contrast, and text where Stories and Reels UI covers it.
  • A property that does not exist throws with a hint, and nothing is applied.

Examples

// Left-align all text, nudge it up 10px and keep the column balanced
const texts = doc.findAll("TEXT");
for (const t of texts) t.textAlignHorizontal = "LEFT";
doc.align(texts, "left");
doc.moveBy(texts, 0, -10);
doc.rebalance(texts, { axis: "vertical" });
// Headline 10% bigger and bold, with the first line in a brand color
const h = doc.findOne({ name: "Headline" });
h.fontSize = Math.round(h.fontSize * 1.1);
h.fontName = { family: h.fontName.family, style: "Bold" };
const end = h.characters.indexOf("\n");
h.setRangeFills(0, end > 0 ? end : h.characters.length, "#C2410C");
// Stack three feature rows 24px apart and center the block
const features = ["Feature 1", "Feature 2", "Feature 3"].map((name) => doc.findOne({ name }));
doc.stack(features, { axis: "vertical", gap: 24, align: "start" });
doc.center(features, { axis: "horizontal" });
return features.map((f) => f.bounds);

Layer names vary from ad to ad. Read them from the outline rather than assuming them.