---
name: tela-studio
description: Skills for interacting with Tela - view, create, and update prompts/canvas, direct Tela workflows, test cases, tasks, and Vault files. Use when user shares a Tela URL (app.tela.com or app.telastaging.com) or asks for Tela API data. Do NOT use WebFetch for Tela URLs.
---

# Tela API Skills

Skills for interacting with the Tela API. Note: Prompts are called "Canvas" in Tela.

**IMPORTANT**: When the user shares a Tela URL like `https://app.tela.com/prompt/{id}/craft`, use these skills to fetch and modify the canvas - do NOT use WebFetch.

## Read next

This file is an index. The companion docs carry the parameter tables, required-field rules, and
failure modes that are not repeated here — **read the relevant one before writing calls**.

| Doing this | Read |
|---|---|
| **The user describes a problem and has not said what to build** | **`start-new-case.md`** |
| Finding, using, or publishing a template | `templates.md` |
| Creating a canvas | `create-canvas.md` |
| Editing a canvas, versions, promotion, deletion | `update-canvas.md` |
| Exporting canvases | `canvas.md` |
| Finding projects and prompts | `list-projects.md`, `list-prompts.md`, `create-project.md` |
| Authoring or running workflows | `workflow.md`, then `workflow-nodes.md` and `workflow-references.md` |
| Running things as tasks, approving results | `workstation.md` |
| Evaluating, commenting on, or judging test case results | `test-case-review.md` |
| Asking whether a change improved anything, comparing versions, cost | `measure.md` |
| Choosing or changing the model, comparing providers | `models.md` |
| Setting up authentication | `setup-session.md` |

## Always hand back a link

Everything this skill touches is a real thing a person can open, edit, and debug in the Tela app.
**End every answer about an entity with the link to it.** An id is not an answer — the user cannot
click it, and the app is where the actual work of fixing a prompt happens.

Do it whenever you create something, whenever you report on something, and whenever you hit
something you cannot fix from here.

```bash
bun --preload ~/.claude/skills/tela-studio/preload.ts -e "
console.log(tela.formatLinks(tela.getCanvasLinks('CANVAS_ID', { hasTestCases: true })))
"
```

```
You can see and debug it here:
- edit the prompt: https://app.tela.com/prompt/…/craft
- run and review test cases: https://app.tela.com/prompt/…/test
```

**Link to where the problem is, not to the front door.** A workflow link that opens the failing step
is worth far more than one that opens the workflow:

| You are talking about | Link |
|---|---|
| a canvas | `getCanvasTabUrl(id, 'craft' \| 'test' \| 'usage' \| 'workstation')` |
| a workflow | `getWorkflowTabUrl(id, { tab, promptVersionId, executionId, nodeId })` |
| a specific workflow step that misbehaved | the same, with `nodeId` — it opens with that node selected |
| a specific workflow run | the same, with `executionId` |
| an agent | `getAgentPageUrl(id)` |
| one agent run | `getAgentSessionUrl(sessionId)` |
| tasks | `getWorkstationUrl(promptId)` |
| a project | `getProjectUrl(projectId)` |
| the template gallery | `getTemplatesUrl()` |

`getCanvasLinks`, `getWorkflowLinks` and `getAgentLinks` assemble the right set for you; `formatLinks`
renders it.

Two limits to be honest about rather than fake:

- **Canvas and workflow tabs are routes, so they link. Agent tabs are not.** You cannot deep-link an
  agent's Test tab — link the agent and say which tab to open.
- **There is no per-test-case or per-task URL.** Link the canvas `test` tab or the Workstation and
  name the row; do not invent a query parameter.

**Workflows**:

- For direct Tela API workflow authoring, workflow URLs, graph validation, remote saves, and workflow runs, use `workflow.md`.
- For source-first Workflow Code proposals and Mermaid drafts, use the independent `workflow-code-builder-framework` skill.
- For source-first Workflow Code TypeScript implementation in a repo, use the independent `workflow-code-builder` skill.
- For Workflow Code project setup, configuration, compilation, sync, publishing, and troubleshooting through the `tela workflow` CLI, use the independent `workflow-code-operations` skill.

## Installation

```bash
bunx @meistrari/tela-skills
```

## Authentication

The API authenticates with a workspace-scoped token. Production and staging use the auth-api device flow: the installer shows a code, the user approves it in the browser and picks a workspace, and the resulting access + refresh tokens are stored as JSON at:

- `~/.tela/session.production` — Production environment
- `~/.tela/session.staging` — Staging environment
- `~/.tela/session.local` — Localhost environment (JWT, 30-day expiry, auto-regenerated when expired)

Device-flow access tokens last 15 minutes and are refreshed automatically (on preload and before each `apiRequest`) as long as the session is used at least once a week; after that, re-run the installer. They are sent as `Authorization: Bearer` and the API gateway exchanges them for a data token. Legacy data-token sessions (a bare JWT in the session file) still work and are sent as `x-data-token`.

The active environment is determined by the `TELA_API_URL` in `~/.tela/.env`. When the URL contains `localhost`, the localhost session is used automatically. All session files are created with `0o600` permissions (user read/write only).

Sessions created by the supported CLI flows contain workspace claims, so workspace-aware operations can infer `workspaceId`. For a legacy or manually managed credential without workspace claims, pass `workspaceId` explicitly.

Inside a hosted Tela agent, the Agent API runtime injects the triggering user's workspace-scoped token as `DATA_TOKEN`. The skill uses that token before looking for a local session file, so API calls retain the user's permissions without persisting credentials in the sandbox. `TELA_API_URL` and `TELA_APP_URL` environment variables take precedence over local `~/.tela/.env` configuration.

## Using These Skills

Use `bun` with `--preload` to make the `tela` object globally available:

Hosted installs must include this entire skill directory, including `package.json`,
`bun.lock`, and `startup.sh`. The Claude runtime runs `startup.sh` during agent
startup (during preparation for snapshot-based agents). It installs locked
dependencies inside this skill and checks the full preload without an API call.
It requires registry access on a cold install and does not modify shared sandbox
dependencies. If setup fails, stop and report the setup error; do not use a shared
older dependency as a fallback. Runtime startup errors may be reported without
preventing publication, so verify successful setup before considering rollout complete.

