---
name: workstation
description: Use when the user wants to run a canvas, workflow, or agent as Workstation tasks — creating the application, creating tasks with variables and files, reviewing and approving results, or turning a task into a test case.
---

# Workstation

Workstation is where an operator runs a canvas, workflow, or agent over real inputs and reviews the
results. Each run is a **task**.

## The model, and how it differs per kind

| Kind | Workstation is | Identified by | Create tasks with |
|---|---|---|---|
| **Canvas** | a *prompt application* over the canvas | `promptApplicationId` | `createTask` |
| **Workflow** | a *prompt application* over the workflow — **the same entity, same payloads** | `promptApplicationId` | `createTask` |
| **Agent** | **nothing** — agents have no application | `agentId` | `createAgentTask` |

Canvas and workflow are the same client-side. The fork happens server-side: a canvas task runs a
completion, a workflow task runs the workflow graph. You do not choose; the target version decides.

Agents are a genuinely different shape — different top-level payload, different file representation,
and several task operations simply refuse agent tasks. Do not reach for an application when the user
says "run my agent over these".

## Canvas and workflow

### The canvas must be in a project

A **personal draft** — a canvas or workflow created without `projectId` — cannot have an application.
`upsertWorkstation` on one answers
`400 Personal drafts cannot have apps. Promote the canvas to a project first.`

Move it in first, then create the workstation:

```bash
bun --preload ~/.claude/skills/tela-studio/preload.ts -e "
await tela.promoteCanvas('PROMPT_ID', 'PROJECT_ID')
const app = await tela.upsertWorkstation('PROMPT_ID', { title: 'Invoice Processing' })
console.log(app.id)
"
```

Everything else in the loop works on a draft — versions, test cases, runs, review, and
`getVersionStats`. Workstation is the one step that requires a project.

### Find or create the application

A prompt has at most one workstation application. Creating a second violates a uniqueness constraint
rather than being a no-op, so look first.

```bash
bun --preload ~/.claude/skills/tela-studio/preload.ts -e "
const app = await tela.getWorkstation('PROMPT_ID')
console.log(app ? 'exists: ' + app.id : 'none yet')
"
```

```bash
bun --preload ~/.claude/skills/tela-studio/preload.ts -e "
const app = await tela.upsertWorkstation('PROMPT_ID', {
  title: 'Invoice Processing',
  usedVersion: 'production',
  approvalRequired: true,
})
console.log(app.id, '->', tela.getWorkstationUrl('PROMPT_ID'))
"
```

`upsertWorkstation` creates the application or updates the existing one, which is what the app's
publish flow does.

**The prompt needs a promoted version first.** With `usedVersion: 'production'` (the default) and
nothing promoted, the call fails with an opaque `404 Resource not found /prompt-application` that
names the collection rather than the missing version. Promote first — see `update-canvas.md` — or
pass a concrete version id.

### `usedVersion` — which version the Workstation runs

Pass `'promoted'`, `'production'`, or a concrete version id. Logical names are **resolved at write
time** and stored as the resolved id: an application created with `'production'` keeps running that
exact version after you promote a newer one. Call `upsertWorkstation` again to move it forward.

Read the current target with `getApplicationTargetVersion(applicationId)`.

### `config`

| Field | Meaning |
|---|---|
| `approvalRequired` | results need human approval before counting as done |
| `isHidden` | hide the application in the app |
| `variables` | **derived server-side** from the target version's variables — never set it yourself |

`config.variables` is how the Workstation form knows its fields. Read it before creating tasks:

```bash
bun --preload ~/.claude/skills/tela-studio/preload.ts -e "
for (const v of await tela.getApplicationVariables('APP_ID'))
  console.log(v.name, v.isRequired ? '(required)' : '(optional)', v.isMultimodal ? '[file]' : '[text]')
"
```

Task variable names must match these exactly. A missing required variable is rejected at create time
with `Completion payload at index 0 is missing variables: <name>`.

### Check readiness before a batch

A Workstation batch is production work: it spends real money producing output a person will act on.
Before creating tasks in bulk on a version, confirm its test suite was actually reviewed.

```bash
bun --preload ~/.claude/skills/tela-studio/preload.ts -e "
const readiness = await tela.getVersionReadiness('VERSION_ID')
if (!readiness.ready) console.log(tela.formatVersionReadiness(readiness))
"
```

**"Every task returned valid JSON" is not a quality result.** It says the shape parsed. A batch of 31
tasks that all produced well-formed output on a version whose 42 attributes were never reviewed has
demonstrated nothing about correctness — and it has already been paid for.

When readiness reports blockers, say them and stop. Running one task to see the shape is reasonable;
running the whole batch is not.

### Create tasks

```bash
bun --preload ~/.claude/skills/tela-studio/preload.ts -e "
const task = await tela.createTask('APP_ID', 'Acme — March invoice', {
  question: 'What is the vendor and total?',
  document: await tela.uploadFile('./invoice.pdf'),
})
const settled = await tela.waitForTask(task.id)
console.log(settled.status, JSON.stringify(settled.outputContent?.content))
"
```

