> ## 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 stream progress with the Python 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

```python theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
from spine import SpineClient, Template, BlockType

with SpineClient() as client:
    handle = client.runs.create(
        prompt="Write a Q4 market memo with supporting charts.",
        template=Template.AUTO,
        blocks=[BlockType.DOCUMENT, BlockType.EXCEL],
        agent_instructions="Keep the tone concise and analytical.",
    )
```

All arguments except `prompt` are keyword-only.

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

`runs.create` returns a `RunHandle` (or `AsyncRunHandle`) that holds
`run_id`, `canvas_id`, and a reference back to the client.

## Uploading files

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

```python theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
handle = client.runs.create(
    prompt="Summarise these attachments.",
    files=[
        "./reports/q4.pdf",                              # str path
        Path("./notes.md"),                              # pathlib.Path
        open("deck.pptx", "rb"),                         # open file object
        ("inline.md", b"# notes\nSome bytes"),           # (name, bytes)
        ("custom.bin", b"\x00\x01", "application/x-custom"),  # with MIME
    ],
)
```

Content types are inferred from the filename if you don't pass one.
Paths opened by the SDK are closed automatically after the request.

Supported file types and size limits are documented on
[File uploads](/api-reference/concepts/file-uploads).

## Waiting for completion

`handle.wait()` polls the server with exponential backoff until the run
reaches a terminal state, then returns the `RunResult`:

```python theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
result = handle.wait(timeout=900, poll_interval=2.0, max_poll_interval=30.0)
print(result.final_output)
```

| Argument            | Default         | Notes                                    |
| ------------------- | --------------- | ---------------------------------------- |
| `timeout`           | `900.0` seconds | Raises `SpineTimeoutError` if exceeded.  |
| `poll_interval`     | `2.0` seconds   | Initial delay between polls.             |
| `max_poll_interval` | `30.0` seconds  | Upper bound for the exponential backoff. |

`wait` raises `SpineAPIError` if the run terminates with status
`failed`. **Partial** successes (some blocks failed, some succeeded)
return normally — inspect `handle.refresh().errors` afterwards.

## Streaming progress

Drive a progress bar with `handle.stream_progress()`. It yields a
`RunProgress` snapshot on each poll and exits silently when the run
terminates — call `wait()` or `refresh()` afterwards to get the result.

```python theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
for progress in handle.stream_progress(poll_interval=2):
    pct = progress.tasks_completed / max(progress.tasks_total, 1) * 100
    print(f"{progress.tasks_completed}/{progress.tasks_total}  ({pct:.0f}%)")

result = handle.wait()  # retrieve the final result
```

Async version:

```python theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
async for progress in handle.stream_progress(poll_interval=2):
    ...
```

## Manual polling

If you want full control, call `client.runs.get()` directly:

```python theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
import time
from spine import RunStatus

while True:
    snapshot = client.runs.get(handle.run_id)
    if snapshot.status.is_terminal:
        break
    print(snapshot.progress)
    time.sleep(10)
```

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