For a host that does not execute skill startup hooks, run
`bash ./.claude/skills/tela-studio/startup.sh` once during setup before invoking the skill.

Inside a hosted Tela agent, use the project-local skill path:

```bash
bun --preload ./.claude/skills/tela-studio/preload.ts -e "console.log(await tela.listProjects())"
```

For a local CLI installation, use the home-directory path shown in the remaining
examples:

```bash
bun --preload ~/.claude/skills/tela-studio/preload.ts -e "console.log(await tela.listProjects())"
```

## Available Functions

### Starting a new case

When someone describes a problem instead of naming a thing to build, read `start-new-case.md` before
writing any call. It carries the canvas / workflow / agent decision and the template search.

The short version: **can you write the steps down in advance?** No → agent. Yes and there is more
than one, or one is not a model call → workflow. Yes and there is one → canvas. Canvas is the
default and can be promoted into a workflow step later without a rewrite.

### Templates

See `templates.md`. A template is a frozen snapshot of a canvas or workflow version plus a manifest
of *slots* the user fills in.

- `tela.listTemplates(options?)` - The catalog: this workspace's templates plus every global one
- `tela.recommendTemplates(description, options?)` - Rank the catalog against a plain-language description. A **shortlist, not a decision** — show the top match and let the user choose. When nothing scores well, say nothing matched
- `tela.isAmbiguousRecommendation(results)` - True when the top two are a near tie. Show both and ask rather than guessing: templates for different jobs often share vocabulary
- `tela.getTemplate(templateId)` / `tela.describeTemplateSlots(template)` - Fetch one, and render what it will ask for **before** creating anything
- `tela.instantiateTemplate(templateId, { projectId, slotValues })` - Create the prompt. The new version is a **draft**, and it takes the template's name — which cannot be changed
- `tela.createTemplate(payload)` - Publish a version as a template. `description`, `useCases`, and `industries` are required, the last two with at least one entry
- `tela.deleteTemplate(id)` / `tela.updateTemplatePreviewImage(id, dataUri)` - There is **no update endpoint**: changing a published template means delete and recreate

A required slot left unfilled does not error — it leaves the `{{slot:key}}` placeholder in the
prompt for the model to read as literal text. Read the result back and confirm none survived.

`listAgentTemplates()` is a **different catalog** for `createAgent({ templateId })`, with no overlap.

### Projects & Prompts

- `tela.listProjects({ limit })` - List projects. **Defaults to 10 with no truncation marker** — always pass an explicit `limit` before concluding a project does not exist
- `tela.createProject({ title })` - Create a new project
- `tela.listPrompts(options?)` - List prompts/canvas with optional filtering
- `tela.getPrompt(promptId)` - Get a single prompt by ID
- `tela.getPromotedVersion(promptId)` - Get the promoted version of a prompt. **Throws 404** when nothing is promoted

### Canvas Operations

See `create-canvas.md` and `update-canvas.md` — the required fields and replacement semantics live there.

- `tela.createCanvas(payload)` - Create a new canvas with optional initial version. Pass `isWorkflow: true` for a workflow shell so it receives the workflow layout and icon. Omit `projectId` for a **personal draft**; omit `workspaceId` to take it from the session
- `tela.promoteCanvas(canvasId, projectId)` - Move a personal draft into a project. The only way — `updateCanvas({ projectId })` discards it and patching the canvas returns `400`. Required before a draft can have a workstation
- `tela.getAuthenticatedWorkspaceId()` - Workspace ID from the session token. Pass `workspaceId` explicitly for a credential with no workspace claim
- `tela.getCanvas(canvasId)` - Get canvas + latest version by canvas ID
- `tela.updateCanvas(canvasId, payload, { merge: true })` - Merges your payload into the latest version and saves it as a **new version — every call**. Writes only to the version: `title` sets the version title, `projectId` is silently discarded
- `tela.updateCanvasVersion(versionId, payload)` - Edit one named version in place. `configuration` **replaces** rather than merges, so send it whole or you will wipe `structuredOutput`
- `tela.createCanvasVersion(canvasId, payload)` - Create a new version. `title`, `configuration` (with `type`), and `variables` are all required — pass `variables: []` when empty

### Version Lifecycle

- `tela.listCanvasVersions(canvasId)` - Every version, oldest first
- `tela.getCanvasVersion(versionId)` - Get a canvas version by version ID
- `tela.promoteCanvasVersion(versionId)` - Publish a version to production. **Exclusive** (demotes the previous) and has no inverse. Setting `promoted: true` on a create/update payload returns 200 and does nothing — this is the only path
- `tela.updateCanvasVersionTitle(versionId, title)` - Rename a version without creating a new one
- `tela.deleteCanvasVersion(versionId)` - Delete a version. Not reversible

Promoted versions cannot be edited or deleted; both are refused with an opaque `500`.

### Workflow Operations

- `tela.createWorkflowVersion(payload)` - Create workflow prompt-version with graph validation
- `tela.updateWorkflowVersion(versionId, payload)` - Update workflow prompt-version with graph validation
- `tela.runWorkflow(payload)` - Run workflow via `/workflow/test`
- `tela.getWorkflowRun(runId)` - Get workflow run by ID
- `tela.waitForWorkflowRun(runId, options?)` - Poll until terminal status
- `tela.cancelWorkflowRun(runId)` - Cancel a workflow run
- `tela.listWorkflowRuns(options)` - List workflow runs by prompt
- `tela.getWorkflowVariables(promptId, payload?)` - Get available variables for graph/input context
- `tela.getWorkflowVariablesByStep(promptId, stepId, payload?)` - Get step-targeted compatibility variables
- `tela.validateWorkflowGraph(graph)` - Local pre-save graph shape checks (synchronous)
- `tela.assertWorkflowGraphValid(graph, { version })` - Throw on local validation errors
- `tela.validateWorkflowGraphServer(promptId, graph)` - Server-side validation with full inference (compatibility, condition branch visibility) — requires the prompt to exist
- `tela.getWorkflowActions()` - Catalog of all workflow actions with config/output schemas, reference field metadata, and public methods
- `tela.getWorkflowDependencies(promptId, versionId)` - All variables visible across a saved workflow version, with schemas
- `tela.getStepDependencies(promptId, versionId, stepId)` - Variables visible at a specific step, including per-reference-field format compatibility — use this instead of guessing what `?format=` values an action supports
- `tela.getStepTypes(promptId, versionId, stepId)` - TypeScript declarations (`.d.ts`) for the `TelaInput` shape inside a `codeExecutionNode` — use to author `input["..."]` accesses with confidence
- `tela.getWorkflowUrl(promptId)` - Get workflow UI URL (`/workflows/:id`)

