Search Docs
Loading...
Skip to content

Your Editor

Let a person finish by hand what the agent started: open the agent’s designs in CE.SDK’s web editor with the same brand, fonts, and images the agent used, and save them back without losing either side’s work.

No example product runs the code on this page; it is checked against the SDK’s types, and each function that throws 'yours to write' is yours to write over your own API.

Before You Start#

This guide assumes the design store and the plugin module from Connect Your Storage: read and write over your designs, and the plugin file, sent as { module, options }, that registers your asset sources (searchable lists of your images, fonts, and colors). Your agent runner, the backend process that calls the model and prepareCoDesign, passes both; the executor, the container from IMG.LY’s image, runs the agent’s CE.SDK engine. read answers a design’s content, its version (a value that changes on every save), and the CE.SDK version that saved it, and write stores only while the design is still at the version it was given. Your editor’s routes call the same two store functions, and your page gets the same module text with a bundler’s raw-text import (Vite’s ?raw, say) or from your API. It gets this person’s options, a short-lived token among them, from your API too.

CE.SDK, IMG.LY’s design engine, comes as a web editor you embed in your page (@cesdk/cesdk-js). The SDK does not ship an editor; its @imgly/codesign-sdk/cesdk-utils entry is a few helpers for your editor’s engine that do the parts that fail silently when done by hand. Your save route and your editor’s UI stay yours. Install the SDK next to @cesdk/cesdk-js at exactly the CE.SDK version the SDK reports as CESDK_VERSION (import { CESDK_VERSION } from '@imgly/codesign-sdk'):

Terminal window
npm install --save-exact @imgly/codesign-sdk@1.83.0 @cesdk/cesdk-js@1.83.0

npm ls @cesdk/cesdk-js then shows 1.83.0, the same version as CESDK_VERSION. The code below adds only these helpers to an editor you already set up as CE.SDK’s own web guides show; the panel ids in it (the ids of CE.SDK’s asset panels) are that editor’s defaults.

Why the Editor Must Match the Agent#

Three things must be the same in your editor as on the agent’s executor, or a design looks or saves differently:

  • The same plugins. The executor applies the list your runner passes in plugins. Your editor applies the same list, with this person’s options, so its panels show the same collections and its images load from the same places.
  • The same CE.SDK version. A CE.SDK that saves a design another version wrote can drop settings with no error, so your editor runs exactly the SDK’s CESDK_VERSION, and loadDesign refuses to open a design in any other. See Versions and Upgrades for why and how to upgrade.
  • The same save rules. The agent saves only over the version it read. A person’s save must too, or it overwrites what the agent did while the editor was open.

Open a Design#

