Search Docs
Loading...
Skip to content

API Reference

Look up the signature, defaults, and failure modes of every function, option, interface, and error in @imgly/codesign-sdk, ./testing, and ./cesdk-utils.

The code here is type-checked against the SDK but not run; the quickstarts are the projects that run. Terms such as scene, design, version, executor, conversation, tenant, and engine session are defined in Terminology, and how the pieces fit together is in Architecture.

Install#

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

Run the executor image of the same version, imgly/codesign-executor:1.83.0 ( Versions and Upgrades).

Entry Points#

Import from What is there
@imgly/codesign-sdk The tools, the interfaces you implement (DesignStore, CoDesignAssetSource), IMG.LY’s catalogs, the tool contract, errors, and versions.
@imgly/codesign-sdk/testing fakeExecutors() and the checks testAssetSource and testDesignStore, for your own tests (Testing).
@imgly/codesign-sdk/cesdk-utils Helpers for your own CE.SDK editor: addPluginModules, loadDesign, saveDesign, and unloadDesign (./cesdk-utils).

The package has no runtime dependencies. . and ./testing need no CE.SDK package, because they carry copies of the CE.SDK types they mention (CE.SDK Types), and run on any server runtime that can reach an executor, such as Node.js 22 or later or a Cloudflare Worker. ./cesdk-utils works on the engine of the CE.SDK editor you already load. There is no in-process engine, no API for custom importers or exporters, and no API for building an executor: every product runs IMG.LY’s executor image.

Preparing CoDesign#

prepareCoDesign#

function prepareCoDesign(options: CoDesignOptions): CoDesign;
interface CoDesignOptions {
conversation: string; // required: your chat, job, or request id; its designs share one executor
executors: Executors; // required: remoteExecutors({ acquire, token })
plugins: readonly EnginePluginModule[]; // required: [googleFonts()] is the smallest list
gateway: GatewayOptions | false; // required: { bearer } adds image generation, false leaves it out
designs?: DesignStore; // your designs: { listSourceId?, read, write }; default: in memory
design?: string; // the design a call works on when the model names none
uploadExport?: (req: UploadExportRequest) => Promise<UploadExportResult>; // lets the agent hand people files
tools?: readonly CoDesignTool[];
events?: readonly CoDesignEventListener[];
}

Returns the tools for your agent loop and methods for your own code. It is synchronous, contacts nothing, runs none of your code, and keeps nothing between calls, so create one per chat message, per job, or per workflow step, and discard it after.

Option Default What it does
conversation required Your id for the work: a chat’s id across all its turns, or one job’s. Every design of the conversation opens on one executor, which never serves another conversation. acquire receives it. If two of your tenants could have equal ids, prefix them with the tenant (`${tenantId}.${chatId}`).
executors required Where the engines are: the return value of remoteExecutors, or fakeExecutors() in a test.
plugins required The CE.SDK plugins every engine session applies, in order: your own modules ({ module, options }) and IMG.LY’s catalogs (googleFonts(), cesdkDefaults(), cesdkDemo()), each off unless listed. The order ranks search results, the font lookup, and which source is offered a new file first (Sources).
gateway required IMG.LY’s AI gateway. { bearer } lets the agent generate images, video, and audio; false leaves that out (Image Generation).
designs in memory Your design store. Without it, designs live in this object’s memory only, and the agent cannot start a new design or list saved ones.
design none with designs; without, a new in-memory design with a made-up id The design a call works on when the model names none, such as the design open in your editor. An id your store does not hold yet reads as an empty design, and its first write creates it.
uploadExport none Stores a file the model exported for the person and says where they get it (uploadExport). Your own codesign.export() never calls it.
tools none Your own tools, in the tool shape. They come last in codesign.tools, unchanged.
events none Listeners that observe each tool call (events).

Throws at once a CoDesignError of kind invalid_options that names the option, when:

  • conversation is not a non-empty string
  • executors is not what remoteExecutors (or fakeExecutors) returned
  • gateway is neither false nor { bearer } with a function
  • uploadExport is not a function
  • plugins is not a list, or an entry is not { module, options }, googleFonts(), cesdkDefaults(), or cesdkDemo()
  • designs has no read and write functions, or its listSourceId is not a non-empty string
  • a listener’s timeoutMs is not a positive number up to 2147483647

One of your tools with a reserved name, or two that share a name, throw tool_name_clash.

Checked at the first call that needs an engine: the token, your modules, your sources, and whether a plugin registered designs.listSourceId (if not: invalid_options, naming the id and the sources the executor reports). Call codesign.handshake() to find out before the person types.

Fails silently: without designs, a CoDesign created per chat message starts every message with no designs. In development the SDK prints one warning when a second CoDesign without designs is built in a process.

CoDesign#

interface CoDesign {
readonly tools: readonly CoDesignTool[];
readonly instructions: string;
readonly fingerprint: string;
create(opts: {
title: string;
group?: string;
from?: DesignOrigin;
signal?: AbortSignal;
}): Promise<LoadResult>;
load(
file: Uint8Array | URL,
opts?: { design?: string; signal?: AbortSignal }
): Promise<LoadResult>;
edit<A, R>(
code:
| string
| ((ctx: { engine: unknown; design: unknown; args: A }) => R | Promise<R>),
opts?: { design?: string; version?: string; args?: A; signal?: AbortSignal }
): Promise<EditResult<Awaited<R>>>;
export(
format: 'scene',
opts?: { design?: string; signal?: AbortSignal }
): Promise<string>;
export(
format: 'pdfx',
opts?: { design?: string; pdfx?: PdfxOptions; signal?: AbortSignal }
): Promise<Uint8Array<ArrayBuffer>>;
export(
format: Exclude<ExportFormat, 'pdfx'>,
opts?: { design?: string; signal?: AbortSignal }
): Promise<Uint8Array<ArrayBuffer>>;
release(opts?: { design?: string; signal?: AbortSignal }): Promise<void>;
handshake(opts?: {
design?: string;
signal?: AbortSignal;
}): Promise<HandshakeResult>;
}
interface LoadResult {
design: string; // the design's id
format: string; // 'archive', 'scene', 'image', 'svg', 'psd', 'idml', 'pptx', 'pdf', …
version: string; // your store's version the load wrote (StoredDesign.version)
warnings?: string[]; // what could not be carried over, such as a substituted font
}
interface EditResult<V> {
design: string;
version: string; // your store's version after the call; the one it read when `changed` is false
changed: boolean;
value: V | undefined; // what the code returned, as JSON
findings: { id: number; label: string; message: string }[]; // design-lint findings; they never block the write
}
type DesignOrigin =
| { template: AssetResult; source: string }
| { file: AssetResult; source: string }
| { design: string };
type ExportFormat =
| 'imgly' | 'png' | 'jpeg' | 'webp' | 'svg'
| 'pdf' | 'pdfx' | 'html' | 'mp4' | 'wav' | 'm4a';

The object prepareCoDesign returns. Each method works on opts.design, or on the design option when you leave that out, as a tool call does.

Member What it is
tools Every tool for your agent loop: the SDK’s for the options you set, then yours. Hand the model all of them.
instructions A few sentences to append to your system prompt (instructions and fingerprint).
fingerprint A hash of the fixed text the model reads from IMG.LY (same section).

Every call, the model’s and yours, reads the design with designs.read, opens the design’s engine session through acquire, runs, and, when it changed the design, writes with designs.write on the version it read. It keeps nothing afterwards. The version the model passes back, parallel calls, and resent calls are explained in Concurrent Edits. There is no close: when your code stops calling, only engine sessions remain open, and they end on their timers (Lifetimes).

create(opts)#

Starts a new design without a model and returns its LoadResult; design is the new id (a UUID the SDK makes up).

  • from is { template, source } or { file, source } (an asset your source’s findAssets or fetchAsset returned, and that source’s id), or { design } (a design to copy). Left out, the design is empty. What a file carried is offered to your sources.
  • The first designs.write creates the design: it carries no ifMatch and passes title and group on to your store.

load(file)#

Replaces a design’s content with a file, given as bytes or a URL.

  • Accepts .imgly, .scene, an image, an SVG, PDF, IDML (InDesign), PPTX (PowerPoint), or PSD (Photoshop), read from the bytes, not the file name (File Formats).
  • Writes with designs.write only if nobody wrote the design since load read it; otherwise rejects with design_conflict.
  • version is your store’s version it wrote. Put it in the model’s first message, so that its first change is not refused for a missing version.
  • A URL is fetched by your runner with globalThis.fetch, from inside your network: never pass a URL a user typed without checking that it is one of yours. A non-2xx answer rejects with http_status, a network failure with fetch_failed. signal stops the download.

edit(code)#

Runs design code without a model, exactly as the model’s code runs: the same engine, sources, design lint (IMG.LY’s checks for problems such as overflowing text), and conditional write. Not charged.

  • code sees engine (CE.SDK’s engine), design (CoDesign’s design API, which the handbook the model reads teaches it), and args. It is a string, or a function ({ engine, design, args }) => value, sent as its source text: a function that uses anything it closes over fails with code_failed, before any write.
  • args and the return value are JSON. Pass your data in args, never inside the code.
  • version runs it only while the design is at that version of your store (default: the latest). A stale version, or a save that landed while it ran, rejects with design_conflict.
  • Code that throws rejects with code_failed and writes nothing. Code that returns something that cannot be a result also rejects with code_failed, but its change was already written.
  • findings are the design linter’s notes on the result; they never block the write.

export(format)#

