Search Docs
Loading...
Skip to content

Quickstart: Claude Agent SDK

Type a brief and get a finished PNG and an editable design file from a design agent on Anthropic’s Claude Agent SDK, in around 3 minutes, depending on the model and how hard the brief is.

12 mins
estimated time
Download
GitHub

To have a coding agent, such as Claude Code or Cursor, set up this kit for you, give it this prompt:

Open https://img.ly/docs/cesdk/codesign/starter-kits/quickstart-claude-agent-sdk-cdsqca.md
and set up this starter kit for me.

What It Does#

You run one command with a brief, such as “A square Instagram post for a spring sale on ceramic mugs”. Claude designs it, looks at what it made, and improves it, then the kit writes two files:

  • out/design.png, a picture of the design.
  • out/design.imgly, the design itself with its images and fonts, which you can open and edit in CE.SDK’s web editor ( Your Editor). CE.SDK is IMG.LY’s design engine and editor.

Start from this kit if your product runs a design agent on its servers and you build on the Claude Agent SDK, the agent loop Claude Code runs on. Two things run on your machine:

  • The executor, a Docker container from the image imgly/codesign-executor, which IMG.LY publishes. It holds CE.SDK’s engine and runs the short JavaScript programs the model writes to change the design, in its own container, apart from your keys and your data. You write no code for it.
  • The agent runner, design.mjs, the kit’s code and later yours. It gets the design tools from the CoDesign SDK, @imgly/codesign-sdk, and serves them to the Claude Agent SDK. A tool is a function the model can ask to call, such as one that changes the design or renders a preview. The Claude Agent SDK runs the agent loop: it calls Claude, runs each tool Claude asks for, and sends the result back, until Claude is done. Then the script exports the design.

Pre-requisites#

  • Docker, to run the executor – Download
  • Node.js v22+ with npm, pnpm, or yarn – Download
  • A CoDesign license key. Contact sales to get a CoDesign license key. It starts the design engine and pays for each exported file.
  • An Anthropic API key, from the Anthropic Console. It pays for the model, because IMG.LY’s AI gateway does not serve Anthropic’s API, which the Claude Agent SDK calls. For one IMG.LY key that pays for the model too, pick any other quickstart kit.

Get Started#

Step 1: Clone the Repository#

Terminal
git clone https://github.com/imgly/starterkit-codesign-quickstart-claude-agent-sdk-js-node.git
git clone https://github.com/imgly/starterkit-codesign-quickstart-claude-agent-sdk-js-node.git

What is inside:

starterkit-codesign-quickstart-claude-agent-sdk-js-node/
├── design.mjs # The agent runner: serves the tools to the Claude Agent SDK, runs Claude, exports the design
├── .env.template # The variables to copy into .env
├── README.md # How to run the kit
├── LICENSE.md # IMG.LY's license, the same as the CoDesign SDK's
├── package.json # The SDK, the Claude Agent SDK, and the MCP SDK, at exact versions; `npm run executor` starts the executor
├── tsconfig.json # `npm run check` type-checks design.mjs
└── tsconfig.base.json # The compiler settings tsconfig.json extends

Read design.mjs after your first run; How It Works below explains it.

Step 2: Install Dependencies#

Terminal
cd starterkit-codesign-quickstart-claude-agent-sdk-js-node
npm install
cd starterkit-codesign-quickstart-claude-agent-sdk-js-node
npm install

The Claude Agent SDK runs Claude Code as a separate program, so the install also downloads Claude Code for your platform, about 250 MB.

package.json pins @imgly/codesign-sdk to the same version as the executor image’s tag in its executor script. Keep the two equal, because the SDK and the executor must come from the same build ( Versions and Upgrades).

Step 3: Set Your Keys#

Copy .env.template to .env and fill in two variables. IMGLY_CESDK_LICENSE holds your CoDesign license key (the variable keeps CE.SDK’s name). ANTHROPIC_API_KEY is your Anthropic API key. EXECUTOR_TOKEN, the token your runner and the executor share, is already filled in:

.env
IMGLY_CESDK_LICENSE=<your CoDesign license key>
EXECUTOR_TOKEN=codesign-quickstart-token
ANTHROPIC_API_KEY=<your Anthropic API key>

The executor script passes .env to the executor, and the start script to design.mjs, so the two always have the same token. Two optional variables are described in .env.template:

Variable What it is
MODEL The Claude model. Default claude-opus-5.
EXECUTOR_URL The executor’s address. Default http://localhost:8080.

Step 4: Start the Executor#

Terminal
npm run executor
curl http://localhost:8080/health
npm run executor
curl http://localhost:8080/health

The executor script runs IMG.LY’s image in Docker with the variables from .env, on port 8080 of your machine only. The first start downloads the image. curl prints a line like this one:

