Give the agent your brand’s fonts, colors, templates, and images, and tell it what the brand means, so every design looks like yours, with Google Fonts covering the rest.
What You Build#
No example product runs the code on this page: it is checked against the SDK’s
types, and each function marked Yours in it is yours to write. You build
three things:
- A route on your API that answers a tenant’s brand kit, where a tenant is one of your customers, as typefaces and colors in the shapes below, from wherever you store the kit today.
- A plugin module, whose asset sources (searchable lists of things a design can use: fonts, colors, templates, logos, and photos) call that route.
- A tool of your own that tells the model what the brand means, such as “headlines in Acme Sans bold” (see below).
A plugin module is one JavaScript file. Its default export is a function
that takes your options and returns a plugin of CE.SDK, IMG.LY’s design engine
and editor: a name, a version, and an initialize function that registers
things on the engine:
export default (options) => ({ name: 'acme.brand', version: '1.0.0', initialize({ engine }) { // engine.asset.addSource({ … }): the brand's sources, below }});The file has no imports, because the executor loads it from its text alone,
where an import has nothing to resolve against. Your runner, the backend
process that calls the SDK and the model, passes the file’s text in plugins
each time it calls prepareCoDesign for a conversation (one chat, job, or
MCP session), along with this person’s options (an API address, a
short-lived token). The executor, the container where the engine runs,
applies it to every engine that opens a design, and your CE.SDK editor applies
the same file (see Your Editor). The sources ask your
API live, so a change to a brand kit needs no redeploy, and one executor
serves every tenant’s brand.
Here is the runner’s part. The brand module comes first in plugins, then
IMG.LY’s Google Fonts:
export function prepareTools(req: { tenantId: string; userId: string; chatId: string;}): CoDesign { return prepareCoDesign({ conversation: `${req.tenantId}.${req.chatId}`, executors: remoteExecutors({ acquire, token: process.env.EXECUTOR_TOKEN }), designs: designStoreFor(req.tenantId), plugins: [ // The brand first: it ranks first in search and in the font lookup. { module: brandSources, options: { api: 'https://api.acme.com', token: tokenFor(req.userId) } }, // Then IMG.LY's Google Fonts, for everything the brand does not cover. googleFonts() ], tools: [brandTool(req.tenantId)], gateway: false });}The whole runner, runner.ts
import { readFileSync } from 'node:fs';
import { googleFonts, prepareCoDesign, remoteExecutors, type AcquiredExecutor, type AssetResult, type CoDesign, type CoDesignTool, type DesignStore} from '@imgly/codesign-sdk';
// The plugin module's text: the executor receives the file, not a function.const brandSources = readFileSync( new URL('./brand.plugin.js', import.meta.url), 'utf8');
// Tells the model what the brand is; the sources only make it searchable.function brandTool(tenantId: string): CoDesignTool { return { name: 'brand_library', description: "The brand: its typefaces, colors and rules. Call it whenever the brief mentions the brand, the company's name or 'our'.", inputSchema: { type: 'object', properties: {} }, async call() { const kit = await brandKitOf(tenantId); return { content: [ { type: 'text', text: JSON.stringify({ typefaces: kit.typefaces, // family names, as acme.brand.typefaces lists them colors: kit.colors, // { name, hex } rules: kit.rules // "headlines in Acme Sans bold, body regular" }) } ] }; } };}
export function prepareTools(req: { tenantId: string; userId: string; chatId: string;}): CoDesign { return prepareCoDesign({ conversation: `${req.tenantId}.${req.chatId}`, executors: remoteExecutors({ acquire, token: process.env.EXECUTOR_TOKEN }), designs: designStoreFor(req.tenantId), plugins: [ // The brand first: it ranks first in search and in the font lookup. { module: brandSources, options: { api: 'https://api.acme.com', token: tokenFor(req.userId) } }, // Then IMG.LY's Google Fonts, for everything the brand does not cover. googleFonts() ], tools: [brandTool(req.tenantId)], gateway: false });}
// Your own code, without a model: a new design from a brand template.export async function startFromTemplate( codesign: CoDesign, templateId: string): Promise<string> { const found = await brandTemplate(templateId); const created = await codesign.create({ title: 'Spring launch post', from: { template: found, source: 'acme.brand.templates' } }); return created.design; // the new design's id, written by your store}
// Yours: hands a conversation its executor (see Host the Executor).async function acquire(_req: { conversation: string;}): Promise<AcquiredExecutor | undefined> { throw new Error('yours to write');}
// Yours: your design store (see Connect Your Storage).function designStoreFor(_tenantId: string): DesignStore { throw new Error('yours to write');}
// Yours: a short-lived token that opens only this person's brand and files on your API.function tokenFor(_userId: string): string { throw new Error('yours to write');}
// Yours: the brand kit, from your API.async function brandKitOf(_tenantId: string): Promise<{ typefaces: string[]; colors: { name: string; hex: string }[]; rules: string;}> { throw new Error('yours to write');}
// Yours: one template as the asset the template source lists, from your API.async function brandTemplate(_id: string): Promise<AssetResult> { throw new Error('yours to write');}Fonts and Colors as a Plugin#
The brand’s typefaces (a typeface is a font family with one font file per weight and style) and colors are two sources in the module:
// Registered first: the font lookup takes the first source with the exact family.engine.asset.addSource({ id: 'acme.brand.typefaces', description: "The brand's typefaces. Use these before any other font.", getSupportedMimeTypes: () => ['application/ubq-typeface'], async findAssets(query) { const families = await get('/brand/typefaces'); // One asset per family, every font in `fonts`, `name` exactly the family's. const assets = families.map((family) => ({ id: family.id, label: family.name, payload: { typeface: { name: family.name, fonts: family.files.map((file) => ({ uri: file.url, // a stable https URL: every design that uses it keeps it subFamily: file.subFamily, // 'Regular', 'Bold Italic', … weight: file.weight, // 'normal', 'bold', … style: file.style // 'normal' or 'italic' })) } } })); return paged(assets, query); }});engine.asset.addSource({ id: 'acme.brand.colors', description: "The brand's colors.", getSupportedMimeTypes: () => ['application/ubq-color'], async findAssets(query) { const colors = await get('/brand/colors'); const assets = colors.map((color) => ({ id: color.id, label: color.name, // CE.SDK's AssetColor: each channel from 0 to 1. payload: { color: { colorSpace: 'sRGB', r: color.r, g: color.g, b: color.b } } })); return paged(assets, query); }});The whole module, brand.plugin.js
// Runs on the executor and in your CE.SDK editor. No imports: the executor// loads this text as it is (bundle it into one file if it uses a library).// `api` and `token` are this person's values for this conversation.export default function brandSources({ api, token }) { const get = async (path) => { const response = await fetch(`${api}${path}`, { headers: { authorization: `Bearer ${token}` } }); if (!response.ok) throw new Error(`${path} answered ${response.status}`); return response.json(); }; // query.page counts from 0. const paged = (items, query) => { const found = items.filter( (item) => !query.query || item.label.toLowerCase().includes(query.query.toLowerCase()) ); const start = query.page * query.perPage; const page = found.slice(start, start + query.perPage); return { assets: page, currentPage: query.page, ...(start + page.length < found.length ? { nextPage: query.page + 1 } : {}), total: found.length }; };
const asTemplate = (template) => ({ id: template.id, label: template.name, meta: { uri: template.sceneURL, // a scene file your editor saved, at a stable URL thumbUri: template.thumbnailURL } });
return { name: 'acme.brand', version: '1.0.0', initialize({ engine }) { // Registered first: the font lookup takes the first source with the exact family. engine.asset.addSource({ id: 'acme.brand.typefaces', description: "The brand's typefaces. Use these before any other font.", getSupportedMimeTypes: () => ['application/ubq-typeface'], async findAssets(query) { const families = await get('/brand/typefaces'); // One asset per family, every font in `fonts`, `name` exactly the family's. const assets = families.map((family) => ({ id: family.id, label: family.name, payload: { typeface: { name: family.name, fonts: family.files.map((file) => ({ uri: file.url, // a stable https URL: every design that uses it keeps it subFamily: file.subFamily, // 'Regular', 'Bold Italic', … weight: file.weight, // 'normal', 'bold', … style: file.style // 'normal' or 'italic' })) } } })); return paged(assets, query); } });
engine.asset.addSource({ id: 'acme.brand.colors', description: "The brand's colors.", getSupportedMimeTypes: () => ['application/ubq-color'], async findAssets(query) { const colors = await get('/brand/colors'); const assets = colors.map((color) => ({ id: color.id, label: color.name, // CE.SDK's AssetColor: each channel from 0 to 1. payload: { color: { colorSpace: 'sRGB', r: color.r, g: color.g, b: color.b } } })); return paged(assets, query); } });
engine.asset.addSource({ id: 'acme.brand.templates', description: "The brand's approved templates. Start a new design from one of these.", getSupportedMimeTypes: () => ['application/ubq-template-string'], async findAssets(query) { return paged((await get('/brand/templates')).map(asTemplate), query); }, // How a template is read when the agent starts a design from it; null: not this person's. async fetchAsset(id) { const template = (await get('/brand/templates')).find( (t) => t.id === id ); return template === undefined ? null : asTemplate(template); } }); } };}Each source says what it holds with CE.SDK’s getSupportedMimeTypes().
application/ubq-typeface (ASSET_MIME_TYPES.typeface, exported by
@imgly/codesign-sdk) makes a
source one the model’s font lookup searches, and application/ubq-color
(ASSET_MIME_TYPES.color) makes it the palette the model reads for “our
colors”. The executor checks this when the first design opens: a source you
register with engine.asset.addSource that returns typefaces or colors without
declaring the type fails the first call that opens a design (a model’s turn,
codesign.load(), or codesign.export()) with an invalid_options error naming the source. Without
that check, the font lookup would skip it silently. A source loaded from a JSON
file needs no declaration: it is typed by what it holds.
A typeface, as the module hands it to the engine:
{ "id": "acme-sans", "label": "Acme Sans", "payload": { "typeface": { "name": "Acme Sans", "fonts": [ { "uri": "https://fonts.acme.com/acme-sans-regular.ttf", "subFamily": "Regular", "weight": "normal", "style": "normal" }, { "uri": "https://fonts.acme.com/acme-sans-bold.ttf", "subFamily": "Bold", "weight": "bold", "style": "normal" } ] } }}The weight names are thin (100), extraLight, light, normal (400),
medium, semiBold (600), bold (700), extraBold, and heavy (900). A
color is CE.SDK’s AssetColor, { "colorSpace": "sRGB", "r": 0.1, "g": 0.5, "b": 0.3 }, each channel from 0 to 1.
The executor fetches your API and your font files itself, so the list of hosts the executor may call must include them (see Host the Executor). Your browser editor loads the same fonts and calls the same API from the person’s browser, so those hosts must also send cross-origin resource sharing (CORS) headers for your editor’s origin.
Four rules decide whether a brand font works. None of them stops a call: the design renders in another font or weight, and your product is not told.
- One asset per family. Return one asset per family, with every font in
payload.typeface.fontsandnameexactly the family’s. With one asset per file, the family has no bold: a request for bold gets the regular font, or an error the model works around. - A typeface name is a lookup key. When the model asks for a family by
name, the first source that has a typeface of that name answers, in the
order of
pluginsand then the order the module registers them, and the same name in a later source is never used. So list your brand module beforegoogleFonts(), register the brand’s typefaces first inside it, and keep a brand family out of the sources listed after it. - List every weight. Of two fonts that claim one weight, only the first is used. A weight the typeface does not list cannot be used: the regular font renders instead. A variable font, which holds every weight in one file, works too: list each weight as its own font that points at the same file.
- A design keeps the font’s address, so it must last: a stable, versioned
httpsURL, or a URI in a scheme of your own that a source’sresolvesigns ( Private Buckets). Never a presigned,file:, ordata:URL.
Check these rules in your backend when someone saves a brand kit, and show the
problem in your brand screen. What is caught for you: a font file that does not
load at all is named, with its text block and address, by every edit,
preview, and export. In your tests, testAssetSource from
@imgly/codesign-sdk/testing flags a family returned as several assets, a
font address that looks presigned, and, with its laterTypefaceSources option
( API Reference),
a brand family that a later typeface source carries too.
When it works, the agent’s previews show your brand’s typefaces and colors, and no edit result names a font file it could not load.
Templates and Your Asset Library#
Templates are one more source. A source that declares
application/ubq-template-string (ASSET_MIME_TYPES.template) holds designs
to start from; each asset’s meta.uri is the URL of a scene file (a design
saved by CE.SDK):
engine.asset.addSource({ id: 'acme.brand.templates', description: "The brand's approved templates. Start a new design from one of these.", getSupportedMimeTypes: () => ['application/ubq-template-string'], async findAssets(query) { return paged((await get('/brand/templates')).map(asTemplate), query); }, // How a template is read when the agent starts a design from it; null: not this person's. async fetchAsset(id) { const template = (await get('/brand/templates')).find( (t) => t.id === id ); return template === undefined ? null : asTemplate(template); }});The model finds a template by searching your asset sources and starts a new
design from it. The new design is written to your design store at once, and
the result names it and its version. fetchAsset is how the SDK reads the one
template the model chose; return null for an id this person may
not use. Your own code starts from a template without a model with
codesign.create, passing the template asset as the source lists it and the
source’s id:
// Your own code, without a model: a new design from a brand template.export async function startFromTemplate( codesign: CoDesign, templateId: string): Promise<string> { const found = await brandTemplate(templateId); const created = await codesign.create({ title: 'Spring launch post', from: { template: found, source: 'acme.brand.templates' } }); return created.design; // the new design's id, written by your store}Logos and photos are a source that holds images (image/png,
image/jpeg, …). The model searches it and places what it finds; if the
source has uploadFile, it also keeps the images the agent generates. Each
image asset carries its size in pixels as meta.width and meta.height, so the
model frames a logo at its aspect instead of cutting it; a search that meets an
image without a size fails with invalid_asset. To build that source, see
Your Files, and its Private Buckets
section if the files must stay private.
Each source’s description is one line the model reads in search results. Say
what the source is for and when to prefer it (“The brand’s approved templates.
Start a new design from one of these.”).
Tell the Agent What the Brand Is#
The sources make the brand searchable, but the model uses them only when told
to. Give it a tool of your own: a function the model may call, with a name,
a description, and its arguments. brand_library below returns the
typefaces, colors, and rules, and its description says when to call it:
// Tells the model what the brand is; the sources only make it searchable.function brandTool(tenantId: string): CoDesignTool { return { name: 'brand_library', description: "The brand: its typefaces, colors and rules. Call it whenever the brief mentions the brand, the company's name or 'our'.", inputSchema: { type: 'object', properties: {} }, async call() { const kit = await brandKitOf(tenantId); return { content: [ { type: 'text', text: JSON.stringify({ typefaces: kit.typefaces, // family names, as acme.brand.typefaces lists them colors: kit.colors, // { name, hex } rules: kit.rules // "headlines in Acme Sans bold, body regular" }) } ] }; } };}Pass it to prepareCoDesign as tools: [brandTool(tenantId)]. codesign.tools lists your tools after the SDK’s, so your
agent loop hands the model one list. A name the SDK uses, or one its handbook
is written around, makes prepareCoDesign throw tool_name_clash, naming the
tool; the list can grow in any release. For the reserved names, see
Reserved Tool Names.
Nothing else enforces the brand. The handbook, the design rules the model
reads at runtime, is the SDK’s and ships as it is. So brand guidance lives in
your tool’s description and result, in each source’s description, and in
your own system prompt.
Google Fonts#
googleFonts() returns a plugin entry for IMG.LY’s Google Fonts catalog. It is
off unless you list it in plugins. It registers two sources:
ly.img.gfonts, the typefaces the font lookup searches, and
ly.img.gfonts-fallbacks, the stand-ins the Photoshop, InDesign, PDF, and
PowerPoint importers use for common fonts a file names, such as Helvetica.
- List it after your brand module. Then a brand family wins the lookup, and Google Fonts covers the rest.
- List it if you import files. Without it, every file import fails with an error that names the missing font sources; see Import and Export Files.
- List only one default catalog.
cesdkDefaults(), CE.SDK’s own default typefaces, shares families such as Roboto with Google Fonts, and the one listed first wins with its own files and weights. List one of the two.
The executor loads the catalog from cdn.img.ly when it opens the first
design, so the list of hosts the executor may call must include it. To serve
the catalog from your own host instead, copy IMG.LY’s catalog folder as it is
and pass googleFonts({ baseURL }); the font files it lists still load from
cdn.img.ly. If the catalog cannot load, the call that opens the design fails
with an error naming the plugin and the file.
Next Steps#
- Connect Your Storage — keep your designs and files, and private files behind your own scheme.
- Import and Export Files — see what happens to fonts when a person hands over a PSD or an InDesign file.
- Add a Design Chat — put the brand into a whole product.