---
name: create-canvas
description: Create a new canvas (prompt) with optional initial version and message. Use when user wants to create a new prompt/canvas in Tela.
---

# Create Canvas

Create a new canvas (prompt) in Tela with an optional initial version including a message, variables, and structured output. Omit `projectId` to create a personal draft owned by the authenticated user. Include it only when the canvas should belong to a project. When omitted, `workspaceId` is inferred from the authenticated session.

## Usage

```bash
bun --preload ~/.claude/skills/tela-studio/preload.ts -e "
const canvas = await tela.createCanvas({
  title: 'My Canvas',
  version: {
    title: 'v1',
    configuration: { model: 'gpt-5', type: 'chat' },
    message: tela.createMessage('system', 'You are a helpful assistant.', 0)
  }
})
console.log('Created:', canvas.id, tela.getCanvasUrl(canvas.id))
"
```

## Parameters

### Required

| Parameter | Type | Description |
|-----------|------|-------------|
| `title` | string | Canvas name (max 256 chars) |

### Optional

| Parameter | Type | Description |
|-----------|------|-------------|
| `projectId` | string (UUID) | Project to create the canvas in. Omit for a personal draft — see below |
| `workspaceId` | string (UUID) | Defaults to the workspace on your session token — the token is workspace-scoped, so this is the right one. Pass it only when the credential carries no workspace claim |
| `customTags` | string[] | Custom tags for the canvas |
| `isWorkflow` | boolean | Defaults to `false`. Set to `true` for a workflow shell; selects the workflow layout and marks any initial version as a workflow. See `workflow.md` to build and save its graph |
| `version` | object | Initial version with a message |

### Personal drafts

Omitting `projectId` creates a **personal draft**: a real canvas owned by the session user, in their
workspace, with no project. It is not orphaned — it is listable, editable, and can be moved into a
project later.

- List them with `tela.listPrompts({ personal: true })`. Plain `listPrompts()` returns project
  canvases *and* your own drafts; `listPrompts({ projectId })` never returns drafts.
- Move one into a project with `tela.promoteCanvas(canvasId, projectId)`. This is the only way.
  `updateCanvas({ projectId })` writes to the version and silently discards it, and patching the
  canvas itself fails with `400 Use the promote endpoint to move a personal draft into a project`.
  A canvas can be promoted once — afterwards it is stuck in that project
  (`400 Prompt already belongs to a project`), so confirm the destination before calling.
- Drafts are visible only to the user who created them. A session with no user claim — a raw API key
  rather than a data token — cannot create one: the API answers `Personal drafts require a user session`.

A draft supports the whole build-and-measure loop: versions, test cases, runs, per-attribute review,
and `getVersionStats`. The one thing it cannot have is a **workstation** — `upsertWorkstation` on a
draft answers `400 Personal drafts cannot have apps. Promote the canvas to a project first.`

Create a draft when the user is exploring and has not named a project. Resolve or ask for a project
when they are building something other people need to see, or when they want a workstation.

Personal drafts and version drafts are different concepts: omitting `projectId` makes the canvas a
personal draft, while `version.draft` controls the prompt-version draft flag.

If the session token carries no workspace claim, `createCanvas()` fails with an instruction to pass
`workspaceId` explicitly rather than guessing one. A missing session fails with the installer
instructions instead — passing `workspaceId` would not fix that.

### Version Object

| Parameter | Type | Description |
|-----------|------|-------------|
| `title` | string | Version title (required) |
| `configuration` | object | Model configuration (required, must include `type`) |
| `variables` | Variable[] | Variable definitions. Pass `[]` when there are none |
| `message` | Message | Chat message — the prompt body |
| `content` | string | HTML content — a legacy path, separate from `message` |
| `markdownContent` | string | Markdown content — same legacy path |
| `draft` | boolean | Mark as draft |

Supply the prompt through `message`. `content` and `markdownContent` are an independent store and stay
`null` when you use `message`; setting both lets them diverge.

`promoted` is accepted by the payload type but **does not work here**: the call returns `200` and the
version stays unpromoted. If the canvas will be consumed by a workflow `canvasNode`, promote it
afterwards with `tela.promoteCanvasVersion(versionId)` and read it back to confirm. See
`update-canvas.md` for the version lifecycle.

## Examples

### Personal Canvas Draft

```bash
bun --preload ~/.claude/skills/tela-studio/preload.ts -e "
const canvas = await tela.createCanvas({
  title: 'Simple Canvas',
})
console.log('Created:', canvas.id, tela.getCanvasUrl(canvas.id))
"
```