export async function openEditor(container: HTMLDivElement, designId: string) {
const design = await readDesign(designId);
const cesdk = await CreativeEditorSDK.create(container, {
license: CESDK_WEB_LICENSE
});
// The same list your runner passes to prepareCoDesign, with this person's options.
await addPluginModules(cesdk.engine, [
await yourPluginModule(),
googleFonts()
]);
// After the plugins: a design loaded first fetches its images before their resolver exists.
// Refuses, before the person works on it, an editor on another CE.SDK than the
// executor's, or a design a newer CE.SDK saved. Remembered for saveDesign.
await loadDesign(cesdk.engine, design);
The whole file, editor.ts
import CreativeEditorSDK, { type AssetDefinition } from '@cesdk/cesdk-js';
import {
googleFonts,
imageSizeOf,
type EnginePluginModule,
type ImageSize,
type StoredDesign
} from '@imgly/codesign-sdk';
import {
addPluginModules,
loadDesign,
saveDesign,
unloadDesign
} from '@imgly/codesign-sdk/cesdk-utils';
export async function openEditor(container: HTMLDivElement, designId: string) {
const design = await readDesign(designId);
const cesdk = await CreativeEditorSDK.create(container, {
license: CESDK_WEB_LICENSE
});
// The same list your runner passes to prepareCoDesign, with this person's options.
await addPluginModules(cesdk.engine, [
await yourPluginModule(),
googleFonts()
]);
// After the plugins: a design loaded first fetches its images before their resolver exists.
// Refuses, before the person works on it, an editor on another CE.SDK than the
// executor's, or a design a newer CE.SDK saved. Remembered for saveDesign.
await loadDesign(cesdk.engine, design);
// Which panel shows which of your sources is your editor's configuration. The ids
// are the sources your plugin module registers (here, Bring Your Brand's).
cesdk.ui.updateAssetLibraryEntry('ly.img.typefaces', {
sourceIds: ['acme.brand.typefaces']
});
cesdk.ui.updateAssetLibraryEntry('ly.img.colors', {
sourceIds: ({ currentIds }) => ['acme.brand.colors', ...currentIds]
});
// A person's upload goes where the agent's go: your route, answering the stored asset.
// An image is kept with its size in pixels, as the SDK tells your sources for the agent's.
cesdk.actions.register('uploadFile', async (file: File) => {
const size = imageSizeOf(
new Uint8Array(await file.arrayBuffer()),
file.type
);
return uploadToYourRoute(file, size);
});
let version = design.version; // what your read route answered: changes on every save
async function save(): Promise<void> {
// An archive stays an archive; a scene keeps only addresses that can be fetched again.
// cesdkVersion is this editor's CE.SDK, for the route's checks and your store.
const { content, cesdkVersion } = await saveDesign(cesdk.engine);
const archive = content.archive !== undefined;
const response = await fetch(`/api/designs/${designId}`, {
method: 'PUT',
headers: {
'If-Match': version,
'X-CESDK-Version': cesdkVersion,
'Content-Type': archive ? 'application/zip' : 'text/plain'
},
body: archive ? content.archive : content.scene
});
if (response.status === 412) return keepMineOrTheirs(); // someone saved first: below
if (response.status === 409) return askToReload(await response.text()); // another CE.SDK version
if (!response.ok) throw new Error(`save failed: ${response.status}`);
version = response.headers.get('ETag') ?? version;
}
async function keepMineOrTheirs(): Promise<void> {
const latest = await readDesign(designId); // the newer design and its version
if (await personWantsTheirs()) {
await loadDesign(cesdk.engine, latest); // the person's unsaved change is dropped
version = latest.version;
} else {
version = latest.version; // the next save lands over the newer design
await save();
}
}
function close(): void {
unloadDesign(cesdk.engine); // frees the archive's temporary blob: URL
cesdk.dispose();
}
return { cesdk, save, close };
}
// Yours: your CE.SDK web license for this page's domain.
const CESDK_WEB_LICENSE = '<your CE.SDK license>';
// Yours: your read route. It answers the design's content, its version (the
// ETag) and the CE.SDK that saved it (null: unknown), as your design store's
// `read` returns them.
async function readDesign(
_id: string
): Promise<Pick<StoredDesign, 'content' | 'version' | 'cesdkVersion'>> {
throw new Error('yours to write');
}
// Yours: the plugin module your runner sends, as text, with this person's
// options (an API address, a short-lived token from your API).
async function yourPluginModule(): Promise<EnginePluginModule> {
throw new Error('yours to write');
}
// Yours: stores a file through your upload route, with an image's size (undefined
// for a file that is no image), and answers the asset your source lists, with
// the size as `meta.width` and `meta.height`.
async function uploadToYourRoute(
_file: File,
_size: ImageSize | undefined
): Promise<AssetDefinition> {
throw new Error('yours to write');
}
// Yours: a dialog. true: load the newer design; false: keep this one.
async function personWantsTheirs(): Promise<boolean> {
throw new Error('yours to write');
}
// Yours: tells the person to reload the page.
function askToReload(_message: string): void {
throw new Error('yours to write');
}

addPluginModules(engine, plugins) does three things CE.SDK’s own addPlugin does not:

  • It takes the list as the runner sends it. Each entry is { module, options }, the shape prepareCoDesign({ plugins }) takes. It loads each module, builds its plugin with the options, and initializes it on the engine, in order, with the executor’s own code and checks. A module that does not load, a default export that is not a function returning a plugin, an initialize that throws, or a source that fails the checks throws a CoDesignError, the SDK’s error, whose kind is invalid_options, naming the plugin.
  • It installs one URI resolver over your sources’ resolve. A URI resolver is the engine’s hook that turns an image’s address, its URI, into a URL it can fetch. If your images use an address scheme of your own (acme://…) that a source turns into a fetchable URL, CE.SDK itself never calls resolve: without the resolver such an image shows as nothing, with no error; see Private Buckets. The resolver takes the engine’s only resolver slot, so answer your schemes in a source’s resolve rather than setting a resolver of your own.
  • It resolves the thumbnails (thumbUri, previewUri) your sources return, so the editor’s panels show them. An image with no thumbUri shows its own uri as the thumbnail; a video or audio file without one shows none.

Call it once per engine, before loadDesign. It loads the modules from data: URLs, so a Content Security Policy (CSP) on your page needs script-src data:. Your sources then run in the person’s browser too, not only on the executor: the images and fonts they answer, and the API a module calls, are fetched from your editor’s page, so those hosts must send cross-origin resource sharing (CORS) headers for its origin, or the browser blocks the requests.

loadDesign(engine, { content, cesdkVersion }) takes the design as your store’s read returned it: a scene string, CE.SDK’s text form of a design; an .imgly archive, a design with its images packed in; or an address ({ uri }) your page fetches first, so it must allow your page’s origin with CORS headers. It remembers which form it loaded, for the save. Content CE.SDK cannot load fails with kind load_failed, and an address that does not answer with fetch_failed. Before it loads, loadDesign refuses what this editor could damage on save, so the person never works on a design they cannot keep:

  • an editor whose CE.SDK is not exactly CESDK_VERSION, the executor’s: a CoDesignError of kind invalid_options that names both versions;
  • a design a newer CE.SDK saved, from cesdkVersion: kind older_saver.

When it works, the editor shows the agent’s design with its brand fonts and images, the panels list your sources, and an image in your own scheme shows the image, not an empty box.

unloadDesign(engine) frees the archive’s blob: URL. Call it when the editor closes, or a page that opens many editors keeps every archive’s bytes:

function close(): void {
unloadDesign(cesdk.engine); // frees the archive's temporary blob: URL
cesdk.dispose();
}

Save It#

let version = design.version; // what your read route answered: changes on every save
async function save(): Promise<void> {
// An archive stays an archive; a scene keeps only addresses that can be fetched again.
// cesdkVersion is this editor's CE.SDK, for the route's checks and your store.
const { content, cesdkVersion } = await saveDesign(cesdk.engine);
const archive = content.archive !== undefined;
const response = await fetch(`/api/designs/${designId}`, {
method: 'PUT',
headers: {
'If-Match': version,
'X-CESDK-Version': cesdkVersion,
'Content-Type': archive ? 'application/zip' : 'text/plain'
},
body: archive ? content.archive : content.scene
});
if (response.status === 412) return keepMineOrTheirs(); // someone saved first: below
if (response.status === 409) return askToReload(await response.text()); // another CE.SDK version
if (!response.ok) throw new Error(`save failed: ${response.status}`);
version = response.headers.get('ETag') ?? version;
}

saveDesign(engine) returns { content, cesdkVersion }: the design ready for your store, and the CE.SDK that saved it, for your save route and designs.write. The content is chosen as the executor’s saves choose:

  • an archive, when loadDesign loaded one, so the images packed in it are kept;
  • a scene string, otherwise, keeping addresses in http, https, bundle, and any scheme of your own the engine names;
  • an archive after all, when the scene holds images only this tab has, such as a pasted image at a blob: address, which would dangle after a reload.

It waits for images still loading, and stores and sends nothing. Your page sends the save with two headers: If-Match, the version your read route answered (then the one each save answers), and X-CESDK-Version, the cesdkVersion that saveDesign returned. When CE.SDK can save the design neither as a scene string nor as an archive, it rejects with a CoDesignError of kind archive_failed.

Your save route, on your server, makes the checks the SDK makes before it calls your designs.write. The browser belongs to the person, who can change what it sends, so the route is where these checks hold (see Security):

import {
assertCanSave,
CESDK_VERSION,
isCoDesignError,
type DesignContent,
type DesignStore
} from '@imgly/codesign-sdk';
// Your editor's save: PUT /api/designs/:id. Any web framework works the same way.
export async function saveDesignRoute(
request: Request,
id: string
): Promise<Response> {
const saver = request.headers.get('X-CESDK-Version'); // the CE.SDK the editor runs
const ifMatch = request.headers.get('If-Match'); // the version the editor opened
if (saver === null || ifMatch === null) {
return new Response('send X-CESDK-Version and If-Match', { status: 428 });
}
// Only the CE.SDK you deploy saves: a tab opened before a release still runs the old one.
if (saver !== CESDK_VERSION) {
return new Response(`Reload the page: this editor runs CE.SDK ${saver}.`, {
status: 409
});
}
// The same store your runner passes to prepareCoDesign, for this person's tenant.
const designs = designStoreFor(await tenantOf(request));
const stored = await designs.read(id, { signal: request.signal });
if (stored === undefined) return new Response(null, { status: 404 });
try {
// Throws kind older_saver when a newer CE.SDK saved the stored design than
// this editor's, and invalid_options for a version it cannot read.
assertCanSave({ saver, recorded: stored.cesdkVersion });
} catch (err) {
if (!isCoDesignError(err)) throw err;
return new Response(err.message, {
status: err.kind === 'older_saver' ? 409 : 400 // the message names both versions
});
}
const bytes = new Uint8Array(await request.arrayBuffer());
const content: DesignContent<ArrayBuffer> =
request.headers.get('Content-Type') === 'application/zip'
? { archive: bytes }
: { scene: new TextDecoder().decode(bytes) };
// Stored only while the design is still at the version the editor opened;
// no editKey: a person's save is no agent edit.
const version = await designs.write(id, content, {
ifMatch,
cesdkVersion: saver
});
if (version === undefined) {
return new Response('changed', { status: 412 }); // the agent or another tab saved first
}
return new Response(null, { status: 200, headers: { ETag: version } });
}
// Yours: the tenant of the signed-in person, from your session cookie.
async function tenantOf(_request: Request): Promise<string> {
throw new Error('yours to write');
}
// Yours: your design store (see Connect Your Storage).
function designStoreFor(_tenantId: string): DesignStore {
throw new Error('yours to write');
}
  • Both headers are required. A save without X-CESDK-Version or If-Match is answered 428: an editor that sends neither is one the route cannot check.
  • The CE.SDK check refuses an editor of another CE.SDK version with 409, typically a tab left open across your release.
  • assertCanSave refuses when a newer CE.SDK saved the stored design, for instance executors you already moved forward. It throws a CoDesignError of kind older_saver whose message names both versions (its saver and recorded fields hold them), which the route answers 409, or of kind invalid_options for a version it cannot read, answered 400. Any other error is not a refusal, so the route rethrows it.
  • If-Match makes the write conditional: designs.write stores only while the design is still at the version the editor opened, and answers undefined otherwise. The route then answers 412.

When Someone Saved First#

A 412 means the stored design changed since the editor opened it, most likely because the agent edited it. Your editor decides with the person: load the newer design (their unsaved change is lost, or they redo it), or keep theirs by saving again with the new version, over the agent’s work. Either way, the agent’s next edit is refused because the design changed since the agent last read it, and the model looks before it builds on what the person did. For versions and parallel edits in depth, see Concurrent Edits.

async function keepMineOrTheirs(): Promise<void> {
const latest = await readDesign(designId); // the newer design and its version
if (await personWantsTheirs()) {
await loadDesign(cesdk.engine, latest); // the person's unsaved change is dropped
version = latest.version;
} else {
version = latest.version; // the next save lands over the newer design
await save();
}
}

Panels and Uploads#

addPluginModules registers your sources. Which panel shows which source is your editor’s configuration, with CE.SDK’s own calls:

// Which panel shows which of your sources is your editor's configuration. The ids
// are the sources your plugin module registers (here, Bring Your Brand's).
cesdk.ui.updateAssetLibraryEntry('ly.img.typefaces', {
sourceIds: ['acme.brand.typefaces']
});
cesdk.ui.updateAssetLibraryEntry('ly.img.colors', {
sourceIds: ({ currentIds }) => ['acme.brand.colors', ...currentIds]
});
// A person's upload goes where the agent's go: your route, answering the stored asset.
// An image is kept with its size in pixels, as the SDK tells your sources for the agent's.
cesdk.actions.register('uploadFile', async (file: File) => {
const size = imageSizeOf(
new Uint8Array(await file.arrayBuffer()),
file.type
);
return uploadToYourRoute(file, size);
});

A person’s upload is kept with its size in pixels, as the agent’s uploads are. Every image asset your sources list carries meta.width and meta.height: CE.SDK sizes a placed image from them, and the agent frames a logo at its aspect only when it knows it. The SDK tells your sources the size of every image the agent stores, but an upload in your editor goes through CE.SDK’s uploadFile action to your route, and the SDK never sees it. So read the size where you have the bytes: imageSizeOf(bytes, file.type) from @imgly/codesign-sdk reads it from the file’s header, with no image decoder, in the browser before the upload as above or in your route on the server. A search that meets an image without a size fails with invalid_asset (see Image Sizes in the API Reference).

The Designs Panel#

The source that lists your designs (the id you gave as listSourceId in your design store) is a plain CE.SDK source, and CE.SDK applies one of its assets as it applies any asset, which places a design as an empty block. If your designs panel should open a design instead, give that source an applyAsset (CE.SDK’s own member) that does what your product does.

What Fails Silently#

Mistake What the person sees Test
No resolver for your own scheme (plugins added with CE.SDK’s addPlugin alone) Images show as nothing; no error Open a design with an image in your scheme and check that engine.block.getState(id).type is not 'Error' ('FileFetch' names a fetch that failed)
A design loaded before addPluginModules The same The same
An archive saved as a scene string Images an import packed into the design are gone at the next open Import a Photoshop file (PSD) with an image no source keeps, edit it in the editor, save, reopen
A pasted image saved in a scene string It dangles after a reload Paste an image, save, reload
An editor on another CE.SDK version The design loses settings at the next save, with no error Open a design in an editor of another version, expect loadDesign to throw invalid_options; save from one, expect 409
An unconditional save The agent’s edit made while the editor was open is overwritten; nobody is told Open a design, let the agent edit it, save in the editor, expect 412
An upload route that keeps no image size Nothing in the editor; the agent’s next search fails with invalid_asset, naming the source and the upload Upload an image in the editor, then run testAssetSource on the source it landed in

Next Steps#

  • Connect Your Storage — build the design store and the plugin module your editor shares with the agent.
  • Concurrent Edits — handle versions and parallel calls, and work on a copy for approval.
  • Versions and Upgrades — move your editor and your executors to a new CE.SDK together.
  • API Reference — look up every option of @imgly/codesign-sdk/cesdk-utils.