Pin the SDK, the executor image, and every editor or job that saves designs to one exact version, and refuse any save that would damage a design. Then upgrade and roll back without losing a design’s settings.
The code on this page is type-checked against the SDK, but no example app runs it, so it is the reference.
Terms#
The SDK (@imgly/codesign-sdk, which your agent runner installs), the
executor, acquire, and a conversation are defined in
Terminology. This page also uses:
- CE.SDK is IMG.LY’s design engine. The executor image runs it as the npm
package
@cesdk/node. A browser runs it as@cesdk/cesdk-js(the editor with its user interface) or@cesdk/engine(the engine alone). IMG.LY publishes these packages under the same version numbers. A release candidate is a test build before a release, such as1.83.0-rc.2. A nightly is a daily build, such as1.84.0-nightly.<date>. - A design is what CE.SDK saves: a text string (a scene), or an
.imglyarchive that also holds the images. Only CE.SDK opens it. - To save a design is for any CE.SDK to write it back to your storage after
it opened it. That includes your editor’s save button, a job of yours that
changes designs, and the agent. The agent saves through the
designsstore you pass toprepareCoDesign: two functions of yours,readandwrite, over the table where you keep designs.
The Two Rules#
A design agent runs from pieces in more than one place, and the designs it makes stay in your storage for years. Two rules keep them intact:
| Rule | What | Must be | Otherwise |
|---|---|---|---|
| 1 | The SDK in your agent runner and the SDK inside every executor | the same published version | The model writes code for a design API the executor does not have |
| 2 | Every CE.SDK that saves designs your agent also works on: executors, your editor, your jobs | exactly the same version | Whichever is older silently deletes the newer one’s settings when it saves |
The SDK checks the first rule only in part. For the second it gives you the
pieces: the exact CE.SDK version to pin (CESDK_VERSION), the CE.SDK version
of every save it makes, and its own refusal to save over a newer one.
Pin the SDK and the Executor Image to One Version#
IMG.LY builds the SDK and the executor image from one source, in one build job, and publishes both under one version, a pair: the SDK on npm and the image on Docker Hub. You build neither. Install and pull the exact version:
npm install --save-exact @imgly/codesign-sdk@1.83.0docker pull imgly/codesign-executor:1.83.0npm ls @imgly/codesign-sdk then shows 1.83.0, and
docker images imgly/codesign-executor lists the 1.83.0 tag.
Never use a range or a moving tag such as @latest or :latest. A moving tag
can point at a new image while your lockfile keeps the old SDK, and then rule 1
breaks. Keep every pair you deploy, the image by its digest (docker inspect
shows it) and the SDK version in your lockfile, so a rollback can redeploy it.
The version follows CE.SDK: 1.83.0 is the build for CE.SDK 1.83.0. A new
SDK version can run a different CE.SDK version. When it does, move your editor
and your jobs to that CE.SDK in the same release (rule 2).
Log What Runs#
The SDK names its own build with two constants:
import { CESDK_VERSION, SDK_VERSION } from '@imgly/codesign-sdk';
// SDK_VERSION: the package version plus a hash of the source, such as '1.83.0+9b3f341487dc'.// CESDK_VERSION: the exact CE.SDK version the executors of the same build run, such as '1.83.0'.console.log(`codesign-sdk ${SDK_VERSION}, CE.SDK ${CESDK_VERSION}`);Log both when your runner starts. The executor’s side comes from
codesign.handshake(), where codesign is the object prepareCoDesign
returns: its versions field is { sdk, cesdk }, the executor’s own
SDK_VERSION and CE.SDK version. Log it next to the runner’s two. Nothing
compares the two SDK_VERSIONs for you, so this log is where a mismatched pair
shows.
Also log codesign.fingerprint. It is a hash of the fixed text the model
reads: the tools’ names, descriptions, and input schemas,
codesign.instructions, and the handbook (the guide the model reads). When it
changes, what your model reads has changed. What the executor adds, such as
whether it encodes video and which asset sources (the catalogs of images and
fonts the engine can search) your plugins registered, is in the same codesign.handshake()
( The Handshake). Attach both to
an evaluation run (a batch test of the agent) or a support ticket.
When a conversation’s executor is lost (it exited, crashed, or was replaced in
a deploy), the SDK asks acquire for another one. If the new executor answers differently from
the old one, in its CE.SDK version, its abilities (such as video), or its asset
sources, the session ends there, and the call rejects with a CoDesignError
whose kind is executor_changed. It reaches your code, not the model,
because the model was told what the old executor could do. Calling again opens
a new session on the new executor. Two executors of different SDK builds on the
same CE.SDK are not told apart this way, so use one image version for all
executors in your pool.
What the SDK Checks#
The runner and the executor each carry a protocol number, which names the
version of the messages they exchange. An executor with
another number refuses to open an engine session with HTTP status 409, and the
call rejects with a CoDesignError of kind executor_version, whose message
names both protocol numbers and the runner’s SDK_VERSION:
the executor at http://10.0.3.17:8080 runs another @imgly/codesign-sdk than this runner (1.83.0+9b3f341487dc): it speaks protocol 25, this runner 26. Run imgly/codesign-executor at this runner's @imgly/codesign-sdk version.The runner does not try another executor: a conversation has exactly one at a time.
Only the protocol numbers are compared, not the SDK versions. The protocol number changes when the messages between runner and executor change, not with every build, so two builds can pass this check and still differ in their design functions, the model’s instructions, or CE.SDK. Running the same version on both sides covers that.
One CE.SDK Version for Everything That Saves Designs#
A newer CE.SDK can save settings that an older one does not know. When the older one saves that design again, those settings are gone. This happens in both directions, so the rule is “the same version”, not “at least”:
- Your editor is older than the executor. A design the agent made loses settings when a user saves it in your editor.
- Your editor is newer. A design a user saved loses settings when the agent next works on it, because the executor saves it.
What an Older CE.SDK Does to a Newer Design#
This holds in the browser and on a server, for a design saved as a string or
as an .imgly archive:
- The older engine opens the design with no error and nothing in its log. The design looks normal.
- Its save drops the settings it does not know, and they come back at their defaults. A 1.83 design saved by 1.82, for example, loses its page’s safe-area guide and its text backgrounds’ padding.
- Even a design that uses nothing new can change. A 1.83 design saved once
by 1.82 comes back with its color-conversion setting switched from
ManagedtoLegacy, because 1.83 reads a design without that setting as one made before it existed. The setting controls how CMYK (the four inks of print) and spot colors convert into the colors the design shows, so those colors can change. - Nothing in the design records which CE.SDK saved it. A design carries a format number, but many CE.SDK versions write the same one, so it cannot tell you whether a save is safe. That is why you keep the version yourself, in one more column of your design table (Make Your Store Refuse a Damaging Save).
Opening a design is harmless. The older CE.SDK’s save is what changes it, and the agent’s first edit of a design saves the whole design.
Pin Your Editor to Exactly That Version#
Read the CE.SDK version from the SDK your runner installed. Run this in your runner’s project folder:
CESDK_VERSION=$(node --input-type=module -e \ "import { CESDK_VERSION } from '@imgly/codesign-sdk'; console.log(CESDK_VERSION)")echo "$CESDK_VERSION" # 1.83.0 for @imgly/codesign-sdk@1.83.0Pass that value from the same CI job into your editor’s build and your jobs’ builds, so nobody types it by hand. Then pin the exact version:
npm install --save-exact "@cesdk/cesdk-js@$CESDK_VERSION"Do not install @cesdk/cesdk-js without a version. npm then installs its
latest tag, which moves on its own schedule. When it points at an older
version than the executor’s, every design a user saves in that editor loses
the settings the agent used, and nobody sees an error.
If your editor loads CE.SDK from IMG.LY’s CDN instead, put the version in the
address. Take the engine’s files (baseURL) from the cesdk-engine path of
the same version, not from the copy under the cesdk-js path, which is not
guaranteed to match it.
// Your build writes the version it read from the SDK here; never type it by hand.const CESDK_VERSION = '1.83.0';// The CE.SDK license your web editor already uses.const YOUR_LICENSE = '<your CE.SDK license>';
const cdn = 'https://cdn.img.ly/packages/imgly';const { default: CreativeEditorSDK } = await import( `${cdn}/cesdk-js/${CESDK_VERSION}/index.js`);export const cesdk = await CreativeEditorSDK.create('#editor', { license: YOUR_LICENSE, // The engine's files, from the cesdk-engine path of the same version. baseURL: `${cdn}/cesdk-engine/${CESDK_VERSION}/assets`, // The editor's own icons and stylesheets exist only on cesdk-js's path. ui: { baseURL: `${cdn}/cesdk-js/${CESDK_VERSION}/assets/ui/` }});A running executor confirms its version in two places: its log, where CE.SDK
prints it as the engine starts
([CreativeEngine] v1.83.0 …), and every designs.write your runner
receives, which carries it as cesdkVersion.
The rule covers every job of yours that opens a design and saves it again, such as a batch job that stamps a logo or a migration. A job that only renders, such as thumbnails, cannot damage a design, but on an older CE.SDK it renders without the settings it does not know.
Some clients cannot move in step with you: an offline client, a partner who opens designs you export, a mobile app. Your save route can still stop them from damaging a design (next section). A mobile app on CE.SDK’s iOS or Android build is not a supported saver of the agent’s designs yet ( Status): open them read-only there.
Make Your Store Refuse a Damaging Save#
Every save of a design goes through a write path of yours: the agent’s through
the designs store you pass to prepareCoDesign, your editor’s through your
save route, and your jobs’ through whatever code of yours writes the row. The
rule is the same on each: a CE.SDK must not save a design that a newer CE.SDK
saved. Keep one more column with each design, the CE.SDK version of its last
save.
The agent’s saves. The SDK hands you the version on every write and reads it back on every read:
designs.writereceivescesdkVersion, the version the executor’s engine reports for this save. Store it with the content. Its other arguments are in Connect Your Storage.designs.read(id)returns it ascesdkVersion, next to the content and its version, ornullfor a design saved before you kept the column.
Before every write, the SDK compares the two:
- When a newer CE.SDK saved the design, the call rejects with a
CoDesignErrorwhosekindisolder_saver, naming both versions, andwriteis not called. Neither the model nor the person can fix this, so it reaches your code as a rejected call, not as text for the model. The check runs in your runner, where the model’s code cannot reach it (see Security). cesdkVersionis required in whatreadreturns, so a store cannot switch the check off by forgetting it: areadwithout it fails the call with kindcheckpoint_failed.nullswitches the check off for that one design, so return the column.- Designs saved before you added the column stay unprotected until their next save. If you know which CE.SDK your editor ran then, write that version into the column.
import type { DesignStore } from '@imgly/codesign-sdk';
import { db } from './yours.js';
// The table: designs(tenant_id, id, content bytea, is_archive boolean,// revision integer, cesdk_version, edit_key, title, primary key (tenant_id, id))
// The agent's path: in your runner, passed as prepareCoDesign({ designs }).// Limited to one tenant, so the model can never name another tenant's design.export function designStoreFor(tenantId: string): DesignStore { return { async read(id) { const { rows } = await db.query( 'SELECT * FROM designs WHERE tenant_id = $1 AND id = $2', [tenantId, id] ); const row = rows[0]; // No such design for this tenant: the SDK refuses the call as unknown_design. if (row === undefined) return undefined; return { content: row.is_archive ? { archive: new Uint8Array(row.content) } : { scene: new TextDecoder().decode(row.content) }, version: String(row.revision), // what the SDK passes back as ifMatch cesdkVersion: row.cesdk_version ?? null, // required; the SDK refuses older savers against it, null: unknown editKey: row.edit_key ?? undefined, // lets the SDK spot a retried edit title: row.title ?? undefined }; },
async write(id, content, { ifMatch, cesdkVersion, editKey, title }) { const bytes = content.archive ?? new TextEncoder().encode(content.scene); const { rows } = ifMatch === undefined ? // A new design, whose id the SDK minted: create it only if the id is free. await db.query( `INSERT INTO designs (tenant_id, id, content, is_archive, revision, cesdk_version, edit_key, title) VALUES ($1, $2, $3, $4, 1, $5, $6, $7) ON CONFLICT DO NOTHING RETURNING revision`, [ tenantId, id, bytes, content.archive !== undefined, cesdkVersion, editKey ?? null, title ?? 'Untitled' ] ) : // Store only while the row is still at the revision the SDK read. await db.query( `UPDATE designs SET content = $3, is_archive = $4, revision = revision + 1, cesdk_version = $5, edit_key = $6 WHERE tenant_id = $1 AND id = $2 AND revision = $7 RETURNING revision`, [ tenantId, id, bytes, content.archive !== undefined, cesdkVersion, // the CE.SDK of this save: keep it with the content editKey ?? null, Number(ifMatch) ] ); // No row: someone saved first, or the id was taken. undefined tells the // model the design changed; it then works on the newer one. return rows[0] === undefined ? undefined : String(rows[0].revision); } };}yours.ts: the database client this page’s code calls
// The clients this page's code calls that are yours to write, over what your product already has.
/** 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'); }};
/** The tenant of the signed-in person, from your session cookie. */export function tenantOf(_request: Request): string { throw new Error('yours to write');}Your editor’s saves and your jobs’. These do not go through the SDK, so
your save route makes the same check with assertCanSave({ saver, recorded }),
which the SDK exports and runs itself:
saveris the CE.SDK of this save: the version your editor sends with it (CreativeEditorSDK.version, in anX-CESDK-Versionheader here), or, in a job,engine.versionof its CE.SDK engine.recordedis the column.assertCanSavethrows whensaveris older. An equal or newer saver passes, and so does a design with no record (nullorundefined). Once a record exists and differs fromsaver, a version it cannot read also throws.- A nightly and a release candidate of the same version number refuse each other, both ways, because their order says nothing about which was built later. A release comes after both.
It throws a CoDesignError of kind older_saver, whose saver and
recorded fields and message name both versions, or of kind
invalid_options for a recorded version that differs and that it cannot
read:
this design was last saved by CE.SDK 1.83.0-rc.2; saving it with 1.82.2 would delete the settings 1.82.2 does not knowimport { assertCanSave, CESDK_VERSION, isCoDesignError} from '@imgly/codesign-sdk';
import { db, tenantOf } from './yours.js';
// Your editor's save: PUT /api/designs/:id. Any web framework works the same way.export async function saveDesignRoute( request: Request, id: string): Promise<Response> { const tenantId = tenantOf(request); const saver = request.headers.get('X-CESDK-Version'); // the CE.SDK the editor runs const ifMatch = request.headers.get('If-Match'); // the revision the editor loaded if (saver === null || ifMatch === null) { return Response.json('send X-CESDK-Version and If-Match', { status: 428 }); } // Only the CE.SDK you deploy saves: a tab opened before a release still runs the old one. if (saver !== CESDK_VERSION) { return Response.json(`reload: this editor runs CE.SDK ${saver}`, { status: 409 }); } const { rows } = await db.query( 'SELECT cesdk_version FROM designs WHERE tenant_id = $1 AND id = $2', [tenantId, id] ); if (rows[0] === undefined) return new Response(null, { status: 404 }); try { // Throws kind older_saver when the editor's CE.SDK is older than the one // that saved last, and invalid_options for a version it cannot read. assertCanSave({ saver, recorded: rows[0].cesdk_version }); } catch (err) { if (!isCoDesignError(err)) throw err; return Response.json(err.message, { status: err.kind === 'older_saver' ? 409 : 400 }); } const archive = request.headers.get('Content-Type') === 'application/zip'; const updated = await db.query( `UPDATE designs SET content = $3, is_archive = $4, revision = revision + 1, cesdk_version = $5, edit_key = NULL -- a person's save is no retried edit WHERE tenant_id = $1 AND id = $2 AND revision = $6 RETURNING revision`, [ tenantId, id, new Uint8Array(await request.arrayBuffer()), archive, saver, Number(ifMatch) ] ); // Another save, a person's or the agent's, landed first: the editor reloads. if (updated.rows[0] === undefined) return new Response(null, { status: 412 }); return new Response(null, { status: 200, headers: { ETag: String(updated.rows[0].revision) } });}The route answers:
428to a save without both headers409to any CE.SDK but the deployed one409to a saver older than the design’s last400to a versionassertCanSavecannot read412when the design changed since the editor opened it
Any other error is rethrown. For the editor’s side, which sends both headers
and takes the new version from the ETag, see
Your Editor.
The check reads before the conditional update, so the two are not one step;
the revision = $6 condition is what keeps the write itself safe
( Concurrent Edits). An editor whose save is refused
can still show the design; it cannot save it.
An editor that loads designs
with loadDesign from @imgly/codesign-sdk/cesdk-utils refuses earlier, before
the person starts work: it throws kind invalid_options when the editor runs
another CE.SDK than CESDK_VERSION, and older_saver when a newer CE.SDK
saved the design. testDesignStore from
@imgly/codesign-sdk/testing checks your store’s conditional write, its
create-only-if-absent, and that cesdkVersion reads back as written.
Refusing older savers is only a safeguard. Exactly equal versions are what
you deploy: a newer saver passes the check but still writes settings the
executor does not know. The least you can do needs no column: your save route
accepts a save only from the deployed CESDK_VERSION, as the route above does
first. That also turns away a browser tab opened before an upgrade. It cannot
protect a design from a job that writes the row directly, and without the
column the SDK’s own check has nothing to compare.
Upgrading#
Upgrading the SDK can change the CE.SDK version. Treat every upgrade as if it did.
- Take the new pair, the SDK and the image of one version, and read its
CESDK_VERSION. - Start executors of the new version beside the old ones
( Host the Executor). Old runners keep
handing out old executors from
acquire. Give old and new runners separate executors: separate address lists, or, with a container per conversation, the image of the runner’s own version. - Switch. Old runners stop taking new work and finish the turns they are
running. New runners start, with an
acquirethat hands out only new executors. Your editor and your jobs move to the new CE.SDK version in the same release. With the check above on every write path, do these three together. Without it, wait for the old runners to finish before the new runners and the new editor start. - Retire the old runners once their turns have ended, then the old executors.
Why separate images per version: the protocol number often stays the same
between two builds. A conversation keeps its executor while that executor
lives, but when it is lost (it exited, crashed, or was replaced in a deploy),
the SDK asks
acquire again and reloads the design there. An acquire that could answer
either version ends that conversation’s session mid-turn when the new executor
runs another CE.SDK: the call rejects with executor_changed
(Log What Runs). And an executor of another SDK build on the
same CE.SDK is taken without a word, with the design functions and
instructions of another build. If you start a container per conversation,
start it from the image of the runner’s own version.
A page of your editor left open across your release keeps the old CE.SDK
until it reloads. Your save route, which accepts only the deployed
CESDK_VERSION, stops it from saving.
Rolling Back#
- To a pair on the same CE.SDK version (a bug in the SDK, not in CE.SDK): redeploy the kept pair, with the same switch of executors. Nothing is lost.
- To an older CE.SDK version: not without the check above. Every design the new version saved changes when the old version saves it, and without the column nothing tells you which designs those are, so fix forward instead. With it, those saves are refused, and the column tells you which designs a newer CE.SDK saved: they stay intact, and can be saved again once you move forward.
Next Steps#
- API Reference — look up
CESDK_VERSION,SDK_VERSION,assertCanSave, and thedesignsstore. - Concurrent Edits — make the conditional write every store needs.
- Your Editor — add the editor’s save, with the check on its route.
- Host the Executor — replace containers when you deploy a new build.