---
name: workflow
description: Use when the user wants to create, update, validate, run, inspect, or cancel Tela workflows through the Tela API/direct graph path, or shares a workflow URL (`/workflows/:id`). Do not use for source-first Workflow Code files.
---

# Workflow

## Overview

Use Tela workflow graph features directly through the Tela API. Do not ask users to choose a workflow version. The user-facing flow should be seamless: every create/update/manage/run request uses the current graph model and `/workflows/:id` UI.

For source-first Workflow Code proposals or TypeScript files, use the independent
`workflow-code-builder-framework` or `workflow-code-builder` skills instead.

Read these companion docs when authoring:

- `workflow-nodes.md` — node builders, ports, action configs, graph examples
- `workflow-references.md` — `step://`, `input://`, path, format, and compatibility rules

## When to Use

- User asks to create, update, patch, validate, publish, run, inspect, wait for, list, or cancel a workflow.
- User shares a workflow URL (`/workflows/:id`).
- User asks how workflow nodes, references, variables, map subgraphs, conditions, or template/canvas composition work.

## Core Rules

1. Always use workflow graph authoring and `/workflows/:id` URLs.
2. Never ask about workflow versions.
3. Build graphs with node builders and `buildGraph()`.
4. Build specs with `buildWorkflowSpec()`; graph is source of truth and `steps` stays empty.
5. Persist with `createWorkflowVersion()` / `updateWorkflowVersion()`.
6. Validate before save/run. Server validation runs automatically when persisting or running with a graph.
7. Use `templateNode` for workflow templates and `canvasNode` for Tela canvas/workflow calls.
8. New workflows can be personal drafts. Omit `projectId` unless the user asks to create the workflow in a project.

## Workflow Creation Flow

### 1. Use the authenticated workspace and optional project

`createCanvas()` infers `workspaceId` from the session token. Pass `workspaceId` explicitly only as a fallback for a legacy or manually managed credential without workspace claims. `projectId` is optional: omit it to create a personal Workflow draft owned by the authenticated user. Only resolve a project when the user explicitly asks to create the Workflow in one; do not require project selection for a personal draft.

When a project is requested, resolve it silently when possible:

```typescript
const projects = await tela.listProjects({ limit: 50 })
const project = projects.find(p => p.title === 'My Project')!
// project.id -> optional projectId
```

If the requested target project is ambiguous, ask the user to choose.

### 2. Create workflow prompt shell

Workflows are prompt applications with workflow versions. Create a canvas/prompt first when needed:

```typescript
const workflow = await tela.createCanvas({
  title: 'Invoice Workflow',
  isWorkflow: true,
})
```

Always pass `isWorkflow: true` when creating a workflow shell. This sets the prompt's workflow layout (`layoutVersion: 'v2'`) so Tela displays it as a workflow. Creating a workflow version later does not update the prompt's layout.

The example above creates a personal draft in the authenticated workspace. To create it in an explicitly requested project, also pass `projectId: project.id`.

A draft is not a dead end: `tela.promoteCanvas(workflow.id, project.id)` moves it into a project later.
That is the only route — `updateCanvas({ projectId })` discards the field, and patching the prompt
returns `400`. List drafts with `tela.listPrompts({ personal: true })`.

### 3. Define inputs

Use stable input IDs so references survive renames.

```typescript
const documentInput = tela.createWorkflowInput({
  id: crypto.randomUUID(),
  name: 'document',
  type: 'file',
  required: true,
  multimodal: false,
})
```

Mandatory file question: if any workflow input has `type: 'file'`, ask whether it should be **multimodal** (model sees file directly; images/scans/layout) or **parsed** (Tela extracts text; text PDFs/CSV). Set `multimodal` accordingly.

### 4. Build nodes

Use builders:

- `llmNode` — LLM text/structured generation
- `agentNode` — multi-round agent with tools
- `codeExecutionNode` — sandboxed JS execution (`function execute(input) { ... }` required)
- `conditionNode` — branch by selector or code expression
- `mapNode` — iterate arrays via subgraph child runs
- `templateNode` — execute a workflow template
- `canvasNode` — execute Tela canvas or workflow
- `documentSplitterNode` — split PDF/CSV into chunks
- `documentCropperNode` — extract PDF pages
- `documentTemplaterNode` — fill DOCX templates
- `stopNode` — stop a condition branch intentionally

### 5. Build graph

```typescript
const classify = tela.llmNode({ name: 'Classify', prompt: `Classify ${tela.inputRef(ticketInput)}` })
const review = tela.llmNode({ name: 'Review', prompt: `Review ${tela.stepRef(classify)}` })
const stop = tela.stopNode({ name: 'Not applicable' })

const graph = tela.buildGraph([classify, review, stop], [
  [classify, review, { branch: 0 }],
  [classify, stop, { branch: 'default' }],
])
```