### Export

- `tela.exportCanvas(payload)` - Export one or more canvases as a ZIP file (returns ArrayBuffer, save with `Bun.write()`)

### Test Cases

- `tela.listTestCases(promptId, options?)` - List test cases with pagination and filtering
- `tela.getTestCase(testCaseId)` - Get a single test case by ID
- `tela.createTestCase(payload)` - Create a test case with variable values
- `tela.createTestCases(payloads)` - Create multiple test cases
- `tela.updateTestCase(testCaseId, payload)` - Update a test case. To change variable values, pass a **full** `createTestCasePayload` — sending `{ variables }` alone updates the stored variables while the run keeps executing the old rich content
- `tela.deleteTestCase(testCaseId)` - Delete a test case
- `tela.runTestCase(testCaseId, versionId)` - Run a test case. Get `versionId` from `getCanvas(promptId).version.id`; `getPromotedVersion` throws 404 when nothing is promoted. Re-running rewrites the previous generation to `aborted`
- `tela.runTestCases(testCaseIds, versionId)` - Run multiple test cases
- `tela.waitForTestCase(testCaseId)` - Wait for test case to complete. Default timeout is 5 minutes; file-bearing runs can exceed it, and the timeout error masks the real failure. Raise it for file test cases
- `tela.abortTestCase(testCaseId)` - Abort a running test case

**Executed is not reviewed.** A finished test case reports `completed`. So does a fully reviewed one,
and so does one that had nothing to review — there is no separate `reviewed` status. Never report a
suite as passing on terminal status alone.

Use `tela.getTestCaseReview(testCase, promptVersion)` to tell them apart. It returns
`{ status, pending, reviewed, unreviewable }`, where `unreviewable: true` means the prompt version
declares no reviewable attributes — the state that makes a suite look green when nobody has checked
anything. See `test-case-review.md`.

Always pass `promptVersionIds` to `listTestCases` when reasoning about review state; without it every
row's status degrades to the raw generation status.

File values are passed as `vault://` references in the variables map. `createTestCase` resolves the
real file name and mime type from the Vault, so `fileNames` is only needed to override them.

### Models

See `models.md`. There are **two catalogs and they do not mix**: `llmModels` for canvas and workflow
steps (bare ids like `gpt-5`), `agentModels` for agents (namespaced ids like `openai/gpt-5.4`), with
different provider sets.

- `tela.listProviders({ surface? })` - Providers with their models. `surface: 'agent'` for agents
- `tela.listModels({ surface?, provider? })` - Flat list, optionally filtered
- `tela.resolveModel(id, { surface? })` - Check an id before writing it; tells you when the id exists but only on the other surface
- `tela.getModelCatalog()` / `tela.formatModelCatalog(providers)` - Raw catalog / render it

### Measuring a change

See `measure.md`. This is the half of the loop that says whether an edit helped.

**Before promoting a version or creating Workstation tasks in bulk, check readiness.** Running
production work on an unreviewed suite spends money on output nobody has checked, and "every run
returned valid JSON" says the shape parsed, not that the content is right.

- `tela.getVersionReadiness(versionId, { minScore? })` - `{ ready, score, reviewed, pending, failing, blockers }`. `minScore` defaults to 1
- `tela.formatVersionReadiness(readiness)` - Render the blockers for the user
### Cost

Cost comes from the **usage service**, which is the billing source of truth. See `measure.md`.

- `tela.getUsageCost({ canvasId, start, end, ... })` - Aggregated cost for a slice of usage
- `tela.getUsageCostByModel(filter)` / `tela.formatModelUsage(rows)` - Per-model breakdown, the shape a model comparison needs
- `tela.listUsageEvents(filter)` - Individual events, each with `cost` (provider) and `effectiveCost` (billed)
- `tela.getCurrentUsage(workspaceId)` - The workspace's position this month
- `tela.waitForUsageEvents(filter, { minEvents })` - Wait for replication before quoting cost. Returns `complete: false` when the events never arrived
- `tela.groupUsageByModel(events)` - Group a set of events you already have

**Usage replicates about a minute after a run finishes.** Querying cost straight after running
something returns an empty result that reads as "it was free". Wait, and when nothing arrives say
"not replicated yet" rather than reporting a zero.

**Never quote cost from a completion run.** `creditsUsed` and `usage.cost` on a run are
execution-time artifacts — not reconciled, not what the workspace is billed. Summing them yields a
number that looks authoritative and is not. Completion runs are for seeing *what was sent*, not what
it cost.

**Usage is attributed to the canvas, not the version** — there is no `promptVersionId` on a usage
event. Compare versions by time window, or by `model` when that is what changed. And always bound the
query with `start` and `end`.

**Never reimplement the credit formula.** To learn what another model costs, run it and read the
usage service.

- `tela.runSuiteAndWait(promptId, versionId, options?)` - Run **every** test case on the prompt against a version and wait. Comparing versions is only valid when both ran the same suite
- `tela.runAllTestCases(versionId, { testCaseIds?, filters? })` - Fire the suite without enumerating ids; narrow by tag or status with `filters`
- `tela.getVersionStats(versionId, filters?)` - The scoreboard: `{ stats: { total, good, bad, pending }, reviewedStats, testCases }`. Counts **attributes**, not test cases. `pending > 0` means the version is not yet measured
- `tela.compareVersions([baselineId, …, candidateId], options?)` - Fetches each version's stats and builds the comparison. First id is the baseline, last is the candidate. Returns `verdict` of `better` / `worse` / `same` / `inconclusive`
- `tela.formatVersionComparison(comparison)` - Render it as a table for the user
- `tela.getVersionCost(versionId)` - `{ runs, credits }` for Workstation and API executions
- `tela.listCompletionRuns(options?)` / `tela.getCompletionRun(id)` - What was actually sent to the model, the raw response, the model configuration, and the credits. Route is under `/v1/`
- `tela.getGenerationStats(versionId)` - Counts the **overall** thumb per generation, not the per-attribute verdicts. Usually disagrees with `getVersionStats`; prefer the latter

