Skip to main content
Voyages are Sail’s primitive for observing long-running background-agent tasks. You instrument your agent with a few SDK calls; Sail records every event, span, model call, and Sailbox exec; the dashboard renders the entire trajectory at app.sailresearch.com/prod/voyages/<voyage_id> (or the matching /dev/ or /staging/ route for your SAIL_MODE). You bring the agent loop. Sail brings the substrate (Sailbox + inference) and the timeline (Voyages).
Voyages are currently in beta. APIs and dashboard surfaces may change as we stabilize the product.
Internal preview: this page is intentionally hidden from the public docs navigation until launch. Direct preview URLs are https://docs.sailresearch.com/voyages, https://docs.sailresearch.com/voyages-quickstart, and https://docs.sailresearch.com/voyages-patterns.

When to use a Voyage

Use a Voyage when:
  • An agent task runs for more than a few seconds and you want to see its trajectory in real time.
  • Multiple cooperating agents (Reviewer, TestRunner, GitHub-poster, etc.) contribute to one logical task.
  • You need a customer-visible record of what an agent did — events, model calls, Sailbox execs, terminal status — for debugging or auditing.
Don’t use a Voyage for one-shot API calls. A single sail.inference.responses.create() outside a Voyage works fine and shows up in your inference dashboard; you don’t need the extra ceremony.

Mental model

with sail.voyage.run(name=..., sailbox_id=..., metadata={...}):

  ├─ with sail.voyage.agent("Agent Name", role=...):
  │     │
  │     ├─ with sail.voyage.span("step name", payload={...}):
  │     │     │
  │     │     ├─ sail.voyage.event(kind, payload={...})
  │     │     ├─ sail.inference.responses.create(...)   ← auto-attributed
  │     │     └─ sailbox.exec(...)                       ← auto-attributed
  │     │
  │     └─ ...

  └─ terminal state emitted on block exit
        (voyage.completed, or voyage.failed on an exception)
  • A Voyage is one task.
  • An agent is a named participant within the task (e.g., “Reviewer”).
  • A span is a logical step the agent performs (e.g., “draft-response”).
  • An event is a timestamped marker within a span.
  • A model call is automatically recorded when you call Sail inference inside a Voyage.
  • A Sailbox exec is automatically recorded when you call sb.exec() inside a Voyage.

Minimal example

import sail

with sail.voyage.run(name="my-first-voyage", metadata={"task": "hello"}) as voyage:
    with voyage.agent("Greeter", role="planner"):
        with voyage.span("say-hello"):
            voyage.event("greeted", payload={"message": "hi"})

print(f"Voyage URL: {sail.voyage.dashboard_url()}")
Run this, then open the printed URL. You’ll see one Voyage with one agent, one span, one event, terminal status voyage completed.

What gets attributed automatically

When you call Sail inference inside a Voyage, the SDK attaches headers so the model call shows up in the Voyage’s Native Model Calls panel, scoped to the active span and agent:
with voyage.agent("Reviewer", role="reviewer"):
    with voyage.span("draft"):
        response = sail.inference.responses.create(
            model="zai-org/GLM-5",
            input="Summarize this diff: ...",
            background=False,
            timeout=120,
        )
When you call sb.exec() inside a Voyage, the SDK attaches gRPC metadata so the exec row in the Sailbox tab is attributed to the active agent and span:
with voyage.agent("TestRunner", role="executor"):
    with voyage.span("unit-tests"):
        req = sb.exec("just test-backend-unit", timeout=600)
        req.wait()
No extra wiring needed — just be inside a with voyage.agent(...) / with voyage.span(...) context when you make the call.

Multiple agents in one Voyage

A real code review agent has at least three distinct participants:
voyage = sail.voyage.create(name="code-review", sailbox_id=sb.sailbox_id)

with voyage.agent("GitHub", role="source_control"):
    with voyage.span("clone"):
        sb.exec(
            "git clone --depth 1 https://github.com/public/repo.git /tmp/repo",
            timeout=120,
        ).wait()

with voyage.agent("TestRunner", role="executor"):
    with voyage.span("run-tests"):
        sb.exec("just test-backend-unit").wait()

with voyage.agent("Reviewer", role="reviewer"):
    with voyage.span("draft-review"):
        sail.inference.responses.create(model="zai-org/GLM-5", input="...")

voyage.complete(message="review posted")
In the dashboard you’ll see three named agents, each with its own spans and events, all under one Voyage. See the Voyages Patterns guide for more.

Terminal status

Every Voyage needs exactly one terminal event before the controller process exits — with sail.voyage.run(...) handles it: clean exit emits voyage.completed, an exception emits voyage.failed and re-raises. Terminal status is first-terminal-wins; events after the terminal are best-effort.
with sail.voyage.run(name="task"):
    do_work()
Controllers whose create and terminal sites live in different places use the create() primitive and call voyage.complete() / voyage.fail() themselves.