Skip to main content
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 for the conceptual model.

Creating a run

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.
ArgumentTypeDefaultDescription
promptstrrequiredThe instruction for the run.
templateTemplate | strTemplate.AUTOCanvas template. See Templates.
blocksIterable[BlockType | str]NoneAllow-list of block types the run may produce.
agent_instructionsstr | NoneNoneExtra system-prompt instructions for the agent.
webhook_urlstr | NoneNoneReserved for future webhook delivery.
filesIterable[...]NoneFiles to upload — see below.
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:
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.

Waiting for completion

handle.wait() polls the server with exponential backoff until the run reaches a terminal state, then returns the RunResult:
result = handle.wait(timeout=900, poll_interval=2.0, max_poll_interval=30.0)
print(result.final_output)
ArgumentDefaultNotes
timeout900.0 secondsRaises SpineTimeoutError if exceeded.
poll_interval2.0 secondsInitial delay between polls.
max_poll_interval30.0 secondsUpper 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.
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:
async for progress in handle.stream_progress(poll_interval=2):
    ...

Manual polling

If you want full control, call client.runs.get() directly:
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 for the server-side semantics (typical run durations, recommended intervals).