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#
npm install --save-exact @imgly/codesign-sdk@1.83.0Run 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:
conversationis not a non-empty stringexecutorsis not whatremoteExecutors(orfakeExecutors) returnedgatewayis neitherfalsenor{ bearer }with a functionuploadExportis not a functionpluginsis not a list, or an entry is not{ module, options },googleFonts(),cesdkDefaults(), orcesdkDemo()designshas noreadandwritefunctions, or itslistSourceIdis not a non-empty string- a listener’s
timeoutMsis 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).
fromis{ template, source }or{ file, source }(an asset your source’sfindAssetsorfetchAssetreturned, 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.writecreates the design: it carries noifMatchand passestitleandgroupon 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.writeonly if nobody wrote the design sinceloadread it; otherwise rejects withdesign_conflict. versionis 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 withhttp_status, a network failure withfetch_failed.signalstops 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.
codeseesengine(CE.SDK’s engine),design(CoDesign’s design API, which the handbook the model reads teaches it), andargs. 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 withcode_failed, before any write.argsand the return value are JSON. Pass your data inargs, never inside the code.versionruns it only while the design is at that version of your store (default: the latest). A staleversion, or a save that landed while it ran, rejects withdesign_conflict.- Code that throws rejects with
code_failedand writes nothing. Code that returns something that cannot be a result also rejects withcode_failed, but its change was already written. findingsare 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 fornew Blob([bytes]). -
A PDF keeps CMYK colors (the four process inks of print) as CMYK.
htmlis one self-contained HTML file with its images embedded. -
pdfxis a print-ready PDF/X file (the ISO standard for exchanging print files) with IMG.LY’s defaults. Onlyexport('pdfx', { pdfx })takes options:PdfxOptionsfieldWhat it sets Default outputStandard'PDF/X-4'(keeps live transparency),'PDF/X-3', or'PDF/X-1a'(CMYK only, always flattened)'PDF/X-4'outputProfileThe 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'profileUriThe color profile (an .iccfile from your printer): anhttpsURL, required with'custom'and refused otherwisenone nonPrinterProfileA display profile such as 'srgb': produce the file anyway ('keep') or refuse ('reject')'keep'embedICCProfileEmbed the profile in the file; falsegives a plain PDF that is no longer PDF/XtrueflattenTransparencyPDF/X-3 only: rasterize transparent content ( falsekeeps it live). Ignored on PDF/X-4; PDF/X-1a always flattenstrueon PDF/X-3flattenResolutionDots per inch of flattened pages, 1 to 65,535 (PDF/X-1a, and PDF/X-3 unless flattenTransparencyisfalse)300titleThe document title the design’s title outputCondition,outputConditionIdentifierThe output condition the file names, as your printer asks from the profile -
mp4,wav, andm4aneed an executor that encodes video; one that does not rejects them withvideo_unsupported. The handshake’scapabilities.videosays whether yours does. For what is supported, see Status. -
Every format except
sceneandimglyis 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 (
signalstops only the wait), or with what youracquirethrows. - 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 withconsole.error. Listener time is not inmsbut 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
prepareCoDesignresult. 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.thrownis the original error with its stack; for an executor error, the executor’s message is inError.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.warningslists what could not be carried over. - The PSD, IDML, and PDF importers need
googleFonts()inplugins. 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 withexport_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. conversationis the only thingacquireroutes by; there is no tenant argument.GET /healthanswersbound: trueorbound: 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_boundfor 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.videoisfalseon an executor without video encoding, andmp4,wav, andm4aare then refused.- It is per session. A lost executor replaced by one with another CE.SDK
version, other
capabilities, or othersourcesends the session withexecutor_changed.pluginsandversions.sdkare not compared. designs.listSourceIdmust be one of itssources.
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 whatFunction.prototype.toStringreturns, and the executor gets a module that throws. - Only
fetch,URL,TextEncoder, timers, and the engine, which both the executor and a browser have; nowindow,document, orprocess. - 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.
initializemay run again on the same engine with newoptions. 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
optionsmay 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’sresolvein list order. - A token in
optionsis 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 theiroptions, 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 namesimage/jpeg), or anapplication/ubq-…type that is none ofASSET_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 withoutASSET_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. |
- The SDK reads the file’s type from its bytes.
- Candidates are the sources that have
uploadFileand whose types admit that type, in list order. A wildcard such asimage/*never admits PSD, PDF, or IDML. - Each candidate is offered the file in turn. The first to return an
asset keeps it;
undefineddeclines, and the next is offered it; a throw fails the operation (throwrefusal(text)to tell the model why). - If nobody takes it, a generated file, an imported picture, or an imported
font stays inside the design, which is then saved as an
.imglyarchive (designs.writegets{ archive }). Saved blocks have no such place and fail withrefused. - 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.
uploadFileis told the size, read from the bytes, aswidthandheight. Keep it and answer it fromfindAssetsandfetchAsset. The assetuploadFilereturns 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%.testAssetSourcereports 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,nameexactly the family, andqueryhonored. Otherwise bold and italic silently do not exist. - A family in two typeface sources comes from the first in
pluginsorder. 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’slaterTypefaceSourcesflags 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
httpsURL, or a uri of your own scheme thatresolveanswers. Never a presigned URL. UploadFileOptions.fontis 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_failederror 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 throughcodesign.edit()andcodesign.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.readmay 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
.imglyarchive (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
cesdkVersionwritereceives and return it fromread(nullwhen 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 witholder_saver(saver,recorded). Anulldesign may be saved by any CE.SDK. Your editor’s save route keeps the same check withassertCanSave. - 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 oneLets 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 neitherdownloadURLnornoterejects withexport_refused. - The model chooses the PDF/X settings; you receive the finished file.
- An export never writes the design.
imglycarries 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: base64ToolResult 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 booleanadditionalProperties. No$ref,$defs,oneOf,anyOf,allOf,not,const, orif, and no exclusive bounds. - Every node has one
type. Anenumis a list of strings on atype: 'string'node, an array hasitems, andrequirednames 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). beareris called for each request and returns your CoDesign license key, so the SDK never holds it.undefinedmakes the SDK tell the model there is no credential, for a key that is briefly unavailable. After a 401 it is called once more withforceRefresh: 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
isErrortool result; one it cannot fix rejects the tool call with aCoDesignError. Your owncreate(),load(),edit(),export(), andhandshake()reject with every kind. isCoDesignErroralso recognizes an error from another copy of the SDK in your bundle. Switch onkindwith adefault.- 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. sourcesgives it the objects your plugins would register, so search, the routing of new files, andresolverun against them, with the executor’s type checks. It builds the handshake fromsourcesandvideo. Itspluginsare not run.- It does not run code, it scans it. Code containing
throwfails as code that threw (youredit()rejects withcode_failed). Eachname('X'), a test marker and not an engine function, records a block namedX, which results report among the design’s block names. The codenoopchanges 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
writewithoutifMatchcreates the design and returns its version readreturns the content (a scene string, or an archive byte for byte), that version, and every write’scesdkVersionandeditKey- a second write without
ifMatchto the same id, and a write with a staleifMatch, are refused (resolveundefined, or throwdesign_conflict) - every write returns a new version, and each of
foreignIdsreadsundefined
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
resolveitself, 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.thumbUriandmeta.previewUrithrough the sameresolve. An image with nothumbUrishows its ownuri; a video or audio file with none shows no thumbnail. - Plugins load from
data:URLs, so a Content Security Policy (CSP) needsscript-src data:. opts.signalstops 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,initializethrows 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.signalrefuses to start the load and stops a{ uri }download; a load under way finishes.- Throws
invalid_options,older_saver,fetch_failed, orload_failedwhen 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#
- Quickstart — run a brief in and get a design out.
- Add a Design Chat — see these names in a whole product.
- Errors — look up what each error kind means and what to do.
- Host the Executor — run the executor image and
acquirean executor per conversation. - Versions and Upgrades — keep the runner, executors, and editors on matching versions.