Studio Browser API
Operate Studio controls by accessible label, read and apply tool source, and export browser-rendered artifacts from scripts with window.glyphfield.studio.
The Studio Browser API (window.glyphfield.studio, version 1) is the programmatic equivalent of the visible Studio workspace. It uses the same controls, source validators, project state, renderers and export actions as the interface. The active Studio tool registers it.
Lifecycle
window.glyphfield.studio resolves to the visible project and tool, including editors that stay mounted across tab switches. Glyphfield dispatches glyphfield:studio-api-ready on window with studio.describe() as the event detail whenever the active editor changes. While a newly selected editor loads, reading window.glyphfield.studio throws No active Studio workspace is ready for automation.
await new Promise((resolve, reject) => {
const ready = () => {
window.removeEventListener('glyphfield:studio-api-ready', ready);
resolve();
};
window.addEventListener('glyphfield:studio-api-ready', ready);
try {
if (window.glyphfield?.studio) ready();
} catch (error) {
if (error.message !== 'No active Studio workspace is ready for automation.') {
window.removeEventListener('glyphfield:studio-api-ready', ready);
reject(error);
}
}
});When a script activates another project or tool, subscribe to the event before the click, wait for it, and then read window.glyphfield.studio again. A cached handle belongs to the editor that created it. After that editor becomes inactive or unmounts, the handle rejects source, control and action calls with This Studio workspace is no longer active. Read the active Studio API again. A pending edit is not redirected into another project.
Core methods
| Method | Result | Use |
|---|---|---|
activeTool() | Studio tool ID | Confirm that the API belongs to the intended tool |
describe() | { version, toolId, source, capabilities, actions, actionContracts } | Discover source support, exports and the input and output of every action before invoking it |
controls() | Array of { kind, label, value? } | List the enabled controls of the active workspace |
activate(label) | void | Click a visible button, tab or role button by exact accessible label |
set(label, value) | void | Change an input, textarea, select, checkbox, contenteditable element or file input |
readSource() | JSON string | Read the active tool's exact source document |
applySource(source) | Promise<void> | Validate and apply a JSON object or string, then wait for the interface to commit |
invoke(action, input?) | Promise<unknown> | Run a standard or tool-specific action |
download(artifact) | void | Save a returned { blob, fileName } artifact |
Standard actions
Every tool supports these actions through invoke. The two source actions are present only when the tool exposes source, so check describe().source first.
| Action | Input | Result |
|---|---|---|
source.read | None | The source JSON string |
source.apply | JSON string or object | The normalized source string, or null when the tool cannot read source |
controls.list | None | The same array as controls() |
control.activate | Accessible label string | null |
control.set | { label, value } | null |
artifact.download | { blob, fileName } | { fileName } |
Accessible control contract
controls() lists enabled button, input, textarea, select, [role="button"] and [role="textbox"] elements inside the active workspace. It also includes an open menu or popover that a control in the workspace owns through aria-controls. It does not search another retained project or tool, even when that workspace has a control with an identical label. Controls without a label are left out. A control's label is the first of these that is present:
aria-labeltitlename- The text of the elements named by
aria-labelledby - The text of a wrapping
<label> - The element's own text
Disabled controls, controls inside a hidden or inert workspace, controls in a closed popover and controls in a collapsed sidebar are excluded. Label matching ignores case and repeated whitespace but is otherwise exact. If two controls in the active workspace share a label, the first match wins.
Slider names stay the same while their values change. Lottie's playback slider is named Speed, so studio.set('Speed', 1.5) works after an earlier adjustment. The displayed value and unit are not part of the name. Some labels appear twice in one workspace. Animation has Scale and Text size in both the sequence defaults and the scene inspector, and set() changes the first match. Use a tool action such as animation.scene.update when a label is ambiguous.
Values and files
set accepts a string, number, Boolean, File or File[].
- A checkbox requires a Boolean. The API clicks the checkbox only when its state differs.
- A file input requires a
Fileor an array ofFileobjects. - Inputs, textareas and selects receive the value through the native setter, followed by bubbling
inputandchangeevents. - A contenteditable control has its text replaced and receives a bubbling
InputEvent.
Automation must not put a file path into a file input. Read the authorized bytes and create a real File object.
Source round trips
const studio = window.glyphfield.studio;
const document = JSON.parse(studio.readSource());
// Make the smallest targeted change and keep every unknown field.
await studio.applySource(document);applySource resolves two animation frames after the tool validator finishes. At that point the interface has committed the change. A remote font, an image decode, a WebGL compilation or a long export can still be in progress, so wait for the relevant preview state before a final capture.
Design Lab actions
Design Lab, the layered composition tool (tool ID material), adds these actions:
design.workspace.describe
design.workspace.activate
design.workspace.move
design.workspace.frameSelection
design.layers.arrange
design.layers.lock
design.layers.rotate
design.layers.copyStyle
design.layers.pasteStyle
design.effects.describe
design.effects.add
design.effects.configure
design.frame.pause
design.frame.capture
design.frame.play
design.frame.seek
design.text.format
design.motion.describe
design.sequence.describe
design.sequence.configure
design.sequence.preview
design.sequence.stop
design.export
design.export.project
design.export.png
design.export.jpg
design.export.gif
design.export.mp4
design.export.shader-sequence.gif
design.export.shader-sequence.mp4Workspace and layers
design.workspace.describe returns { activeSurface, canvas: { layerIds }, artboards: [{ id, name, x, y, width, height, layerIds }] }. design.workspace.activate takes { target: 'canvas' | artboardId }. design.workspace.move takes { target, layerIds, placement?: 'preserve' | 'beside' } and keeps each layer's position and size in canvas space. design.workspace.frameSelection creates an artboard around the selected loose canvas layers. Export, saving, and source covers artboards in more detail.
design.layers.arrange takes { arrangement, layerIds? } and aligns layers to the bounds of the selection or spaces them evenly. arrangement is left, horizontal-center, right, top, vertical-center, bottom, distribute-horizontal or distribute-vertical. Omit layerIds, or pass an empty array, to use the current selection. Locked layers are skipped, and a fully selected group moves as one unit. Alignment needs two units and distribution needs three.
design.layers.lock takes { layerIds, locked } and writes element.locked. design.layers.rotate takes { layerIds, rotation }. It turns each unlocked layer clockwise about its own center to rotation degrees and writes bounds.rotation. The angle is normalized to the range −180 to 180, so 270 becomes −90, and 0 removes the rotation. design.layers.copyStyle takes { layerId } and copies the layer's appearance. design.layers.pasteStyle applies that style to { layerIds } and skips locked layers. These layer actions accept only text, mark, image and shader layers on the active surface, and they resolve after the change commits.
Effect layers
design.effects.add adds an Effect layer above every layer of the active artboard and returns { layerId }. Pass { kind } for a built-in converter (bayer, ascii, halftone or posterize), or { effectId, effectProps? } for a Shaders.com image effect listed under imageEffects in /api/materials. An optional opacity runs from 0 to 1. The action fails on the loose canvas because an Effect layer applies to a whole artboard.
design.effects.configure takes { layerId } plus one or more of kind, effectId, effectProps and opacity. kind and effectId cannot be combined. effectProps replaces the layer's explicit edits, and {} restores the defaults. effectProps must match the effect's nativeControls. The API rejects sourceImage because the layers beneath the Effect layer are its source. Setting kind resets the converter settings to that converter's defaults, and setting kind or effectId renames the layer.
design.effects.describe lists the active artboard's Effect layers in layer order with the effects on offer. Each image effect layer reports its WebGPU status (loading, ready, unsupported or error) once it has rendered a preview. Image effects need WebGPU. Without it the canvas shows the layers beneath, and an export stops with an error that names the layer.
Text
design.text.format applies inline styling to a text layer on the active surface with { layerId, start, end, style }. Offsets count UTF-16 code units, the end is exclusive, and the range must be non-empty and inside the text. style accepts color, fontRole, fontSize, weight, fontStyle, underline, strikethrough and tracking. Font sizes are authored pixels before layer scale. Characters outside the range keep their styles. Read element.data.runs back after a change. Text describes the source model and box-wide settings.
Shader frames and motion
To keep the exact visible shader look, capture it before backing up source:
const motion = await studio.invoke('design.motion.describe');
// Check each layer's capabilities before seeking. Fluid Energy cannot replay history.
const source = await studio.invoke('design.frame.capture');
const capturedDocument = JSON.parse(source);
// Keep this complete source, including CanvasDocument.assets, outside the browser.design.frame.pause freezes the current native frame synchronously and returns null. It does not encode a PNG or write captured-frame assets.
design.frame.capture waits for lossless PNG snapshots to be stored and returns a portable CanvasDocument JSON string. The capture is an appearance checkpoint plus the editable recipe. It is not a pre-rendered motion sequence. A storage, renderer or encoding failure rejects the action instead of returning an incomplete save. A saved PNG that cannot be loaded is reported as unavailable and is not replaced with a newly animated approximation.
design.frame.play resumes native motion. design.frame.seek accepts { timeMs }, a non-negative finite number such as 1250.5. The value is seekable shader time. Export frame indices do not apply. Fluid Energy is a stateful simulation and rejects seeking. Pausing keeps its running simulation, but playing from a saved PNG starts a new simulation. design.motion.describe reports each shader layer's provider and its capture, seek and loop capabilities. A continuous animation clock does not prove that a material loops seamlessly. The time explorer's range grows only when it is extended explicitly, and playback does not add saved frames.
Export
Use the unified action for new clients:
const artifact = await studio.invoke('design.export', {
format: 'mp4',
mode: 'shader-sequence',
download: true,
});format accepts png, jpg, gif or mp4. mode accepts standard or shader-sequence, and shader-sequence applies to GIF and MP4. When download is false or omitted, the action returns the Blob without saving it, and studio.download(artifact) saves it later. Design Lab exports the active artboard. Loose canvas layers are not exported, so the action fails while the loose canvas is active.
PNG and JPG freeze the frame that is visible when the export starts and compose it at the selected output size. GIF and MP4 sample motion from the current frame and restore that paused pose afterward. Neither path adds captured-frame assets to IndexedDB or resumes playback. Fluid Energy supports still export, but motion export of Fluid Energy is rejected because its earlier simulation history does not exist. Fonts, images, the renderer and the requested browser encoder must all be available.
For an editable multi-artboard project file, use the separate action without input:
const project = await studio.invoke('design.export.project');
if (!(project.blob instanceof Blob) || project.blob.size === 0
|| project.blob.type !== 'application/json'
|| !project.fileName.endsWith('.glyphfield.json')) {
throw new Error('Invalid project artifact');
}
studio.download(project);
// To reopen the same authorized bytes through the file picker:
studio.set('Choose project file', new File([project.blob], project.fileName, {
type: 'application/json',
}));design.export.project returns an ExportPreviewAsset with format: 'JSON' and previewKind: 'file'. It does not download automatically or render a composite PNG first. It captures the active artboard's current shader appearance and keeps the saved snapshots of inactive artboards without rendering them. It embeds local images, saved frames and the brand typography and font files in the CanvasDocument. Missing assets and failed captures reject the action. Opening a project file uses the same source validator and keeps the imported typography local to Design Lab. Setting the file control starts the import but does not wait for it, so wait for the status message that begins with Opened or for an error, then read the source again. Project files describes the format.
Other tool actions
describe().actionContracts and the /api/agent manifest are authoritative. These tools expose direct operations, so a script does not have to click an export button and infer when it finished:
| Tool | Direct actions |
|---|---|
| Brand identity | identity.export.json, identity.read, identity.validate, identity.apply, identity.asset.add, identity.font.add, identity.audit, identity.fix, identity.save, identity.export.kit, identity.setup.prompt, identity.project.create |
| Brand elements | brand-element.export.png, brand-element.export.html, brand-element.export.brief |
| Moodboard | moodboard.export.png, moodboard.export.svg, moodboard.export.identity |
| Brand book | brand-book.print |
| Color tokens | colors.tokens.read, colors.tokens.copy, colors.contrast.read, colors.pairing.toggle, colors.semantic.read, colors.scale.read, colors.vision.read, colors.proportion.balance |
| Typography | typography.tokens.read, typography.tokens.copy, typography.scale.read, typography.guidance.read, typography.axes.read |
| Component library | components.list, components.code.read, components.code.copy |
| Animation | animation.export, animation.export.gif, animation.export.mp4, animation.export.png, animation.export.project, animation.describe, animation.seek, animation.scene.update, animation.keyframe.set, animation.keyframe.remove, animation.motion.preset, animation.starter.apply |
| Lottie | lottie.export, lottie.export.frame.png, lottie.export.gif, lottie.export.mp4, lottie.export.source, lottie.describe, lottie.edit, lottie.colors.brand, lottie.segment.set |
| OpenGraph, Blog cover | The shared expression actions below |
| Terminal card | The shared expression actions plus terminal.tab.add, terminal.tab.select, terminal.tab.update, terminal.tab.remove |
| Partnership lockup | The shared expression actions plus partnership.partner.add, partnership.partner.remove, partnership.lockup.update |
| Slide templates | The shared expression actions plus slides.export.pdf, slides.slide.add, slides.slide.duplicate, slides.slide.move, slides.slide.remove, slides.slide.select |
| All expression tools | <tool>.document.describe, <tool>.export.png, <tool>.export.svg, <tool>.layer.add, <tool>.layer.update, <tool>.layer.remove, <tool>.layer.duplicate, <tool>.layer.reorder, <tool>.layer.select, <tool>.layer.align, <tool>.format.set, <tool>.layout.apply, <tool>.template.reset, <tool>.history.undo, <tool>.history.redo (Expression tools) |
| Design Lab | The design.* actions in Design Lab actions |
Format-specific export actions such as animation.export.gif take no input and return without downloading, so the caller can verify the file first.
Animation and Lottie
animation.export accepts { format: 'gif' | 'mp4' | 'png', download?: boolean }. The PNG is the full-size poster frame at the playhead. MP4 includes the same timeline audio mix as the visible export, and GIF has no audio. animation.describe returns durationMs, fps, width, height, playheadMs and starters, and every scene's ID, start, hold, outgoing transition, keyframes and text entrance. The scene, keyframe, preset, seek and starter actions return that same description after their edit commits. animation.seek pauses playback and moves the playhead, and a time past the end of the timeline stops at the end. Pose values sent to animation.scene.update and animation.keyframe.set are clamped to the keyframe limits: alignX and alignY −1.5 to 1.5, scale 0.05 to 8, rotation −720 to 720 and opacity 0 to 1.
lottie.export accepts { format: 'png' | 'source' | 'gif' | 'mp4', download?: boolean }. source is the edited JSON or the active .lottie archive. lottie.describe lists the layer keys, text layers, colors and markers that lottie.edit and lottie.segment.set use. lottie.edit and lottie.colors.brand reject .lottie archives. Lottie's readSource() returns the CanvasDocument envelope, and lottie.export.source returns the edited Lottie JSON.
Color, typography and components
colors.tokens.read returns CSS without touching the clipboard. Use colors.tokens.copy only when writing to the clipboard is intended. Both accept { format?: 'oklch-css' | 'css' | 'tailwind' | 'dtcg' }. typography.tokens.* accept { format?: 'css' | 'tailwind' | 'dtcg' }, and components.code.* accept { pattern?, format?: 'react' | 'html' }. colors.vision.read defaults to deuteranopia. Color tokens and Typography edit the identity. Their changes, including colors.pairing.toggle and colors.proportion.balance, stay pending until Save changes is selected or identity.save runs in the Brand identity tool. Color tokens, Typography and Component library describe these tools.
Brand identity
Brand identity actions validate before they change anything. identity.apply accepts { identity, mode?: 'merge' | 'replace', replace?, remove?, save? }. Records merge by id, and typography merges by role, unless the collection is listed in replace. String lists are replaced. Unknown fields and the project's id, kind, revision, builtIn and presetFingerprint are kept. A failure throws one TypeError that lists every issue path.
identity.asset.add and identity.font.add take a File. A logo slot (mark-dark, mark-light, wordmark, wordmark-light or mark-mono) replaces that slot in place. Font files can be TTF, OTF, WOFF or WOFF2 up to 2.5 MB. identity.audit returns the brand health report, identity.fix applies one finding's fix.id, and identity.save keeps pending changes. identity.export.kit accepts { format?: 'zip' | 'dtcg' | 'css' | 'tailwind' | 'markdown' | 'logos', download? }. identity.project.create accepts { from?: 'starter' | 'scratch' | '<project id>', name? }, opens the new project in a new tab and returns { id, name, kind, from, url }. Read window.glyphfield.studio again afterward, because the new project's workspace owns the API. Brand setup with an agent walks through the full flow.
Brand elements
brand-element.export.png accepts { elementId?, scale?: 1 | 2, download? }. It selects elementId first and renders the element with its in-place edits at its artboard size, or at twice that size. An OpenGraph element is 1200 × 630, and an email element is 640 px wide. brand-element.export.html accepts { elementId?, assetOrigin?, download? } for email elements and renders elementId without selecting it. It returns a document ready to send: a 600 px table layout with inlined styles, an Outlook wrapper, a hidden preheader, a bulletproof button and absolute image URLs resolved against assetOrigin (the Studio origin by default). Generated header art is embedded as a PNG data URL. Host that image and replace its src before sending through clients that block data URLs, such as Gmail.
Controls and source in every tool
Every other visible feature is reachable through workspace-scoped controls(), activate() and set(). Each tool's capability record separates a direct Browser API action, an interactive control and deterministic HTTP generation, so a script can tell a clickable flow from an action that returns a value.
Every tool keeps its editable source registered whether or not the source drawer is open. Brand book source holds only the book's presentation edits, and its content comes from the active identity.
Completion and verification
For a change:
- Apply the source or operate the control.
- Read the source or controls again and confirm the expected state.
- Inspect the rendered canvas when the visual result matters.
For an export:
- Wait for the action's Promise to resolve.
- Confirm that
artifact.blobis a non-empty Blob. - Confirm that the file name and MIME type match the request.
- When
downloadistrue, confirm that the browser finished the download. - For motion, inspect more than the first frame and check the loop when one was requested.
Error model
The Browser API throws a TypeError for malformed action input or a source operation the tool does not support. It throws a RangeError when no control matches a label or the tool does not expose an action. It throws a plain Error for a stale handle, for an action that must wait (for example during an export), and when no workspace is ready. Tool renderers can throw Error for WebGL, image decoding, Canvas, GIF, MP4 or browser codec failures.
Do not catch and ignore these errors. Log the active tool ID, the action and the describe() output, then either correct the operation or report the browser limitation.
Endpoint reference
Look up every public Glyphfield HTTP route with its method, response shape, cache header, size limit and error codes before calling it from code.
Source format reference
Read and edit the JSON source behind each Studio tool, including the portable CanvasDocument, its validation rules, migrations and safe round trips.