Returns the design as your store holds it now. It never writes the design and never calls uploadExport.

  • 'scene' returns the scene string. Every other format returns the file’s bytes, rendered on the executor, ready for new Blob([bytes]).

  • A PDF keeps CMYK colors (the four process inks of print) as CMYK. html is one self-contained HTML file with its images embedded.

  • pdfx is a print-ready PDF/X file (the ISO standard for exchanging print files) with IMG.LY’s defaults. Only export('pdfx', { pdfx }) takes options:

    PdfxOptions field What it sets Default
    outputStandard 'PDF/X-4' (keeps live transparency), 'PDF/X-3', or 'PDF/X-1a' (CMYK only, always flattened) 'PDF/X-4'
    outputProfile The output intent, the printing condition the file is made for: 'fogra39' (European coated paper), 'gracol' (US), 'srgb' (not print-compliant; refused with PDF/X-1a), or 'custom' 'fogra39'
    profileUri The color profile (an .icc file from your printer): an https URL, required with 'custom' and refused otherwise none
    nonPrinterProfile A display profile such as 'srgb': produce the file anyway ('keep') or refuse ('reject') 'keep'
    embedICCProfile Embed the profile in the file; false gives a plain PDF that is no longer PDF/X true
    flattenTransparency PDF/X-3 only: rasterize transparent content (false keeps it live). Ignored on PDF/X-4; PDF/X-1a always flattens true on PDF/X-3
    flattenResolution Dots per inch of flattened pages, 1 to 65,535 (PDF/X-1a, and PDF/X-3 unless flattenTransparency is false) 300
    title The document title the design’s title
    outputCondition, outputConditionIdentifier The output condition the file names, as your printer asks from the profile
  • mp4, wav, and m4a need an executor that encodes video; one that does not rejects them with video_unsupported. The handshake’s capabilities.video says whether yours does. For what is supported, see Status.

  • Every format except scene and imgly is charged (File Formats).

release()#

Ends a design’s engine session now instead of on its timers. release() works on the design option, release({ design }) on another design.

  • An operation still running there finishes first. The next call on that design, from anywhere, loads it again.
  • Succeeds when the session is already gone, and when the executor cannot be reached or answers an error. Rejects only with the signal’s reason when you abort it (signal stops only the wait), or with what your acquire throws.
  • Once the conversation’s last design is released, its executor exits about 30 seconds later, and the next call waits for a new one. Call it when a conversation or job ends, not after every turn.

handshake()#

Opens the design’s engine session early and returns the executor’s HandshakeResult, or rejects with the error the first tool call would have thrown.

instructions and fingerprint#

codesign.instructions is a few sentences IMG.LY writes for this tool set: read the handbook (IMG.LY’s guide to the engine and to finished designs, which the model reads through the tools) before the first change, look at the design after every change, score it against the handbook’s judging guide before calling it done, and pass back the design and version the last result gave. Append it to your system prompt:

const system = `${yourProductPrompt}\n\n${codesign.instructions}`;

Fails silently: left out, nothing fails, but the model uses the tools worse and learns about versions only from its first refused change. Log one turn’s system prompt and check that it is there.

codesign.fingerprint is a 16-character hex hash of the fixed text the model reads from IMG.LY: the SDK tools’ names, descriptions, and input schemas, instructions, and the handbook. It leaves out your own tools and anything per session (plugins, sources, store). It changes between SDK versions and with uploadExport, gateway, designs, and design. Record it with the handshake’s answer for each eval run and support ticket: together they are what a model saw. The text is byte-identical for one fingerprint, so a model provider’s prompt cache holds.

Tool names, descriptions, and schemas can change in any release, so neither your code nor your prompts depend on them ( Tools). CoDesignToolName, the type of the SDK’s tool names as ToolCallEvent.tool carries them, is for labeling metrics, never for deciding what your product does.

Cancellation: signal#

A signal on create, load, edit, export, or a tool call (ToolCallContext.signal) cancels that call: its promise rejects with the signal’s reason, and nothing is written. A tool call with events rejects once your listeners return.

  • Work already running on the executor finishes, and its answer is dropped unwritten. The next call on that design loads the design again.
  • A signal also ends a call’s wait for an exiting executor.
  • Image generation closes its requests to the AI gateway, but a generation the gateway had already started may still be charged.

Watching Each Call: events#

type CoDesignEventListener = {
on: 'tool.call';
timeoutMs?: number; // default 5000
run: (e: ToolCallEvent) => void | Promise<void>;
};
interface ToolCallEvent {
type: 'tool.call';
tool: CoDesignToolName | (string & {}); // or the name of one of your tools
ok: boolean; // false for an error result or a throw
error?: {
kind?: CoDesignErrorKind | 'aborted'; // the reason, when the SDK knows it
thrown?: unknown; // the error the tool threw
};
ms: number; // the whole call, without the listeners
phases: readonly { name: ToolCallPhase; ms: number }[];
design?: string; // the design the call worked on (the first, if several)
wrote?: string; // your store's version the call wrote (StoredDesign.version)
}
type ToolCallPhase = 'store_read' | 'engine_open' | 'engine_run' | 'store_write';

One frozen event per call of each tool in codesign.tools, your own tools included, for latency and error dashboards. create, load, export, and release give none. An event with wrote means that design changed, so a chat can refresh its preview. Build the listener where you handle a request, so it closes over that request’s ids; the event carries none of your data:

import {
prepareCoDesign,
type CoDesignEventListener,
type DesignStore,
type EnginePluginModule,
type Executors
} from '@imgly/codesign-sdk';
import { errorTracker, metrics } from './yours.js';
interface Request {
tenantId: string;
chatId: string;
id: string;
}
function eventsFor(req: Request): CoDesignEventListener[] {
return [
{
on: 'tool.call', // the only event type today
run: async (e) => {
try {
metrics.timing(`codesign.${e.tool}`, e.ms, {
ok: String(e.ok),
tenant: req.tenantId
});
for (const p of e.phases) {
metrics.timing(`codesign.${e.tool}.${p.name}`, p.ms);
}
// A cancel by your user is not an error.
if (e.error?.thrown !== undefined && e.error.kind !== 'aborted') {
errorTracker.capture(e.error.thrown, { request: req.id });
}
} catch {
metrics.increment('codesign.events.listener_failed');
}
}
}
];
}
export function toolsFor(
req: Request,
executors: Executors,
plugins: EnginePluginModule[],
designs: DesignStore
) {
return prepareCoDesign({
conversation: `${req.tenantId}.${req.chatId}`,
executors,
plugins,
designs,
gateway: false,
events: eventsFor(req)
});
}

Tag metrics only with values from a small set (tenant, tool, ok, kind); every new value makes a new metric series, so per-request ids go to logs or traces.

A call ends in ok error
success true absent
an error result false kind when the SDK knows the reason: a CoDesignErrorKind or one of the tools’ own codes
a throw false thrown is the error your loop receives; kind when it is an SDK error
an abort false kind is 'aborted': the user canceled, so count these apart from failures

Later versions add kinds, phases, and events, so handle a value you do not know and compare kinds as strings. Every kind and code is on Errors.

Phase What it measures
store_read Your designs.read, in your runner.
engine_open Getting an engine session: your acquire, and any wait while an executor exits.
engine_run The engine’s work on the executor, including loading the design when the session lacks it.
store_write Your designs.write, including the read after a conflict.

Phases never overlap, and their sum is at most ms. A phase can occur more than once in a call, so add up entries with the same name. Reading the handbook and your own tools have no phases.

  • When a listener runs: after the tool’s work, before the result goes back to your loop. Listeners are awaited in order, each for at most its timeoutMs; past that the SDK goes on and logs one line with console.error. Listener time is not in ms but delays the result. Await your send if your platform freezes the process after the response.
  • When a listener fails: the tool result does not change. The SDK logs the error once per listener and message, at most 100 lines per prepareCoDesign result. Catch errors in your listener, as the example does.
  • Fails silently: the list is copied by prepareCoDesign, so a listener you add to your array later gets no events.
  • Your data in errors: error.thrown is the original error with its stack; for an executor error, the executor’s message is in Error.cause. It can contain your store’s URLs or keys, so check it before you send it to an external service.

File Formats#

The executor reads and writes every format itself; you configure nothing.

Direction Formats Through
In .imgly, .scene, PNG, JPEG, WebP, GIF, SVG, PDF, IDML, PPTX, and PSD load, create, or a design the model starts from a file
Out every ExportFormat: png, jpeg, webp, svg, pdf, pdfx, html, imgly, mp4, wav, and m4a; 'scene' too your codesign.export(), or an export the model asks for (no 'scene')
  • Pictures a file carries are offered to your writable sources, then stay inside the design if none keeps them (Where a New File Goes). Fonts in a PSD, IDML, or PDF are looked up in your typeface sources first, then in Google Fonts. LoadResult.warnings lists what could not be carried over.
  • The PSD, IDML, and PDF importers need googleFonts() in plugins. Without it, such an import fails with an error that says “sources”.
  • An export the model asks for in a format the executor cannot render is refused in words.
  • Billing: the executor charges every file export to your IMG.LY account, once per design, version, format, and options. Saving the design itself (scene, imgly) is free ( Billing). A refused charge rejects with export_refused.

Executors#

remoteExecutors#

function remoteExecutors(options: RemoteExecutorsOptions): Executors;
interface RemoteExecutorsOptions {
// required
acquire: (req: {
conversation: string; // prepareCoDesign's `conversation`
signal?: AbortSignal; // aborted with the tool call that needs the executor
}) => AcquiredExecutor | undefined | Promise<AcquiredExecutor | undefined>;
token?: string; // optional here; IMG.LY's image requires one
}
interface AcquiredExecutor {
url: string;
fetch?: typeof globalThis.fetch;
}
interface Executors {
readonly remote: true; // opaque: pass it as `executors`, nothing else
}

