# 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 [#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.`

```js
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 [#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 [#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 [#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:

1. `aria-label`
2. `title`
3. `name`
4. The text of the elements named by `aria-labelledby`
5. The text of a wrapping `<label>`
6. 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 [#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 `File` or an array of `File` objects.
* Inputs, textareas and selects receive the value through the native setter, followed by bubbling `input` and `change` events.
* 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 [#source-round-trips]

```js
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-actions]

Design Lab, the layered composition tool (tool ID `material`), adds these actions:

```text
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.mp4
```

### Workspace and layers [#workspace-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](/docs/studio/design-lab/export#browser-api) 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 [#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 [#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](/docs/studio/design-lab/layers#text) describes the source model and box-wide settings.

### Shader frames and motion [#shader-frames-and-motion]

To keep the exact visible shader look, capture it before backing up source:

```js
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 [#export]

Use the unified action for new clients:

```js
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:

```js
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](/docs/studio/project-files) describes the format.

## Other tool actions [#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](/docs/studio/templates#browser-api)) |
| Design Lab            | The `design.*` actions in [Design Lab actions](#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-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 [#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](/docs/studio/colors), [Typography](/docs/studio/typography) and [Component library](/docs/studio/components) describe these tools.

### Brand identity [#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](/docs/agents/brand-setup) walks through the full flow.

### Brand elements [#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 [#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 [#completion-and-verification]

For a change:

1. Apply the source or operate the control.
2. Read the source or controls again and confirm the expected state.
3. Inspect the rendered canvas when the visual result matters.

For an export:

1. Wait for the action's Promise to resolve.
2. Confirm that `artifact.blob` is a non-empty Blob.
3. Confirm that the file name and MIME type match the request.
4. When `download` is `true`, confirm that the browser finished the download.
5. For motion, inspect more than the first frame and check the loop when one was requested.

## Error model [#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.
