---
name: templates
description: Use when the user wants to start from a template, asks whether a template exists for their case, wants to browse the catalog, or wants to publish one of their own canvases or workflows as a reusable template.
---

# Templates

A template is a **snapshot of a published canvas or workflow version**, plus a manifest of *slots* —
the parts a new user fills in. Instantiating one creates a real prompt in their project with those
slots interpolated.

Templates are how someone starts a case without writing a prompt from nothing. Check for one before
building from scratch — see `start-new-case.md` for choosing the path first.

## Finding one

```bash
bun --preload ~/.claude/skills/tela-studio/preload.ts -e "
for (const t of await tela.listTemplates())
  console.log(t.type.padEnd(8), '|', t.name, '—', (t.useCases ?? []).join('; '))
"
```

The catalog is the workspace's own templates plus every template published as global.

Each record carries the fields that make matching possible:

| Field | What it is |
|---|---|
| `type` | `canvas` or `workflow` — tells you what will be created |
| `useCases` | natural-language phrases: "Extração de dados de documentos" |
| `industries` | coarse tags: `legal`, `finance`, `operations`, `customer-support` |
| `utilities` | capability tags: `pdf-treatment` |
| `slots` | what the user will be asked to fill in |

## Recommending one

```bash
bun --preload ~/.claude/skills/tela-studio/preload.ts -e "
for (const r of await tela.recommendTemplates('ler notas fiscais em PDF e extrair fornecedor e total'))
  console.log(r.score.toFixed(2), '|', r.template.type, '|', r.template.name, '—', r.reason)
"
```

```
0.38 | canvas | Leitura de documento — Leitura de notas fiscais, contratos e formulários
0.13 | canvas | Checagem automática — matches: pdf
```

The scoring is lexical, weighted so that a hit inside a `useCase` phrase counts for far more than a
hit on a generic tag like `pdf`. It produces a **shortlist, not a decision**.

- Present the top match, what it does, and what it will ask for. Let the user choose.
- **Check for a near tie before picking.** Two templates can describe genuinely different jobs and
  still score alike — "extrair dados de contratos" and "checar contratos contra regras" share most of
  their vocabulary. Use `isAmbiguousRecommendation(results)` and, when it is true, show both and ask
  which one matches the intent.
- When nothing scores well, say nothing matched rather than dressing up a weak result. A template the
  user did not pick is a prompt they cannot explain later.
- Filter by kind with `{ kind: 'workflow' }` when the path is already decided.

```bash
bun --preload ~/.claude/skills/tela-studio/preload.ts -e "
const recs = await tela.recommendTemplates('ler contratos e extrair as partes e o valor')
if (tela.isAmbiguousRecommendation(recs))
  console.log('two close matches — ask the user:', recs.slice(0, 2).map(r => r.template.name))
"
```

## Showing what it will ask for

Do this **before** creating anything, so the user answers with real content instead of discovering
the fields afterwards.

```bash
bun --preload ~/.claude/skills/tela-studio/preload.ts -e "
const t = await tela.getTemplate('TEMPLATE_ID')
console.log(tela.describeTemplateSlots(t))
"
```

```
Leitura de documento asks for:
- instrucoes (required, multiline text): Instruções — O que extrair de cada documento
- campos (required, list of rows (nome, descricao)): Campos a extrair
```

## Slots

Two kinds.

**`text`** — a string. Written into the prompt wherever `{{slot:key}}` appears.

**`list`** — rows of columns. Also written into the prompt, and optionally into the **structured
output schema** when the slot declares a `schemaTarget`:

| `schemaTarget.mode` | Effect |
|---|---|
| `properties` | each row becomes an output field, named from `nameFrom` and described from `descriptionFrom` |
| `enum` | the rows become the allowed values of the property named in `property`, read from `valueFrom` |

This is what lets one template produce genuinely different extractors: the user lists the fields they
want, and the instantiated canvas comes out with those fields in its schema.

In a workflow template, a condition case can carry `repeatFor: <listSlotKey>`, which clones that
branch once per row — so "route by category" becomes N branches from N rows the user typed.

## Instantiating

```bash
bun --preload ~/.claude/skills/tela-studio/preload.ts -e "
const prompt = await tela.instantiateTemplate('TEMPLATE_ID', {
  projectId: 'PROJECT_ID',
  slotValues: {
    instrucoes: 'Extraia os dados fiscais de cada nota.',
    campos: [
      { nome: 'fornecedor', descricao: 'Razão social do emissor' },
      { nome: 'total', descricao: 'Valor total da nota' },
    ],
  },
})
console.log(prompt.id, tela.getCanvasUrl(prompt.id))
"
```

The server interpolates the slots, merges list slots into the schema, and creates the prompt and its
first version in one call.

**The new version is a draft and is not promoted.** Instantiating gives you a prompt with one
unpromoted version, so it will not serve a Workstation or a production run until you promote it —
see `update-canvas.md`.

Two more things to say out loud before you run it:

- **The new prompt takes the template's name and cannot be renamed.** The endpoint accepts only
  `projectId` and `slotValues`, and `updateCanvas({ title })` retitles the version, not the canvas.
- **A required slot left empty does not error.** It leaves the `{{slot:key}}` placeholder sitting in
  the prompt, which the model will read as literal text. Fill every required slot, and read the
  result back to confirm no placeholder survived.

After instantiating, treat it like any new prompt: add test cases with real inputs, review the
attributes, and iterate. See `start-new-case.md`.