{"result":{"sessions":0,"accepting":true,"bound":false,...}}

If /health does not answer, run docker logs codesign-executor to see why. The executor stops at start when the license key is missing or cannot be activated, or when it cannot reach api.img.ly.

Step 5: Design Something#

Terminal
npm start – “A square Instagram post for a 20% spring sale on ceramic mugs. Calm, premium, lots of white space.”
npm start – “A square Instagram post for a 20% spring sale on ceramic mugs. Calm, premium, lots of white space.”

A run takes around 3 minutes, depending on the model and how hard the brief is. You see a line for each tool call as it happens, a ✓ and the tool’s name, as Claude reads IMG.LY’s design rules, changes the design, and looks at a render, a picture of the design. Then you see Claude’s one-sentence summary and a line with the number of steps, the seconds, and the tokens used. The last line is:

Wrote out/design.png and out/design.imgly

Open out/design.png to see the design. out/design.imgly opens in CE.SDK’s web editor, which you embed in a page of your product ( Your Editor). Use exactly the CE.SDK version the executor runs, which the SDK exports as CESDK_VERSION: an older or newer editor that saves the file drops what the other version wrote ( Versions and Upgrades).

To stop and remove the executor, run npm run executor:stop.

Run It Again#

The executor serves one conversation: your id for one chat or job, which design.mjs sets to quickstart on every run. At the end of a run, the script calls release(), which closes the design on the executor. Half a minute later the executor exits, and Docker starts it again, ready for your next run.

A run that starts in the few seconds while the container restarts fails with executor_unreachable. Wait until curl http://localhost:8080/health answers again.

Before you run the executor anywhere but your machine, follow Host the Executor.

What It Costs#

Each run’s PNG export is charged to the IMG.LY account behind your CoDesign key, and the model’s tokens to your Anthropic account; the .imgly file, the edits, and the previews are free ( What It Costs).

How It Works#

design.mjs does four things. The excerpts below are from it, and the whole file follows them.

  1. It prepares the tools with prepareCoDesign.
    • conversation is your id for the work. Every design of a conversation opens on one executor that serves no other conversation, because the model’s code runs there.
    • executors: remoteExecutors({ acquire, token }) says where a conversation’s executor is. acquire is a function you write. Here it always answers the container on port 8080. In production, it answers a new container for each new conversation, and the same one while the conversation lasts.
    • plugins lists the CE.SDK plugins the engine applies. A plugin adds asset sources: searchable lists of what a design can use, such as photos, typefaces, and colors. googleFonts() is IMG.LY’s Google Fonts catalog, the smallest useful list.
    • design is the id of the design the tools work on. With no designs store, the design lives in the runner’s memory for the run.
    • gateway: false leaves out image generation, which runs through IMG.LY’s AI gateway. The option is required, so you decide.
const codesign = prepareCoDesign({
// Your id for this job. Every design of a conversation opens on one executor
// that serves no other conversation. Every run here is yours, so all share one.
conversation: 'quickstart',
executors: remoteExecutors({
// The executor for a conversation. Here there is one; a product starts a
// new one for each conversation.
acquire: () => ({
url: process.env.EXECUTOR_URL || 'http://localhost:8080'
}),
token: process.env.EXECUTOR_TOKEN
}),
// The CE.SDK plugins the engine applies: everything the agent can search and
// place. Google Fonts is the smallest useful list; your own plugins go here.
plugins: [googleFonts()],
design: `quickstart-${Date.now()}`, // any id; with no `designs` store, the design lives in memory
gateway: false // no image generation
});
  1. It serves the tools to the Claude Agent SDK as an MCP server. The Claude Agent SDK takes custom tools from MCP servers. MCP, the Model Context Protocol, is a standard way to offer tools to a model. Each SDK tool is { name, description, inputSchema, call }, and its schema and its result already have MCP’s shape, so the server in design.mjs lists the tools and calls them, with nothing converted. A server object from the MCP SDK, @modelcontextprotocol/sdk, runs inside the same process; nothing listens on a port. The renders stay images inside the tool results, which Claude reads as they are.