Connections use node objects, not IDs. The first node with no incoming edge is the root; there is no
`startNode` builder. Port selectors:

- omitted: default output -> default input
- `'subgraph'`: map subgraph output
- `{ branch: 0 }`: condition case branch
- `{ branch: 'default' }`: condition default branch

#### Graph shape rules

A UI-authored workflow is a **linear chain**. It branches only through `conditionNode` ports and
`mapNode` subgraphs. `buildGraph` enforces eight rules and throws when one is broken:

| Rule | Meaning |
|---|---|
| `invalid_root_count` | exactly one root node |
| `stop_cannot_be_root` | a `stopNode` cannot be the root |
| `cycle_detected` | no cycles |
| `unreachable_node` | every node must be reachable from the root |
| `default_fan_out` | a node's default output has at most one outgoing edge |
| `arbitrary_fan_in` | multiple incoming edges are only allowed when all are condition-scoped |
| `condition_branch_edge_count` | one edge per condition branch |
| `condition_connect_scope_mismatch` | condition edges must connect within their branch scope |

**Fan-out and fan-in graphs are rejected.** When a user asks for steps to run "at the same time",
do not draw a diamond graph. The supported answers are `settings: { parallelExecution: true }` on the
spec (see below), `mapNode` to iterate one subgraph over an array, or a plain sequential chain.

`buildGraph` accepts a third options argument including `{ validate: false }`. **Do not use it.** It
only suppresses the local check; `createWorkflowVersion` re-validates and refuses the save, and a
graph that slips past local validation is one the workflow editor cannot render or edit.

### 6. Build workflow spec

**Mandatory output question**: every workflow must declare what it returns. `outputs` is a map of
field name to `{ type, source }`, and those fields are exactly what becomes reviewable, attribute by
attribute, on a test case result.

Leaving `outputs` empty — the default — produces a workflow whose runs return the leaf step's raw
blob and whose test cases have nothing to review. They finish, report as completed, and look
validated while no human has looked at anything. Never save a workflow with empty `outputs`.

Giving the internal AI steps structured outputs is not enough on its own: nothing lifts a step's
schema to the workflow level. Use `outputsFromStep` to do exactly that.

```typescript
const schema = { vendor: { type: 'string' }, total: { type: 'number' } }
const extract = tela.llmNode({ name: 'Extract', prompt: `Read ${tela.inputRef(invoiceInput)}`, schema })

const spec = tela.buildWorkflowSpec({
  inputs: [invoiceInput],
  outputs: tela.outputsFromStep(extract, schema),
})
```

Merge the maps when the result draws on more than one step:

```typescript
const spec = tela.buildWorkflowSpec({
  inputs: [contractInput],
  outputs: {
    ...tela.outputsFromStep(extractParties, partiesSchema),
    ...tela.outputsFromStep(extractTerms, termsSchema),
  },
})
```

`source` is an ordinary reference, so you can also write entries by hand:
`{ total: { type: 'number', source: tela.stepRef(extract, { path: 'total' }) } }`.

### Parallel execution

Independent steps run concurrently only when the workflow asks for it:

```typescript
const spec = tela.buildWorkflowSpec({
  inputs: [documentInput],
  outputs,
  settings: { parallelExecution: true },
})
```

This setting is the **only** supported form of parallelism. It does not let you draw a fan-out graph
— see the graph shape rules above, which still apply.

### 7. Persist version

```typescript
const version = await tela.createWorkflowVersion({
  promptId: workflow.id,
  title: 'Initial workflow',
  workflowSpec: spec,
  graph,
  configuration: { model: 'gpt-5', type: 'chat', temperature: 0 },
})

console.log(tela.getWorkflowUrl(workflow.id))
```

`createWorkflowVersion()` validates the graph locally (strict authoring unless disabled) and with the server before saving.

## Updating Workflows

Resolve the prompt ID from URL/name, fetch latest version, fork/mutate graph, save a new version.

```typescript
const latest = await tela.getLatestVersion(promptId)
const { graph, spec, configuration } = await tela.forkVersion(latest.id)

const node = graph.nodes.find(n => n.name === 'Extract Data')!
node.input = { ...node.input, prompt: 'New prompt text' }

await tela.createWorkflowVersion({
  promptId,
  title: 'Update extraction prompt',
  workflowSpec: spec,
  graph,
  configuration,
})
```

For one-node patches:

```typescript
await tela.patchWorkflowNode({
  promptId,
  versionId: latest.id,
  nodeName: 'Extract Data',
  patch: { prompt: 'New prompt text' },
  title: 'Patch extraction prompt',
})
```

For in-place prompt-version updates:

