Skip to content
Go To Dashboard

Choose a call surface

ctx.sapiom gives you three different-shaped ways to get an LLM to do something from a step. Picking the wrong one is a common authoring mistake — usually a trivial task landing on the heaviest surface and running for minutes when it should have returned in a couple of seconds.

One LLM call → ctx.sapiom.llm.run. A platform-driven agentic loop (multi-turn, tool use, no filesystem) → ctx.sapiom.models.run. Dispatching an already-deployed agent by its slug → ctx.sapiom.agents.run. You never pick a raw provider model — you pick a routing label (or omit it for the platform default), and the platform discloses back the resolved class and lane it actually served you on. On the surfaces that support scheduling, you set deadlineMinutes instead of naming a model and let the platform place the call.

Your taskSurfaceWhat to expect
One prompt, one response — extract a field, classify text, translate, generate short JSONllm.runA single synchronous call, always immediate (run_now) — never queues.
Multi-turn reasoning with tools, but no filesystem or sandboxmodels.runRuns Sapiom’s in-server agent loop; returns final text once it’s done.
Editing a real code checkout — needs a filesystem and shellmodels.coding.runRuns in an isolated sandbox; returns a live Sandbox handle. See Agent authoring.
Running an already-deployed agent (a separate automation) from a stepagents.runWhatever that agent’s own steps take. Addressed by its slug, not inlined.
A routed call you’re willing to queue for capacity, or a burst of repeated callsllm.submit / llm.redeem, or llm.createSession / callSessionDeferred: deadlineMinutes controls how long you’re willing to wait. Sessions add repeatable calls against one reservation.
const reply = await ctx.sapiom.llm.run({
request: {
messages: [{ role: "user", content: "Classify the sentiment of: 'This is fantastic!'" }],
max_tokens: 64,
},
model: "smart", // a routing label — omit for the platform default
});
// `reply` is the verbatim Anthropic Messages response. A `thinking` block can
// be present alongside the text block, so filter rather than assume index 0:
const text = (reply.content as Array<{ type: string; text?: string }>).find(
(block) => block.type === "text",
)?.text;

There is no output: schema parameter and no .text accessor today — request is the verbatim Anthropic Messages body, and the return value is that API’s verbatim response (typed Record<string, unknown> by default; pass a generic if you want to assert your own type). For structured output, use tool calling: define a tool whose input schema is the shape you want, force it with tool_choice, and read the tool call’s input.

const reply = await ctx.sapiom.llm.run({
request: {
messages: [{ role: "user", content: "Extract the name and age from: 'Priya is 34.'" }],
max_tokens: 256,
tools: [
{
name: "record_person",
input_schema: {
type: "object",
properties: { name: { type: "string" }, age: { type: "number" } },
required: ["name", "age"],
},
},
],
tool_choice: { type: "tool", name: "record_person" },
},
model: "smart",
});
const call = (reply.content as Array<{ type: string; input?: unknown }>).find(
(block) => block.type === "tool_use",
);
const person = call?.input as { name: string; age: number } | undefined;

Reach for these when you’d rather queue for available capacity than pay for immediate execution, or when you’re making several calls against the same reservation.

// One deferred call — pause the step until the platform grants it:
const handle = await ctx.sapiom.llm.submit({
request: { messages: [{ role: "user", content: "Summarize this transcript…" }], max_tokens: 1024 },
model: "smart",
deadlineMinutes: 30,
});
return pauseUntilSignal(handle, { resumeStep: "useReply" });
// Repeated calls against one reservation — a session:
const session = await ctx.sapiom.llm.createSession({
label: "smart",
deadlineMinutes: 60,
budget: { maxTokens: 2_000_000, ttlMinutes: 120 },
});
const ready = await session.wait();
const reply = await ctx.sapiom.llm.callSession(session, {
messages: [{ role: "user", content: "" }],
max_tokens: 512,
});

A deferred call that can’t be granted by its deadline fails visibly — the resumed step’s status is "failed" with an error such as deadline_exhausted, never a silent hang.

For a task that needs a few turns of reasoning or a remote MCP tool call, but no filesystem:

const result = await ctx.sapiom.models.run({
prompt: "Look up the current weather in Lisbon using the provided tool, then summarize it in one sentence.",
model: "smart", // omit for the platform default
mcps: [{ url: "https://example.com/weather-mcp" }],
});
if (result.status !== "completed" || result.output === null) {
throw new Error(result.error?.message ?? `Model run ${result.status}`);
}
console.log(result.output);

models.run awaits completion; models.launch returns a handle for pauseUntilSignal on longer work. See Agent authoring for the sandboxed coding-agent sibling (models.coding.run), which adds a filesystem for tasks that actually need to edit files.

Composition, not inlining: call an already-deployed agent by its slug from inside another agent’s step.

const result = await ctx.sapiom.agents.run({
definition: "enrich-lead", // the child agent's deployed slug
input: { domain: "acme.com" },
});

Prefer several small, independently deployed agents wired together with agents.run over one large multi-step agent. A small agent is independently testable, versioned, and reusable from more than one caller; a large one couples unrelated concerns into a single deploy unit and step graph. Use agents.launch + pauseUntilSignal when the child is long-running, so the parent step doesn’t time out. See Sub-agents for the full pattern.

Every surface above resolves your (optional) routing label to an actual served deployment — you never name a provider or a specific model id. What comes back instead:

  • served_class — the size the label resolved to (small / medium / large), the vocabulary you’re billed in.
  • lane — which of the lanes below it ran in.
  • usage — metered token counts.

llm.run’s response carries served_class and lane as top-level fields alongside the usual Anthropic response fields — read reply.served_class / reply.lane off the returned object. The typed models.run result does not yet surface servedClass/lane on its outcome — that disclosure is on the wire but not yet mapped into the SDK’s typed return value, so don’t rely on it from models.run today.

On the surfaces that take deadlineMinutes (llm.submit, llm.createSession) — llm.run and the other synchronous surfaces are always run_now and take no deadline:

deadlineMinutesLane
Omitted, or <= 0run_now — immediate, top priority
<= 15priority
<= 60standard
> 60flex

A longer deadline gives the platform more room to place your call cheaply; there’s no way to name a lane directly — you express your patience as a deadline, and the platform derives the lane from it.

A step’s evidence (the exact request it sent and the exact response it got back) is part of the ordinary run audit — see Inspect a run for reading it from Run Inspector or sapiom_dev_agents_inspect, the same way you’d debug any other step.

For a programmatic escape hatch — full-fidelity input, output, error, and logs for one specific step attempt, at a raised size cap — call:

GET /v1/workflows/executions/:id/steps/:stepId/io

The step-attempt row id is steps[].id on the execution detail (GET /v1/workflows/executions/:id), not the step’s name — a retried step writes a new row per attempt, each with its own id.