snapshot of https://github.com/tldraw/tldraw.git (main, 2026-09-23; blobs > 131072B omitted)
Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled

This commit is contained in:
upstream-mirror
2026-09-23 16:12:52 +05:00
commit a774ac673a
4541 changed files with 770499 additions and 0 deletions

View File

@@ -0,0 +1,769 @@
---
title: Agent starter kit
sidebarTitle: Agent
status: published
author: tldraw
date: 9/16/2024
order: 4
description: The Agent Starter Kit demonstrates how to build an AI agent that can interpret and manipulate the [tldraw canvas](https://github.com/tldraw/tldraw). It features a chat panel on the righthand side of the screen where the user can communicate with the agent, add context and see chat history. It works as a foundation to create diagram generators, drawing assistants, or visual AI applications.
embed: agent
githubLink: https://github.com/tldraw/tldraw/tree/main/templates/agent
keywords:
- ai
- agent
- artificial intelligence
- automation
readability: 7
voice: 6
completeness: 9
accuracy: 9
---
To build with an agent starter kit, run this command in your terminal:
```bash
npm create tldraw@latest -- --template agent
```
Then create a `.dev.vars` file in the project root with API keys for the model providers you want to use:
```
ANTHROPIC_API_KEY=<your key>
GOOGLE_API_KEY=<your key>
OPENAI_API_KEY=<your key>
```
We recommend Anthropic for best results. Get an API key from the [Anthropic dashboard](https://console.anthropic.com/settings/keys).
---
## Use cases
Use the agent starter kit to build visual AI assistants that read and modify drawings, diagram generators that turn text descriptions into flowcharts or architecture diagrams, and tools that convert hand-drawn sketches into structured digital content.
## How it works
### What the agent can do
With its default configuration, the agent can perform the following actions:
- Create, update and delete shapes.
- Draw freehand pen strokes.
- Use higher-level operations on multiple shapes at once: rotate, resize, align, distribute, stack and reorder shapes.
- Write out its thinking and send messages to the user.
- Keep track of its task by writing and updating a todo list.
- Move its viewport to look at different parts of the canvas.
- Count shapes matching a given expression.
- Schedule further work and reviews to be carried out in follow-up requests.
- Call external APIs.
### Architecture
![Flowchart showing how user input is processed by an agent, which gathers a canvas screenshot, analyzes shapes, and builds context. These feed into an AI model, which performs further shape analysis, updates the canvas, and produces results that go back to the user as feedback.](/images/starter-kits/chart-agent.png)
### 1. User input
To make decisions on what to do, the agent gathers information from various sources:
- The user's message.
- The user's current selection of shapes.
- What the user can currently see on their screen.
- Any additional context the user has provided, such as specific shapes or a particular position or area on the canvas.
- Actions the user has recently taken.
- A screenshot of the agent's current view of the canvas.
- A simplified format of all shapes within the agent's viewport.
- Information about clusters of shapes outside the agent's viewport.
- The history of the current session, including the user's messages and all of the agent's actions.
- Lints identifying potential issues with shapes on the canvas.
### 2. Visual context system: Canvas understanding
The agent captures both visual screenshots and structured shape data from the canvas. This dual approach means the model understands both the visual layout and the underlying data structure, including spatial relationships between shapes.
The agent's "eyes" are defined by `PromptPartUtil` classes that gather different types of context, from user messages and canvas screenshots to shape data and interaction history.
### 3. Action system: Canvas manipulation
The agent performs canvas operations through a modular action system. Each action type handles a specific kind of canvas modification, from creating shapes to multi-step operations like aligning or distributing groups of shapes. Every action includes its own validation, execution, and chat-panel presentation logic.
The agent's "hands" are defined by `AgentActionUtil` classes that specify what operations the agent can perform on the canvas.
### 4. Mode system: Contextual capabilities
The agent operates in one of several modes, each with its own set of capabilities tailored to a specific kind of task. The default `working` mode has full access to the canvas. A narrower mode might strip away drawing actions and add critique-specific ones for reviewing work, or swap in different tools for a planning phase. Modes let the same agent take on different roles at different points in a task.
### 5. Streaming system: Real-time AI responses
AI responses stream in real time. Users see the agent's thinking and its canvas modifications as they happen. Shapes get created, updated, and deleted incrementally as each action finishes streaming, so the canvas stays responsive throughout long, multi-step tasks.
### 6. Manager architecture: State decomposition
The agent's state is organized into focused managers, each responsible for a single concern: chat history, model selection, contextual shapes, the todo list, mode transitions, and more. This decomposition keeps the agent's behavior easy to reason about as the template grows, and adding a new capability means adding a new manager rather than expanding a monolith.
### 7. Integration system: Model flexibility
This starter kit supports multiple AI providers (Anthropic, OpenAI, Google) with consistent interfaces. The system abstracts provider differences, so you can switch models or use different providers for different operations. Adding support for a new provider doesn't require changes to the rest of the system.
## Use the agent programmatically
Aside from using the chat panel UI, you can also prompt the agent programmatically.
Call the `prompt()` method to start an agentic loop. The agent will continue until it has finished the task you've given it.
```tsx
// Inside a component wrapped by TldrawAgentAppProvider
const agent = useAgent()
agent.prompt('Draw a cat')
```
You can optionally specify further details about the request in the form of an `AgentInput` object:
```tsx
agent.prompt({
message: 'Draw a cat in this area',
bounds: {
x: 0,
y: 0,
w: 300,
h: 400,
},
})
```
There are more methods on the `TldrawAgent` class that can help when building an agentic app:
- `agent.cancel()` - Cancel the agent's current task.
- `agent.reset()` - Reset the agent's chat and memory.
- `agent.request(input)` - Send a single request to the agent and handle its response _without_ entering into an agentic loop.
---
## Customize the agent
The agent's behavior is defined in `client/modes/AgentModeDefinitions.ts`. The `AGENT_MODE_DEFINITIONS` array contains mode definitions. Each mode has two arrays:
- `parts` determine what the agent can **see**.
- `actions` determine what the agent can **do**.
Add, edit or remove an entry in either array to change what the agent can see or do in a given mode.
### The mode system
Modes control which parts and actions the agent has access to at any given time. They're defined in `client/modes/AgentModeDefinitions.ts`.
The default `working` mode includes all standard capabilities. You can create additional modes with different subsets of parts and actions.
Call `agent.mode.setMode(modeType)` to change modes during a prompt. To control each mode's lifecycle, implement any of the lifecycle hooks in `client/modes/AgentModeChart.ts`:
- `onEnter(agent, fromMode)` - runs when the agent enters a mode.
- `onExit(agent, toMode)` - runs when the agent exits a mode.
- `onPromptStart(agent, request)` - runs when a prompt commences.
- `onPromptEnd(agent, request)` - runs when a prompt ends.
- `onPromptCancel(agent, request)` - runs when a prompt is canceled.
### Change what the agent can see
**Change what the agent can see by adding, editing or removing a prompt part.**
Prompt parts assemble the prompt that the model receives, with each part adding a different piece of information. This includes the user's message, the model name, the system prompt, chat history and more.
A prompt part has two pieces: a `PromptPartUtil` class on the client that gathers data, and a `PromptPartDefinition` on the worker that turns that data into messages.
This example shows how to let the model see what the current time is.
First, define a prompt part type:
```tsx
interface TimePart extends BasePromptPart<'time'> {
time: string
}
```
Then, create a prompt part util in `client/parts/`:
```tsx
export const TimePartUtil = registerPromptPartUtil(
class TimePartUtil extends PromptPartUtil<TimePart> {
static override type = 'time' as const
override getPart(): TimePart {
return {
type: 'time',
time: new Date().toLocaleTimeString(),
}
}
}
)
```
Next, create the prompt part definition in `shared/schema/PromptPartDefinitions.ts`:
```tsx
export const TimePartDefinition: PromptPartDefinition<TimePart> = {
type: 'time',
priority: -100,
buildContent({ time }: TimePart) {
return [`The user's current time is: ${time}`]
},
}
```
To enable the prompt part, import its util in `client/modes/AgentModeDefinitions.ts` and add `TimePartUtil.type` to a mode's `parts` array. Its methods will be used to assemble its data and send it to the model.
- `getPart()` - Gather any data needed to construct the prompt.
- `buildContent()` - Turn the data into messages to send to the model.
There are other fields available on the `PromptPartDefinition` interface that you can override for more granular control.
- `priority` - Control where this prompt part will appear in the list of messages sent to the model. Higher priority appears later in the prompt.
- `getModelName()` - Determine which AI model to use.
- `buildMessages()` - Manually override how prompt messages are constructed from the prompt part.
### Change what the agent can do
**Change what the agent can do by adding, editing or removing an `AgentActionUtil`.**
Agent action utils define which actions the agent can perform. Each `AgentActionUtil` adds a different capability.
This example shows how to allow the agent to clear the screen.
First, define an agent action by creating a schema for it in `shared/schema/AgentActionSchemas.ts`:
```tsx
export const ClearAction = z
// All agent actions must have a _type field
// The underscore encourages the model to put this field first
.object({
_type: z.literal('clear'),
})
// A title and description tell the model what the action does
.meta({
title: 'Clear',
description: 'The agent deletes all shapes on the canvas.',
})
// Infer the action's type
export type ClearAction = z.infer<typeof ClearAction>
```
Then, create an agent action util in `client/actions/`:
```tsx
export const ClearActionUtil = registerActionUtil(
class ClearActionUtil extends AgentActionUtil<ClearAction> {
static override type = 'clear' as const
override applyAction(action: Streaming<ClearAction>) {
// Don't do anything until the action has finished streaming
if (!action.complete) return
// Get the editor
const { editor } = this
// Delete all shapes on the page
const shapes = editor.getCurrentPageShapes()
editor.deleteShapes(shapes)
}
}
)
```
To enable the agent action, import its util in `client/modes/AgentModeDefinitions.ts` and add `ClearActionUtil.type` to a mode's `actions` array. Its method will be used to execute the action.
- `applyAction()` - Execute the action.
There are other methods available on the `AgentActionUtil` class that you can override for more granular control.
- `getInfo()` - Determine how the action gets displayed in the chat panel UI.
- `savesToHistory()` - Control whether actions get saved to chat history or not.
- `sanitizeAction()` - Apply transformations to the action before saving it to history and applying it. More details on transformations below.
### Change how actions appear in chat history
**Configure the icon and description of an action in the chat panel UI using the `getInfo()` method.**
```tsx
override getInfo() {
return {
icon: 'trash' as const,
description: 'Cleared the canvas',
}
}
```
You can make an action collapsible by adding a `summary` property.
```tsx
override getInfo() {
return {
summary: 'Cleared the canvas',
description: 'After much consideration, the agent decided to clear the canvas',
}
}
```
To customize an action's appearance via CSS, define styles for the `agent-action-type-{TYPE}` class where `{TYPE}` is the type of the action.
```css
.agent-action-type-clear {
color: red;
}
```
### Managers
Managers are classes that extend `TldrawAgent` or `TldrawAgentApp` with a focused responsibility, such as chat history, model selection, or context management. They're available as properties on the agent instance, for example `agent.chat`, `agent.modelName`, and `agent.context`.
To add a custom manager, extend `BaseAgentManager` or `BaseAgentAppManager` and attach it to the agent in `client/agent/TldrawAgent.ts`.
### Registering utils
Utils use a self-registration pattern. When you create a new `PromptPartUtil` or `AgentActionUtil`, wrap it with a registration function:
```tsx
export const MyPartUtil = registerPromptPartUtil(
class MyPartUtil extends PromptPartUtil<MyPart> {
// ...
}
)
```
This ensures the util is discovered automatically when its module is imported in `AgentModeDefinitions.ts`.
### Mode-scoped actions
Different modes can implement actions with the same `_type`. For example, a `team-member` mode and a `solo` mode might both have a `mark-task-done` action, but with different implementations.
To register a mode-specific action util, pass the `forModes` option:
```tsx
export const MarkTeamMemberTaskDoneActionUtil = registerActionUtil(
class MarkTeamMemberTaskDoneActionUtil extends AgentActionUtil<MarkTeamMemberTaskDoneAction> {
static override type = 'mark-task-done' as const
override applyAction(action: Streaming<MarkTeamMemberTaskDoneAction>) {
// Team member-specific implementation
}
},
{ forModes: ['team-member'] }
)
```
Default schemas are auto-registered when exported from `AgentActionSchemas.ts`. For mode-specific schemas, call `registerActionSchema` explicitly with the `forModes` option.
See the [template README](https://github.com/tldraw/tldraw/blob/main/templates/agent/README.md#mode-scoped-actions) for a full worked example.
### Schedule further work
You can let the agent work over multiple turns by scheduling further work using the `schedule` method as part of an action.
This example shows how to schedule an extra step for adding detail to the canvas.
```tsx
override applyAction(action: Streaming<AddDetailAction>) {
if (!action.complete) return
if (!this.agent) return
this.agent.schedule('Add more detail to the canvas.')
}
```
As with the `prompt` method, you can specify further details about the request.
```tsx
agent.schedule({
message: 'Add more detail in this area.',
bounds: { x: 0, y: 0, w: 100, h: 100 },
})
```
You can schedule multiple things by calling the `schedule()` method more than once.
```tsx
agent.schedule('Add more detail to the canvas.')
agent.schedule('Check for spelling mistakes.')
```
To interrupt the agent with a new prompt instead of waiting for the current prompt to end, use the `interrupt` method. `interrupt` also lets you specify a mode to transition into.
```tsx
override applyAction(action: Streaming<EnterReviewingModeAction>) {
if (!action.complete) return
this.agent.interrupt({
mode: 'reviewing',
input: {
message: 'Review the new area thoroughly for any mistakes',
bounds: action.bounds,
},
})
}
```
### Retrieve data from an external API
To let the agent retrieve information from an external API, fetch the data within `applyAction` and schedule a follow-up request with any data you want the agent to have access to.
```tsx
override async applyAction(action: Streaming<CountryInfoAction>) {
if (!action.complete) return
if (!this.agent) return
// Fetch from the external API
const data = await fetchCountryInfo(action.code)
// Schedule a follow-up request with the data
this.agent.schedule({ data: [data] })
}
```
### Sanitize data received from the model
The model can make mistakes. Sometimes this is due to hallucinations, and sometimes this is due to the canvas changing since the last time the model saw it. Either way, an incoming action might contain invalid data by the time you receive it.
To correct incoming mistakes, apply fixes in the `sanitizeAction()` method of an action util. They'll get carried out before the action is applied to the editor or saved to chat history.
For example, ensure that a shape ID received from the model refers to an existing shape by using the `ensureShapeIdExists()` method.
```tsx
override sanitizeAction(action: Streaming<DeleteAction>, helpers: AgentHelpers) {
if (!action.complete) return action
// Ensure the shape ID refers to an existing shape
action.shapeId = helpers.ensureShapeIdExists(action.shapeId)
// If the shape ID doesn't refer to an existing shape, cancel the action
if (!action.shapeId) return null
return action
}
```
The `AgentHelpers` class contains more helpers for sanitizing data received from the model.
- `ensureShapeIdExists()` - Ensure that a shape ID refers to a real shape. Useful for interacting with existing shapes.
- `ensureShapeIdIsUnique()` - Ensure that a shape ID is unique. Useful for creating new shapes.
- `ensureValueIsVec()`, `ensureValueIsNumber()` - Ensure that a value is a certain type. Useful for more complex actions where the model is more likely to make mistakes.
### Send positions to and from the model
By default, every position sent to the model is offset by the starting position of the current chat.
**To apply this offset to a position sent to the model, use the `applyOffsetToVec` method.**
```tsx
override getPart(request: AgentRequest, helpers: AgentHelpers): ViewportCenterPart {
if (!this.editor) return { part: 'user-viewport-center', center: null }
// Get the center of the user's viewport
const viewportCenter = this.editor.getViewportBounds().center
// Apply the chat's offset to the vector
const offsetViewportCenter = helpers.applyOffsetToVec(viewportCenter)
// Return the prompt part
return {
part: 'user-viewport-center',
center: offsetViewportCenter,
}
}
```
**To remove the offset from a position received from the model, use the `removeOffsetFromVec` method.**
```tsx
override applyAction(action: Streaming<MoveAction>, helpers: AgentHelpers) {
if (!action.complete) return
// Remove the offset from the position
const position = helpers.removeOffsetFromVec({ x: action.x, y: action.y })
// Do something with the position...
}
```
It's a good idea to round numbers before sending them to the model. If you want to be able to restore the original number later, use the `roundAndSaveNumber` and `unroundAndRestoreNumber` methods.
```tsx
// In `getPart`...
const roundedX = helpers.roundAndSaveNumber(x, 'my_key_x')
const roundedY = helpers.roundAndSaveNumber(y, 'my_key_y')
// In `applyAction`...
const unroundedX = helpers.unroundAndRestoreNumber(x, 'my_key_x')
const unroundedY = helpers.unroundAndRestoreNumber(y, 'my_key_y')
```
To round all the numbers on a shape, use the `roundShape` and `unroundShape` methods. See the [Send shapes to the model](#send-shapes-to-the-model) section below for more details.
### Send shapes to the model
By default, the agent converts tldraw shapes to various simplified formats to improve the model's understanding and performance.
There are three main formats used in this starter:
- `BlurryShape` - The format for shapes within the agent's viewport. It contains a shape's bounds, its ID, its type, and any text it contains. The "blurry" name refers to the fact that the agent can't make out the details of shapes from this format. Instead, it gives the model an overview of what it's looking at.
- `FocusedShape` - The format for shapes that the agent is focusing on, such as shapes you've manually added to its context. The format contains most of a shape's properties, including color, fill, alignment, and any other shape-specific information. This is also the format the model outputs when creating shapes.
- `PeripheralShapeCluster` - The format for shapes outside the agent's viewport. Nearby shapes are grouped together into clusters, each with the group's bounds and a count of how many shapes are inside it. This is the least detailed format. Its role is to give the model an awareness of shapes elsewhere on the page.
To send the model some shapes in one of these formats, use one of the conversion functions found within the `shared/format` folder, such as `convertTldrawShapeToFocusedShape()`.
This example picks one random shape on the canvas and sends it to the model in the Focused format.
```tsx
override getPart(request: AgentRequest, helpers: AgentHelpers): RandomShapePart {
const { editor } = this
if (!this.editor) return { type: 'random-shape', shape: null}
// Get a random shape
const shapes = editor.getCurrentPageShapes()
const randomShape = shapes[Math.floor(Math.random() * shapes.length)]
// Convert the shape to the Focused format
const focusedShape = convertTldrawShapeToFocusedShape(editor, randomShape)
// Normalize the shape's position
const offsetShape = helpers.applyOffsetToShape(focusedShape)
const roundedShape = helpers.roundShape(offsetShape)
return {
type: 'random-shape',
shape: roundedShape,
}
}
```
### Change the system prompt
To change the default system prompt, edit the sections in `worker/prompt/sections/`. These sections are assembled by `worker/prompt/buildSystemPrompt.ts`.
The system prompt is rebuilt for each step in the agentic loop depending on which actions and parts are available in the agent's current mode. To give the model more detailed instructions for how to use any custom actions or parts you add, edit `worker/prompt/sections/rules-section.ts`.
### Change to a different model
You can set an agent's model by calling `setModelName` on the `modelName` manager.
```tsx
agent.modelName.setModelName('gemini-3.8-flash')
```
To override an agent's model, specify a different model name with a request.
```tsx
agent.prompt({
modelName: 'gemini-3.8-flash',
message: 'Draw a diagram of a volcano.',
})
```
You can conditionally override the model name by overriding the `getModelName()` method on any `PromptPartDefinition`.
```tsx
override getModelName(part: MyCustomPromptPart) {
return part.fastMode ? 'gemini-3.8-flash' : 'claude-sonnet-5'
}
```
### Support a different model
To add support for a different model, add the model's definition to `AGENT_MODEL_DEFINITIONS` in the `shared/models.ts` file.
```tsx
'claude-sonnet-5': {
name: 'claude-sonnet-5',
id: 'claude-sonnet-5',
provider: 'anthropic',
supportsPrefill: false,
supportsTemperature: false,
thinking: 'adaptive',
effort: 'low',
}
```
If you need to add any extra setup or configuration for your provider, you can add it to the `worker/do/AgentService.ts` file.
### Support custom shapes
If your app includes [custom shapes](/docs/shapes#Custom-shapes), the agent will be able to see, move, delete, resize, rotate and arrange them with no extra setup. However, you might want to also let the agent create and edit them, and read their custom properties.
To support custom shapes, you have two main options:
1. Add an action that lets the agent create your custom shape.
See the [Let the agent create custom shapes with an action](#let-the-agent-create-a-custom-shape-with-an-action) section below.
2. Add your custom shape to the schema so that the agent read, edit and create it like any other shape.
See the [Add your custom shape to the schema](#add-a-custom-shape-to-the-schema) section below.
#### Let the agent create a custom shape with an action
To add partial support for a custom shape, let the agent create it with an [agent action](#change-what-the-agent-can-do). For example, this action lets the agent create a custom "sticker" shape:
```tsx
// In shared/schema/AgentActionSchemas.ts
export const StickerAction = z
.object({
_type: z.literal('sticker'),
stickerType: z.enum(['❤️', '⭐']),
x: z.number(),
y: z.number(),
})
.meta({
title: 'Sticker',
description: 'Add a sticker to the canvas.',
})
export type StickerAction = z.infer<typeof StickerAction>
```
Define how the action gets applied to the canvas by creating an action util:
```tsx
// In client/actions/StickerActionUtil.ts
export const StickerActionUtil = registerActionUtil(
class StickerActionUtil extends AgentActionUtil<StickerAction> {
static override type = 'sticker' as const
// Tell the model how to display the action in chat history
override getInfo(action: Streaming<StickerAction>) {
return {
icon: 'pencil' as const,
description: 'Added a sticker',
}
}
// Execute the action
override applyAction(action: Streaming<StickerAction>, helpers: AgentHelpers) {
if (!action.complete) return
if (!this.editor) return
// Normalize the position
const position = helpers.removeOffsetFromVec({ x: action.x, y: action.y })
// Create the custom shape
this.editor.createShape({
type: 'sticker',
id: createShapeId(),
x: position.x,
y: position.y,
props: { stickerType: action.stickerType },
})
}
}
)
```
#### Add a custom shape to the schema
To let the agent see the custom properties of your custom shape, add it to the schema in `shared/format/FocusedShape.ts`.
For example, here's a schema for a custom sticker shape.
```tsx
const FocusedStickerShape = z
.object({
// Required properties
_type: z.literal('sticker'),
note: z.string(),
shapeId: z.string(),
// Custom properties
stickerType: z.enum(['❤️', '⭐']),
x: z.number(),
y: z.number(),
})
.meta({
// Information about the shape to give to the agent
title: 'Sticker Shape',
description:
'A sticker shape is a small symbol stamped onto the canvas. There are two types of stickers: heart and star.',
})
```
The `_type` and `shapeId` properties are required so that the app can identify your shape. The `note` property is also required. The agent uses it to leave notes for itself.
For optional properties, it's worth considering how the agent should see your custom shape. You might want to leave out some properties and focus on showing the most important ones. It's also best to keep them in alphabetical order for better performance with Gemini models.
Enable your custom shape schema by adding it to the list of `FOCUSED_SHAPES` in the same file.
```tsx
const FOCUSED_SHAPES = [
FocusedDrawShape,
FocusedGeoShape,
FocusedLineShape,
FocusedTextShape,
FocusedArrowShape,
FocusedNoteShape,
FocusedUnknownShape,
// Our custom shape
FocusedStickerShape,
] as const
```
Tell the app how to convert your custom shape into the `FocusedShape` format by adding a case in `shared/format/convertTldrawShapeToFocusedShape.ts`:
```tsx
export function convertTldrawShapeToFocusedShape(editor: Editor, shape: TLShape): FocusedShape {
switch (shape.type) {
// ...
case 'sticker':
const bounds = getShapeBounds(shape)
return {
_type: 'sticker',
note: (shape.meta.note as string) ?? '',
shapeId: convertTldrawIdToSimpleId(shape.id),
stickerType: shape.props.stickerType,
x: bounds.x,
y: bounds.y,
}
// ...
}
}
```
To allow the agent to edit your custom shape's properties, tell the app how to convert your shape from the `FocusedShape` format that the model outputs to the actual format of your shape:
```tsx
export function convertFocusedShapeToTldrawShape(
editor: Editor,
focusedShape: FocusedShape,
{ defaultShape }: { defaultShape: Partial<TLShape> }
) {
switch (focusedShape._type) {
// ...
case 'sticker':
const shapeId = convertSimpleIdToTldrawId(focusedShape.shapeId)
return {
shape: {
id: shapeId,
x: focusedShape.x,
y: focusedShape.y,
// ...
props: {
// ...
stickerType: focusedShape.stickerType,
},
meta: {
note: focusedShape.note ?? '',
},
},
}
// ...
}
}
```
---
## Further reading
- **[Multiplayer starter kit](/starter-kits/multiplayer)**: Use the tldraw multiplayer sync starter kit to build multi-user agent environments.
- **[Shape utilities](/docs/shapes#Custom-shapes)**: Learn how to create custom shapes and extend tldraw's shape system with advanced geometry, rendering, and interaction patterns.
- **[Editor state management](/docs/editor)**: Learn how to work with tldraw's reactive state system, editor lifecycle, and event handling for complex canvas applications.
---
## Building with this starter kit?
If you build something great, please share it with us in our [#show-and-tell](https://discord.tldraw.com/?utm_source=docs&utm_medium=organic&utm_campaign=sociallink) channel on Discord. We want to see what you've built!