`verdict` is `inconclusive` unless **both** sides are fully reviewed — a score over a partially
reviewed version describes only what someone happened to look at. Report inconclusive as
inconclusive rather than picking the version with more `good`.

**Test case runs produce no completion run**, so the prompt a test case actually sent is not
retrievable. Workflow step outputs (`getWorkflowRun`) and agent session threads do cover their test
runs; canvas test cases are the blind spot.

### Test Case Review (canvas & workflow)

See `test-case-review.md`. These are canvas/workflow only — agent test cases use the `/agent` surface
with a different payload shape.

- `tela.getTestCaseReview(testCase, promptVersion)` - Derive `pending_review` / `partially_reviewed` / `completed` / `unreviewable` locally
- `tela.updateAttributeFeedback(generationId, attributeKey, feedback)` - Thumbs on one attribute (`1` up, `0` down, `null` clears). Records the verdict, logs the reviewer, and files the value into the test case's answer bank
- `tela.updateAttributeFeedbackBulk(generationId, feedbacks)` - `[{ attributeKey, feedback }]`, 1..1000
- `tela.addValidationRule(testCaseId, attributeKey, description)` - An acceptance criterion. **Judged by an LLM on every subsequent run**, and adding one suppresses the automatic exact-match verdict for that attribute
- `tela.deleteValidationRule(testCaseId, attributeKey, ruleId)`
- `tela.createGenerationComment({ content, generationId, promptVersionId })` - Free-text note on a result
- `tela.listGenerationComments(testCaseId)` / `tela.deleteGenerationComment(id)`
- `tela.listTestCaseTags(promptId)` / `createTestCaseTag({ name, color, promptId })` / `updateTestCaseTag` / `deleteTestCaseTag` - Tag CRUD. `color` is required
- `tela.getTestCaseTags(testCaseId)` / `addTestCaseTags` / `removeTestCaseTag` / `bulkAddTestCaseTags` / `bulkRemoveTestCaseTags` - Assignment. These return no useful body; re-read to confirm

`attributeKey` is a dot-path into the canvas's `structuredOutput` schema (`address.city`); an array
is a single key. It is never validated server-side, so a typo is stored and then invisible.
`updateAttributeFeedback` returns 500 on a generation whose content is empty or not JSON — only
review finished runs.

### Agents (FCC)

Create and iterate on Tela agents 100% via API — never clone the agent repository. An agent is a versioned git repo behind the API: every edit creates a commit (`commitHash` = agent version), `main` HEAD is the draft, and the published commit is what production runs.

**Golden rule — instructions**: agent instructions live in the *Purpose* block inside `CLAUDE.md`. The rest of `CLAUDE.md` belongs to the agent template: it is not shown in the Tela UI and gets overwritten on template syncs. Always use `updateAgentPurpose` (writes to `putAgentFile('CLAUDE.md')` are refused). For extra reusable behavior, use skills (`createAgentCustomSkill`/`addAgentSkill`) and subagents — both survive template syncs.

**Referencing things in the Purpose** (agents are NOT canvas — `{var}` syntax does not apply). Exact forms, written as plain text: text inputs as `<inputName>`, file inputs as `input/<inputName>/`, skills as `./.claude/skills/<name>/SKILL.md`, subagents as `./.claude/agents/<name>.md`, repo files by path. The Tela UI renders these as interactive chips — but only when they are plain text surrounded by spaces/punctuation and the target already exists (declare inputs / add skills first). **Never wrap references in backticks or code formatting** — the UI stops recognizing them. Never describe the output shape in the Purpose — that belongs in `output-format.json`.