// The SDK's tools as an MCP server inside this process. Their JSON Schemas and
// results already have MCP's shape, so they pass through unchanged, renders
// included: Claude sees images in a tool result.
const server = new McpServer(
{ name: 'codesign', version: '1.0.0' },
{ capabilities: { tools: {} } }
);
server.server.setRequestHandler(ListToolsRequestSchema, () => ({
tools: codesign.tools.map((t) => ({
name: t.name,
description: t.description,
inputSchema: t.inputSchema
}))
}));
server.server.setRequestHandler(
CallToolRequestSchema,
async ({ params }, { signal }) => {
const tool = codesign.tools.find((t) => t.name === params.name);
if (!tool) throw new Error(`No tool named ${params.name}`);
const result = await tool.call(params.arguments ?? {}, { signal });
console.log(`${result.isError ? '✗' : '✓'} ${tool.name}`);
return result; // a result with isError reads to the model as a failed call
}
);
  1. It runs Claude with only those tools. The Claude Agent SDK runs Claude Code, which has its own tools for the shell, files, and the web. A design agent needs none of them, and they would act on the machine the runner runs on ( Security), so the options turn them off:
    • tools: [] removes all of Claude Code’s own tools.
    • allowedTools: ['mcp__codesign'] lets the design tools run without asking, and permissionMode: 'dontAsk' refuses anything else instead of asking.
    • settingSources: [] and strictMcpConfig load no settings, hooks, plugins, MCP servers, or CLAUDE.md from the machine, so the run is the same on your laptop and on a server.
    • persistSession: false writes no transcript to disk.
    • systemPrompt replaces Claude Code’s own prompt with yours, followed by codesign.instructions, which tell Claude how to use the tools, so your prompt names none of them. The brief goes in as prompt.
    • maxTurns: 60 lets Claude take up to 60 turns; without it, a run has no limit.
const run = query({
prompt: brief,
options: {
model: process.env.MODEL || 'claude-opus-5',
systemPrompt: `You design marketing visuals. When you are done, say in one sentence what you made.\n\n${codesign.instructions}`,
mcpServers: {
codesign: { type: 'sdk', name: 'codesign', instance: server }
},
tools: [], // none of Claude Code's own tools: no shell, no files, no web
allowedTools: ['mcp__codesign'], // the design tools run without asking
permissionMode: 'dontAsk', // anything else is refused, never asked about
settingSources: [], // no settings, hooks, plugins, or CLAUDE.md from this machine
strictMcpConfig: true, // no MCP servers but the one above
persistSession: false, // no transcript written to ~/.claude
maxTurns: 60 // without it, the run has no step limit
}
});
  1. It reads the result and exports. query() returns the run’s messages as they happen, and its last message is the result: Claude’s final text, the number of turns, and the tokens. When it succeeded, the script exports the PNG and the .imgly file and calls release().
The whole file
design.mjs
// A brief in, a finished design out: the Claude Agent SDK runs Claude with your Anthropic API key, the engine runs in one local executor.
import { mkdir, writeFile } from 'node:fs/promises';
import { query } from '@anthropic-ai/claude-agent-sdk';
import {
googleFonts,
prepareCoDesign,
remoteExecutors
} from '@imgly/codesign-sdk';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema
} from '@modelcontextprotocol/sdk/types.js';
const brief = process.argv[2];
const missing = [
...(brief ? [] : ['a brief (the first argument)']),
...['IMGLY_CESDK_LICENSE', 'EXECUTOR_TOKEN'].filter((n) => !process.env[n])
];
if (missing.length > 0) {
console.error(
`Missing ${missing.join(', ')}.\nUsage: npm start -- "<brief>", with the variables of .env.template set in .env` +
(process.env.IMGLY_CESDK_LICENSE
? ''
: '\nContact sales (https://img.ly/forms/contact-sales/) to get a CoDesign license key.')
);
process.exit(1);
}
const codesign = prepareCoDesign({
// Your id for this job. Every design of a conversation opens on one executor
// that serves no other conversation. Every run here is yours, so all share one.
conversation: 'quickstart',
executors: remoteExecutors({
// The executor for a conversation. Here there is one; a product starts a
// new one for each conversation.
acquire: () => ({
url: process.env.EXECUTOR_URL || 'http://localhost:8080'
}),
token: process.env.EXECUTOR_TOKEN
}),
// The CE.SDK plugins the engine applies: everything the agent can search and
// place. Google Fonts is the smallest useful list; your own plugins go here.
plugins: [googleFonts()],
design: `quickstart-${Date.now()}`, // any id; with no `designs` store, the design lives in memory
gateway: false // no image generation
});
// The SDK's tools as an MCP server inside this process. Their JSON Schemas and
// results already have MCP's shape, so they pass through unchanged, renders
// included: Claude sees images in a tool result.
const server = new McpServer(
{ name: 'codesign', version: '1.0.0' },
{ capabilities: { tools: {} } }
);
server.server.setRequestHandler(ListToolsRequestSchema, () => ({
tools: codesign.tools.map((t) => ({
name: t.name,
description: t.description,
inputSchema: t.inputSchema
}))
}));
server.server.setRequestHandler(
CallToolRequestSchema,
async ({ params }, { signal }) => {
const tool = codesign.tools.find((t) => t.name === params.name);
if (!tool) throw new Error(`No tool named ${params.name}`);
const result = await tool.call(params.arguments ?? {}, { signal });
console.log(`${result.isError ? '✗' : '✓'} ${tool.name}`);
return result; // a result with isError reads to the model as a failed call
}
);
const started = Date.now();
const run = query({
prompt: brief,
options: {
model: process.env.MODEL || 'claude-opus-5',
systemPrompt: `You design marketing visuals. When you are done, say in one sentence what you made.\n\n${codesign.instructions}`,
mcpServers: {
codesign: { type: 'sdk', name: 'codesign', instance: server }
},
tools: [], // none of Claude Code's own tools: no shell, no files, no web
allowedTools: ['mcp__codesign'], // the design tools run without asking
permissionMode: 'dontAsk', // anything else is refused, never asked about
settingSources: [], // no settings, hooks, plugins, or CLAUDE.md from this machine
strictMcpConfig: true, // no MCP servers but the one above
persistSession: false, // no transcript written to ~/.claude
maxTurns: 60 // without it, the run has no step limit
}
});
/** @type {import('@anthropic-ai/claude-agent-sdk').SDKResultMessage | undefined} */
let result;
let authFailed = false;
try {
for await (const message of run) {
if (
message.type === 'assistant' &&
message.error === 'authentication_failed'
)
authFailed = true;
if (message.type === 'result') result = message;
}
} catch (err) {
if (!result) throw err; // after a failed result the SDK throws too; that result says why
}
if (result?.subtype === 'success' && !result.is_error) {
console.log(`\n${result.result}`);
const u = result.usage;
const input =
u.input_tokens + u.cache_creation_input_tokens + u.cache_read_input_tokens;
console.log(
`${result.num_turns} steps, ${Math.round((Date.now() - started) / 1000)} s, ` +
`${input} input / ${u.output_tokens} output tokens`
);
await mkdir('out', { recursive: true });
await writeFile('out/design.png', await codesign.export('png'));
await writeFile('out/design.imgly', await codesign.export('imgly'));
console.log('Wrote out/design.png and out/design.imgly');
} else {
const why = result?.subtype === 'success' ? result.result : result?.subtype;
console.error(`The model stopped early (${why}); nothing exported.`);
if (authFailed)
console.error('Set ANTHROPIC_API_KEY in .env to your Anthropic API key.');
process.exitCode = 1;
}
await codesign.release(); // ends the design's session now; the engine closes about 30 s later

