Skip to main content

Public API

Etch exposes a typed scripting API on window.etch that lets your own scripts and plugins read and mutate the document the builder is editing — blocks, styles, loops, components, custom fields, and the builder UI itself. Every mutation routes through the same guarded paths as the UI, so undo/redo keeps working and nothing bypasses validation.

The contract is published as an MIT-licensed npm package that ships types plus a thin accessor — no heavy runtime is bundled. The runtime itself is provided by the Etch builder on the page.

npm install @digital-gravy/etch-public-api
Experimental surface

The current contract version is 0.x — the surface is experimental and may change without a major version bump until it stabilizes. Prefer feature detection over version comparison, and don't pin production plugins to 0.x.

Acquiring the API​

Two functions get you a typed handle on the API:

getEtch(options?: ConnectOptions): Etch
isEtchAvailable(): boolean
FunctionReturnsNotes
isEtchAvailable()booleantrue when window.etch is present. Use it to guard code that should no-op outside the builder.
getEtch(options?)EtchReturns the typed API. Throws EtchApiError with code NOT_AVAILABLE when the builder isn't loaded.
import { getEtch, isEtchAvailable } from "@digital-gravy/etch-public-api";

function run() {
if (!isEtchAvailable()) return; // not running inside the Etch builder
const etch = getEtch();
const textIds = etch.blocks.find({ type: "etch/text" });
etch.blocks.setText(textIds[0], "Hello world");
}

Waiting for Etch to load​

window.etch is injected by the builder during page load. If your script can run before the builder finishes initializing, poll for availability:

async function whenEtchReady(timeoutMs = 10_000) {
const start = Date.now();
while (!isEtchAvailable()) {
if (Date.now() - start > timeoutMs) throw new Error("Etch did not load");
await new Promise((resolve) => setTimeout(resolve, 100));
}
return getEtch();
}

const etch = await whenEtchReady();

ConnectOptions​

interface ConnectOptions {
apiVersion?: string; // e.g. "^1.0" — reserved for the future 1.x runtime
id?: string; // your plugin identifier, for telemetry
}

On today's 0.x runtime, getEtch() returns the global directly after a best-effort version check (it warns, but does not throw, on a major-version mismatch). When a future stable runtime exposes a native connect(), version negotiation is delegated to it.

const etch = getEtch({ apiVersion: "^1.0", id: "my-plugin" });

The Etch object​

getEtch() returns the root Etch interface — its namespaces plus a few top-level members:

interface Etch {
blocks: EtchBlocksApi;
loops: EtchLoopsApi;
styles: EtchStylesApi;
stylesheets: EtchStylesheetsApi;
components: EtchComponentsApi;
navigation: EtchNavigationApi;
fields: EtchFieldsApi;
ui: EtchUiApi;
history: EtchHistoryApi;
skills: EtchSkillsApi;
ai: EtchAiApi;
dataSources?: EtchDataSourcesApi; // optional: only where the `dataSources` capability is backed
content?: EtchContentApi; // optional: only where the `content` capability is backed
routes?: EtchRoutesApi; // optional: only where the `routes` capability is backed
assets?: EtchAssetsApi; // optional: only where the `assets` capability is backed

saveAsync(): Promise<void>;
connect?(options?: ConnectOptions): Etch; // reserved for the future stable runtime
readonly apiVersion: string; // scripting contract version, e.g. "0.x"
readonly version: string; // Etch product version
readonly environment?: EtchEnvironment; // what this runtime is and backs
}
NamespacePageWhat it covers
etch.blocksBlocksSelect, read, create, mutate, and delete blocks; component edit mode
etch.stylesStylesCSS rules and :root custom properties (variables)
etch.stylesheetsStylesheetsGlobal stylesheets and @custom-media definitions
etch.componentsComponentsReusable component definitions and their properties
etch.assetsAssetsUpload, download, compress and organize the project's files
etch.contentContentContent types, their fields, and their items
etch.dataSourcesData SourcesNamed json, api and js sources, read through data('key')
etch.navigationNavigationMove around the builder UI; switch posts/templates
etch.routesRoutesThe route tree: which content is published, and at which URL
etch.ui / etch.historyUI & HistoryColor scheme, chrome visibility, undo/redo
etch.skillsSkillsBundled, read-only authoring guides (e.g. ACSS conventions)
etch.aiAI PresenceAdvertise an external agent's presence and phase to the builder
—Types ReferenceBlock JSON union, shared types, errors

Saving and the persistence model​

How a change reaches the database depends on which namespace it lives in:

  • blocks, styles, loops — buffered in the editor. They are persisted when you call etch.saveAsync() (the same save the UI performs).
  • stylesheets, components, fields, dataSources, content, routes, assets — every mutating method is *Async and persists immediately; you do not need saveAsync() for them.