- `tela.listAgents(options?)` / `tela.getAgent(agentId)` - List/get agents (inputs synced with the repo)
- `tela.createAgent({ title, projectId, description?, templateId? })` - Create an agent (provisions the repo)
- `tela.updateAgent(agentId, payload)` / `tela.deleteAgent(agentId)` - Update metadata / soft delete
- `tela.listAgentTemplates()` - Templates for `createAgent({ templateId })`
- `tela.getAgentPurpose(agentId, options?)` / `tela.updateAgentPurpose(agentId, content, options?)` - Read/write the agent's instructions (the ONLY way to edit them)
- `tela.getAgentFileTree(agentId, options?)` / `tela.getAgentFile(agentId, path, options?)` - Browse the repo (pass `branch`/`commitHash` for other versions)
- `tela.putAgentFile(agentId, path, content, options?)` - Create/update a file (commits; refuses managed files — CLAUDE.md/AGENTS.md, `agent.config.json`, `output-format.json`, `.claude/settings.json` — which go through their dedicated functions)
- `tela.deleteAgentFile` / `tela.renameAgentFile` - Remove/move files (protected runtime files are refused)
- `tela.uploadAgentFiles(agentId, files, options?)` - Multipart upload for binaries/batches. Layout conventions: static supporting material goes in `references/`; never write into `input/` (creating `input/<x>/` implicitly declares variable `x`); `output/` is runtime-only. Execution `attachments` mount at `input/references/` in the sandbox
- `tela.getAgentOutputSchema(agentId)` / `tela.updateAgentOutputSchema(agentId, schema)` - Read/write `output-format.json`. Flat map `attributeName -> { type, description, ... }` (no JSON Schema wrapper). Non-empty → structured JSON result; `{}` → markdown. These attribute paths are what test case attribute feedback and metrics reference
- `tela.getAgentConfig(agentId)` / `tela.updateAgentConfig(agentId, partial)` - Read/merge `agent.config.json` (model/harness rejected — use the dedicated functions)
- `tela.updateAgentModel(agentId, model)` / `tela.updateAgentHarness(agentId, harness)` - Change model/harness (server keeps template + layout in sync)
- `tela.listAgentInputs` / `createAgentInput` / `updateAgentInput` / `deleteAgentInput` - Declared input variables. Name: `^[a-zA-Z0-9_-]+$` (`references` is reserved). Declaring commits `input/<name>/.gitkeep` to the repo — the folder is the declaration. Executions are validated against declarations (unknown name, type mismatch, or missing required input → 400)
- `tela.discoverAgentSkill(ref)` / `tela.addAgentSkill(agentId, ref)` / `tela.listAgentSkillsStore()` - Skills from GitHub (`gh:owner/repo/path[@branch]`)
- `tela.createAgentCustomSkill(agentId, name, skillMd)` - Author a skill directly in the repo
- `tela.createAgentSubagent(agentId, name)` / `tela.updateAgentSubagent(agentId, name, content)` - Subagents (`.claude/agents/<name>.md`)
- `tela.addAgentCapability(agentId, { kind, sourceId })` - Attach a published canvas/workflow/agent as a tool
- `tela.restoreAgentVersion(agentId, commitHash)` - Move the draft to another commit. **Destructive**: it applies that commit's tree, so declared inputs and `output-format.json` added since are deleted from the draft. Works forward as well as back. Production keeps running the published commit until you publish again — confirm with the user first
- `tela.publishAgent(agentId, commitHash)` - Publish a specific commit
- `tela.testAgent(agentId, payload)` / `tela.runAgent(agentId, payload)` - Execute draft (`main`) / production (published). Async: 202 + `sessionId`. `runAgent` returns 400 `Agent has no published version` on an unpublished agent
- `tela.runAgentAndWait(agentId, payload, options?)` - Execute + wait. **Runs the draft by default** — pass `{ published: true }` to hit production. Returns `{ sessionId, status, output, thread }`
- `tela.waitForAgentSession(sessionId)` - Poll to completion. Returns `{ thread, status }` with **no `output`** — read the last assistant turn from `thread.turns`, or use `runAgentAndWait`
- A successful agent run settles at **`waiting_messages`**, not `completed` — the session is idle awaiting a follow-up, which is success. Treating only `completed` as success reports working agents as broken
- `tela.getAgentSessionThread` / `getAgentSessionTimeline` / `getAgentSessionFiles` / `getAgentSessionFile` - Inspect a session
- `tela.cancelAgentSession` / `tela.endAgentSession` - Stop sessions
- `tela.buildAgentExecutionInputs(variables, options?)` - Payload helper (vault:// values become file inputs)
- `tela.getAgentUrl(agentId)` - Link to the agent in the Tela app

### Agent Test Cases

Test cases for agents (FCC). Runs are keyed by `(testCaseId, commitHash)` — one run per test case per agent version. Run/continue/run-all return `202 { executionId }` immediately; poll with `waitForAgentTestCaseRun`. Derived statuses: `new`, `running`, `executed`, `success`, `failed`, `error` (terminal: `success`/`failed`/`error`).

- `tela.listAgentTestCases(agentId, options?)` - List test cases (pass `commitHash` to attach runs/stats)
- `tela.createAgentTestCase(agentId, payload)` - Create a test case
- `tela.updateAgentTestCase(agentId, testId, payload)` - Update a test case
- `tela.deleteAgentTestCase(agentId, testId)` - Delete a test case
- `tela.runAgentTestCase(agentId, testId, commitHash)` - Run against an agent version
- `tela.continueAgentTestCase(agentId, testId, commitHash, message)` - Continue session (multiturn)
- `tela.runAllAgentTestCases(agentId, { commitHash, testCaseIds? | filters? })` - Batch run (returns `skipped` reasons)
- `tela.getAgentTestCaseRun(agentId, testId, commitHash)` - Get the run for a commit
- `tela.listAgentTestCaseRuns(agentId, testId, options?)` - Run history across commits (cursor pagination)
- `tela.waitForAgentTestCaseRun(agentId, testId, commitHash, options?)` - Poll until terminal status
- `tela.updateAgentTestCaseAttributeFeedback(agentId, testId, commitHash, attributes)` - Thumbs per attribute (feeds the answer bank). `attributes` is an **array**: `[{ path: 'total', feedback: 1 | 0 | null }]`, where `path` is an output attribute path as it appears in `validationSummary` (nested and indexed forms like `parties.0` are valid)
- `tela.getAgentTestCaseStats(agentId, commitHash, options?)` - Aggregated stats for a commit
- `tela.listAgentTestMetrics(agentId)` / `createAgentTestMetric` / `updateAgentTestMetric` / `deleteAgentTestMetric` - Custom metrics over output attributes
- `tela.listAgentTestCaseTags(agentId)` / `createAgentTestCaseTag` / `updateAgentTestCaseTag` / `deleteAgentTestCaseTag` - Tag CRUD
- `tela.getAgentTestCaseTags(agentId, testId)` / `addAgentTestCaseTags` / `removeAgentTestCaseTag` - Tag assignment
- `tela.bulkAddAgentTestCaseTags(agentId, testCaseIds, tagIds)` / `bulkRemoveAgentTestCaseTags(...)` - Bulk tagging
- `tela.getAgentHistory(agentId, options?)` / `tela.getLatestAgentCommit(agentId)` - Resolve agent versions (commitHash)
- `tela.buildAgentTestInputs(variables, options?)` / `tela.createAgentTestCasePayload(variables, options?)` - Payload helpers (vault:// values become file inputs)
- `tela.getAgentTestCaseRunStatus(run)` - Derive run status locally

### Workstation (Applications)

See `workstation.md`. Workstation is a **prompt application** over a canvas or workflow — the same
entity and the same payloads for both. **Agents have no application**: an agent's tasks are keyed by
`agentId` alone.

- `tela.getWorkstation(promptId)` - The workstation application for a canvas/workflow, or null
- `tela.upsertWorkstation(promptId, { title, usedVersion?, approvalRequired?, isHidden? })` - Create it, or update the existing one
- `tela.listApplications({ promptId? })` / `tela.getApplication(id)` - List / fetch (there is no `GET /prompt-application/:id` route; `getApplication` filters the list)
- `tela.getApplicationVariables(applicationId)` - The input fields a task must supply. **Derived server-side** from the target version's variables — never set `config.variables` yourself
- `tela.getApplicationTargetVersion(applicationId)` - Which version it runs
- `tela.createApplication(payload)` / `updateApplication` / `deleteApplication` - Low level. One application per `(promptId, key)`, so a second create fails rather than being idempotent
- `tela.getWorkstationUrl(promptId)` - Link to the Workstation

`usedVersion` takes `'promoted'`, `'production'`, or a version id. Logical names are resolved at
write time and stored as the resolved id, so the application does not follow later promotions.

### Tasks

Canvas and workflow tasks are identical client-side; agent tasks are a different shape.

- `tela.createTask(applicationId, name, variables?, options?)` - Create one task from a plain variable map. `vault://` values become file references with their real name and mime type
- `tela.createTasks(payloads)` - Batch, max 100, all sharing one `promptApplicationId`
- `tela.buildTaskVariables(variables)` - The normalization `createTask` applies. **A bare `vault://` string is accepted by the API and then the run fails with nothing recorded on the task** — always normalize
- `tela.createAgentTask(agentId, name, variables?, options?)` / `tela.createAgentTasks(agentId, tasks)` - Agent tasks. File values are `{ vaultRef, filename }`, one file per variable
- `tela.buildAgentTaskVariables(variables)` - Agent-shaped normalization
- `tela.listAgentTasks(agentId, options?)` - An agent's tasks. `agentId` and `promptApplicationId` cannot be combined
- `tela.waitForTask(taskId, options?)` - Poll until the task leaves `running`
- `tela.rerunTask(taskId)` - Re-execute. **Only a `failed` task can be rerun**
- `tela.createTestCaseFromTask(taskId, { title? })` - Promote a real task into a regression test case, carrying its inputs
- `tela.getTaskExecutionHistory(taskId)` - Attempts

`name` is required on every task. `promptVersionId` is optional and defaults to the application's
target version. Always send variables: a task created without `rawInput` skips the required-variable
check and runs with nothing bound.

`validating` is the normal resting state for a finished task — it means "awaiting human review", not
a failure.

- `tela.listTasks(options?)` - List tasks with pagination and filtering
- `tela.getTask(taskId, options?)` - Get a single task by ID (optionally with analytics)
- `tela.updateTask(taskId, payload)` - Update task status, name, tags, or outputContent
- `tela.approveTask(taskId)` - Approve a task (shorthand for status='completed'). **Treat as one-way**: `undoApprovalTasks` currently fails with 422 in every payload shape, and `updateTask({ status: 'validating' })` is refused. Confirm with the user before approving anything you did not create
- `tela.deleteTask(taskId)` - Delete a single task (soft delete)
- `tela.deleteTasks(taskIds)` - Delete multiple tasks (soft delete)
- `tela.undoApprovalTasks(taskIds)` - Undo approval for tasks (reverts to 'validating')
- `tela.getTaskVersions(taskId)` - Get task version history
- `tela.getTaskInsightsByVersion(promptVersionId)` - Get task completion insights
- `tela.getTaskMetrics()` - Get overall task metrics for workspace

### Vault (File Management)

- `tela.uploadFile(filePath, opts?)` - Upload local file, returns `vault://` reference
- `tela.uploadContent(name, content, opts?)` - Upload content directly (string or Blob)
- `tela.downloadFile(vaultRef)` - Download file content as Blob
- `tela.downloadFileUrl(vaultRef)` - Get pre-signed download URL
- `tela.deleteFile(vaultRef)` - Delete a file
- `tela.getFileMetadata(vaultRef)` - Get file metadata
- `tela.createPermalink(vaultRef, opts?)` - Create shareable permalink
- `tela.listPermalinks(vaultRef)` - List permalinks for a file
- `tela.deletePermalink(vaultRef, permalinkId)` - Delete a permalink
- `tela.isVaultReference(url)` - Check if string is `vault://` reference
- `tela.bulkUploadFiles(files: Array<{ name: string; content: Blob | string; mimeType?: string }>)` - Upload multiple in-memory files, returns `vault://` references. For disk files, use `tela.uploadFile(filePath)` with `Promise.all()` instead
- `tela.uploadFileForVariable(filePath)` - Upload file, return reference for test case variables

### Helper Functions

- `tela.createStructuredOutput(properties, { title?, description? })` - Create structured output schema. **Marks every property required** and has no working `required` option — write the object by hand when some fields are optional, and read the canvas back to confirm the persisted `required` array
- `tela.createVariable(name, type, options?)` - Create a variable definition
- `tela.createMessage(role, markdown, index)` - Create a message from markdown; fills both `markdownContent` and the editor HTML `content`. Always build messages with it — never pass raw text or hand-written HTML as `content`
- `tela.createTestCasePayload(promptId, variables, options?)` - Create a test case payload
- `tela.markdownToHtml(markdown)` - Convert markdown (CommonMark + GFM: nested and ordered lists, all heading levels, italic, tables, code) to the editor HTML, with `{variable}` nodes. XML-style tags such as `<rules>` are kept as literal text
- `tela.getProjectUrl(projectId)` - Get URL to view project in Tela app
- `tela.getCanvasUrl(canvasId)` - Get URL to view canvas in Tela app

## Defaults

- **Model**: `gpt-5` for new work — a starting point, not a conclusion. When the user asks about cost, speed, or alternatives, **read the catalog** (`models.md`) instead of restating the default
- **Temperature**: Always use `0`
- **Prompts**: Use markdown with `## Headings` for readability
- **Output format**: Use `structuredOutput` config, NEVER put format templates in the prompt text

## Verify writes before reporting them

Several calls return `200` while doing nothing. Read the entity back and report what it actually says
rather than what the call was meant to do — particularly for promotion, renames, moves between
projects, structured-output `required` arrays, and workflow file inputs.

## Not supported by this skill

Say so plainly when asked for these, rather than doing something adjacent and calling it done:

- **Moving a canvas that already belongs to a project into a different project.** `promoteCanvas`
  moves a personal draft into its first project only; on a canvas that already has one it answers
  `400 Prompt already belongs to a project`.
- **Renaming a canvas** (as opposed to a version).
- **Unpromoting a version.** Promote a different version instead.
- **Parallel graph shapes.** See `workflow.md` — `settings.parallelExecution` and `mapNode` are the
  supported forms of concurrency.
- **Reading all comments on a result by every author.** The list endpoint is broken server-side;
  `listGenerationComments` reads what is attached to the test case instead.
- **Seeing the resolved prompt behind a canvas test case run.** Test cases create no completion run
  and the generation's `resolvedInput` is empty, so it can only be reconstructed, not read.
- **Aborting an agent test case run**, and **validation rules or comments on agent test cases** —
  those exist for canvas and workflow only.

## Quick Examples

### List Projects (returns max 10 by default)
```bash
bun --preload ~/.claude/skills/tela-studio/preload.ts -e "console.log(JSON.stringify(await tela.listProjects(), null, 2))"
```

### Create Personal Canvas Draft with Structured Output
```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',
      temperature: 0,
      structuredOutput: tela.createStructuredOutput({
        summary: { type: 'string', description: 'Brief summary' },
        keyPoints: { type: 'array', items: { type: 'string' } }
      })
    },
    message: tela.createMessage('system', '## Task\n\nAnalyze the document.\n\n## Input\n\n{document}', 0),
    variables: [tela.createVariable('document', 'file', { allowMultimodal: true })]
  }
})
console.log('Created:', canvas.id, tela.getCanvasUrl(canvas.id))
"
```

**IMPORTANT**: Define output format in `structuredOutput`, not in the prompt text!

### Update Canvas (merges config by default)
```bash
bun --preload ~/.claude/skills/tela-studio/preload.ts -e "
const updated = await tela.updateCanvas('CANVAS_ID', {
  configuration: { model: 'gpt-5' },
  variables: [tela.createVariable('input', 'file', { allowMultimodal: true })]
})
console.log('Updated:', updated.id)
"
```

### Get Canvas Info
```bash
bun --preload ~/.claude/skills/tela-studio/preload.ts -e "
const { canvas, version } = await tela.getCanvas('CANVAS_ID')
console.log('Title:', canvas.title)
console.log('Model:', version?.configuration?.model)
console.log('Variables:', version?.variables?.map(v => v.name))
"
```

### Create an Agent and Iterate via API (no repo cloning)
```bash
bun --preload ~/.claude/skills/tela-studio/preload.ts -e "
const agent = await tela.createAgent({ title: 'Contract Analyzer', projectId: 'PROJECT_ID' })

// Declare inputs BEFORE the purpose that references them — the UI only renders a
// reference as a chip once its target exists.
await tela.createAgentInput(agent.id, { name: 'contract', type: 'file', required: true })

await tela.updateAgentPurpose(agent.id, [
  'You analyze contracts and extract the parties and total amount.',
  'The contract PDF is at input/contract/.',
  'Always answer in Brazilian Portuguese.',
].join('\n'))

await tela.updateAgentOutputSchema(agent.id, {
  parties: { type: 'array', items: { type: 'string' }, description: 'All contract parties' },
  total: { type: 'number', description: 'Total amount' },
})

// Draft run. Add { published: true } only after publishAgent.
const { status, output } = await tela.runAgentAndWait(agent.id, {
  message: 'Analyze the attached contract',
  inputSchema: tela.buildAgentExecutionInputs({ contract: await tela.uploadFile('./contract.pdf') }),
})
console.log(status, output)   // status settles at 'waiting_messages' on success
console.log('Edit in UI:', tela.getAgentUrl(agent.id))
"
```

### Publish an Agent Version
```bash
bun --preload ~/.claude/skills/tela-studio/preload.ts -e "
const commitHash = await tela.getLatestAgentCommit('AGENT_ID')
await tela.publishAgent('AGENT_ID', commitHash)
console.log('Published', commitHash)
"
```

### Create and Run an Agent Test Case
```bash
bun --preload ~/.claude/skills/tela-studio/preload.ts -e "
const agentId = 'AGENT_ID'
const vaultRef = await tela.uploadFile('./contract.pdf')

// Variable names must match the agent's declared inputs, or the run 400s.
const testCase = await tela.createAgentTestCase(agentId, tela.createAgentTestCasePayload({
  contract: vaultRef,
  instructions: 'Extract the parties and total amount',
}, {
  title: 'Contract extraction',
  evaluationInstructions: 'The output must list both parties and the correct total.',
  fileNames: { contract: 'contract.pdf' },
}))

const commitHash = await tela.getLatestAgentCommit(agentId)
await tela.runAgentTestCase(agentId, testCase.id, commitHash)

const { run, status } = await tela.waitForAgentTestCaseRun(agentId, testCase.id, commitHash)
console.log('Status:', status)
console.log('Output:', run.output)
console.log('Validation:', run.validationSummary)

// Record the review, per attribute. Paths come from validationSummary.
await tela.updateAgentTestCaseAttributeFeedback(agentId, testCase.id, commitHash, [
  { path: 'parties', feedback: 1 },
  { path: 'total', feedback: 0 },
])
"
```

`continueAgentTestCase` re-evaluates the run against the follow-up message, which can invent attribute
paths and drop a passing score to zero. `waitForAgentTestCaseRun` can also return the *previous*
turn's result before the backend marks the continuation pending, so a quick success after continuing
is not trustworthy. Re-read the run before reporting.

### List Tasks
```bash
bun --preload ~/.claude/skills/tela-studio/preload.ts -e "
const result = await tela.listTasks({
  promptApplicationId: 'APP_ID',
  limit: 10
})
console.log('Tasks:', result.data.map(t => ({ id: t.id, name: t.name, status: t.status })))
console.log('Total:', result.meta.totalCount)
"
```

Passing `status` currently returns a 500 for every value — filter the returned array instead.
`promptApplicationId` is the only filter that actually scopes the query; `projectId`, `promptId`, and
`search` are accepted and ignored, so verify the results match what you asked for.

### Get Task Details
```bash
bun --preload ~/.claude/skills/tela-studio/preload.ts -e "
const task = await tela.getTask('TASK_ID', { includeAnalytics: true })
console.log('Name:', task.name)
console.log('Status:', task.status)
console.log('Output:', task.outputContent)
"
```

### Approve a Task
```bash
bun --preload ~/.claude/skills/tela-studio/preload.ts -e "
const task = await tela.approveTask('TASK_ID')
console.log('Approved:', task.id, task.status)
"
```

### Get Task Metrics
```bash
bun --preload ~/.claude/skills/tela-studio/preload.ts -e "
const metrics = await tela.getTaskMetrics()
console.log('Total tasks:', metrics.total)
console.log('Validating:', metrics.validating)
console.log('Reviewed this week:', metrics.reviewedThisWeek)
"
```

### Export Canvas (returns ZIP file)
```bash
bun --preload ~/.claude/skills/tela-studio/preload.ts -e "
const buf = await tela.exportCanvas({
  promptIds: ['CANVAS_ID_1', 'CANVAS_ID_2'],
  includeTestCases: true,
  includeApplications: true,
  includeFiles: true
})
await Bun.write('export.zip', buf)
console.log('Saved export.zip (' + buf.byteLength + ' bytes)')
"
```

### Upload File and Get Metadata
```bash
bun --preload ~/.claude/skills/tela-studio/preload.ts -e "
const ref = await tela.uploadFile('./document.pdf')
console.log('Vault ref:', ref)
const meta = await tela.getFileMetadata(ref)
console.log('Name:', meta.originalFileName, 'Size:', meta.size)
"
```

### Create Permalink
```bash
bun --preload ~/.claude/skills/tela-studio/preload.ts -e "
const permalink = await tela.createPermalink('vault://file-id')
console.log('URL:', permalink.url.toString())
"
```

## Data Structures

### Variables
```typescript
{ name: 'input', type: 'text' | 'file', required: true, description?: 'Description' }
```
Referenced in content as `{variableName}` (single braces)

### Structured Output
```typescript
{
  enabled: true,
  schema: {
    properties: {
      fieldName: { type: 'string', description: 'Field description' }
    },
    title: 'defaultOutput',
    description: 'Schema description',
    type: 'object',
    required: ['fieldName']
  }
}
```

### Message
```typescript
{ role: 'system' | 'user' | 'assistant', content: 'Message content', index: 0 }
```

## Environment Variables

Hosted agents receive `DATA_TOKEN` from the runtime. Never print, persist, or include it in model output.

The API and application URLs can be supplied by the host environment or configured automatically by the CLI installer in `~/.tela/.env`:

- `DATA_TOKEN` - Workspace-scoped runtime credential; takes precedence over local session files
- `TELA_API_URL` - API base URL (production: `https://api.tela.com`, staging: `https://api.telastaging.com`, localhost: `http://localhost:<port>`)
- `TELA_APP_URL` - App base URL (production: `https://app.tela.com`, staging: `https://app.telastaging.com`, localhost: `http://localhost:<port>`)

For localhost, ports are auto-discovered from Docker containers (`auth-api`, `tela-api`, `tela-app`). If `tela-app` is not running, `TELA_APP_URL` defaults to `http://localhost:3000`.

## Localhost Environment

For developing against locally running Tela services, use the localhost environment:

```bash
bunx @meistrari/tela-skills    # Select "Localhost" during setup
```

Prerequisites: Docker running with tela services (`auth-api`, `auth-postgres`, `tela-api`), with auth settings available in `.repositories/auth-api/.env` and `AUTH_API_SECRET` in `packages/api/.env`. The CLI discovers ports, reads the local auth configuration, and generates a JWT token through the auth-api JWKS signing flow.

For workspace management after setup, see the `tela-localhost` skill (switch workspaces, check status, regenerate tokens).

Run commands against localhost with the helper script:
```bash
bun localhost.ts "console.log(await tela.listProjects())"
```

## File Structure

```
~/.claude/skills/tela-studio/
├── SKILL.md              # This file - index and function reference
├── create-canvas.md      # Canvas creation: required fields, variables, structured output
├── update-canvas.md      # Canvas edits and the version lifecycle (promote, rename, delete)
├── canvas.md             # Canvas export
├── list-projects.md      # Project lookup (mind the default limit)
├── list-prompts.md       # Prompt/canvas listing
├── create-project.md     # Project creation
├── setup-session.md      # Authentication
├── workflow.md           # Workflow authoring, graph rules, outputs, runs
├── workflow-nodes.md     # Node builders and action configs
├── workflow-references.md # Reference syntax and formats
├── start-new-case.md     # Canvas vs workflow vs agent, and where to start
├── links.ts              # App URLs and the "see and debug it here" helpers
├── templates.md          # Finding, instantiating, and publishing templates
├── workstation.md        # Applications and tasks (canvas, workflow, agent)
├── test-case-review.md   # Evaluating, commenting on, and judging results
├── measure.md            # Version scoreboards, comparison, cost, diagnosis
├── preload.ts            # Preload script - makes `tela` globally available
├── index.ts              # Barrel export
├── common.ts             # Shared utilities (loadApiKey, apiRequest, env detection)
├── projects.ts           # Project functions
├── prompts.ts            # Prompt/Canvas list functions
├── canvas.ts             # Canvas create/update functions
├── workflows.ts          # Workflow create/update/run/validation functions
├── test-cases.ts         # Test case functions (prompt/canvas)
├── agents.ts             # Agent editing/execution functions (FCC)
├── agent-test-cases.ts   # Agent test case functions (FCC)
├── applications.ts       # Workstation / prompt application functions
├── tasks.ts              # Task management and creation functions
├── test-case-review.ts   # Attribute feedback, comments, validation rules, tags
├── measure.ts            # Version stats, comparison, completion runs
├── templates.ts          # Template catalog, recommendation, instantiation
├── vault.ts              # Vault file management functions
└── localhost-utils.ts    # Docker discovery, JWT signing, workspace fetching (internal, not on tela global)
```