### Canvas in a Project

```bash
bun --preload ~/.claude/skills/tela-studio/preload.ts -e "
const canvas = await tela.createCanvas({
  title: 'Project Canvas',
  projectId: 'PROJECT_ID'
})
console.log('Created:', canvas.id, tela.getCanvasUrl(canvas.id))
"
```

### Canvas with Message and Variables

```bash
bun --preload ~/.claude/skills/tela-studio/preload.ts -e "
const canvas = await tela.createCanvas({
  title: 'Document Analyzer',
  version: {
    title: 'v1',
    configuration: {
      model: 'gpt-5',
      type: 'chat',
      temperature: 0
    },
    variables: [
      tela.createVariable('document', 'file', { description: 'Document to analyze' }),
      tela.createVariable('question', 'text', { description: 'Question about the document' })
    ],
    message: tela.createMessage('system', 'Analyze {document} and answer: {question}', 0)
  }
})
console.log('Created:', canvas.id, tela.getCanvasUrl(canvas.id))
"
```

### Canvas with Structured Output

```bash
bun --preload ~/.claude/skills/tela-studio/preload.ts -e "
const canvas = await tela.createCanvas({
  title: 'Sentiment Analyzer',
  version: {
    title: 'v1',
    configuration: {
      model: 'gpt-5',
      type: 'chat',
      temperature: 0,
      structuredOutput: tela.createStructuredOutput({
        sentiment: { type: 'string', description: 'positive, negative, or neutral' },
        confidence: { type: 'number', description: 'Confidence score 0-1' },
        keywords: { type: 'array', description: 'Key phrases', items: { type: 'string' } }
      })
    },
    message: tela.createMessage('system', 'Analyze the sentiment of the given text: {text}', 0),
    variables: [
      tela.createVariable('text', 'text')
    ]
  }
})
console.log('Created:', canvas.id, tela.getCanvasUrl(canvas.id))
"
```

## Structured output: only some fields required

`createStructuredOutput` marks **every** property as required and silently ignores a `required` option.
When some fields are optional, write the object by hand:

```bash
bun --preload ~/.claude/skills/tela-studio/preload.ts -e "
const structuredOutput = {
  enabled: true,
  schema: {
    type: 'object',
    title: 'defaultOutput',
    description: 'Always use this output schema.',
    properties: {
      vendor: { type: 'string', description: 'Vendor legal name' },
      total: { type: 'number', description: 'Invoice total' },
      lineItems: { type: 'array', description: 'Line items', items: { type: 'string' } }
    },
    required: ['vendor']
  }
}
console.log(JSON.stringify(structuredOutput.schema.required))
"
```

Read the canvas back after creating and report the persisted `required` array rather than the one you
intended.

Define the output shape here, never as a format template inside the prompt text.

## Helper Functions

`createVariable(name, type, options?)` defaults `required` to **true** — pass `{ required: false }`
explicitly for optional inputs. File options go under `processingOptions`:
`createVariable('docs', 'file', { allowMultimodal: true })` produces
`{ name: 'docs', type: 'file', required: true, processingOptions: { allowMultimodal: true } }`.

For any `file` variable, decide between **multimodal** (the model sees the file directly — images,
scans, layout-heavy documents) and **parsed** (Tela extracts the text — text-native PDFs, CSVs).
State which you chose and why, or ask the user.

Use helper functions to simplify creation:

```bash
bun --preload ~/.claude/skills/tela-studio/preload.ts -e "
const canvas = await tela.createCanvas({
  title: 'My Canvas',
  projectId: 'PROJECT_ID',
  version: {
    title: 'v1',
    configuration: {
      model: 'gpt-5',
      type: 'chat',
      structuredOutput: tela.createStructuredOutput({
        answer: { type: 'string', description: 'The answer' },
        sources: { type: 'array', items: { type: 'string' } }
      })
    },
    variables: [
      tela.createVariable('query', 'text', { description: 'User query' }),
      tela.createVariable('docs', 'file', { allowMultimodal: true })
    ],
    message: tela.createMessage('system', 'Based on {docs}, answer: {query}', 0)
  }
})
console.log('Created:', canvas.id, tela.getCanvasUrl(canvas.id))
"
```


## Close with the link

A canvas id is not an answer. End with where the user can open it:

```bash
bun --preload ~/.claude/skills/tela-studio/preload.ts -e "
console.log(tela.formatLinks(tela.getCanvasLinks('CANVAS_ID')))
"
```
