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!

View File

@@ -0,0 +1,109 @@
---
title: Branching chat starter kit
sidebarTitle: Branching chat
status: published
author: tldraw
date: 9/16/2024
order: 6
description: The Branching Chat Starter Kit demonstrates how to build visual conversation interfaces where users create branching chat trees with AI integration. It features interactive dialogue nodes that connect to form conversation flows, with real-time AI responses and context-aware message handling. You can use this foundation to create interactive chatbots, conversation design tools, or storytelling applications.
embed: branching-chat
githubLink: https://github.com/tldraw/tldraw/tree/main/templates/branching-chat
keywords:
- branching
- chat
- conversation
- dialogue
- tree
readability: 8
voice: 6
completeness: 8
accuracy: 8
notes: "'Use Cases' heading should be sentence case. Uses bolded bullet format. Good architecture explanation and file references."
---
To build with a branching chat starter kit, run this command in your terminal:
```bash
npm create tldraw@latest -- --template branching-chat
```
Then create a `.env` file in the project root with an API key from [Google AI Studio](https://aistudio.google.com/apikey):
```
GOOGLE_GENERATIVE_AI_API_KEY=<your key>
```
---
## Use cases
Use the branching chat starter kit to build:
<StarterKitBento type="branching" />
---
## How it works
![Diagram showing how nodes, ports, and connections form a flow of operations. Two shapes, Node A and Node B illustrate that connections link specific ports, defined as start or end points.](/images/starter-kits/chart-branching-chat.png)
### 1. Visual node system: Interactive message containers
Each conversation message appears as a draggable node on the infinite canvas. The `NodeShapeUtil` class extends tldraw's shape system to create custom chat message containers. These containers dynamically resize based on content length and provide input fields for user messages, areas for AI responses, and connection ports for linking conversations. You can also create nodes directly from the toolbar.
### 2. Connection architecture: Conversation flow management
The `ConnectionBindingUtil` manages relationships between message nodes and creates visual lines that represent conversation flow. You create connections by dragging between node ports to branch conversations and link context. When users send a message, the system traces backwards through connected nodes to build complete conversation history. This gives AI responses the full dialogue context.
### 3. AI streaming integration: Real-time response handling
The backend uses Cloudflare Workers with the Vercel AI SDK to stream responses from Google's Gemini API. As AI text generates, the streaming fetch implementation decodes response chunks and updates the tldraw document state in real time. Users see responses appear progressively within the connected node.
### 4. Port-based connection system: Visual conversation linking
Nodes feature input and output ports that let users create branching dialogue structures by dragging connections between messages. You can build complex conversation trees where multiple messages feed into a single AI response or diverge into parallel branches. Connected nodes automatically establish context relationships across different paths in the dialogue graph.
---
## Customization
This starter kit is built on top of tldraw's extensible architecture. You can customize everything. The canvas renders using React DOM, so you can use familiar React patterns, components, and state management across your conversation interface. Let's have a look at some ways to change this starter kit.
### Adding custom node types
To create new types of conversation nodes beyond basic messages, you can extend the node system with custom node definitions. The system uses a pluggable architecture where each node type defines its own behavior, rendering, and port configuration.
See `client/nodes/types/MessageNode.tsx` as an example. This file shows how to define a complete node type with TypeScript validation, React component rendering, AI streaming integration, and dynamic sizing based on content length.
### Customizing AI integration
To integrate with different AI providers or modify response behavior, you can customize the streaming implementation and API endpoints. The system uses the Vercel AI SDK which supports multiple providers including OpenAI, Anthropic, and Google.
See `worker/worker.ts` as an example. This file demonstrates how to configure AI providers, handle streaming responses, and process conversation context from connected nodes.
### Customizing node appearance
To modify how conversation nodes look and behave, you can override the node rendering and styling system. Each node type has complete control over its visual presentation while maintaining integration with tldraw's interaction system.
See `client/nodes/NodeShapeUtil.tsx` as an example. This file shows how the shape utility defines node geometry, interaction behavior, and visual indicators including port positioning and selection bounds.
### Customizing connection behavior
To modify how conversation flows connect and interact, you can customize the connection and binding system. This controls how nodes link together, how context flows between connected messages, and how the visual connections appear.
See `client/connection/ConnectionBindingUtil.tsx` as an example. This file demonstrates how to define binding behavior between shapes, including automatic cleanup when nodes are deleted and visual feedback during connection creation.
---
## Further reading
- [Workflow starter kit](/starter-kits/workflow): Learn how to build visual programming interfaces with node-based workflows on infinite canvas.
- [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.
- [Binding system](/sdk-features/bindings): Learn more about tldraw's binding system for creating relationships between shapes, automatic updates, and connection management.
- [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!

View File

@@ -0,0 +1,90 @@
---
title: Chat starter kit
sidebarTitle: Chat
status: published
author: tldraw
date: 9/16/2024
order: 3
description: The Chat Starter Kit shows how to integrate tldraw's infinite canvas into an existing chat application. Built on [Vercel's useChat hook](https://ai-sdk.dev/docs/reference/ai-sdk-ui/use-chat), this template shows you how to add visual annotation capabilities to any app (in this case, a chat).
embed: chat
githubLink: https://github.com/tldraw/tldraw/tree/main/templates/chat
keywords:
- chat
- messaging
- collaboration
- communication
---
To build with a chat starter kit, run this command in your terminal:
```bash
npm create tldraw@latest -- --template chat
```
Once you've followed the instructions, you'll need to create a `.env.local` file with an API key from [Google AI Studio](https://aistudio.google.com/apikey):
```
GOOGLE_GENERATIVE_AI_API_KEY=<your key>
```
---
## Use cases
This starter kit shows how to add visual capabilities to:
<StarterKitBento type="chat" />
---
## How it works
This starter kit demonstrates how to add tldraw's visual capabilities to an existing chat application. The integration uses a modal to bring annotation tools directly into your chat UI.
![A screenshot of a chat interface with labels pointing to different features. The interface includes a text box with two previously added sketches/images, and icons to upload and annotate an image, create a whiteboard sketch, and send a message. Red arrows with labels point to features: “Start a new chat,” “Message history,” “Previously added images & sketches,” “Upload & annotate an image,” “Create a whiteboard sketch,” and “Send message.”](/images/starter-kits/chat-screenshot.png)
### 1. Whiteboard modal
The whiteboard opens a modal tldraw editor whenever users want to create or edit visual content. The modal supports uploaded images, freehand drawing, and editing of images from chat history.
### 2. Automatic image annotation
When an image is uploaded for the first time, the editor places it on the canvas, centers the camera on it, and switches into cropping mode using `editor.setCurrentTool('select.crop')`. From there, users can quickly crop their image to the desired section or add annotations on top.
### 3. Snapshot saving
Once the user has finished sketching or annotating, the editor exports the work as a PNG ready to send to the LLM. At the same time, it saves a tldraw snapshot of the session. This allows users to reopen the image as a whiteboard and continue working from where they left off, without flattening the individual shapes into pixels.
---
## Customization
This starter kit provides a minimal integration between tldraw and a chat app. You can tailor the modal's appearance, available tools, and overall behavior.
### Modifying the upload workflow
You can customize the automatic crop behavior, add image processing steps, or extend support for additional file types to match your app's needs.
See `src/components/WhiteboardModal.tsx:195` for the automatic crop activation and `src/components/Chat.tsx:148` for drag-and-drop handling.
### Adapting to your design system
Since tldraw is built with React, you can adapt both the modal behavior and its appearance to integrate with your chat interface. This includes controlling when the modal opens, how it looks, and which tools are available.
For example, see `src/components/WhiteboardModal.tsx:92` for UI component overrides, `src/components/Chat.tsx:113` for the history image click handler, and `src/app/styles.css` for styling.
---
## Further reading
- **[Custom shape utilities](/docs/shapes#Custom-shapes)**: Learn how to create custom shapes with their own interactions.
- **[Editor API](/docs/editor)**: Learn about the tldraw Editor.
- **[Customize the user interface](/docs/user-interface)**: Learn how to customize the user interface of your tldraw application.
---
## 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!

View File

@@ -0,0 +1,135 @@
---
title: Image pipeline starter kit
sidebarTitle: Image pipeline
status: published
author: tldraw
date: 2/8/2026
order: 5
description: Demonstrates how to build visual node-based image generation pipelines on infinite canvas. Features a type-safe port system with six data types, parallel DAG execution, AI image generation via Replicate, and a Cloudflare Worker backend. Includes 17 node types across input, processing, and output categories.
embed: image-pipeline
githubLink: https://github.com/tldraw/tldraw/tree/main/templates/image-pipeline
keywords:
- image
- pipeline
- generation
- nodes
- ai
- comfyui
---
To build with an image pipeline starter kit, run this command in your terminal:
```bash
npm create tldraw@latest -- --template image-pipeline
```
The template works out of the box with placeholder images. To connect real AI providers, create a `.dev.vars` file:
```
REPLICATE_API_TOKEN=<your key>
```
---
## Use cases
Use the image pipeline starter kit to build:
<StarterKitBento type="image-pipeline" />
---
## How it works
### 1. Shape system: Nodes and connections
Nodes are custom tldraw shapes that represent pipeline steps. Each node has typed input and output ports — there are six port data types (image, text, model, number, latent, and any) that enforce connection compatibility. Connections are also shapes, rendered as color-coded bezier curves that use tldraw's binding system to stay attached to specific ports.
### 2. Type-safe port system
Ports are color-coded by data type and enforce compatibility during connection. When you drag a connection, only compatible ports highlight as valid targets. The system also prevents cycles, replaces existing connections on single-input ports, and supports multi-input ports that accept multiple connections.
### 3. Pipeline execution
The execution engine builds a DAG (directed acyclic graph) from your node connections and resolves dependencies automatically. Independent branches execute in parallel. Each node receives its input values, runs its operation, and passes results downstream. Nodes show loading states during execution and cache their results.
### 4. AI image generation
The starter kit integrates with AI providers through a Cloudflare Worker backend. The Generate node sends prompts, model selection, and parameters (steps, CFG scale, seed) to the worker, which dispatches to providers like Replicate. When no backend is configured, the system returns placeholder SVGs so you can explore the full UI without API credentials.
### 5. Image processing pipeline
Beyond generation, the template includes processing nodes for upscaling, style transfer, ControlNet-guided generation, IP-Adapter reference images, blending, and image adjustments. These can be chained together to build complex multi-step image processing workflows.
---
## Customization
### Adding custom nodes
To add a custom node type, create a new file in `src/nodes/types/`. The easiest approach is to duplicate an existing node like `AdjustNode.tsx`.
Define your node's data shape:
```tsx
import { T } from 'tldraw'
export const CustomNode = T.object({
type: T.literal('custom'),
// your node's parameters...
})
export type CustomNode = T.TypeOf<typeof CustomNode>
```
Then create a node definition:
```tsx
export class CustomNodeDefinition extends NodeDefinition<CustomNode> {
static type = 'custom'
static validator = CustomNode
title = 'My custom node'
icon = <span>🔧</span>
category = 'process'
getDefault() { ... }
getBodyHeightPx(shape, node) { ... }
getPorts(shape, node) { ... }
async execute(shape, node, inputs) { ... }
getOutputInfo(shape, node, inputs) { ... }
Component = CustomNodeComponent
}
```
Register your node in `src/nodes/nodeTypes.tsx` by adding it to `NodeDefinitions`, and add it to the sidebar in `src/components/ImagePipelineSidebar.tsx`.
### Adding AI providers
The worker uses a provider abstraction. To add a new provider, create a class implementing the `ImageProvider` interface in `worker/providers/`. The provider receives generation parameters and returns image URLs. Register it in the provider resolution logic so it can be selected from the Model node.
### UI customization
The template overrides several tldraw UI components:
- A sidebar node palette replaces the default toolbar, organized by category (input, process, output, utility)
- Region overlays detect connected node groups and provide per-region play/stop controls
- An on-canvas node picker appears when dragging a connection to empty space, filtered to show only type-compatible nodes
- A template system lets users save and restore workflow configurations
To further customize the UI, read up on [customizing tldraw's UI as a whole](/docs/user-interface).
---
## Further reading
- **[Shape utilities](/docs/shapes)**: Learn how to create custom shapes and extend tldraw's shape system with advanced geometry, rendering, and interaction patterns.
- **[Binding system](/sdk-features/bindings)**: Learn more about tldraw's binding system for creating relationships between shapes, automatic updates, and connection management.
- **[Editor state management](/docs/editor)**: Learn how to work with tldraw's reactive state system, editor lifecycle, and event handling for complex canvas applications.
- **[Customize the user interface](/docs/user-interface)**: Learn how to customize the user interface of your tldraw application.
---
## 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!

View File

@@ -0,0 +1,254 @@
---
title: Multiplayer starter kit
sidebarTitle: Multiplayer
status: published
author: tldraw
date: 9/16/2024
order: 7
description: The Multiplayer Starter Kit demonstrates how to build self-hosted tldraw applications with real-time multiplayer collaboration using Cloudflare Durable Objects. It features production-ready backend infrastructure that handles WebSocket connections, automatic persistence, and asset management. You can use this foundation to create collaborative whiteboards, design tools, or any multi-user canvas application.
embed: multiplayer
githubLink: https://github.com/tldraw/tldraw/tree/main/templates/sync-cloudflare
keywords:
- multiplayer
- sync
- collaboration
- real-time
readability: 8
voice: 7
completeness: 8
accuracy: 9
notes: 'Revised to reflect current SQLite DO storage architecture. Code examples updated to show correct ShapeUtil patterns.'
---
The multiplayer starter kit provides a production-ready foundation for building collaborative tldraw applications with real-time synchronization. It uses Cloudflare Durable Objects for room management, WebSocket connections for instant updates, and SQLite storage for automatic persistence.
To build with this starter kit, run this command in your terminal:
```bash
npm create tldraw@latest -- --template multiplayer
```
---
## Use cases
This starter kit is perfect for building:
<StarterKitBento type="multiplayer" />
---
## How it works
### 1. Durable Objects: Room management
Each collaborative room runs in its own Cloudflare Durable Object instance. There's only ever one authoritative copy of each room's data, and all users connect to that same instance. This ensures strong consistency. Presence indicators (avatars and names) show who is currently in the room, and real-time cursors show where other users are pointing or selecting.
### 2. WebSocket synchronization: Real-time updates
Changes are synchronized instantly via WebSocket connections. When a user draws or modifies content, the change is sent to the Durable Object, applied to the in-memory document, and broadcast to all connected clients. The sync protocol keeps state consistent across all clients. If a connection drops, the client automatically reconnects and replays missed changes to restore consistency.
### 3. Persistent storage: SQLite
Room data is automatically persisted to Durable Object SQLite storage. Every change is saved immediately, so data survives restarts without manual save logic.
### 4. Asset management: Scalable file handling
Images, videos, and other files are uploaded directly to R2 storage and served through Cloudflare's global edge network. Shared assets are synchronized across all connected users. Pasted URLs automatically unfurl into link previews.
---
## Customization
This starter kit is built on top of tldraw's extensible architecture, which means that everything can be customized. The canvas renders using React DOM, so you can use familiar React patterns, components, and state management. Here are some ways to customize this starter kit.
### Custom shapes integration
To add custom shapes that work with multiplayer sync, you need to configure both the client-side shape utilities and the server-side schema. Both sides must know about your custom shapes so that validation, synchronization, and version compatibility work correctly.
See `worker/TldrawDurableObject.ts` and `client/pages/Room.tsx` as examples. The server file shows schema configuration while the client shows how to connect custom shapes to the sync client.
In the code example below, we add a custom sticky note shape on both client and server:
```tsx
// 1. Server-side: Add custom shape schema in worker/TldrawDurableObject.ts
import { createTLSchema, defaultShapeSchemas } from '@tldraw/tlschema'
const schema = createTLSchema({
shapes: {
...defaultShapeSchemas,
'sticky-note': {
props: {
text: { type: 'string', default: '' },
color: { type: 'string', default: 'yellow' },
},
migrations: {
currentVersion: 1,
migrators: {},
},
},
},
})
// 2. Client-side: Create ShapeUtil class in client/StickyNoteShapeUtil.tsx
import { ShapeUtil, Rectangle2d, HTMLContainer } from 'tldraw'
class StickyNoteShapeUtil extends ShapeUtil<StickyNoteShape> {
static override type = 'sticky-note' as const
getDefaultProps() {
return { text: '', color: 'yellow' }
}
getGeometry(shape: StickyNoteShape) {
return new Rectangle2d({ width: 200, height: 200, isFilled: true })
}
component(shape: StickyNoteShape) {
return <HTMLContainer>{shape.props.text}</HTMLContainer>
}
getIndicatorPath(shape: StickyNoteShape) {
const path = new Path2D()
path.rect(0, 0, 200, 200)
return path
}
}
// 3. Client-side: Pass ShapeUtil to Tldraw in client/pages/Room.tsx
;<Tldraw store={store} shapeUtils={[StickyNoteShapeUtil]} />
```
### Asset upload customization
To customize how assets are uploaded and served, modify the asset store configuration. You can add authentication, preprocessing, or serve different asset variants based on user permissions.
See `client/multiplayerAssetStore.tsx` as an example. This file shows how to upload assets to your Cloudflare Worker and retrieve them for display.
In the code example below, we add authentication to asset uploads:
```tsx
// Custom asset store with authentication
export const authenticatedAssetStore: TLAssetStore = {
async upload(asset, file) {
const id = uniqueId()
const objectName = `${id}-${file.name}`.replace(/[^a-zA-Z0-9.]/g, '-')
const response = await fetch(`/api/uploads/${objectName}`, {
method: 'POST',
body: file,
headers: {
Authorization: `Bearer ${getAuthToken()}`,
'X-User-ID': getCurrentUserId(),
},
})
if (!response.ok) {
throw new Error(`Upload failed: ${response.statusText}`)
}
return { src: `/api/uploads/${objectName}` }
},
resolve(asset) {
return `${asset.props.src}?token=${getAuthToken()}`
},
}
```
### Deployment configuration
Modify the Cloudflare Worker configuration to customize your deployment. You can adjust resource limits, add custom domains, and set environment-specific variables.
See `wrangler.toml` as an example. This configuration file controls how your worker is deployed, including bucket names, environment variables, and routing rules.
In the code example below, we configure production deployment settings:
```toml
# Production deployment configuration
name = "my-multiplayer-app"
main = "worker/worker.ts"
compatibility_date = "2024-08-01"
[env.production]
vars = { ENVIRONMENT = "production" }
[[env.production.r2_buckets]]
binding = "TLDRAW_BUCKET"
bucket_name = "my-app-production-bucket"
[[env.production.durable_objects.bindings]]
name = "TLDRAW_DURABLE_OBJECT"
class_name = "TldrawDurableObject"
[[env.production.routes]]
pattern = "myapp.com/api/*"
zone_name = "myapp.com"
```
### Room persistence
Room data is automatically persisted via SQLite storage in the Durable Object. The `SQLiteSyncStorage` class handles all persistence logic. You don't need to implement custom save logic.
See `worker/TldrawDurableObject.ts` as an example. The room is created lazily and supports WebSocket hibernation:
```tsx
import {
DurableObjectSqliteSyncWrapper,
type SessionStateSnapshot,
SQLiteSyncStorage,
TLSocketRoom,
} from '@tldraw/sync-core'
import { DurableObject } from 'cloudflare:workers'
export class TldrawDurableObject extends DurableObject {
// Room is created lazily on first connection
private room: TLSocketRoom<TLRecord, void> | null = null
private getOrCreateRoom(): TLSocketRoom<TLRecord, void> {
if (!this.room) {
const sql = new DurableObjectSqliteSyncWrapper(this.ctx.storage)
const storage = new SQLiteSyncStorage<TLRecord>({ sql })
this.room = new TLSocketRoom<TLRecord, void>({
schema,
storage,
// Disable idle timeout — Cloudflare handles keep-alive via auto-response
clientTimeout: Infinity,
// Persist session state to WebSocket attachments for hibernation recovery
onSessionSnapshot: (sessionId, snapshot) => {
const ws = this.sessionIdToWs.get(sessionId)
if (ws) ws.serializeAttachment({ sessionId, snapshot })
},
})
// Resume any sessions that survived hibernation
for (const ws of this.ctx.getWebSockets()) {
const attachment = ws.deserializeAttachment() as SocketAttachment | null
if (!attachment?.sessionId) continue
if (attachment.snapshot) {
this.room.handleSocketResume({
sessionId: attachment.sessionId,
socket: ws,
snapshot: attachment.snapshot,
})
}
}
}
return this.room
}
}
```
---
## Further reading
- **[Sync documentation](/docs/sync)**: Learn how to integrate tldraw sync into existing applications and customize the synchronization behavior.
- **[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!

View File

@@ -0,0 +1,136 @@
---
title: Starter kits
sidebarTitle: Overview
status: published
author: tldraw
date: 1/30/2026
order: 1
keywords:
- starter kit
- templates
- examples
---
Starter kits provide example implementations of common canvas-based use cases. Use them to prototype ideas or as the foundation for new projects. The source code is yours to hack on.
---
## Create with tldraw
You can access starter kits using the `npm create tldraw@latest` command:
```bash
npm create tldraw@latest
```
Or by using a template repository on [GitHub](https://github.com/orgs/tldraw/repositories).
---
## Available kits
### [Workflow](/starter-kits/workflow)
Build visual tools where users drag, connect, and execute nodes on the canvas. Includes node and connection shapes, automatic connection routing, and a replaceable execution engine.
<StarterKitBento
type="workflow"
href="/starter-kits/workflow"
img={{ src: '/images/starter-kits/workflow.png', alt: 'Workflow Starter Kit' }}
/>
### [Chat](/starter-kits/chat)
A canvas for sketching, annotating, and marking up images before sending them to an AI chat.
<StarterKitBento
type="chat"
href="/starter-kits/chat"
img={{ src: '/images/starter-kits/chat.png', alt: 'Chat Starter Kit' }}
/>
### [Agent](/starter-kits/agent)
An AI agent that interprets and manipulates drawings and other elements on the canvas.
<StarterKitBento
type="agent"
href="/starter-kits/agent"
img={{ src: '/images/starter-kits/agent.png', alt: 'Agent Starter Kit' }}
/>
### [Image pipeline](/starter-kits/image-pipeline)
Visual node-based builder for AI image generation pipelines. Chain prompts, models, and processing steps to create complex image workflows with real-time preview.
<StarterKitBento
type="image-pipeline"
href="/starter-kits/image-pipeline"
img={{ src: '/images/starter-kits/pipeline.png', alt: 'Image Pipeline Starter Kit' }}
/>
### [Branching chat](/starter-kits/branching-chat)
A visual branching conversation interface for creating interactive chat trees with AI integration.
<StarterKitBento
type="branching"
href="/starter-kits/branching-chat"
img={{ src: '/images/starter-kits/branching.png', alt: 'Branching Starter Kit' }}
/>
### [Multiplayer](/starter-kits/multiplayer)
Self-hosted tldraw with real-time multiplayer collaboration powered by tldraw sync and Cloudflare Durable Objects.
<StarterKitBento
type="multiplayer"
href="/starter-kits/multiplayer"
img={{ src: '/images/starter-kits/multi.png', alt: 'Multiplayer Starter Kit' }}
/>
### [Shader](/starter-kits/shader)
WebGL shader backgrounds that respond to shapes and interactions on the canvas.
<StarterKitBento
type="shader"
href="/starter-kits/shader"
img={{ src: '/images/starter-kits/shader.png', alt: 'Shader Starter Kit' }}
/>
---
## Why build with starter kits
<CheckItem>Complete starter code built around the tldraw SDK</CheckItem>
<CheckItem>Ready for real-time collaboration</CheckItem>
<CheckItem>Custom shape systems and tool implementations</CheckItem>
<br />
<div className="flex flex-col gap-4">
<Feature icon="bolt" title="Ship features in weeks, not months">
Skip the months of research and implementation. Each kit provides tested solutions for complex canvas interactions.
</Feature>
<Feature icon="cube" title="Production-ready from day one">
Start further, prototype faster. Get extra features, patterns, and components relevant to your use-case.
</Feature>
<Feature icon="wrench" title="Built for customization">
Use as a reference or the foundation for something new. The starter kit code is MIT licensed and free to hack with.
</Feature>
<Feature icon="shield" title="Validated by engineering teams">
Built around common use cases. Every component uses the SDK's extensible APIs. Add custom shapes, tools, and behaviors on top.
</Feature>
</div>
---
Starter kits use the same [license](/community/license) as the tldraw SDK.

View File

@@ -0,0 +1,110 @@
---
title: Shader starter kit
sidebarTitle: Shader
status: published
author: tldraw
date: 10/14/2024
order: 8
description: The Shader Starter Kit demonstrates how to integrate WebGL shaders with [tldraw](https://github.com/tldraw/tldraw). It creates dynamic backgrounds that respond to canvas interactions, and includes four complete examples and a reusable `WebGLManager` base class for building custom shader effects.
embed: shader
githubLink: https://github.com/tldraw/tldraw/tree/main/templates/shader
keywords:
- shader
- webgl
- graphics
- visual effects
- renderer
---
To build with a shader starter kit, run this command in your terminal:
```bash
npm create tldraw@latest -- --template shader
```
---
## Use cases
Use the shader starter kit to build:
<StarterKitBento type="shader" />
---
## How it works
### WebGLManager lifecycle
The reusable `WebGLManager` class ([`src/WebGLManager.ts`](https://github.com/tldraw/tldraw/tree/main/templates/shader/src/WebGLManager.ts)) creates and manages a WebGL2 context that is synchronized with the tldraw canvas. It owns the render loop and exposes lifecycle hooks—`onInitialize()`, `onUpdate()`, `onRender()`, and `onDispose()`. Each shader manager can focus on its effect-specific logic while sharing viewport coordination, resolution control, and animation timing. The Minimal, Rainbow, and Shadows examples extend this base class; the Fluid example uses a different architecture with its own simulation system.
### Config panel system
The config panel components ([`src/config-panel/`](https://github.com/tldraw/tldraw/tree/main/templates/shader/src/config-panel/)) provide ready-made UI controls for editing shader uniforms. Each panel stores settings in reactive atoms and persists them to `localStorage`, so your shader parameters survive reloads without extra wiring.
### Example gallery
The template ships with four complete demos that follow the same pattern of manager, renderer, config panel, and GLSL files:
- **Fluid simulation** ([`src/fluid/`](https://github.com/tldraw/tldraw/tree/main/templates/shader/src/fluid/)) — Navier-Stokes-based flow, built on Pavel Dobryakov's WebGL fluid implementation, that turns shape and pointer movement into velocity splats. Includes an in-depth [guide](https://github.com/tldraw/tldraw/tree/main/templates/shader/src/fluid/fluid.md).
- **Rainbow** ([`src/rainbow/`](https://github.com/tldraw/tldraw/tree/main/templates/shader/src/rainbow/)) — Gradient animation that demonstrates time-based uniforms and color cycling.
- **Shadows** ([`src/shadow/`](https://github.com/tldraw/tldraw/tree/main/templates/shader/src/shadow/)) — Raymarched shadow effect using signed distance fields derived from canvas geometry.
- **Minimal** ([`src/minimal/`](https://github.com/tldraw/tldraw/tree/main/templates/shader/src/minimal/)) — Dark-mode-aware solid color shader that makes it easy to start your own effect, with a [step-by-step walkthrough](https://github.com/tldraw/tldraw/tree/main/templates/shader/src/minimal/minimal-example.md).
Switch between demos from the example menu next to the style panel to see how different managers plug into the same infrastructure.
---
## Customization
### Start from the minimal template
Copy the minimal shader to create a new effect:
```bash
cp -r src/minimal src/my-shader
```
Then update:
- `config.ts` — Define uniforms and UI controls for your shader.
- `fragment.glsl` / `vertex.glsl` — Implement rendering logic.
- `MyShaderManager.ts` — Extend `WebGLManager` and coordinate buffers, uniforms, and lifecycle hooks.
- `MyRenderer.tsx` — Mount the manager from React and handle cleanup.
- `MyConfigPanel.tsx` — Customize the controls exposed to users.
Finally, register the new manager in [`src/App.tsx`](https://github.com/tldraw/tldraw/tree/main/templates/shader/src/App.tsx).
### Extend WebGLManager hooks
Each manager can override lifecycle hooks to add render targets, resize behavior, or post-processing passes. Use `onInitialize()` to set up buffers, textures, and framebuffers; `onUpdate()` for per-frame uniform updates (such as time, resolution, or editor-driven state); and `onRender()` to issue draw calls. The base class also exposes helpers for handling pixel density and canvas resizes.
### Integrate with tldraw data
Every manager receives the live tldraw editor instance, which lets you:
- Access shapes with `editor.getCurrentPageShapes()`.
- Subscribe to document changes through `editor.store.listen()`.
- Read the current camera via `editor.getCamera()`.
- Convert coordinates with `editor.pageToViewport()`.
- Track pointer data from `editor.inputs.getCurrentScreenPoint()`.
See [`src/fluid/FluidManager.ts`](https://github.com/tldraw/tldraw/tree/main/templates/shader/src/fluid/FluidManager.ts) for a full example of mixing editor state with GPU simulation.
## Resources
- [WebGL2 Fundamentals](https://webgl2fundamentals.org/) - WebGL tutorials
- [The Book of Shaders](https://thebookofshaders.com/) - GLSL shader programming
- [Shadertoy](https://www.shadertoy.com/) - Shader examples
---
## Further reading
For multi-user shader environments, see the [Multiplayer starter kit](/starter-kits/multiplayer). To create custom shapes with advanced geometry and rendering, see [Shape utilities](/docs/shapes#Custom-shapes). For working with tldraw's reactive state system and event handling, see [Editor state management](/docs/editor).
---
## 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!

View File

@@ -0,0 +1,162 @@
---
title: Workflow starter kit
sidebarTitle: Workflow
status: published
author: tldraw
date: 9/16/2024
order: 2
description: Demonstrates how to build visual programming interfaces with node-based workflows on infinite canvas. Features an interactive workflow system where users can create, connect, and execute nodes. Includes node and connection shapes, port-based bindings, a graph execution engine, and data flow between nodes.
embed: workflow
githubLink: https://github.com/tldraw/tldraw/tree/main/templates/workflow
keywords:
- workflow
- process
- diagram
- flowchart
---
To build with a workflow starter kit, run this command in your terminal:
```bash
npm create tldraw@latest -- --template workflow
```
---
## Use cases
Use the workflow starter kit to build:
<StarterKitBento type="workflow" />
---
## How it works
![Diagram showing how nodes, ports, and connections form a flow of operations. Nodes like Multiply, Divide, and Subtract take inputs and produce outputs through ports. Results are passed via connections (arrows) between ports: Multiply outputs 40, Divide outputs 2.5, and Subtract combines them to get 37.5. At the bottom, abstract Node A and Node B illustrate that connections link specific ports, defined as start or end points.](/images/starter-kits/chart-workflow.png)
### 1. Shape system: Nodes and connections
Nodes are custom tldraw shapes that represent workflow steps. Each node has input and output ports defined in its shape utility, and new node types can be created or customized. Nodes accept inputs and produce outputs, which can be joined together with connections. Connections are also shapes, but they use tldraw's binding system to stay attached to specific ports on nodes.
### 2. Binding system: Smart connections
When you create a connection between nodes, tldraw's binding system tracks the relationship. To create a connection, drag from a port. If you move a node, its connections update automatically. You can also drag an existing connection to reconnect it elsewhere or disconnect it entirely. The binding utilities handle connection lifecycle as nodes change.
### 3. Interaction layer: Port tools
The starter kit extends tldraw's select tool with custom port interactions. When you click or drag near a port, the `PointingPort` tool activates. You can create connections by dragging from any port, or click an output port to open a node picker and insert a new connected node. These behaviors are implemented using tldraw's state machine system, where tools are organized as states that can have child states.
### 4. Execution engine
The execution system is designed to be replaced by your own business logic. It reads your node graph, resolves dependencies (which nodes need to run before others), and executes them in the correct order. Nodes expose their computation through a simple interface, and the engine handles the orchestration. You can run a workflow graph (for example, using a "Play" action) to see how information flows between nodes.
### 5. Data flow and processing
You control the data that flows through your workflow and what happens to it. The starter kit provides the infrastructure: nodes update instantly to show their results as data flows through the connections. The framework moves data between nodes and triggers updates when values change. Your data can come from user inputs, external APIs, databases, or file uploads.
---
## Customization
### Adding custom nodes
To add custom node types, create a new file in `src/nodes/types`. The easiest way is to duplicate an existing node, like `MultiplyNode.tsx`.
Start by defining the type of your node:
```tsx
import { T } from 'tldraw'
// First, we create a validator for our node type
export const CustomNode = T.object({
type: T.literal('custom'), // each node needs a unique "type"
someData: T.number,
// ...
})
// Then, we can derive a typescript type from the validator
export type CustomNode = T.TypeOf<typeof CustomNode>
```
Once you have your node’s type definition, create a node definition for it:
```tsx
export class CustomNodeDefinition extends NodeDefinition<CustomNode> {
static type = 'custom' // This must match "type" from above
static validator = CustomNode
// How to label your node in the UI
title = 'My custom node'
heading = 'Custom' // Shown in the node header
icon = <span>🐝</span>
// Return a default version of your node
getDefault() { ... }
// Return the height of your node, in pixels
getBodyHeightPx(shape, node) { ... }
// Return all ports for your node. Each port has a terminal:
// 'end' = input port (receives data), 'start' = output port (sends data)
getPorts(shape, node) { ... }
// Run this node! Work through the input port values and
// produce values for the output ports.
async execute(shape, node, inputs) { ... }
// Get values to use as outputs when we're NOT running
// this node. Often, you might return a previously computed
// value from `execute`.
getOutputInfo(shape, node, inputs) { ... }
// A react component for rendering your node on the canvas.
Component = CustomNodeComponent
}
function CustomNodeComponent({ shape, node }) {
return <div>...</div>
}
```
Next, add your node definition to the system. In `src/nodes/nodeTypes.tsx`, include it in `NodeDefinitions`. Finally, add your node to the UI: open `src/components/WorkflowToolbar.tsx` and insert a `<ToolbarItem tool="node-custom" />` wherever you’d like your node to appear in the toolbar. If your node has any input ports (terminal = end), you can also add an `OnCanvasComponentPickerItem` for it in `src/components/OnCanvasComponentPicker`.
### Data fetching and integrations
Adding data fetching or integrations to a node is straightforward. You can make `fetch` requests from your custom node’s `execute` method. See `src/nodes/types/EarthquakeNode.tsx` for an example of a node that, when run, fetches recent earthquakes from the [USGS API](https://earthquake.usgs.gov/fdsnws/event/1/). Once fetched, it picks a random earthquake, displays some data, and outputs the magnitude to be used in downstream nodes.
You may also want to store data from the last execution to display in the UI. In the earthquake example, the data is stashed in an `earthquakeData` prop in the node definition. In `execute`, the shape is updated to store this data. Then, both the UI and `getOutputInfo` can reference it.
### Extending execution
Because it’s just a demo, this starter kit has a very minimal execution model: values are either numbers or a special `STOP_EXECUTION` flag used to implement conditionals.
If you want to change the type of data flowing through the system, edit `WorkflowValue` in `src/nodes/types/shared.tsx`. You’ll need to resolve type errors elsewhere, since several places assume the system only works with numbers.
The execution system is defined in `src/execution/ExecutionGraph.tsx`. It’s designed to be easily replaced by your own custom engine. For example, you might want executions to run entirely server-side, or you might want to “test” nodes individually from the canvas and then “deploy” a running workflow as an automation on a server.
### UI customization
Customizing tldraw’s UI mostly works by replacing specific components. Take a look at `src/App.tsx` to see how we diverge from tldraw’s default UI:
- We add the on-canvas component picker and workflow outline/play buttons in `InFrontOfTheCanvas`.
- We add a custom vertical toolbar to the left of the screen and some extra actions in the bottom of the screen in `Toolbar`.
- We remove the `MenuPanel` entirely.
- We selectively hide the `StylePanel` depending on what’s selected.
To further customize the UI of this starter kit, read up on [customizing tldraw’s UI as a whole](/docs/user-interface).
---
## Further reading
- **[Shape utilities](/docs/shapes)**: Learn how to create custom shapes and extend tldraw's shape system with advanced geometry, rendering, and interaction patterns.
- **[Binding system](/sdk-features/bindings)**: Learn more about tldraw's binding system for creating relationships between shapes, automatic updates, and connection management.
- **[Editor state management](/docs/editor)**: Learn how to work with tldraw's reactive state system, editor lifecycle, and event handling for complex canvas applications.
- **[Customize the user interface](/docs/user-interface)**: Learn how to customize the user interface of your tldraw application.
---
## 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!