npm package: @meistrari/tela-sdk-js
Description: The Tela SDK for JavaScript provides a simple and powerful way to interact with the Tela API. This SDK allows you to create chat completions, handle file uploads, and manage various resources with ease.
Latest version: 2.10.0

# Tela SDK for JavaScript

The Tela SDK for JavaScript provides a simple and powerful way to interact with the Tela API. This SDK allows you to create chat completions, handle file uploads, and manage various resources with ease.

## Table of Contents

- [Installation](#installation)
- [Usage](#usage)
- [Examples](#examples)
  - [Canvas API](#canvas-api)
    - [Synchronous Execution](#synchronous-execution)
    - [Asynchronous Execution with Polling](#asynchronous-execution-with-polling)
    - [Streaming Execution](#streaming-execution)
    - [Event-Driven Execution](#event-driven-execution)
    - [Schema Validation](#schema-validation)
    - [Multiple Files](#multiple-files)
    - [Webhook Notifications](#webhook-notifications)
    - [Execution Tags](#execution-tags)
  - [File Handling](#file-handling)
  - [Tasks API](#tasks-api)
  - [Vault API](#vault-api)
- [Migration Guide from v1.x to v2](#migration-guide-from-v1x-to-v2)
  - [Breaking Changes](#breaking-changes)
  - [Migration Checklist](#migration-checklist)
  - [New Features in v2](#new-features-in-v2)

## Installation

You can install the Tela SDK via npm, yarn, or pnpm:

```bash
npm install @meistrari/tela-sdk-js
yarn add @meistrari/tela-sdk-js
pnpm add @meistrari/tela-sdk-js
```

## Usage

First, you need to import the SDK and initialize it with your API key:

```typescript
import { TelaSDK } from '@meistrari/tela-sdk-js'

const tela = new TelaSDK({ apiKey: 'your-api-key' })
```

## Examples

### Canvas API

The Canvas API provides a type-safe way to execute prompts with schema validation and multiple execution modes.

#### Synchronous Execution

Execute a canvas and wait for the complete result:

```typescript
import { TelaSDK } from '@meistrari/tela-sdk-js'

const tela = new TelaSDK({
    apiKey: 'your-api-key'
})

// Get canvas with input/output schemas
const canvas = await tela.canvas.get({
    id: 'canvas-id',
    input: schema => schema.object({
        query: schema.string()
    }),
    output: schema => schema.object({
        result: schema.string()
    }),
})

// Execute synchronously - direct result access
const result = await canvas.execute({ query: 'What is the capital of France?' }).result

console.log(result) // { result: "Paris" }

// Alternative: get execution object for more control
const execution = await canvas.execute({ query: 'What is the capital of France?' })
const result2 = await execution.result
```

#### Asynchronous Execution with Polling

For long-running operations, use async mode with automatic polling:

```typescript
const canvas = await tela.canvas.get({
    id: 'canvas-id',
    input: schema => schema.object({
        document: schema.file(),
        query: schema.string()
    }),
    output: schema => schema.object({
        summary: schema.string()
    }),
})

// Execute asynchronously with polling - direct result access
const result = await canvas.execute(
    {
        document: TelaFile.create(documentBlob, { name: 'report.pdf' }),
        query: 'Summarize this document'
    },
    {
        async: true,
        pollingInterval: 1000, // Poll every 1 second
        pollingTimeout: 60000, // Timeout after 60 seconds
    }
).result

console.log(result.summary)
```

#### Streaming Execution

Stream results as they're generated:

```typescript
const canvas = await tela.canvas.get({
    id: 'canvas-id',
    input: schema => schema.object({
        prompt: schema.string()
    }),
    output: schema => schema.object({
        response: schema.string()
    }),
})

// Execute with streaming - direct iteration
for await (const chunk of canvas.execute(
    { prompt: 'Write a story about a robot' },
    { stream: true }
).result) {
    process.stdout.write(chunk.response || '')
}
```

#### Event-Driven Execution

Monitor execution lifecycle with events. **Events are only useful with async executions** - synchronous and streaming executions complete too quickly for event-driven monitoring to be practical.

```typescript
const canvas = await tela.canvas.get({
    id: 'canvas-id',
    input: schema => schema.object({
        query: schema.string()
    }),
    output: schema => schema.object({
        result: schema.string()
    }),
})

// Start async execution to attach event listeners
const execution = await canvas.execute(
    { query: 'What is the capital of France?' },
    {
        async: true,
        pollingInterval: 1000,
        pollingTimeout: 60000,
    }
)

// Access current status at any time
console.log(execution.status) // 'created'

// Listen for status changes
execution.on('statusChange', (status) => {
    console.log(`Status: ${status}`) // created → running → succeeded
})

// Listen for polling updates
execution.on('poll', (pollResult) => {
    console.log(`Poll - Status: ${pollResult.status}, Request ID: ${pollResult.requestId}`)
})

// Listen for successful completion
execution.on('success', (result) => {
    console.log('Success!', result)
    console.log('Request ID:', result.requestId)
})

// Listen for errors (prevents throwing)
execution.on('error', (error) => {
    console.error('Failed:', error.message)
    // With error listener: error event is emitted, promise resolves to undefined
})

// Start event-driven polling (non-blocking)
execution.poll()

// Events will fire as polling progresses
// The result will be available in the success event callback
// Continue with other work while polling happens in the background...
```

**Why Async Only?**

Events are designed for tracking progress over time. Synchronous executions complete immediately (blocking until done), so by the time you can access the execution object, all events have already fired. Streaming executions provide their own iteration mechanism via async generators, making events redundant.

**Status Property:**

The `status` property provides the current execution state:
- **Async**: `created` → `running` → `succeeded` or `failed`

Status is set to `succeeded` only after successful validation. If validation fails, status will be `failed` even if the API request succeeded.

**Available Events (Async Executions Only):**
- `success` - Emitted when execution completes successfully (after validation)
- `error` - Emitted on failure (if listeners exist, errors won't throw)
- `statusChange` - Emitted on status transitions
- `poll` - Emitted during polling (initial response + each poll)

**Error Handling:**
- **With error listener**: Errors emitted as events, promise resolves to `undefined`
- **Without error listener**: Errors throw and reject the promise

**Important:** You must call `execution.poll()` or `await execution.result` to start polling - events won't fire until polling begins.

See [Event Examples](./examples/events/) for detailed usage patterns.

#### Accessing Request IDs

Every API request to Tela returns a unique `requestId` (from the `x-request-id` header) that can be used for debugging, tracking, and correlating with server logs. The SDK exposes request IDs in three ways:

```typescript
const canvas = await tela.canvas.get({ id: 'canvas-id' })

// Start an async execution
const execution = await canvas.execute(
    { query: 'test' },
    { async: true }
)

// Method 1: Access directly from execution object
// This is the request ID from the initial request that created the execution
const requestId = await execution.requestId
console.log('Request ID:', requestId)

// Method 2: Access from poll events
// Each poll event includes the request ID from that specific polling request
execution.on('poll', (pollResult) => {
    console.log('Request ID from this poll:', pollResult.requestId)
})

// Method 3: Access from success event
// The success event includes the request ID from the final successful polling request
execution.on('success', (result) => {
    console.log('Request ID from final request:', result.requestId)
})

// Start polling
execution.poll()
```

**Important Notes:**
- `execution.requestId` - Request ID from the execution creation request (POST /v2/chat/completions)
- `pollResult.requestId` - Request ID from each individual polling request (GET /v2/chat/completions/:id)
- `result.requestId` - Request ID from the final successful polling request

**Use Cases:**
- **Debugging**: Identify specific API requests when troubleshooting issues
- **Logging**: Track individual requests across distributed systems
- **Support**: Reference specific requests in support tickets
- **Monitoring**: Correlate client requests with server logs and traces

See [Request ID Example](./examples/events/request-id.ts) for a complete demonstration.

#### Schema Validation

The Canvas API validates your inputs and outputs against the server schema:

```typescript
const canvas = await tela.canvas.get({
    id: 'canvas-id',
    input: schema => schema.object({
        text: schema.string(),
        file: schema.file(), // TelaFile validator
        settings: schema.object({
            temperature: schema.number(),
            maxTokens: schema.number(),
        }),
    }),
    output: schema => schema.object({
        analysis: schema.string(),
        confidence: schema.number(),
    }),
    skipSchemaValidation: false, // Enable schema validation warnings during canvas retrieval (default)
})

// TypeScript will ensure you provide the correct types
const result = await canvas.execute({
    text: 'Analyze this document',
    file: TelaFile.create(blob, { name: 'data.pdf' }),
    settings: {
        temperature: 0.7,
        maxTokens: 1000,
    }
}).result

// Skip result validation to trust API response without Zod validation
const resultWithoutValidation = await canvas.execute(
    {
        text: 'Analyze this document',
        file: TelaFile.create(blob, { name: 'data.pdf' }),
        settings: {
            temperature: 0.7,
            maxTokens: 1000,
        }
    },
    {
        skipResultValidation: true // Skip Zod validation on results, just typecast
    }
).result
```

#### Multiple Files

Process multiple files in a single canvas execution:

```typescript
const canvas = await tela.canvas.get({
    id: 'canvas-id',
    input: schema => schema.object({
        documents: schema.array(schema.file()),
        instructions: schema.string()
    }),
    output: schema => schema.object({
        comparison: schema.string()
    }),
})

const result = await canvas.execute({
    documents: [
        TelaFile.create('https://example.com/doc1.pdf', { range: [0, 5] }),
        TelaFile.create('https://example.com/doc2.pdf', { range: [0, 5] }),
        TelaFile.create(localFileBlob, { name: 'doc3.pdf' })
    ],
    instructions: 'Compare these documents and identify key differences'
}).result

console.log(result.comparison)
```

#### Webhook Notifications

Receive completion notifications via webhook for async executions:

```typescript
const canvas = await tela.canvas.get({
    id: 'canvas-id',
    input: schema => schema.object({
        document: schema.file(),
        query: schema.string()
    }),
    output: schema => schema.object({
        analysis: schema.string()
    }),
})

// Execute with webhook notification - you can still poll if needed
const result = await canvas.execute(
    {
        document: TelaFile.create('https://example.com/large-report.pdf'),
        query: 'Analyze this report'
    },
    {
        async: true,
        webhookUrl: 'https://your-server.com/webhook',
        pollingTimeout: 300000, // 5 minutes
    }
).result
// OR wait for the webhook notification on your server instead of polling
```

#### Execution Tags

Add tags to your canvas executions for filtering, categorization, and analytics:

```typescript
const canvas = await tela.canvas.get({
    id: 'canvas-id',
    input: schema => schema.object({
        query: schema.string()
    }),
    output: schema => schema.object({
        result: schema.string()
    }),
})

// Execute with tags (works with all execution modes)
const result = await canvas.execute(
    { query: 'Analyze customer feedback' },
    {
        tags: ['production', 'analytics', 'customer-feedback']
    }
).result

// Tags work with async executions
const asyncResult = await canvas.execute(
    { query: 'Process large dataset' },
    {
        async: true,
        tags: ['batch-processing', 'analytics']
    }
).result

// Tags work with streaming executions
for await (const chunk of canvas.execute(
    { query: 'Generate report' },
    {
        stream: true,
        tags: ['streaming', 'reports']
    }
).result) {
    process.stdout.write(chunk.result || '')
}
```

**Use Cases:**
- **Environment tracking**: Tag executions by environment (`production`, `staging`, `development`)
- **Feature categorization**: Organize executions by feature (`analytics`, `reporting`, `summarization`)
- **User segmentation**: Track executions by user tier or department
- **Cost allocation**: Attribute API usage to specific projects or teams
- **Performance monitoring**: Filter and analyze execution metrics by tag

Tags are sent to the Tela API and can be used for filtering and analytics in your Tela dashboard.

#### Fetching Existing Executions

Retrieve and monitor async executions by their ID, useful for job queues, resuming workflows, or multi-user dashboards:

```typescript
// Start an async execution
const execution = await canvas.execute(
    { query: 'Process this data' },
    { async: true }
)

// Store the execution ID (e.g., in a database)
const executionId = execution.id

// Later, fetch the execution by ID
const fetchedExecution = await canvas.getExecution(executionId, {
    pollingInterval: 2000, // Optional: custom polling interval
    pollingTimeout: 120000 // Optional: custom timeout
})

console.log(fetchedExecution.status) // 'running', 'succeeded', or 'failed'

// Use event-driven polling for non-blocking progress tracking
// Note: poll() works for ANY async execution, not just fetched ones!
fetchedExecution.on('statusChange', (status) => {
    console.log(`Status: ${status}`)
})

fetchedExecution.on('success', (result) => {
    console.log('Completed!', result)
})

fetchedExecution.on('error', (error) => {
    console.error('Failed:', error)
})

// Start polling without blocking (returns immediately)
fetchedExecution.poll()

// Continue with other work while polling runs in background...
console.log('Polling in background, doing other work...')

// Later, you can still await the result if needed
const result = await fetchedExecution.result
```

**Alternative: Use `poll()` on newly started executions**

You can also use event-driven polling immediately when starting an execution:

```typescript
const execution = await canvas.execute(
    { query: 'Process this data' },
    { async: true }
)

// Set up event listeners
execution.on('statusChange', status => console.log('Status:', status))
execution.on('success', result => console.log('Done!', result))
execution.on('error', error => console.error('Failed:', error))

// Start non-blocking polling
execution.poll()

// Do other work...
console.log('Execution polling in background')
```

**Key Features:**

- **`canvas.getExecution(id, options?)`** - Fetch execution by UUID
  - Only async executions have UUIDs and can be fetched
  - Returns execution in current state (running, succeeded, or failed)
  - Supports custom polling options

- **`execution.poll()`** - Start event-driven polling
  - Returns `void` (non-blocking)
  - Emits events: `statusChange`, `poll`, `success`, `error`
  - Perfect for UI updates, dashboards, and concurrent monitoring

**Important Notes:**

⚠️ **Events require polling or awaiting `.result`**: Events are only emitted when polling is active. You must either:
- Call `execution.poll()` to start event-driven polling, OR
- Await `execution.result` which starts polling automatically

```typescript
// ❌ Events won't fire - no polling started
const execution = await canvas.execute({ query: 'test' }, { async: true })
execution.on('success', result => console.log('Done')) // This will never fire!

// ✅ Correct - poll() starts event-driven polling
const execution = await canvas.execute({ query: 'test' }, { async: true })
execution.on('success', result => console.log('Done'))
execution.poll() // Now events will fire

// ✅ Also correct - awaiting result starts polling
const execution = await canvas.execute({ query: 'test' }, { async: true })
execution.on('success', result => console.log('Done'))
await execution.result // Polling starts, events fire
```

⚠️ **Already completed executions don't emit events**: If you fetch an execution that has already finished (succeeded or failed), the `success` and `error` events won't fire because there's no polling needed. Instead, check the status and access the result directly:

```typescript
const execution = await canvas.getExecution(executionId)

if (execution.status === 'succeeded') {
    // Access result directly - success event won't fire
    const result = await execution.result
    console.log('Already completed:', result)
}
else if (execution.status === 'failed') {
    // Handle failure - error event won't fire
    try {
        await execution.result
    }
    catch (error) {
        console.error('Already failed:', error)
    }
}
else {
    // Still running - events will fire when you start polling
    execution.on('success', result => console.log('Completed!', result))
    execution.on('error', error => console.error('Failed:', error))
    execution.poll()
}
```

**Use Cases:**
- Job queue management systems
- Resuming monitoring after page refresh or server restart
- Multi-user dashboards showing execution status
- Background processing with real-time updates
- Retry mechanisms with persistent execution IDs

See [Fetch and Poll Examples](./examples/events/) for detailed usage patterns.

### File Handling

The `TelaFile` API provides flexible file handling with support for various input types:

```typescript
import { TelaFile } from '@meistrari/tela-sdk-js'

// From a Blob or File
const fileFromBlob = TelaFile.create(blob, { name: 'custom.pdf' })

// From a URL
const fileFromUrl = TelaFile.create('https://example.com/document.pdf')

// From bytes with explicit MIME type
const fileFromBytes = TelaFile.create(
    new Uint8Array(['...']),
    {
        mimeType: 'application/pdf',
        name: 'document.pdf'
    }
)

// From a ReadableStream with explicit MIME type
const fileFromStream = TelaFile.create(
    readableStream,
    {
        mimeType: 'image/jpeg',
        name: 'photo.jpg'
    }
)

// Vault reference (for files already uploaded)
const vaultFile = TelaFile.create('vault://file-id')

// With page range for PDFs
const fileWithRange = TelaFile.create(
    'https://example.com/document.pdf',
    {
        range: [0, 5], // Pages 0-5
        parserType: 'pdf'
    }
)
```

### Tasks API

The Tasks API exposes the persistent task records that workstation (application) canvases create. Use it to fetch, list, update, approve, edit, re-run, or delete tasks without dropping down to raw HTTP.

```typescript
import { TelaSDK, extractTaskOutput, normalizeTaskStatus } from '@meistrari/tela-sdk-js'

const tela = new TelaSDK({ apiKey: 'your-api-key' })

// Fetch a task by id
const task = await tela.tasks.get('task-id')

// Read the parsed output (handles canvas vs workflow shape automatically)
const output = await tela.tasks.getOutput<MyOutput>('task-id')
// or, if you already have the task in hand:
const sameOutput = extractTaskOutput<MyOutput>(task)

// List input files attached to the task (e.g. uploaded PDFs)
const files = await tela.tasks.getInputFiles('task-id')

// List tasks with filters
const { data, meta } = await tela.tasks.list({
    promptApplicationId: 'app-id',
    status: ['validating', 'completed'],
    tags: 'product:my-app',
    createdAtSince: new Date('2024-01-01'),
    orderBy: ['createdAt'],
    order: 'desc',
    limit: 50,
})

// Mutations
await tela.tasks.rename('task-id', 'New name')
await tela.tasks.approve('task-id') // sets status: 'completed'
await tela.tasks.setTags('task-id', ['reviewed', 'q2'])

// ─── Updating output content ────────────────────────────────────
// The server uses *deep-merge* semantics on outputContent.content.
// Keys you do not provide are left untouched. There is no way to
// delete a field through this API. Three methods cover different
// use-cases:

// 1. Top-level partial patch (single PATCH, no fetch)
await tela.tasks.patchOutputContent('task-id', { reviewedBy: 'me' })

// 2. Single-field shortcut for nested *object* paths
//    Do NOT use this for array indices — use updateOutputContent below.
await tela.tasks.patchOutputField('task-id', 'profile.email', 'b@x.com')

// 3. Read-modify-write — safe for arrays, multiple edits, and any
//    change that needs the previous value. SDK fetches current,
//    runs your updater, then sends the full content via patchOutputContent.
await tela.tasks.updateOutputContent<MenuOutput>('task-id', (current) => {
    current.menu.items[2].price = 9.99 // mutate in place
    current.menu.items.push({ name: 'New', price: 5 }) // grow arrays
})
// Updaters can also return a new value:
await tela.tasks.updateOutputContent('task-id', current => ({ ...current, reviewed: true }))

// Re-run a task (only allowed when status === 'failed')
const rerun = await tela.tasks.rerun('task-id')
console.log(rerun.executionRunId)

// Delete
await tela.tasks.delete('task-id') // → { success: true }
await tela.tasks.deleteBulk(['id1', 'id2']) // → { deleted: 2, deletedIds: [...] }
await tela.tasks.undoApprovalBulk(['id1', 'id2']) // → { undoApproval: 2, undoApprovalIds: [...] }

// Map server statuses to coarse client-friendly buckets
const uiStatus = normalizeTaskStatus(task.status)
// 'created' | 'running'   → 'running'
// 'validating'            → 'pending'
// 'completed'             → 'succeeded'
// 'failed' | 'cancelled'  → 'failed'
```

The full `Task`, `TaskInputFile`, `TaskListQuery`, `TaskUpdatePayload`, and `TaskStatus` types are exported from the package root, so you don't need to type any task-related responses by hand.

### Vault API

The Vault API resolves and downloads files stored in the Tela vault. It accepts both `vault://<id>` references and bare ids, and passes through plain `https://` URLs unchanged.

```typescript
const tela = new TelaSDK({ apiKey: 'your-api-key' })

// Get a temporary signed URL
const signedUrl = await tela.vault.getSignedUrl('vault://abc-123')

// Resolve any reference (vault:// or https://) to a downloadable URL
const downloadUrl = await tela.vault.resolve(file.url)

// Download as a Blob
const blob = await tela.vault.download('vault://abc-123')

// Stream the file
const stream = await tela.vault.stream('vault://abc-123')
```

This is particularly useful when reading task input files: `tela.tasks.getInputFiles(id)` returns the file references, and `tela.vault.download(file.url)` downloads them in one call.

## Migration Guide from v1.x to v2

Version 2.0 of the Tela SDK introduces significant improvements to type safety, schema validation, and API design. This guide will help you migrate your code from v1.x to v2.

### Breaking Changes

#### 1. API Surface Changed: `completions.create()` → `canvas.get()` + `execute()`

The v1 API used a single `completions.create()` method. V2 introduces a two-step pattern: first retrieve a canvas, then execute it.

**v1.x:**
```typescript
const tela = new TelaSDK({ apiKey: 'your-api-key' })

const completion = await tela.completions.create({
    canvasId: 'canvas-id',
    variables: {
        query: 'What is the capital of France?'
    }
})

console.log(completion.choices[0].message.content)
```

**v2:**
```typescript
const tela = new TelaSDK({ apiKey: 'your-api-key' })

// First, get the canvas
const canvas = await tela.canvas.get({
    id: 'canvas-id'
})

// Then execute it
const result = await canvas.execute({
    query: 'What is the capital of France?'
}).result

console.log(result)
```

#### 2. Schema Validation and Type Safety (Optional)

V2 introduces **optional** runtime and compile-time type safety with Zod schema validation. Schemas validate data at runtime (catching invalid data) and provide TypeScript types at compile-time. You can use the SDK without schemas if you prefer.

**v1.x (no schema validation):**
```typescript
const completion = await tela.completions.create<
    { query: string },
    { result: string }
>({
    canvasId: 'canvas-id',
    variables: {
        query: 'Hello'
    }
})
```

**v2 (without schemas - works just like v1.x):**
```typescript
const canvas = await tela.canvas.get({
    id: 'canvas-id'
})

const result = await canvas.execute({ query: 'Hello' }).result
// result is typed as unknown, no runtime validation
```

**v2 (with optional schema validation for runtime + compile-time safety):**
```typescript
const canvas = await tela.canvas.get({
    id: 'canvas-id',
    input: schema => schema.object({
        query: schema.string()
    }),
    output: schema => schema.object({
        result: schema.string()
    })
})

// Runtime validation ensures data matches schema
// TypeScript knows the exact types at compile-time
const result = await canvas.execute({ query: 'Hello' }).result
// result.result is a string (validated and typed)
```

#### 3. File Handling Changes

The `TelaFile` constructor now requires `mimeType` for `Uint8Array` and `ReadableStream` inputs (for compatibility with storage solutions).

**v1.x:**
```typescript
import { TelaFile } from '@meistrari/tela-sdk-js'

const file = new TelaFile(blob, { range: [0, 5] })
const fileFromBytes = new TelaFile(new Uint8Array([/* ... */])) // mimeType optional
```

**v2:**
```typescript
import { TelaFile } from '@meistrari/tela-sdk-js'

// No change needed for Blob, File, or URL strings
const file = new TelaFile(blob, { range: [0, 5] })

// For Uint8Array or ReadableStream, mimeType is now required:
const fileFromBytes = new TelaFile(
    new Uint8Array([/* ... */]),
    {
        mimeType: 'application/pdf',
        name: 'document.pdf'
    }
)

// Alternative: use tela.createFile() helper
const fileFromStream = tela.createFile(readableStream, { mimeType: 'image/jpeg' })
```

#### 4. Streaming API Changes

Streaming now returns an async generator directly via the `.result` property.

**v1.x:**
```typescript
const stream = await tela.completions.create({
    canvasId: 'canvas-id',
    stream: true,
    variables: { query: 'Tell me a story' }
})

for await (const chunk of stream) {
    process.stdout.write(chunk.message.content || '')
}
```

**v2:**
```typescript
const canvas = await tela.canvas.get({
    id: 'canvas-id',
    input: schema => schema.object({
        query: schema.string()
    }),
    output: schema => schema.object({
        response: schema.string()
    })
})

const execution = canvas.execute(
    { query: 'Tell me a story' },
    { stream: true }
)

for await (const chunk of execution.result) {
    process.stdout.write(chunk.response || '')
}
```

#### 5. Async Execution with Polling

V2 introduces async execution with automatic polling. V1.x only supported synchronous execution (the request would block until completion).

**v1.x:**
```typescript
// Synchronous only - blocks until the execution completes
const response = await tela.completions.create({
    canvasId: 'canvas-id',
    variables: { query: 'Analyze this' }
})
// This could take a very long time for long-running executions
console.log(response.choices[0].message.content)
```

**v2:**
```typescript
const canvas = await tela.canvas.get({
    id: 'canvas-id',
    input: schema => schema.object({ query: schema.string() }),
    output: schema => schema.object({ result: schema.string() })
})

// Automatic polling
const execution = canvas.execute(
    { query: 'Analyze this' },
    {
        async: true,
        pollingInterval: 1000, // Poll every 1 second
        pollingTimeout: 60000, // Timeout after 60 seconds
        webhookUrl: 'https://your-server.com/webhook' // Optional
    }
)

const result = await execution.result
console.log(result.result)
```

#### 6. Response Structure Changes

The response structure has been simplified.

**v1.x:**
```typescript
const completion = await tela.completions.create({
    canvasId: 'canvas-id',
    variables: { query: 'Hello' }
})

// Access via nested structure
console.log(completion.choices[0].message.content)
```

**v2:**
```typescript
const canvas = await tela.canvas.get({
    id: 'canvas-id',
    output: schema => schema.object({
        answer: schema.string()
    })
})

const response = await canvas.execute({ query: 'Hello' }).result
// Direct access to structured output
console.log(response.answer)
```

### Migration Checklist

- [ ] Replace `tela.completions.create()` with `tela.canvas.get()` + `canvas.execute()`
- [ ] (Optional) Add input/output schemas using the `schema` builder function for type safety
- [ ] Add `mimeType` parameter when creating `TelaFile` from `Uint8Array` or `ReadableStream`
- [ ] Update streaming code to use `.result` property for iteration
- [ ] Update async execution to use polling parameters instead of manual polling
- [ ] Update response access patterns to match new structure
- [ ] (If using schemas) Test schema validation warnings to ensure schemas match server configuration

### New Features in v2

#### Event-Driven Execution Monitoring

V2 introduces a comprehensive event system for monitoring execution lifecycle across all execution modes (sync, async, streaming):

```typescript
const execution = await canvas.execute(
    { query: 'Analyze this' },
    { async: true }
)

// Monitor progress with events
execution.on('statusChange', (status) => {
    console.log(`Status: ${status}`)
})

execution.on('poll', (pollResult) => {
    console.log(`Polling - ${pollResult.status}`)
})

execution.on('success', (result) => {
    console.log('Completed!', result)
})

// Graceful error handling
execution.on('error', (error) => {
    console.error('Failed:', error.message)
    // With listener: no throw, promise resolves to undefined
})

const result = await execution.result
```

**Available Events:**
- `success` - Completion with validated result
- `error` - Failure notification (prevents throwing if listener exists)
- `statusChange` - Status transitions
- `poll` - Polling updates (async only)

See [Event Examples](./examples/events/) for comprehensive patterns and use cases.

#### Promise-like Execution API

V2 introduces a flexible execution API that supports both direct result access and execution object retrieval:

```typescript
// Direct result access (recommended for simple cases)
const result = await canvas.execute({ query: 'Hello' }).result

// Get execution object for more control
const execution = await canvas.execute({ query: 'Hello' })
const result2 = await execution.result
```

#### Schema Validation Warnings

V2 validates your schemas against the server configuration and warns you about mismatches:

```typescript
const canvas = await tela.canvas.get({
    id: 'canvas-id',
    input: schema => schema.object({
        wrongField: schema.string() // Will warn if not in server schema
    }),
    skipSchemaValidation: false // Enable warnings (default)
})
```

#### Vault File References

V2 supports vault file references:

```typescript
const vaultFile = new TelaFile('vault://file-id')
```

#### Skip Result Validation

When you have schemas defined, you can skip runtime validation for better performance (you'll still get TypeScript types):

```typescript
const canvas = await tela.canvas.get({
    id: 'canvas-id',
    input: schema => schema.object({
        query: schema.string()
    }),
    output: schema => schema.object({
        answer: schema.string()
    })
})

const execution = canvas.execute(
    { query: 'Hello' },
    { skipResultValidation: true } // Skip runtime validation, trust API response
)

// Still typed as { answer: string }, but no validation at runtime
const result = await execution.result
```

#### Access Raw API Responses

V2 provides access to the raw, unprocessed API response alongside the parsed result:

```typescript
const canvas = await tela.canvas.get({
    id: 'canvas-id',
    output: schema => schema.object({
        answer: schema.string()
    })
})

const execution = await canvas.execute({ query: 'Hello' })

// Get the parsed, validated result
const result = await execution.result
console.log(result.answer) // Type-safe access

// Get the raw API response
const rawResult = await execution.rawResult
console.log(rawResult) // Complete API response with all metadata

// You can await either one first - both will be available
const raw = await execution.rawResult // Triggers execution
const parsed = await execution.result // Reuses same execution
```

This is useful when you need:
- Full API response metadata (execution IDs, timestamps, etc.)
- Debugging and logging complete responses
- Access to data not in your schema

#### Fetch Existing Executions and Event-Driven Polling

V2 introduces the ability to fetch existing async executions by ID and monitor them with event-driven polling:

```typescript
// Start an async execution
const execution = await canvas.execute(
    { query: 'Process this data' },
    { async: true }
)

// Store the execution ID (e.g., in a database)
const executionId = execution.id

// Later, fetch the execution by ID
const fetchedExecution = await canvas.getExecution(executionId, {
    pollingInterval: 2000,
    pollingTimeout: 120000
})

// Use event-driven polling (non-blocking)
fetchedExecution.on('statusChange', (status) => {
    console.log(`Status changed: ${status}`)
})

fetchedExecution.on('success', (result) => {
    console.log('Completed!', result)
})

fetchedExecution.on('error', (error) => {
    console.error('Failed:', error)
})

// Start polling without blocking
fetchedExecution.poll()

// Continue with other work...
console.log('Polling in background')
```

This enables powerful use cases:
- **Job queue management**: Start jobs, store IDs, fetch later to check status
- **Resumable workflows**: Continue monitoring after page refresh or server restart
- **Multi-user dashboards**: Monitor executions started by different users
- **Real-time UI updates**: Non-blocking polling with event listeners
- **Background processing**: Track multiple executions concurrently

See [Fetch and Poll Examples](./examples/events/) for detailed patterns.

#### Execution Tags

V2 introduces support for tagging executions for filtering, categorization, and analytics:

```typescript
// Add tags to any execution mode
const result = await canvas.execute(
    { query: 'Analyze customer feedback' },
    {
        tags: ['production', 'analytics', 'customer-feedback']
    }
).result

// Tags work with async executions
const asyncResult = await canvas.execute(
    { query: 'Process large dataset' },
    {
        async: true,
        tags: ['batch-processing', 'analytics']
    }
).result

// Tags work with streaming executions
for await (const chunk of canvas.execute(
    { query: 'Generate report' },
    {
        stream: true,
        tags: ['streaming', 'reports']
    }
).result) {
    process.stdout.write(chunk.result || '')
}
```

**Use Cases:**
- **Environment tracking**: Tag executions by environment (`production`, `staging`, `development`)
- **Feature categorization**: Organize executions by feature (`analytics`, `reporting`, `summarization`)
- **User segmentation**: Track executions by user tier or department
- **Cost allocation**: Attribute API usage to specific projects or teams
- **Performance monitoring**: Filter and analyze execution metrics by tag

#### Application Execution Labels

V2 adds support for setting workspace task titles when using deployed applications via `applicationId`:

```typescript
const canvas = await tela.canvas.get({
    applicationId: 'app-id' // Using deployed application instead of canvas ID
})

// Set the title of the workspace task
const result = await canvas.execute(
    { query: 'Process request' },
    {
        label: 'Customer Dashboard Query'
    }
).result
```

When you execute a canvas using `applicationId`, it creates a task in the application's workspace. The `label` field sets the title of that workspace task, making it easier to identify and track executions in your Tela workspace.

**Note:** The `label` field is only applicable when using `applicationId`. A warning will be logged if you provide a label without an `applicationId`.

**Use Cases:**
- **Task identification**: Give meaningful titles to workspace tasks for easier tracking
- **Execution categorization**: Organize tasks by feature or workflow in the workspace
- **User-friendly naming**: Display clear task names instead of generic execution IDs
- **Dashboard clarity**: Make workspace task lists more readable and searchable

### Need Help?

If you encounter issues during migration, please:
- Check the [examples](./examples/) directory for updated usage patterns
- Review the [API documentation](./docs/)
- Open an issue at [GitHub Issues](https://github.com/meistrari/tela-sdk-js/issues)