## Publishing your own

Templates are captured from a **specific version** of a canvas or workflow. The snapshot is frozen —
later edits to the source do not propagate.

```bash
bun --preload ~/.claude/skills/tela-studio/preload.ts -e "
const { version } = await tela.getCanvas('CANVAS_ID')
const template = await tela.createTemplate({
  name: 'Conformidade documental',
  description: 'Avalia um documento contra regras que você define.',
  promptId: 'CANVAS_ID',
  promptVersionId: version.id,
  useCases: ['Checagem de conformidade', 'Validação de documento contra regras'],
  industries: ['legal'],
  utilities: ['pdf-treatment'],
  slots: [
    { key: 'regras', type: 'text', label: 'Regras', required: true, multiline: true,
      description: 'Uma regra por linha' },
  ],
})
console.log(template.id)
"
```

`description`, `useCases`, and `industries` are all **required**, and `useCases` and `industries`
must each have at least one entry — the API refuses to publish an uncategorized template.
`isGlobal` defaults to `false` (workspace-private). `workspaceId` is required by the API but then
overwritten server-side with the caller's workspace — the wrapper fills it from the source prompt so
you never have to pass it.

`type` is **not** something you set: it is derived from whether the source version is a workflow.
`variables` and `configuration` are copied from the version, not accepted from you.

To make a slot do anything, the source prompt must contain the matching `{{slot:key}}` placeholder.
A slot declared but never referenced is collected from the user and then silently discarded — write
the placeholder into the prompt first, then declare the slot.

Good `useCases` are what makes a template findable. Write them as the sentences a user would say
about their own problem, not as a description of the mechanism.

### Publishing a workflow flattens its canvas steps

This is the part that produces broken templates if you do not know it. When you publish a **workflow**
as a template, its steps are rewritten at create time:

| Step | What the template stores |
|---|---|
| a `canvas` step pointing at a plain canvas | **inlined** — the step becomes an `llm-completion` carrying that canvas's production-version markdown, model, temperature, and schema, with the step's static variables already substituted |
| a `canvas` step pointing at a **workflow** | left as a **live pointer** to the original id |
| a `template` node | left as a **live pointer** to the other template |

Consequences worth saying out loud before publishing:

- An inlined canvas is **frozen** at its production version as of publish time. Later edits to that
  canvas never reach the template. Re-publish to pick them up.
- A template containing a live pointer **cannot be global** — the API refuses `isGlobal: true` for a
  workflow-as-module or a `template` node, so it stays instantiable only inside its own workspace.
  Deleting the pointed-at canvas breaks every clone.
- Every referenced canvas must have a **production version**, at least one message with markdown
  content, and a configuration. If any is missing, publishing fails with a generic `400` rather than
  naming the offending step — check the referenced canvases first when you see one.
- **Nested canvas steps are not flattened.** Only top-level steps are walked, so a `canvas` step
  inside a `condition` case or a `map` body keeps its pointer regardless.
- `environmentName` on a canvas step is silently dropped; the flattener always reads Production.

### Creating is not idempotent

Calling `createTemplate` twice creates two template rows. There is no upsert. Check
`listTemplates()` for an existing one by name before publishing again.

### There is no update endpoint

Only create, delete, and preview-image exist. Changing a template's name, description, slots, or
`isGlobal` means **deleting it and creating a new one** pointing at the same
`promptId` / `promptVersionId`. Say that before editing a published template, because the delete is
not undoable and any link to the old id breaks.

```bash
bun --preload ~/.claude/skills/tela-studio/preload.ts -e "
await tela.updateTemplatePreviewImage('TEMPLATE_ID', 'data:image/png;base64,...')
await tela.deleteTemplate('TEMPLATE_ID')
"
```

The preview image is the one field that can be changed in place. It takes a data URI; keep the source
under ~700KB, since base64 inflates it by about a third against a 1MB body limit.

## Agent templates are a different thing

`listAgentTemplates()` returns the templates available to `createAgent({ templateId })`. They are a
**separate catalog with nothing in common** with canvas and workflow templates: not the same table,
not created through `createTemplate`, not returned by `listTemplates`, and not slot-based.

They are git repositories served by the agent service, shaped
`{ id, organizationName, repository, scope, name, description?, tags }`. `createAgent({ templateId })`
forwards the id straight through — it is never matched against the `templates` catalog, so passing a
canvas template id there fails in a confusing way.

Publishing one is not something a normal user does: an agent template is created by **promoting an
existing agent**, which is admin-only, and publishing globally is restricted further. If a user asks
to turn their agent into a template, say that it goes through an admin rather than attempting it.

So when someone asks "is there a template for this?", the answer depends on the path chosen: check
`listTemplates` for canvas and workflow, and `listAgentTemplates` for agents. Say which you searched.

## Deleting

```bash
bun --preload ~/.claude/skills/tela-studio/preload.ts -e "
await tela.deleteTemplate('TEMPLATE_ID')
"
```

Deleting a template that does not exist returns a **400**, not a 404 — so a failed delete does not
tell you whether the id was wrong or the call was. Confirm with `listTemplates()` first.


## Close with the link

After instantiating, the user needs to open the result and check the slots landed as they meant.

```bash
bun --preload ~/.claude/skills/tela-studio/preload.ts -e "
console.log('You can see and debug it here: ' + tela.getCanvasTabUrl('NEW_PROMPT_ID', 'craft'))
console.log('Browse the gallery: ' + tela.getTemplatesUrl())
"
```
