Skip to main content
Voyages are Sail’s telemetry layer for long-running background-agent tasks. Add a few SDK calls to your existing agent harness. Sail records the run as a trace of named agents, spans, events, Sail inference calls, and Sailbox execs. The dashboard shows the full trajectory at app.sailresearch.com/prod/voyages/<voyage_id>. You keep control of the agent loop. Sail provides the runtime pieces (Sailbox and inference) plus the Voyage timeline. Voyages are not an agent framework. They do not own your planner, memory, orchestration, tool abstractions, prompts, or retry policy. Use them with your own Python scripts, LangGraph, CrewAI, job runners, subprocesses, or any custom loop where you want visibility into what happened.
Using a coding agent? Start with the public Sail skills package. Tell your agent: “Use Sail Voyages to add automatic telemetry to my background agent workload. Keep my harness, use the sail package, wrap the run with sail.voyage.run(..., version=1), add @sail.agent and @sail.span where they fit naturally, and attribute Sail inference calls plus any Sailbox execs the workload already uses.” You do not need to know the individual skill names. The package gives your coding agent the right Voyage instructions when they are relevant.

Install

pip install sail
export SAIL_API_KEY=sk_your_key_here
Use SAIL_API_KEY for SDK agents. sail auth login is a CLI login helper. It does not configure a plain python my_agent.py SDK process for authentication.

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, and other evidence for debugging or auditing.
Do not 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 without extra instrumentation.

Mental model

with sail.voyage.run(name="research-agent", version=1):
    researcher_step()
  • 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. If no span is active, the SDK creates an auto-span.
  • A Sailbox exec is automatically recorded when you call sb.exec() inside a Voyage. If no span is active, the SDK creates an auto-span.

Minimal example

import sail

@sail.agent("Researcher")
@sail.span("draft answer")
def draft_answer(topic: str):
    sail.voyage.event("research.started", payload={"topic": topic})
    response = sail.inference.responses.create(
        model="zai-org/GLM-5.2-FP8",
        input=f"Give one concise research note about: {topic}",
        background=False,
    )
    sail.voyage.event("research.finished", payload={"response_id": response["id"]})


with sail.voyage.run(name="research-demo", version=1):
    draft_answer("long-term health benefits of running")

print(f"Voyage URL: {sail.voyage.dashboard_url()}")
Run this, then open the printed URL. You should see one Voyage with one Researcher agent, one explicit span, two events, one attributed model call, and terminal status voyage completed.

What gets attributed automatically

When you call Sail inference inside a Voyage, the SDK attaches active Voyage, agent, and span context so the model call appears in the dashboard. Decorators are the most direct way to attribute function-shaped work:
@sail.agent("Reviewer")
@sail.span("draft")
def draft_review():
    return sail.inference.responses.create(
        model="zai-org/GLM-5.2-FP8",
        input="Summarize this diff: ...",
        background=False,
        timeout=120,
    )
When you call sb.exec() inside a Voyage, the SDK also carries the active Voyage, agent, and span context into the Sailbox command. sb.exec(...) returns a request handle. Call .wait() for foreground commands so your program observes completion, return code, stdout/stderr tails, and the attributed exec row:
@sail.agent("TestRunner")
@sail.span("unit-tests")
def run_tests():
    req = sb.exec("pytest -q", timeout=600)
    req.wait()
You do not need to pass IDs by hand. If no span is active, Sail inference and Sailbox execs still get a timed auto-span named from the calling code when possible. Auto-spans do not invent agents, so declare an agent when the dashboard should show ownership.

Explicit vs auto spans

Use an explicit span when the step name matters to humans:
@sail.agent("Researcher")
@sail.span("score evidence")
def score_evidence():
    sail.inference.responses.create(model="zai-org/GLM-5.2-FP8", input="...")
If you leave the span out, Sail still captures SDK-owned work. The model call below is recorded under the Researcher agent and wrapped in a timed auto-span:
@sail.agent("Researcher")
def score_evidence():
    sail.inference.responses.create(model="zai-org/GLM-5.2-FP8", input="...")
Auto-spans are useful when you are adding telemetry to an existing agent harness. Add @sail.span(...) when you want a specific step name, payload, or parent/child shape.

Multiple agents in one Voyage

A real code-review agent has at least three distinct participants — e.g. a GitHub agent that clones, a TestRunner that runs the suite in a Sailbox, and a Reviewer that drafts the summary via inference — each declared with @sail.agent(...) so the dashboard shows three named agents, with their own spans and events, under one Voyage. See the Voyages Patterns guide for the full multi-agent example.

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 delivery only.
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.

Stop recording and delete

Use voyage.cancel() or sail.voyage.cancel() when you want to stop recording a running Voyage without emitting a voyage.cancelled event:
voyage = sail.voyage.create(name="overnight-eval")
...
voyage.cancel()
Cancel marks the server-side Voyage cancelled and stops this SDK instance from recording further events. The server does not reject late events from other sources, and cancel does not terminate external or non-Sailbox agent code. If you own that process, stop it separately. You can delete a finished Voyage from its dashboard detail page; this removes its history and content from the dashboard and API. Security audit records are retained. There is no API or SDK delete yet.