Where your executors are. Pass the result as prepareCoDesign’s executors. It builds a value and contacts nothing. The executor runs the engine and your plugins; your design store stays in your runner, which sends a design’s content to the executor when an engine needs it.

Option What it does
acquire Yours. Returns the executor of conversation. Called before each engine session opens, again when that executor was lost, and on release(). It must answer the same executor for a conversation while that executor lives, and, for a new conversation, an executor no other conversation used. undefined means none can be had now.
token Sent as Authorization: Bearer <token> with every request. Must equal the executor’s EXECUTOR_TOKEN, which IMG.LY’s image requires. Default: none.
AcquiredExecutor field What it is
url The executor’s origin (http://executor-1:8080). Requests go to url plus a path. With your own fetch, it only has to mean something to that fetch.
fetch How to reach this executor, such as a Cloudflare container’s fetch, one that adds a routing header, or a test double. Default globalThis.fetch. codesign.load(url) never uses it.

Binding to a Conversation#

  • An executor binds to the first conversation that opens a design on it and refuses every other with HTTP 409, which rejects with executor_bound. The error carries short hashes of the two conversations, never the ids, and its message calls the executor a sandbox.
  • conversation is the only thing acquire routes by; there is no tenant argument.
  • GET /health answers bound: true or bound: false. The image keeps the binding in a volume at /var/lib/codesign-executor, so a restarted container stays bound, and only a new container starts unbound.
  • Two runners that claim one executor for two conversations at once get executor_bound for the second, and nothing leaks.

Why every conversation gets a new container, and how to run them: see A New Container for Each Conversation.

Engine Sessions#

An executor runs one CE.SDK engine, and every design of its conversation opens on it in turn. A call for another design swaps that design in from your store, which costs about one load; nothing is lost, because every change was already written. Calls run one at a time, in arrival order. Each design has its own engine session on that one engine: its place there, with its own timers, named from the design’s id, the SDK’s build (SDK_VERSION), and your plugin modules; your plugins’ options are not part of the name, so a call with new options sets the sources up again.

What a call gets from the executor:

The executor The call
is retiring (503: its last session ended and it is about to exit) A call that opens a design waits and asks again after Retry-After, up to 60 seconds, then rejects with executor_capacity. Any other call rejects at once.
cannot be had (acquire returns undefined) or cannot be reached Rejects with executor_unreachable. A short retry is worth it.
is bound to another conversation (409) Rejects with executor_bound: fix acquire.
speaks another wire protocol (409), the format of SDK–executor requests Rejects with executor_version, naming both protocol numbers and the runner’s SDK_VERSION. Run one version ( Versions and Upgrades).
refuses the token (401) Rejects with executor_unauthorized: token must equal EXECUTOR_TOKEN.
took the operation and could not finish it (422), such as code past its five-minute limit A tool call answers the model with the executor’s message; your own create, load, edit, or export rejects with executor_failed.
holds a session of this conversation with other plugin modules (409) Rejects with a plain Error with no kind, whose message says the session sends other plugin modules. Build every session’s plugins in one place.
refuses anything else, such as a module that fails or a malformed source Rejects at once, naming the module or source (Opening a Session). An error acquire throws is thrown as it is.

A tool call that rejects is not a result the model reads: your loop decides whether to report a failed call, retry, or end the run.

When the executor is lost (404 for a forgotten session, an idle close, a restart, unreachable, or a proxy’s 502 or 504), the SDK calls acquire again for the same conversation and loads the design there from your store. If the new executor runs another CE.SDK version, other capabilities, or other sources, the session ends and the call rejects with executor_changed, to your code, never to the model; calling again opens a new session there. Two executors on the same CE.SDK but different SDK builds are not told apart, so run one build. Never put a round-robin load balancer in front of the executors.

Lifetimes#

What Lives by default At most What ends it What survives it
Tool call as long as its work takes 60 s waiting for a retiring executor (executor_capacity) its result or error, or its signal the design and its version, in your store
Turn from one message to the model’s answer your loop’s step limit the model answering in text, or your loop stopping it the design, in your store; its engine session, for up to 3 minutes
Conversation as long as your product keeps it your product’s choice your product the design, in your store
Engine session until 3 minutes after its last call until the executor’s engine closes, 30 minutes after it started either timer; codesign.release({ design }); its executor exiting nothing of its own: the design was written to your store after every change
Executor process one conversation: from start until its last engine session ends 10 minutes waiting for its first conversation its last engine session ending (it exits 30 s later); no session within 10 minutes; your platform stopping it its binding, in the container’s volume

An operation still running when a timer fires or release arrives finishes first. When an executor exits, its next call finds it gone, acquire answers a new, unbound executor for the same conversation, and the design loads there from your store; that is how a conversation longer than 30 minutes goes on. While a session is open between turns, its container is held, and billed on a platform that bills a container while it runs. The executor’s own limits are in Limits the Executor Sets.

The Handshake#

interface HandshakeResult {
versions: { sdk: string; cesdk: string }; // the executor's SDK_VERSION and CESDK_VERSION
capabilities: {
video: boolean; // the executor encodes video and audio: mp4, wav, m4a, and previews of a moment in time
exportFormats: ExportFormat[]; // the formats an export can render here
};
// The sources, in plugin order.
sources: {
id: string;
mimeTypes: string[]; // its getSupportedMimeTypes()
roles: ('media' | 'typefaces' | 'palette' | 'templates' | 'components' | 'files')[];
writable: boolean; // it has `uploadFile`
description?: string;
}[];
plugins: { name: string; version: string }[]; // for diagnostics
}

The executor’s answer when a session opens: what it can do and which sources your plugins registered. It runs on the first call of a session, whatever that call is; later calls on the session do not repeat it.

  • The handbook the model reads leaves out what this session cannot do, and an export the executor cannot render is refused in words. The tool list does not change.
  • capabilities.video is false on an executor without video encoding, and mp4, wav, and m4a are then refused.
  • It is per session. A lost executor replaced by one with another CE.SDK version, other capabilities, or other sources ends the session with executor_changed. plugins and versions.sdk are not compared.
  • designs.listSourceId must be one of its sources.

await codesign.handshake() returns it early, or rejects as the first tool call would (invalid_options naming a source, executor_unreachable, executor_capacity, executor_version, executor_unauthorized). Use it to show a setup error before the person types, or to start a container while they type.

The Executor Image’s Environment Variables#

Per-tenant setup arrives with each engine session, in plugins; the image itself takes only these. How to run it: Host the Executor.

Variable Default What it does
IMGLY_CESDK_LICENSE required Your CoDesign license key. CESDK_LICENSE is read when it is unset. Missing, or a license file or grant instead of a key: the process exits at start.
EXECUTOR_TOKEN required The bearer token the runner must send (remoteExecutors’s token). Missing: the process exits at start.
PORT 8080 The port it listens on, a whole number from 1 to 65535; anything else stops it at start.

Optional, for an outbound proxy, used by the server and the engine process ( Allow Only the Hosts the Executor Needs):

Variable What it does
HTTP_PROXY, HTTPS_PROXY, NO_PROXY Your forward proxy, and the hosts that bypass it (lower-case spellings too).
NODE_USE_ENV_PROXY Set it to 1 with a proxy. Node.js’s fetch ignores the proxy variables without it, so the license activation goes direct, fails behind an allow-list, and the process exits at start.
NODE_EXTRA_CA_CERTS A path to extra CA certificates, for a proxy or platform that re-signs HTTPS.

Everything else is fixed in the image. Setting EXECUTOR_ONE_SESSION, EXECUTOR_IDLE_MS, EXECUTOR_MAX_LIFETIME_MS, or EXECUTOR_MAX_SESSIONS stops it at start, and so does running it under --init, tini, or a shared process namespace ( Run the Image Hardened).

GET /health needs no token and answers { result: { sessions, accepting, bound, protocol, idleTimeoutMs, maxLifetimeMs } }:

Field What it is
sessions How many engine sessions are open now.
accepting false only while the executor is retiring or its engine failed to start.
bound true once a conversation holds the executor.
protocol The wire protocol it speaks.
idleTimeoutMs, maxLifetimeMs The engine session’s idle limit and the engine’s lifetime limit, in milliseconds.

Sources#

An asset source is CE.SDK’s name for a searchable list of things a design can use: your product’s photos, brand typefaces and colors, templates, and saved components. A plugin is CE.SDK’s name for setup code an engine applies (EnginePlugin: { name, version, initialize({ engine }) }); the ones here register asset sources. You list plugins, never the sources: the executor applies them when a session opens, checks each source they registered, and reports them in the handshake. Your runner never runs source code: it sends each module’s text and options with every call, so a source’s findAssets calls your API from wherever the executor runs. How to build them for your files and brand: Connect Your Storage and Bring Your Brand.

Plugin Modules#

A module is one ES module whose default export is (options) => EnginePlugin and that registers each source with engine.asset.addSource. It is the same file your editor applies with cesdk.engine.addPlugin. A complete module, for a product with a media library, a design list, and brand typefaces behind one API:

// Runs in your CE.SDK editor and on the executor. No imports.
// `api` and `token` are this person's values for this session.
export default function acmeSources({ api, token }) {
const headers = { authorization: `Bearer ${token}` };
const get = async (path) => {
const response = await fetch(`${api}${path}`, { headers });
if (response.status === 404) return undefined;
if (!response.ok) throw new Error(`${path} answered ${response.status}`);
return response.json();
};
// Your API's file as the asset CE.SDK and the agent place. `meta.uri` is
// what the design keeps for good: a stable address, never a presigned URL.
const asAsset = (file) => ({
id: file.id,
label: file.name,
groups: [file.folder], // groups are folders
meta: {
uri: `acme://${file.id}`, // your own scheme; `resolve` below makes it fetchable
thumbUri: file.thumbnailURL,
mimeType: file.mimeType,
width: file.width,
height: file.height
}
});
return {
name: 'acme.sources', // shows in errors
version: '1.0.0',
initialize({ engine }) {
engine.asset.addSource({
id: 'acme.media',
// The model reads this line in search results: say what the source is for.
description:
"This team's photos, logos and videos; groups are folders.",
// What this source holds: it decides the source's role.
getSupportedMimeTypes: () => ['image/png', 'image/jpeg', 'video/mp4'],
async findAssets(query) {
const params = new URLSearchParams({
q: query.query ?? '',
page: String(query.page), // from 0
perPage: String(query.perPage),
folders: (query.groups ?? []).join(',')
});
const found = await get(`/files?${params}`);
return {
assets: found.items.map(asAsset),
currentPage: query.page,
...(found.hasMore ? { nextPage: query.page + 1 } : {}),
total: found.total
};
},
async fetchAsset(id) {
const file = await get(`/files/${encodeURIComponent(id)}`);
return file === undefined ? null : asAsset(file); // null: no such file for this person
},
async getGroups() {
return (await get('/folders')).map((folder) => folder.id);
},
async getGroupLabels() {
return Object.fromEntries(
(await get('/folders')).map((folder) => [folder.id, folder.name])
);
},
// For uris of your own scheme: a URL the engine can fetch right now.
// `undefined`: not mine.
async resolve(uri) {
if (!uri.startsWith('acme://')) return undefined;
const id = encodeURIComponent(uri.slice('acme://'.length));
return (await get(`/files/${id}/signed-url`)).url;
},
// Having this makes the source writable. Keep the file and return its
// asset, or return `undefined` to decline, and the next source is offered it.
async uploadFile(file, { action, group, width, height }) {
if (action === 'import') return undefined; // pictures inside an imported PSD stay in the design
const query = new URLSearchParams({
name: file.name,
folder: group ?? 'Agent'
});
// An image's size in pixels, read by the SDK from the bytes (none for
// a video). Your API keeps it and answers it as `width` and `height`.
if (width !== undefined) query.set('width', String(width));
if (height !== undefined) query.set('height', String(height));
const response = await fetch(`${api}/files?${query}`, {
method: 'POST',
headers: { ...headers, 'content-type': file.type },
body: file
});
if (!response.ok)
throw new Error(`upload answered ${response.status}`);
return asAsset(await response.json());
}
});
engine.asset.addSource({
id: 'acme.designs', // the designs store's listSourceId
description: "This team's saved designs, by title.",
// A design is not a file to place or to be offered new files: no types.
getSupportedMimeTypes: () => [],
async findAssets(query) {
const params = new URLSearchParams({
q: query.query ?? '', // the agent finds saved designs by title
page: String(query.page),
perPage: String(query.perPage)
});
const found = await get(`/designs?${params}`);
return {
assets: found.items.map((design) => ({
id: design.id, // the design id the SDK reads and writes
label: design.title,
meta: { thumbUri: design.thumbnailURL } // a preview for your editor's panel
})),
currentPage: query.page,
...(found.hasMore ? { nextPage: query.page + 1 } : {}),
total: found.total
};
}
});
engine.asset.addSource({
id: 'acme.fonts',
description: "The brand's typefaces. Use these before any other font.",
getSupportedMimeTypes: () => ['application/ubq-typeface'],
async findAssets(query) {
const all = (await get('/typefaces')).filter(
(family) =>
!query.query ||
family.name.toLowerCase().includes(query.query.toLowerCase())
);
const start = query.page * query.perPage;
const page = all.slice(start, start + query.perPage);
return {
// One asset per family, with every cut in `fonts`.
assets: page.map((family) => ({
id: family.id,
label: family.name,
payload: {
typeface: {
name: family.name, // exactly the family name
fonts: family.files.map((file) => ({
uri: file.url, // durable: designs keep it
subFamily: file.subFamily,
weight: file.weight, // 'normal', 'bold', …
style: file.style // 'normal' or 'italic'
}))
}
}
})),
currentPage: query.page,
...(start + page.length < all.length
? { nextPage: query.page + 1 }
: {}),
total: all.length
};
}
});
}
};
}

The runner sends the file’s text and its options. The smallest list is plugins: [googleFonts()]:

import { readFileSync } from 'node:fs';
import {
googleFonts,
prepareCoDesign,
type DesignStore,
type Executors
} from '@imgly/codesign-sdk';
import { mintToken } from './yours.js';
// The module's text, sent with every call; the file is the one your editor applies.
const acmeSources = readFileSync(
new URL('./acme-sources.plugin.js', import.meta.url),
'utf8'
);
export function toolsForChat(req: {
tenantId: string;
chatId: string;
userId: string;
designId: string;
executors: Executors;
designs: DesignStore;
}) {
return prepareCoDesign({
conversation: `${req.tenantId}.${req.chatId}`, // this chat: its one executor
executors: req.executors,
plugins: [
{
module: acmeSources,
// Passed to the module's default export. The model's code can read
// these, so the token is this person's own, for this session, and short-lived.
options: { api: 'https://api.acme.com', token: mintToken(req.userId) }
}, // brand typefaces rank first
googleFonts() // after the brand's typefaces
],
designs: req.designs,
design: req.designId,
gateway: false
});
}
yours.ts: the clients this page’s code calls
// The clients this page's code calls that are yours to write, over what your product already has.
import type { AcquiredExecutor } from '@imgly/codesign-sdk';
/** Your metrics client, such as StatsD or Datadog. */
export const metrics = {
timing(_name: string, _ms: number, _tags?: Record<string, string>): void {
throw new Error('yours to write');
},
increment(_name: string): void {
throw new Error('yours to write');
}
};
/** Your error tracking, such as Sentry. */
export const errorTracker = {
capture(_error: unknown, _context?: Record<string, string>): void {
throw new Error('yours to write');
}
};
/** A short-lived token that carries exactly this person's rights in your API. */
export function mintToken(_userId: string): string {
throw new Error('yours to write');
}
/** Your database client, such as node-postgres's `Pool`. */
export const db = {
async query(
_sql: string,
_params: readonly unknown[]
): Promise<{ rows: Record<string, any>[] }> {
throw new Error('yours to write');
}
};
/** Your object storage. */
export const bucket = {
async put(_key: string, _bytes: Uint8Array, _type: string): Promise<void> {
throw new Error('yours to write');
},
async delete(_key: string): Promise<void> {
throw new Error('yours to write');
},
async get(_key: string): Promise<Uint8Array> {
throw new Error('yours to write');
}
};
/** A download link to one stored file that expires after a while. */
export async function signedLink(_key: string): Promise<string> {
throw new Error('yours to write');
}
/** Your `acquire`: the conversation's executor, the same one while it lives, and an unused one for a new conversation. */
export async function acquire(_req: {
conversation: string;
signal?: AbortSignal;
}): Promise<AcquiredExecutor | undefined> {
throw new Error('yours to write');
}
Runner How it gets the file’s text
Node.js readFileSync(new URL('./acme-sources.plugin.js', import.meta.url), 'utf8'), as above
Vite, or a bundle built by it import acmeSources from './acme-sources.plugin.js?raw'
esbuild build with --loader:.plugin.js=text, then import acmeSources from './acme-sources.plugin.js'
Cloudflare Workers (wrangler) a rules entry { type = "Text", globs = ["**/*.plugin.js"] }, then import it as with esbuild

Rules for a module:

  • No imports. Nothing on the executor installs a package; bundle a module that uses a library into one file first (esbuild acme-sources.js --bundle --format=esm).
  • No text built from a function (`export default ${fn}`): a bundler or coverage tool changes what Function.prototype.toString returns, and the executor gets a module that throws.
  • Only fetch, URL, TextEncoder, timers, and the engine, which both the executor and a browser have; no window, document, or process.
  • Per-session values go in options, never in the code. The executor keeps every module it loads for the life of its process. A session’s modules together may be at most 256 KB, because the runner sends them with every call.
  • State lives inside the function, not at the top of the file: one loaded module serves every session that sends it.
  • initialize may run again on the same engine with new options. The executor removes the sources the last setup added; anything else a module installs must be safe to install twice.
  • Every design of one conversation sends the same module texts. Only options may differ between calls (Engine Sessions).
  • Do not call setURIResolver. CE.SDK has one resolver slot per engine; the SDK installs one that asks each source’s resolve in list order.
  • A token in options is this person’s, per session, and short-lived, and stays valid until the last call that sends it ends. The model’s code runs beside your sources and can read their options, with the person’s rights ( Security).
  • Your engineers write modules; your users never do. A module runs with everything the engine process can reach.

Source Roles#

Each source answers CE.SDK’s getSupportedMimeTypes() with the kinds of file it holds. From that answer alone the SDK derives the source’s role, which decides how the agent uses it. There are no role options.

A source holding Role The agent uses it to
image/*, video/*, audio/*, or any one of their types (never PSD, PDF, or IDML) media search and place; it is offered new files of those types
font/*, a font file type, or ASSET_MIME_TYPES.typeface typefaces font: { family } in the model’s code: the sources in list order, the first exact family wins
ASSET_MIME_TYPES.color palette answer “our colors”
ASSET_MIME_TYPES.template, ASSET_MIME_TYPES.scene, application/zip templates start a design from one, or merge one into a design
ASSET_MIME_TYPES.blocks components insert; design.saveComponent in the model’s code stores blocks it built here
application/pdf, PSD, IDML files open them as a new design with editable layers; never placed as a fill

A source can hold several kinds. The SDK reads a new file’s type from its bytes, never its name, and always spells it one way (font/ttf, not font/sfnt; audio/mp4, not audio/x-m4a). The ASSET_MIME_TYPES constants, for a module that cannot import them:

Constant Value Meaning
ASSET_MIME_TYPES.typeface application/ubq-typeface a font family, in payload.typeface
ASSET_MIME_TYPES.color application/ubq-color a color, in payload.color
ASSET_MIME_TYPES.template application/ubq-template-string a template
ASSET_MIME_TYPES.scene application/ubq-scene-string a scene
ASSET_MIME_TYPES.blocks application/ubq-blocks-string saved blocks

The session fails with invalid_options, whose source is the source’s id, when:

  • a type is an alias the SDK never passes (image/jpg; the message names image/jpeg), or an application/ubq-… type that is none of ASSET_MIME_TYPES
  • the source is writable (it has uploadFile) but names no type
  • the source holds typeface assets without declaring ASSET_MIME_TYPES.typeface, or color assets without ASSET_MIME_TYPES.color; without this check the font lookup would skip it with no error

testAssetSource runs the same checks in your tests.

Where a New File Goes#

UploadFileOptions.action Why the file is offered
upload Someone added a file they had, from your editor or through your own upload route.
import An imported file carried it, such as a PSD layer’s picture. label is the layer’s name.
generate Image generation made it. label is the prompt, cut to 200 characters.
saveComponent The model saved blocks for reuse. label is the name it gave, thumbnail a PNG of the blocks.
  1. The SDK reads the file’s type from its bytes.
  2. Candidates are the sources that have uploadFile and whose types admit that type, in list order. A wildcard such as image/* never admits PSD, PDF, or IDML.
  3. Each candidate is offered the file in turn. The first to return an asset keeps it; undefined declines, and the next is offered it; a throw fails the operation (throw refusal(text) to tell the model why).
  4. If nobody takes it, a generated file, an imported picture, or an imported font stays inside the design, which is then saved as an .imgly archive (designs.write gets { archive }). Saved blocks have no such place and fail with refused.
  5. When two or more candidates remain, the model may choose which one keeps the file.

Who acted is not passed: the editor and the agent apply the same module with different options, so a product that treats the agent’s writes differently says so there, such as a token that grants less.

Fails silently: a source with uploadFile that never declines takes every file its types admit, including ones you wanted a later source to keep. Check it with testAssetSource’s uploads.

Searching#

A search runs on the executor, as the same engine.asset.findAssets the model’s code calls, so a file uploaded a second ago is found. The result lists each source with its description, its group names (getGroupLabels), and the assets of its page. A source that throws is named as unavailable while the others answer. A search filtered by type passes CE.SDK filter data to findAssets; a source may ignore it, because the SDK drops what does not match.

Image Sizes#

Every image asset a source returns carries its size in pixels, as a positive meta.width and meta.height (a number or a numeric string, 640 or "640"), as CE.SDK’s own catalogs do. CE.SDK sizes a placed image’s block from them, and the model reads them to keep the image’s aspect ratio. The rule covers PNG, JPEG, WebP, GIF, AVIF, and SVG assets; PSDs, videos, typefaces, and colors need no size.

  • uploadFile is told the size, read from the bytes, as width and height. Keep it and answer it from findAssets and fetchAsset. The asset uploadFile returns may leave it out: the SDK adds it.
  • Fails loudly: a search that meets an image asset without a size rejects with invalid_asset, naming the source (error.source) and the asset. So does storing a new image whose size neither the asset nor its bytes give, such as an SVG sized only in %. testAssetSource reports it first.

For an image your own code keeps, such as an upload in your editor:

function imageSizeOf(
bytes: Uint8Array,
mimeType: string
): { width: number; height: number } | undefined;

Reads the size from the file’s header without decoding it: PNG, JPEG, WebP, GIF, and AVIF, and an SVG’s width and height in pixels or its viewBox. Returns undefined for another type, or for bytes that do not say.

Typefaces#

A typeface is a font family: a name and one file per weight and style. A font is one of those files.

  • One asset per family, with every cut in payload.typeface.fonts, name exactly the family, and query honored. Otherwise bold and italic silently do not exist.
  • A family in two typeface sources comes from the first in plugins order. Google Fonts and CE.SDK’s default typefaces share many names, such as Roboto, so keep a brand family out of the sources listed after it; testAssetSource’s laterTypefaceSources flags it.
  • A font file’s uri is written into every design that uses it, so it must last as long as the design: a public, versioned https URL, or a uri of your own scheme that resolve answers. Never a presigned URL.
  • UploadFileOptions.font is for a typeface source to add an imported font file to the right family; the SDK leaves it unset.

googleFonts#

function googleFonts(opts?: { baseURL?: string }): EnginePluginModule;

IMG.LY’s Google Fonts: the source ly.img.gfonts, which the font lookup searches, and ly.img.gfonts-fallbacks, the stand-ins the PSD, IDML, PDF, and PowerPoint importers use for common fonts a file names, such as Helvetica. Nothing is added unless listed. To serve the catalog yourself, copy IMG.LY’s folder as it is and pass it as baseURL; the font files it lists still point at cdn.img.ly. When the catalog cannot be loaded, the call that opens the session rejects:

plugin 0 (ly.img.google-fonts): initialize threw: Google Fonts: could not load …/ly.img.gfonts/content.json (…)

Allow cdn.img.ly, or your copy’s host, in the executor’s egress ( Allow Only the Hosts the Executor Needs).

cesdkDefaults and cesdkDemo#

function cesdkDefaults(opts?: {
include?: readonly string[]; // source ids; default: all of them
baseURL?: string;
}): EnginePluginModule;
function cesdkDemo(opts?: {
include?: readonly string[];
baseURL?: string;
}): EnginePluginModule;

CE.SDK’s own catalogs for exactly CESDK_VERSION, with that editor’s ids and content. cesdkDefaults() is what an editor ships with: stickers, vector shapes, text components, ly.img.typeface, the color palette, text styles, effects, filters, blurs, and crop and page presets. cesdkDemo() is trial content (ly.img.image, ly.img.video, ly.img.audio, and ly.img.templates) for trying the agent before your own sources exist; do not ship it as your content. include takes source ids and throws invalid_options for one that is not in the set. ly.img.typeface and googleFonts() share family names (Typefaces).

Opening a Session#

The executor loads every module, calls each default export with its options, and awaits its initialize, in order, 10 seconds each. It adds nothing of its own, then checks each source and answers with the handshake.

Fails at open, naming the module by its position and name (plugin 0 (acme.sources): initialize threw: /typefaces answered 401), when a module does not load, imports anything, or has no function as its default export; when initialize throws or takes over 10 seconds; when the modules are over 256 KB together; or when a source fails its checks. Values of the module’s options, URL queries, and Bearer tokens are removed from its error message.

An image, video, audio, or font the engine cannot load, such as a scheme no resolve answers, a signer that answered 403, or a deleted file:

  • One the model’s code adds now is refused: the new blocks are rolled back, and the change fails with the engine’s error.
  • One already in the design does not stop it from opening or saving. Every result, every look at the design, every export, and a render_failed error name it with its uri and the engine’s error ('Hero' (id 12): acme://a1b2.png could not be loaded (FileFetch)). Run a design that uses each of your schemes and fonts through codesign.edit() and codesign.export() before you rely on a source.

Schemes a saved design may hold: http, https, and bundle (CE.SDK’s built-in assets). When the engine refuses to save a scheme it does not know, such as your acme://, the session allows it for the rest of its life and saves again; you list no schemes. Never allowed: buffer:, blob:, data:, file:, javascript:, vbscript:, about:, the SDK’s own codesign-pending:, and any one-letter scheme (a drive letter).

The Source Interface#

CoDesignAssetSource is what your module passes to addSource: CE.SDK’s AssetSource plus description, resolve, and uploadFile, which CE.SDK ignores and CoDesign reads.

interface CoDesignAssetSource {
id: string; // unique among every source of the session
description?: string;
getSupportedMimeTypes(): SupportedMimeType[]; // synchronous, pure
findAssets(
query: AssetQueryData,
opts?: SourceCallOptions
): Promise<AssetsQueryResult | undefined>;
fetchAsset?(
id: string,
params?: { locale?: string },
opts?: SourceCallOptions
): Promise<AssetResult | null>;
getGroups?(opts?: SourceCallOptions): Promise<string[]>;
getGroupLabels?(
opts?: { locale?: string } & SourceCallOptions
): Promise<Record<string, string>>;
resolve?(
uri: string,
opts?: SourceCallOptions
): string | undefined | Promise<string | undefined>;
uploadFile?(
file: File,
opts: UploadFileOptions
): Promise<AssetDefinition | AssetResult | undefined>;
}
interface SourceCallOptions {
signal?: AbortSignal; // aborted with the call that asked; ignoring it is allowed
design?: string; // the design the call is for
}
interface UploadFileOptions extends SourceCallOptions {
action: 'upload' | 'import' | 'generate' | 'saveComponent';
group?: string; // `upload`: the folder a person uploaded into
label?: string;
thumbnail?: File; // `saveComponent`: a PNG of the blocks
font?: { family: string; weight: FontWeight; style: FontStyle }; // left unset by the SDK
width?: number; // an image: its size in pixels, read from the bytes; keep it as meta.width
height?: number; // … and as meta.height
}
Member Default Called when, and what the SDK needs
id required Unique among the session’s sources; shows in search results, errors, and the handshake.
description none Read by the model in search results. One line: what the source is for and when to prefer it.
getSupportedMimeTypes required Decides the source’s role and what it is offered (Source Roles). Checked when a session opens.
findAssets required Every search, live. Page numbers start at 0. An asset’s meta.uri (and a font’s uri) is written into designs, so it must stay fetchable. An image asset carries meta.width and meta.height (Image Sizes).
fetchAsset absent Also how a new design reads its file or template. Return null for an id this caller may not see, so another tenant’s ids stay private.
getGroups, getGroupLabels absent Group ids (folders), and the names the model reads.
resolve absent For a uri of your own scheme: a URL the engine can fetch now, or undefined for a uri that is not this source’s.
uploadFile absent Makes the source writable (Where a New File Goes). For an image, width and height are its size read from the bytes; keep them.

Your CE.SDK web editor applies the same plugin list, with this person’s options, through addPluginModules, so its panels and the agent see the same sources ( Your Editor).

What You Implement#

Besides your sources: a design store over the table where your product keeps designs, and, if the model should hand people files, an uploadExport function. How to build them over your storage: Connect Your Storage.

Designs#

interface DesignStore {
listSourceId?: string; // the asset source that lists the designs; lets the agent list saved designs
read(
id: string,
opts: { signal?: AbortSignal }
): Promise<StoredDesign | undefined>;
write(
id: string,
content: DesignContent<ArrayBuffer>,
opts: DesignWriteOptions
): Promise<string | undefined>; // the version written; undefined: the stored version moved on
}
interface DesignWriteOptions {
ifMatch?: string; // the version the SDK read; absent: create, only if the id is free
cesdkVersion: string; // the CE.SDK that produced this content
editKey?: string; // a key of this edit; absent for an import or a load
title?: string; // the new design's title, on its first write only
group?: string; // the new design's folder in your list, on its first write only
signal?: AbortSignal; // aborted with the tool call that writes
}
interface StoredDesign {
content: DesignContent | { uri: string }; // the content of exactly `version`
version: string; // changes with every write and is never reused for this design
cesdkVersion: string | null; // the CE.SDK that made `version`, as `write` received it; null: unknown
title?: string; // what the model calls it, and the name of files it hands out; default: the id
editKey?: string; // the editKey of the write that made `version`
}
type DesignContent<B extends ArrayBufferLike = ArrayBufferLike> =
| { scene: string; archive?: undefined }
| { archive: Uint8Array<B>; scene?: undefined };

Your designs, as prepareCoDesign({ designs }). They run only in your runner: the model’s code runs on the executor and never reaches them or their credentials, so the checks below cannot be skipped. Build the store per request, already limited to the signed-in person’s tenant.

Member Who calls it, and when What it returns If it fails
read The SDK, at the start of every call on a design. So it must be one cheap query, and it is where your access check runs. { content, version, cesdkVersion, title?, editKey? }, or undefined when there is no such design or this person may not open it. undefined: refused as unknown_design, except for the design option’s id, which reads as an empty design. A throw, or an answer with no content, version, or cesdkVersion (a string, or null when unknown), fails the call with checkpoint_failed.
write The SDK, after every call that changed the design, and once to create a new one. Store the content only while the stored version is still ifMatch; with no ifMatch, only if nothing is stored under id. The new version, a non-empty string. undefined when the stored version is another, or, without ifMatch, the id is taken. undefined over an existing design: nothing was written, and the model is told the design changed; your own load rejects with design_conflict. undefined on a first write fails with checkpoint_failed. A throw fails the call with checkpoint_failed; throw refusal(text) and the model reads text.
listSourceId Read once per engine session: the id of an asset source one of your plugins registers, one asset per design (asset id = design id, label = title). — Absent: listing designs refuses in words. An id no plugin registered: the first call rejects with invalid_options.
Rule Why If you break it
read returns a version that changes on every write, whoever writes, such as a row revision or an ETag. It is how a call knows what it worked from, and how the model’s version is checked against your editor’s saves. The call fails with checkpoint_failed, naming the missing version.
write stores the content only if the stored version is still ifMatch (UPDATE … WHERE revision = ?); otherwise it returns undefined. Two calls, two runners, or the model and a person can write one design at once. Silent: the last write wins, and the other change is lost. testDesignStore writes with a stale ifMatch and reports it.
write without ifMatch creates the design only if the id is free (INSERT … ON CONFLICT DO NOTHING, or If-None-Match: *). The SDK makes up new designs’ ids (UUIDs); over an existing id it would replace a design. Silent: a design is replaced. testDesignStore reports a store that accepts the second write.
The version, cesdkVersion, and editKey are stored together, and every write replaces editKey. The SDK derives editKey from the call, and answers a resent call “already applied” only when the key your store holds is its own ( Resent Calls). Kept past another write, a resend could be taken as applied on top of a person’s save.
read returns the content exactly as it was written, byte for byte. The engine skips loading a design it already holds at that version. Correct, but slower: every call loads the design again.
read returns undefined for an id outside the person’s tenant, or one they may not open. The id comes from the model ( Security). Silent: another tenant’s design is opened. testDesignStore takes foreignIds to check it.
  • { uri } content. read may return a URL of exactly this version, which the executor fetches itself, so a large design never passes through your runner. The executor must be allowed to reach that host, and the bytes behind the URL must never change for that version. Return the same URL on every read of one version: the engine recognizes a design it holds by URL and version, and an export’s render id derives from them, so a URL signed afresh on every read loads the design on every call and charges a retried export again.
  • Archives. A design that holds files no source kept is stored as an .imgly archive (content.archive), and a call that reads one writes an archive again until the files find a home. Your editor saves it as an archive too, never as a scene string, which would lose the images inside.
  • CE.SDK versions. Keep the cesdkVersion write receives and return it from read (null when unknown; the field is required, so a store cannot turn the check off by forgetting it). The SDK refuses to write over, or copy, a design a newer CE.SDK saved, rejecting with older_saver (saver, recorded). A null design may be saved by any CE.SDK. Your editor’s save route keeps the same check with assertCanSave.
  • Existing designs need no conversion: a scene from saveToString() loads as it is, and its image URLs must be fetchable from the executor or resolved by one of your sources. Give each row a version; "0" is a version.

The listing source is a plain CE.SDK asset source in your plugin module. It only lists, so its token needs no right to write designs. The engine refuses to place its assets, and a search of your sources leaves it out:

engine.asset.addSource({
id: 'acme.designs', // the designs store's listSourceId
description: "This team's saved designs, by title.",
// A design is not a file to place or to be offered new files: no types.
getSupportedMimeTypes: () => [],
async findAssets(query) {
const params = new URLSearchParams({
q: query.query ?? '', // the agent finds saved designs by title
page: String(query.page),
perPage: String(query.perPage)
});
const found = await get(`/designs?${params}`);
return {
assets: found.items.map((design) => ({
id: design.id, // the design id the SDK reads and writes
label: design.title,
meta: { thumbUri: design.thumbnailURL } // a preview for your editor's panel
})),
currentPage: query.page,
...(found.hasMore ? { nextPage: query.page + 1 } : {}),
total: found.total
};
}
});

A complete store over a SQL table and a bucket is in Connect Your Storage.

uploadExport#

uploadExport?: (req: UploadExportRequest) => Promise<UploadExportResult>;
interface UploadExportRequest {
format: ExportFormat; // 'png', 'pdf', …
title: string; // the design's title; a file name is `<title>.<ext>`
mimeType: string; // such as 'image/png'; 'application/zip' for an imgly file
bytes: Uint8Array<ArrayBuffer>; // the file, ready for `new Blob([bytes])`
design: string;
version: string; // your store's version that was rendered (StoredDesign.version)
signal?: AbortSignal; // aborted with the tool call
}
type UploadExportResult =
| { downloadURL: string; note?: string }
| { note: string; downloadURL?: string }; // at least one

Lets the agent hand people files. When the model asks for an export, the SDK renders and charges the file on the executor, then calls uploadExport with the bytes. Store them where your product keeps a person’s files and answer with the link they open (downloadURL), a line for the model to pass on (note), or both.

import type {
UploadExportRequest,
UploadExportResult
} from '@imgly/codesign-sdk';
import { bucket, signedLink } from './yours.js';
export async function uploadExport(
req: UploadExportRequest
): Promise<UploadExportResult> {
const key = `exports/${req.design}/${req.version}-${req.format}`;
await bucket.put(key, req.bytes, req.mimeType);
return { downloadURL: await signedLink(key) };
}
  • A throw fails the export. The file was already charged, and a retry renders under the same render id, so it is charged once. Throw refusal(text) to give the model a reason. An answer with neither downloadURL nor note rejects with export_refused.
  • The model chooses the PDF/X settings; you receive the finished file.
  • An export never writes the design. imgly carries the design’s images, including those from private sources.
  • For an approval step before a change reaches a person’s design, run the turn on a copy ( Run a Turn on a Copy).
  • Known limit: the bytes travel from the executor through your runner to your storage, held in memory on the way.

The Tool Contract#

Every tool in codesign.tools, the SDK’s and yours, has one shape, which maps onto any model API’s tool calling, with no Model Context Protocol (MCP) server involved. These types do not change within a major version. For complete mappings to agent frameworks, see the quickstart kits, such as Quickstart: Vercel AI SDK.

CoDesignTool#

interface CoDesignTool {
name: string;
description?: string;
inputSchema: JSONSchema; // the arguments, as a JSON Schema object
annotations?: { readOnlyHint?: boolean };
call: (args: unknown, ctx?: ToolCallContext) => Promise<ToolResult>;
}
interface ToolCallContext {
signal?: AbortSignal | undefined; // cancels this call
}
type ToolResult = {
content: ToolResultPart[];
isError?: boolean; // the call failed; `content` says why, in words for the model
};
type ToolResultPart =
| { type: 'text'; text: string }
| { type: 'image'; data: string; mimeType: string }; // data: base64

ToolResult is a subset of MCP’s CallToolResult; a new part type would be a major version. The read-only tools carry annotations: { readOnlyHint: true }, MCP’s field for this; pass it on if your framework uses it.

A call ends in Meaning Your loop
a result success hands it to the model
isError: true the model made a mistake, or the design refused something: its code threw, an argument was wrong, or the design changed under it; the text says what to do hands it to the model as a failed call
a rejected promise nothing the model can fix: an abort, an executor error, a source module or check, older_saver, export_refused, generation_refused, your store (checkpoint_failed), a source’s uploadFile (source_failed), or acquire reports a failed call, retries (an edit already written is answered “already applied”), or ends the run

Your own tools take the same shape; see Adding Your Own Tools.

JSONSchema#

A copy of the JSON Schema object type (draft 7), so inputSchema passes to the Vercel AI SDK’s jsonSchema() with no cast. The SDK’s own schemas keep to a subset that Anthropic, Gemini, and OpenAI (without strict mode) all accept:

  • The root is { type: 'object', properties }.
  • Only type, description, properties, required, items, enum, minimum, maximum, and a boolean additionalProperties. No $ref, $defs, oneOf, anyOf, allOf, not, const, or if, and no exclusive bounds.
  • Every node has one type. An enum is a list of strings on a type: 'string' node, an array has items, and required names only declared properties.

OpenAI’s strict mode, which requires every property to be required, is not a target. Keep your own tools’ schemas to the same subset if you use several providers.

Reserved Tool Names#

Your tools may not use a name the SDK’s tools use, or a name the handbook is gated on: the handbook has passages shown only when a tool or capability of that name exists, and a tool of yours with such a name would switch them on.

skill, edit, preview, import, open, export, designs, asset_generate, asset_add, asset_search, checklist, components, diagnostics, drafts, files, login, ly.img.gfonts, ly.img.workspace.images, media, palette, scorecard, templates, typefaces, version, video, view.

The list can grow in any release, so prefix your tools with your product’s name (acme_brand_kit). A clash, or two of your tools with one name, makes prepareCoDesign throw tool_name_clash, naming the tool.

Images in Tool Results#

Every image the SDK’s tools render, such as the pages and blocks the agent looks at and the render after a change, is a PNG sized for a model’s context:

  • about 262,144 pixels (512 × 512) per image, at the block’s own aspect ratio
  • no side longer than 1,568 pixels, above which Anthropic’s models downscale an image silently
  • at most 10 images in one result

A look at a stored asset shows your file as stored, in its own type (PNG, JPEG, GIF, WebP, or SVG), and refuses one over 512 kB. A chat-completions API takes a tool result as text only, so your loop moves the images into a user message after it ( Troubleshooting).

Image Generation: gateway#

interface GatewayOptions {
bearer: (opts?: { forceRefresh?: boolean }) => Promise<string | undefined>; // required
baseURL?: string; // default https://gateway.img.ly
fetch?: typeof globalThis.fetch;
}

gateway is required: { bearer } lets the agent generate images, video, speech, music, and sound effects through IMG.LY’s AI gateway, a paid service; false leaves that out, for products that supply every image or jobs that only rearrange a design, such as resizing.

import {
googleFonts,
prepareCoDesign,
remoteExecutors
} from '@imgly/codesign-sdk';
import { acquire } from './yours.js';
export function toolsForJob(jobId: string) {
return prepareCoDesign({
conversation: jobId,
executors: remoteExecutors({ acquire, token: process.env.EXECUTOR_TOKEN }),
plugins: [googleFonts()], // add a plugin whose source has `uploadFile` to keep generated files
design: jobId,
// Your gateway key, from your secrets; without one, false rather than a tool that fails.
gateway: process.env.IMGLY_GATEWAY_API_KEY
? { bearer: async () => process.env.IMGLY_GATEWAY_API_KEY }
: false
});
}
  • Generation runs in your runner, next to your license key, never on the executor. Each result is a new file, routed with action: 'generate' (Where a New File Goes).
  • bearer is called for each request and returns your CoDesign license key, so the SDK never holds it. undefined makes the SDK tell the model there is no credential, for a key that is briefly unavailable. After a 401 it is called once more with forceRefresh: true.
  • A resent generation call generates, and is charged, again.
  • Out of credits (HTTP 402): the call rejects with generation_refused, and nothing is charged. Its message is the sentence for the person (“This account is out of credits — this request was not charged. Top up at …”). Tell the person and end or continue the turn. Every other gateway problem reaches the model as a refusal it can act on.

Errors#

interface CoDesignError extends Error {
readonly kind: CoDesignErrorKind; // open: a later version adds kinds
readonly design?: string | undefined;
readonly source?: string | undefined;
readonly retryAfterMs?: number | undefined;
readonly saver?: string | undefined;
readonly recorded?: string | undefined;
}
function isCoDesignError(err: unknown): err is CoDesignError;
function refusal(text: string): CoDesignError;
  • A failure the model can fix reaches it as an isError tool result; one it cannot fix rejects the tool call with a CoDesignError. Your own create(), load(), edit(), export(), and handshake() reject with every kind.
  • isCoDesignError also recognizes an error from another copy of the SDK in your bundle. Switch on kind with a default.
  • Throw refusal(text) from a member you write to tell the model no, in words.

Every kind, when it is raised, who gets it, and what to do: Errors. Retrying your own calls while an executor exits or cannot be reached:

import { isCoDesignError } from '@imgly/codesign-sdk';
// For example: await whenEngineAvailable(() => codesign.load(file))
export async function whenEngineAvailable<T>(
call: () => Promise<T>
): Promise<T> {
const retiringUntil = Date.now() + 5 * 60_000; // executor_capacity: an executor still exiting after the SDK's own minute
const downUntil = Date.now() + 30_000; // executor_unreachable: 30 seconds for an executor that does not answer
for (;;) {
try {
return await call();
} catch (err) {
if (!isCoDesignError(err)) throw err;
let waitMs: number;
switch (err.kind) {
case 'executor_capacity':
if (Date.now() > retiringUntil) throw err;
waitMs = err.retryAfterMs ?? 1000;
break;
case 'executor_unreachable':
if (Date.now() > downUntil) throw err;
waitMs = 1000;
break;
default:
throw err; // anything else, and any kind a later version adds
}
await new Promise((resolve) => setTimeout(resolve, waitMs));
}
}
}

Versions#

const SDK_VERSION: string; // '<package version>+<12 hex characters>'
const CESDK_VERSION: string; // the CE.SDK the executor of this build runs, such as '1.83.0'
function assertCanSave(opts: {
saver: string; // the CE.SDK that wants to write
recorded: string | null | undefined; // the CE.SDK that saved what is stored; none: anyone may write
}): void; // throws a CoDesignError: older_saver, or invalid_options for a recorded version that differs and cannot be read
Name What it is
SDK_VERSION The SDK source exactly: its package version and a hash of its source files. Your runner and your executors must come from one build, and the SDK does not compare them, so log it when your runner starts.
CESDK_VERSION The CE.SDK every saver of a design the agent works on must run. Pin your browser editor and any job that saves designs to exactly this version.
assertCanSave The SDK’s older_saver check, for your editor’s save route. Passes with no record, equal versions, or a newer saver; otherwise throws older_saver with saver and recorded, or invalid_options for an unreadable version.

For pinning, the store’s check, and the rollout, see Versions and Upgrades.

Testing#

fakeExecutors#

// From '@imgly/codesign-sdk/testing':
function fakeExecutors(opts?: {
sources?: readonly CoDesignAssetSource[]; // what your plugins would register
video?: boolean; // the handshake says the executor encodes video; default false
}): FakeExecutors;
interface FakeExecutors extends Executors {
readonly calls: FakeExecutorsCall[]; // every engine call, in order
archiveAssets: number; // how many images a loaded .imgly reports as carried; default 1
restart(): void; // forget every engine session, as an executor that restarted would
}
interface FakeExecutorsCall {
op: string; // 'reset', 'load', 'settle', 'execute', 'preview', 'export', 'close', …
arg?: unknown;
options?: unknown;
}

A stand-in for remoteExecutors with no executor, no license, and no rendering, to test your design store, sources, and agent loop end to end.

  • It keeps engine sessions by name as an executor does; restart() makes the next call load the design again from your store.
  • sources gives it the objects your plugins would register, so search, the routing of new files, and resolve run against them, with the executor’s type checks. It builds the handshake from sources and video. Its plugins are not run.
  • It does not run code, it scans it. Code containing throw fails as code that threw (your edit() rejects with code_failed). Each name('X'), a test marker and not an engine function, records a block named X, which results report among the design’s block names. The code noop changes nothing. The stored scene is JSON listing the edits, and renders are a 1×1 PNG.
import { googleFonts, prepareCoDesign } from '@imgly/codesign-sdk';
import { fakeExecutors } from '@imgly/codesign-sdk/testing';
import { designStoreFor } from './design-store.js';
export async function tryTheFakeExecutors() {
const executors = fakeExecutors();
const codesign = prepareCoDesign({
conversation: 'test-conversation',
executors,
plugins: [googleFonts()], // not run by the fake, but required
designs: designStoreFor('test-tenant'), // yours, under test
design: 'test-design', // reads as empty until the first write creates it
gateway: false
});
// Your store now holds a scene with a block named "headline".
const { version } = await codesign.edit("name('headline')");
const png = await codesign.export('png'); // a 1×1 PNG
await codesign.release();
console.log(version, png.byteLength);
console.log(executors.calls.map((c) => c.op)); // the engine calls, in order
}

calls then lists the engine calls made (reset, execute, export, close).

testAssetSource and testDesignStore#

// From '@imgly/codesign-sdk/testing':
function testAssetSource(
source: CoDesignAssetSource,
opts?: {
foreignIds?: readonly string[]; // another tenant's asset ids: fetchAsset must return null
uploads?: readonly { file: File; action: UploadAction; expect: 'take' | 'decline' }[]; // UploadAction: UploadFileOptions['action']
laterTypefaceSources?: readonly CoDesignAssetSource[]; // typeface sources listed after this one
}
): Promise<void>;
function testDesignStore(
store: DesignStore,
opts?: {
foreignIds?: readonly string[]; // designs this store's person may not open: read must return undefined
}
): Promise<void>;

Each resolves when it finds no problem and otherwise rejects with an AggregateError whose errors name one problem each, so a bare await fails the test.

testDesignStore uses your store as the SDK does, under a fresh UUID, and checks that:

  • a first write without ifMatch creates the design and returns its version
  • read returns the content (a scene string, or an archive byte for byte), that version, and every write’s cesdkVersion and editKey
  • a second write without ifMatch to the same id, and a write with a stale ifMatch, are refused (resolve undefined, or throw design_conflict)
  • every write returns a new version, and each of foreignIds reads undefined

It writes test designs and deletes none, so run it against a test tenant. It reports an archive that read returns as { uri }; a scene returned as { uri } it does not fetch, so check that URL’s bytes, and that it is the same on every read of one version, in a test of your own.

import { testDesignStore } from '@imgly/codesign-sdk/testing';
import { it } from 'vitest';
import { designStoreFor } from './design-store.js';
it('keeps designs as the SDK needs', async () => {
// Over your database and bucket, for a test tenant.
const store = designStoreFor('test-tenant');
// A design you seeded for another tenant: this tenant must not read it.
// Rejects with an AggregateError listing every problem.
await testDesignStore(store, { foreignIds: ['other-tenants-design'] });
});

testAssetSource checks paging and query, one asset per typeface family, placeable uris that look presigned, image assets without meta.width and meta.height, another tenant’s ids, a family a later typeface source also carries, whether uploadFile takes and declines as you expect, and the executor’s checks at session open. Your module registers sources only inside initialize, so capture them with a stand-in engine:

import type { CoDesignAssetSource } from '@imgly/codesign-sdk';
import { testAssetSource } from '@imgly/codesign-sdk/testing';
import { it } from 'vitest';
import acmeSources from './acme-sources.plugin.js'; // the file the runner sends
// A 1×1 PNG: the SDK reads a file's type from its bytes.
const png = Uint8Array.from(
atob(
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII='
),
(c) => c.charCodeAt(0)
);
it('keeps what it should and declines what it should not', async () => {
// The module registers its sources only inside `initialize`: capture them.
const registered: CoDesignAssetSource[] = [];
// The check calls your source: answer `https://api.test` with a stand-in server or a fetch mock
// that serves your API's shapes (files, folders, signed URLs, uploads).
await acmeSources({ api: 'https://api.test', token: 'test' }).initialize({
// A stand-in engine with only what this module calls.
engine: {
asset: {
addSource: (source: CoDesignAssetSource) => registered.push(source)
}
}
});
const media = registered.find((s) => s.id === 'acme.media')!;
// Rejects with an AggregateError listing every problem.
await testAssetSource(media, {
foreignIds: ['someone-elses-file'],
uploads: [
{
file: new File([png], 'a.png', { type: 'image/png' }),
action: 'upload',
expect: 'take'
},
{
file: new File([png], 'layer.png', { type: 'image/png' }),
action: 'import',
expect: 'decline'
}
]
});
});

To check a module against a real engine, apply it to a @cesdk/node engine with await plugin.initialize({ engine }) and search with engine.asset.findAssets; that needs a CE.SDK license and your API reachable from CI.

@imgly/codesign-sdk/cesdk-utils#

Helpers for your own CE.SDK editor’s engine: the session’s plugins, and saving designs as the executor saves them. How to use them together: Your Editor.

addPluginModules#

function addPluginModules(
engine: CreativeEngineLike,
plugins: readonly EnginePluginModule[],
opts?: { signal?: AbortSignal }
): Promise<void>;

CE.SDK’s addPlugin, for the list you pass to prepareCoDesign({ plugins }): loads each module’s text, builds its plugin with its options, and initializes it on engine, in order, with the executor’s code and checks. Then it installs one URI resolver over the sources’ resolve.

  • Fails silently without it: CE.SDK never calls a source’s resolve itself, so an image of your own scheme shows as nothing, with no error.
  • Call it once per engine, before loadDesign; the resolver takes the engine’s one resolver slot.
  • Asset panels get meta.thumbUri and meta.previewUri through the same resolve. An image with no thumbUri shows its own uri; a video or audio file with none shows no thumbnail.
  • Plugins load from data: URLs, so a Content Security Policy (CSP) needs script-src data:.
  • opts.signal stops before the next plugin; a plugin already added stays.
  • Throws invalid_options, naming the plugin, when a module does not load, its default export is not a plugin factory, initialize throws or takes over 10 s, or a source fails the executor’s checks.

loadDesign#

function loadDesign(
engine: CreativeEngineLike,
design: Pick<StoredDesign, 'content' | 'cesdkVersion'>, // what DesignStore.read returned
opts?: { signal?: AbortSignal }
): Promise<void>;

Loads a design as DesignStore.read returned it: { scene } with CE.SDK’s loadFromString, { archive } from a blob: URL with loadFromArchiveURL, and { uri } fetched first. It remembers which of the two it was, per engine, for saveDesign; the archive’s blob: URL lives until unloadDesign or the next loadDesign on this engine. Call it after addPluginModules.

  • Refuses before loading when this editor could damage the design on save: an editor on another CE.SDK than CESDK_VERSION (invalid_options), or a design a newer CE.SDK saved (older_saver).
  • opts.signal refuses to start the load and stops a { uri } download; a load under way finishes.
  • Throws invalid_options, older_saver, fetch_failed, or load_failed when CE.SDK cannot load it.

saveDesign#

function saveDesign(engine: CreativeEngineLike): Promise<{
content: DesignContent<ArrayBuffer>;
cesdkVersion: string; // this editor's CE.SDK, for the write
}>;

The design as the engine holds it, ready for DesignStore.write(id, content, { cesdkVersion, ifMatch }): an archive when loadDesign loaded one; otherwise a scene string that keeps http, https, bundle, and any scheme of your own; and an archive after all when the scene holds bytes only this tab has (blob:, buffer:, data:). It waits for loading images and ignores one that fails. It stores and sends nothing: your save route checks ifMatch and assertCanSave. Throws archive_failed when CE.SDK cannot write the archive, or cannot save the scene string for a reason other than a scheme.

unloadDesign#

function unloadDesign(engine: CreativeEngineLike): void;

Lets go of the archive’s blob: URL loadDesign kept for this engine. Call it when the editor closes; without it, a page that opens many editors keeps every archive’s bytes until the page unloads.

CreativeEngineLike#

The part of a CE.SDK engine these functions use, as a structural type. The web editor’s cesdk.engine, or a CreativeEngine, is one. It is checked against @cesdk/engine at every CE.SDK upgrade, so you install no CE.SDK types for it.

CE.SDK Types#

Copies of CE.SDK’s own types, with CE.SDK’s names and fields, so your code names them without installing a CE.SDK package. A test checks at every CE.SDK upgrade that CE.SDK’s values are assignable to them.

Type What it is
Asset { id, meta?, payload?, groups? }: one placeable thing. meta (AssetMetaData) holds uri, thumbUri, mimeType, width, height, filename, and more.
AssetDefinition An asset as an asset-source file declares it, with a label and tags per language (Locale).
AssetResult An asset as an asset source’s search returns it.
AssetPayload What an asset carries besides meta: typeface, color, properties, sourceSet. stylePreset and transformPreset are typed unknown.
AssetColor A color in payload.color: AssetRGBColor, AssetCMYKColor, or AssetSpotColor.
Typeface { name, fonts: Font[] }: a font family, in payload.typeface. A Font is { uri, subFamily?, weight?, style? }.
FontWeight 'thin', 'extraLight', 'light', 'normal', 'medium', 'semiBold', 'bold', 'extraBold', 'heavy'. FontStyle is 'normal' or 'italic'.

The rest, AssetGroups, AssetProperty and its five members, Color, RGBAColor, CMYKColor, SpotColor, Source, and Locale, are what those fields reach.

Next Steps#