```typescript
await tela.updateWorkflowVersionPreservingState(latest.id, {
  title: 'Renamed workflow version',
})
```

## References

Use references in string config fields to consume workflow inputs and step outputs.

```typescript
const input = tela.createWorkflowInput({ id: 'input-topic', name: 'topic', type: 'text' })
const draft = tela.llmNode({
  name: 'Draft',
  prompt: `Write about ${tela.inputRef(input)}`,
})
const summarize = tela.llmNode({
  name: 'Summarize',
  prompt: `Summarize ${tela.stepRef(draft)}`,
})
```

Common forms:

- `{{input://input-id}}` — workflow input
- `{{input://input-id?format=raw}}` — **file** workflow input
- `{{step://step-id}}` — full step output
- `{{step://step-id?path=data[items]}}` — nested output
- `{{step://step-id?format=json}}` — format override
- `{{step://step-id?path=file&format[file]=raw}}` — per-path format

### File inputs require `?format=raw`

A file input embedded in a prompt without `?format=raw` **never reaches the model**. The run still
reports `status: 'completed'` with `error: null`; the step simply sees nothing and fills its schema
with empty values. There is no failure signal anywhere.

`tela.inputRef` handles this: pass the input object returned by `createWorkflowInput` and it emits
`?format=raw` for `type: 'file'` automatically.

```typescript
const invoiceInput = tela.createWorkflowInput({ id: crypto.randomUUID(), name: 'invoice', type: 'file' })
tela.inputRef(invoiceInput)      // {{input://…?format=raw}}  — correct
tela.inputRef('some-input-id')   // {{input://some-input-id}} — type unknown, no format added
```

Referencing an input by bare id or name cannot detect the type, so always pass the input object for
files. When checking a run, compare `inputTokens` against the document size: a file that was dropped
shows a token count consistent with the prompt text alone.

Note that `getStepDependencies` reports `defaultFormat: 'ai-xml'` for file inputs. That default does
not deliver the file — do not follow it for file inputs.

Use `getStepDependencies(promptId, versionId, stepId)` when a saved workflow exists and you need format compatibility for a consuming step.

## Running Workflows

```typescript
const inputs = {
  document: tela.buildRunInput('document', '', {
    id: documentInput.id,
    type: 'file',
    files: [{ fileUrl: 'vault://file-ref', name: 'invoice.pdf', vaultReference: 'vault://file-ref' }],
  }),
}

const run = await tela.runWorkflow(tela.buildRunPayload({
  promptVersionId: version.id,
  promptId: workflow.id,
  graph,
  spec,
  inputs,
}))

const result = await tela.waitForWorkflowRun(run.id)
```

Use `vault://` refs as file URLs. Do not pass pre-signed URLs.

## Inspecting and Managing Runs

- `getWorkflowRun(runId)` — fetch run status/snapshot
- `listWorkflowRuns({ promptId, promptVersionId? })` — list runs
- `waitForWorkflowRun(runId, options?)` — poll until terminal status
- `cancelWorkflowRun(runId)` — cancel active run
- `getWorkflowAttributes(promptId, { promptVersionId, environment })` — runtime attributes

## Discovery APIs

Prefer API discovery over hardcoded assumptions:

- `getWorkflowActions()` — action catalog, config/output schemas, reference fields, public methods
- `getWorkflowDependencies(promptId, versionId)` — variables visible across saved workflow
- `getStepDependencies(promptId, versionId, stepId)` — variables visible at one step plus format compatibility
- `getStepTypes(promptId, versionId, stepId)` — TypeScript declarations for code node `input`
- `validateWorkflowGraphServer(promptId, graph)` — authoritative server validation

## Common Mistakes

- Asking which workflow version to use.
- Populating `workflowSpec.steps`; graph is source of truth.
- Passing full JSON Schema to `llmNode.schema`; pass properties map only.
- Missing `function execute(input)` in `codeExecutionNode`.
- Using 0-based indexes in `documentCropperNode`; indexes are 1-based.
- Adding `stopNode` as normal terminator; workflows end naturally at leaf nodes.
- Wrapping `{{step://...}}` or `{{input://...}}` refs in code fences.
- Guessing reference formats instead of using `getStepDependencies()` when available.

## Close with the link

Workflows are the case where a good link saves the most time, because the editor shows the graph and
the step outputs that this skill can only print as JSON.

```bash
bun --preload ~/.claude/skills/tela-studio/preload.ts -e "
console.log(tela.formatLinks(tela.getWorkflowLinks('PROMPT_ID', { nodeId: 'STEP_ID' })))
"
```

`nodeId` opens the editor with that step selected, `executionId` opens a specific run, and
`promptVersionId` pins the page to a version. When one step is misbehaving, link the step.
