Search Docs
Loading...
Skip to content

Bring Your Brand

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:

  1. 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.
  2. A plugin module, whose asset sources (searchable lists of things a design can use: fonts, colors, templates, logos, and photos) call that route.
  3. 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.

  1. One asset per family. Return one asset per family, with every font in payload.typeface.fonts and name exactly 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.
  2. 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 plugins and then the order the module registers them, and the same name in a later source is never used. So list your brand module before googleFonts(), register the brand’s typefaces first inside it, and keep a brand family out of the sources listed after it.
  3. 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.
  4. A design keeps the font’s address, so it must last: a stable, versioned https URL, or a URI in a scheme of your own that a source’s resolve signs ( Private Buckets). Never a presigned, file:, or data: 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#