> ## Documentation Index
> Fetch the complete documentation index at: https://docs.getspine.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Runs and polling

> Create runs, wait for them, and poll progress with the TypeScript SDK.

A **run** is an asynchronous canvas-generation task. You submit a prompt,
optionally constrain the templates or block types, and poll until the
run reaches a terminal state. See
[Canvases and runs](/api-reference/concepts/canvases-and-runs) for the
conceptual model.

## Creating a run

```ts theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
const run = await client.runs.create({
  prompt: 'Write a Q4 market memo with supporting charts.',
  template: 'auto',
  blocks: ['document-block', 'excel-block'],
  agent_instructions: 'Keep the tone concise and analytical.',
});
```

| Argument             | Type          | Default     | Description                                                          |
| -------------------- | ------------- | ----------- | -------------------------------------------------------------------- |
| `prompt`             | `string`      | required    | The instruction for the run.                                         |
| `template`           | `Template`    | `'auto'`    | Canvas template. See [Templates](/api-reference/concepts/templates). |
| `blocks`             | `BlockType[]` | `undefined` | Allow-list of block types the run may produce.                       |
| `agent_instructions` | `string`      | `undefined` | Extra system-prompt instructions for the agent.                      |
| `webhook_url`        | `string`      | `undefined` | Reserved for future webhook delivery.                                |
| `files`              | `FileInput[]` | `undefined` | Files to upload — see [below](#uploading-files).                     |

`runs.create` returns a `CreatedRun` holding `run_id`, `status`
(always `'running'`), `poll_url`, and `estimated_duration_ms`.

Passing `blocks: []` is rejected client-side with
`SpineBadRequestError` — omit the field entirely to use the template
default.

## Uploading files

The SDK normalises several shapes so you can mix them freely:

```ts theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
import { readFile } from 'node:fs/promises';

const pdf = await readFile('./reports/q4.pdf');

const run = await client.runs.create({
  prompt: 'Summarise these attachments.',
  files: [
    new Uint8Array(pdf),                                              // raw bytes
    { data: new Uint8Array(pdf), filename: 'q4.pdf' },                // named
    { data: new TextEncoder().encode('# notes'), filename: 'notes.md', contentType: 'text/markdown' },
    new Blob([pdf], { type: 'application/pdf' }),                      // Blob
  ],
});
```

Content types are inferred when you don't pass one. In a browser you can
pass `File` objects directly. Supported file types and size limits are
documented on [File uploads](/api-reference/concepts/file-uploads).

## Waiting for completion

`client.runs.waitForCompletion()` polls the server until the run reaches
a terminal state (`completed`, `partial`, or `failed`), then returns the
terminal `Run`:

```ts theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
const terminal = await client.runs.waitForCompletion(run.run_id, {
  pollIntervalMs: 5000,
  timeoutMs: 15 * 60_000,
});
```

| Option             | Default            | Notes                                                        |
| ------------------ | ------------------ | ------------------------------------------------------------ |
| `pollIntervalMs`   | `5000`             | Delay between polls in milliseconds.                         |
| `timeoutMs`        | `900_000` (15 min) | Throws `SpineTimeoutError` if exceeded.                      |
| `signal`           | —                  | `AbortSignal` for cooperative cancellation.                  |
| `resolveOnFailure` | `false`            | If `true`, resolves with the failed run instead of throwing. |

By default, a `'failed'` terminal state throws `SpineRunFailedError`.
**Partial** successes return normally — some blocks succeeded, some
failed; inspect `terminal.errors` on the returned object.

### Cancellation

```ts theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
const controller = new AbortController();
setTimeout(() => controller.abort(), 60_000);

try {
  await client.runs.waitForCompletion(run.run_id, { signal: controller.signal });
} catch {
  console.log('cancelled');
}
```

## Narrowing the result

The returned `Run` is a discriminated union on `status`. Narrow before
accessing state-specific fields:

```ts theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
switch (terminal.status) {
  case 'completed':
    terminal.result.artifacts.forEach((a) => console.log(a.download_url));
    break;
  case 'partial':
    console.warn('partial', terminal.errors);
    terminal.result.artifacts.forEach((a) => console.log(a.download_url));
    break;
  case 'failed':
    console.error(terminal.errors);
    break;
}
```

## Manual polling

If you want full control — custom cadence, streaming progress to a UI,
non-standard backoff — call `client.runs.retrieve()` directly:

```ts theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
for (;;) {
  const run = await client.runs.retrieve(runId);
  if (run.status !== 'running') break;
  console.log(`${run.progress.tasks_completed}/${run.progress.tasks_total}`);
  await new Promise((r) => setTimeout(r, 10_000));
}
```

See [Polling and status](/api-reference/concepts/polling-and-status) for
the server-side semantics (typical run durations, recommended intervals).