What to Replace#

Piece In the kit In your product
The agent runner design.mjs, run once from a terminal with a brief your server or job, on the Claude Agent SDK with your Anthropic key
The tools codesign.tools, served from one MCP server the same
The executor one container from npm run executor, shared by every run IMG.LY’s executor image, a new container for each conversation, answered by acquire
The design the runner’s memory, then two files in out/ your database, through designs ( Connect Your Storage)
Fonts and media googleFonts() your own CE.SDK plugin in plugins ( Bring Your Brand)

To grow the script into your product:

  • To start from a customer’s file, such as a Photoshop file (PSD), a PDF, or an .imgly file, call codesign.load(bytes) before the model runs.
  • To let the model hand files to a person, pass uploadExport: a function that stores an exported file and answers where the person gets it.
  • To let the model generate images, pass gateway: { bearer }, a function that returns your CoDesign key; the images are charged to your IMG.LY account.

For each of these options, see the API Reference.


Troubleshooting#

The Executor Does Not Start#

  • docker: open .env: no such file or directory. Copy .env.template to .env in the kit’s folder and set your key.
  • The container name "/codesign-executor" is already in use. The executor is already running: check curl http://localhost:8080/health, or run npm run executor:stop first.
  • /health does not answer. Run docker logs codesign-executor. The executor stops at start when the license key is missing or cannot be activated, or when it cannot reach api.img.ly.

The Run Fails#

  • node: .env: not found. Copy .env.template to .env in the kit’s folder.
  • Missing a brief (the first argument). Pass the brief after the command, in quotes, as in step 5.
  • Missing IMGLY_CESDK_LICENSE, EXECUTOR_TOKEN. Set both in .env.
  • Set ANTHROPIC_API_KEY in .env. The key is missing, or Anthropic refused it. Set a valid ANTHROPIC_API_KEY in .env.
  • missing or wrong executor token. The executor answered 401: it was started with another EXECUTOR_TOKEN. After you change the token, run npm run executor:stop and npm run executor again, so the executor gets it.
  • executor_unreachable. The executor is not running, or the run started while it restarts after the last run. Wait until curl http://localhost:8080/health answers.
  • The model stopped early. Claude reached the 60-turn limit or stopped for another reason. Nothing was exported.

For more failures and their fixes, see Troubleshooting.


Next Steps#