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

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.',
});
ArgumentTypeDefaultDescription
promptstringrequiredThe instruction for the run.
templateTemplate'auto'Canvas template. See Templates.
blocksBlockType[]undefinedAllow-list of block types the run may produce.
agent_instructionsstringundefinedExtra system-prompt instructions for the agent.
webhook_urlstringundefinedReserved for future webhook delivery.
filesFileInput[]undefinedFiles to upload — see below.
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:
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.

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:
const terminal = await client.runs.waitForCompletion(run.run_id, {
  pollIntervalMs: 5000,
  timeoutMs: 15 * 60_000,
});
OptionDefaultNotes
pollIntervalMs5000Delay between polls in milliseconds.
timeoutMs900_000 (15 min)Throws SpineTimeoutError if exceeded.
signalAbortSignal for cooperative cancellation.
resolveOnFailurefalseIf 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

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:
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:
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 for the server-side semantics (typical run durations, recommended intervals).