etch.blocks.setText(id, "Hello world");
etch.blocks.addClass(id, "lead");
await etch.saveAsync(); // persists the block changes above

Error handling​

Every API error is an EtchApiError carrying a machine-readable code. Use the isEtchApiError type guard to narrow caught values:

import { isEtchApiError } from "@digital-gravy/etch-public-api";

try {
etch.blocks.getJson("does-not-exist");
} catch (err) {
if (isEtchApiError(err)) {
console.warn(err.code, err.message); // e.g. "BLOCK_NOT_FOUND"
}
}
class EtchApiError extends Error {
readonly code: EtchApiErrorCode;
name: "EtchApiError";
constructor(code: EtchApiErrorCode, message: string);
}

type EtchApiErrorCode =
| "BLOCK_NOT_FOUND"
| "WRONG_BLOCK_TYPE"
| "READONLY"
| "INVALID_ARGUMENT"
| "LOOP_NOT_FOUND"
| "DATA_SOURCE_NOT_FOUND"
| "STYLE_NOT_FOUND"
| "STYLESHEET_NOT_FOUND"
| "COMPONENT_NOT_FOUND"
| "POST_NOT_FOUND"
| "ROUTE_NOT_FOUND"
| "CONTENT_TYPE_NOT_FOUND"
| "FIELD_NOT_FOUND"
| "ASSET_NOT_FOUND"
| "COLLECTION_NOT_FOUND"
| "ASSET_IN_USE"
| "OPERATION_FAILED"
| "NOT_AVAILABLE"
| (string & {}); // open-ended for forward compatibility

Feature detection​

Since: 1.6.7

Because the surface is experimental — and because a namespace can be declared but not backed on the runtime you happen to be on, in which case its methods throw EtchApiError with code NOT_AVAILABLE — detect features rather than comparing version strings.

etch.environment (contract @digital-gravy/etch-public-api 0.10.0) describes the runtime your script is actually running against:

interface EtchEnvironment {
readonly product: EtchProduct; // which build this is — identity only
readonly capabilities: EtchCapabilities; // which optional surfaces it backs
readonly blockTypes: readonly string[]; // every block type it can construct
}

type EtchCapabilities = Readonly<Partial<Record<EtchCapability, boolean>>>;

A capability that's missing from the map counts as unavailable, so test with === true. A namespace tied to an optional capability (routes, assets, …) is left off etch entirely when the runtime doesn't back it.

CapabilityWhen trueOn Etch Studio
dataSourcesetch.dataSources is backed: named json, api and js sources, reached from a loop's target through data('key').yes
loopsThe older loop registry: a loop block binds to a named definition by loopId. Superseded by dataSources.no
fieldsetch.fields is backed by a custom-field store.no
templatesTemplates exist and navigation.goTo("templates") works. On Studio they are records of a template content type.yes
contentetch.content is backed: content types, their fields and their items.yes
routesetch.routes is backed: the site's URLs are a route tree.yes
assetsetch.assets is backed: the product's own asset store.yes
wpMediaMedia ids resolve against the WordPress media library.no
if (etch.environment?.capabilities.routes === true) {
const paths = await etch.routes!.getPathsAsync();
}

blockTypes is read live off the runtime's block registry, so it cannot drift from what the runtime can actually construct. Check it instead of assuming a block type exists:

if (etch.environment?.blockTypes.includes("etch/passthrough")) {
// safe to author one here
}

environment itself is optional: a runtime that predates the descriptor omits it. Treat a missing environment as everything being available, which is what every runtime that predates it was.

For an individual method that may not exist on an older runtime, typeof still applies:

if (typeof etch.blocks.someNewMethod === "function") {
// safe to use
}

Module exports​

// Runtime
export { getEtch, isEtchAvailable } from "@digital-gravy/etch-public-api";
export { EtchApiError, isEtchApiError } from "@digital-gravy/etch-public-api";
export { ETCH_API_VERSION } from "@digital-gravy/etch-public-api"; // "0.x"

// Types (all contract interfaces, unions, and the Etch root)
export type {
Etch,
ConnectOptions,
EtchApiErrorCode /* …and the rest */,
} from "@digital-gravy/etch-public-api";
ExportKindDescription
getEtchfunctionAcquire the API from window.etch
isEtchAvailablefunctionCheck whether the API is present
isEtchApiErrorfunctionType guard for EtchApiError
EtchApiErrorclassTyped error with a code property
ETCH_API_VERSIONconst"0.x" — the contract version this package targets
Etch + all contract typestypeThe root interface plus every namespace/type