`createTask` takes a plain variable map. A `vault://` value becomes a file reference, with its real
name and mime type read from the Vault.

**Never pass a bare `vault://` string yourself.** The API accepts it, stores it verbatim, leaves the
task with no attached file, and the run then fails with nothing recorded on the task explaining why.
The file value must be `{ files: [{ file_url, file_name, mime_type }] }` — which is what
`createTask` and `buildTaskVariables` produce.

Variable value shapes:

| Need | Pass |
|---|---|
| text | `'some text'` |
| one file | a `vault://` reference |
| several files on one variable | an array of `vault://` references |
| text and files on one variable | `{ text, files: [...] }` |
| a declared variable left empty | `{ files: [] }` |

Batch with `createTasks`, capped at 100 per call, all sharing one `promptApplicationId`:

```bash
bun --preload ~/.claude/skills/tela-studio/preload.ts -e "
const variables = await tela.buildTaskVariables({ question: 'Total?', document: 'vault://…' })
const tasks = await tela.createTasks([
  { name: 'Row 1', promptApplicationId: 'APP_ID', rawInput: { variables } },
  { name: 'Row 2', promptApplicationId: 'APP_ID', rawInput: { variables } },
])
console.log(tasks.length + ' created')
"
```

`name` is required. `promptVersionId` is optional and defaults to the application's target version.

**Always send `rawInput`**, even for a prompt with no variables. A task created without it skips the
required-variable check entirely and runs with nothing bound, producing a confidently wrong result.

## Agents

Agents have no application. Identify the Workstation by the agent id.

```bash
bun --preload ~/.claude/skills/tela-studio/preload.ts -e "
const task = await tela.createAgentTask('AGENT_ID', 'Acme contract', {
  instructions: 'Extract the parties and total',
  contract: await tela.uploadFile('./contract.txt'),
})
console.log(task.id, task.status)
console.log(JSON.stringify(await tela.listAgentTasks('AGENT_ID', { limit: 20 })).slice(0, 200))
"
```

Differences that will bite if you assume the prompt shape:

- A file value is `{ vaultRef, filename }` — **not** `file_url`, and there is no `files: []` array.
  One file per variable; an array is rejected. `createAgentTask` builds this for you.
- Variable names must match the agent's **declared inputs** (`listAgentInputs`), not an application's
  config. An unknown name or a missing required one is rejected.
- `agentId` and `promptApplicationId` cannot be combined on a list query.
- Agent tasks **reject** rerun, undo-approval, task-to-test-case, analytics, versions, and progress.

## Reviewing and approving

A settled task is `validating`, `completed`, or `failed`. **`validating` is the normal resting
state** — it means the run finished and is waiting for a human, not that something went wrong.

```bash
bun --preload ~/.claude/skills/tela-studio/preload.ts -e "
const { data } = await tela.listTasks({ promptApplicationId: 'APP_ID', limit: 100 })
for (const t of data.filter(t => t.status === 'validating'))
  console.log(t.reference, t.name, JSON.stringify(t.outputContent?.content).slice(0, 80))
"
```

Read the output before approving. Approve only what you can verify, and say which ones you left for a
human.

```bash
bun --preload ~/.claude/skills/tela-studio/preload.ts -e "
await tela.approveTask('TASK_ID')
await tela.undoApprovalTasks(['TASK_ID'])   // reversible
"
```

`updateTask({ status })` only accepts `'completed'`; `undoApprovalTasks` is the only way back to
`validating`.

Filtering by `status` in `listTasks` currently returns a 500 for every value — filter the returned
array instead. `promptApplicationId` is the only filter that genuinely scopes the query;
`projectId`, `promptId`, and `search` are accepted and ignored.

## Turning a task into a regression test

The most valuable Workstation flow: a task came out wrong, you fixed the prompt, now lock the case in.

```bash
bun --preload ~/.claude/skills/tela-studio/preload.ts -e "
const testCase = await tela.createTestCaseFromTask('TASK_ID', { title: 'Acme invoice — subtotal bug' })
console.log('test case:', testCase.id)
"
```

This carries the task's real inputs across, including file references. Add an expectation afterwards
so the test can actually fail — see `test-case-review.md`.

Not available for agent tasks.

## Other operations

- `rerunTask(taskId)` — re-execute with the same inputs. **Only works on a `failed` task**; anything else returns `400 Only failed tasks can be rerun`
- `getTaskExecutionHistory(taskId)` — the attempts
- `getTask(taskId, { includeAnalytics: true })` — review timing and edit counts
- `deleteTasks(ids)` / `deleteTask(id)` — soft delete
- `getTaskMetrics()` — workspace totals. Counts come back as strings and disagree with `listTasks`
  totals for the same scope; prefer counting `listTasks` results.


## Close with the link

Tasks are reviewed by a person, in the app — so the link is the whole point of the answer.

```bash
bun --preload ~/.claude/skills/tela-studio/preload.ts -e "
console.log('You can see and debug it here: ' + tela.getWorkstationUrl('PROMPT_ID'))
"
```

There is no per-task URL. Link the Workstation and name the rows you mean by their `reference` and
`name`, rather than inventing a query parameter.
