Starting a turn
Supply the request, agent, trusted context, and execution grant.
envelope
{ threadId, message,
clientTurnId, attachments }application
{ agent, ambient,
budget: { steps: 8 } }runtime.runTurn(...)
→ admit → assemble → execute → settleYour application decides which agent handles a request and what work it may
perform. runTurn starts that invocation; it does not choose the agent or
infer an execution budget from the user's message.
Open a root request
This example uses the starter app's factory:
import { createApp } from "@app/app.js";
const app = createApp();
const outcome = await app.runtime.runTurn({
agent: app.rootAgent,
ambient: app.defaultAmbient,
budget: { steps: 8, timeMs: 30_000 },
envelope: {
threadId: app.defaultAmbient.threadId,
message: "What happens inside a turn?",
clientTurnId: "request-1",
attachments: [],
},
});For a deployed application, derive ambient context from your authenticated
request and selected workspace. The starter's defaultAmbient is a
local example configuration, not an authentication mechanism.
Separate request data from ambient context
| Input | Role |
|---|---|
agent | The definition whose prompt, chain, tools, reads, and emits this turn uses. |
ambient | The shared core (userId, threadId) plus your app-declared scope — plans, tenancy, roles — typed by defineAmbient and validated at admission. |
envelope | The root request's thread ID, message, client turn ID, and attachment references. |
budget | Explicit ordinary steps, an optional time ceiling, and an optional cumulative output-token grant. |
hooks | Optional output callback (onEvent) and cooperative cancellation signal. |
deliverable | A contract this root turn is opened under; completed then requires a validated submission of the named part. |
continuesFrom | Records a relationship to an earlier turn on this turn's record; it restores nothing. |
Ambient values inform access checks and context rendering. They should come from trusted application logic. A client's message cannot grant itself a higher plan or another organization's access.
clientTurnId is part of the request shape; it does not currently provide
idempotency or deduplication. Attachment references also do not load file
contents automatically. Your application must resolve and expose needed data.
Understand admission
The ambient rides in first: userId and threadId must be non-empty, and
ambient.scope is parsed against the schema your app declared with
defineAmbient. A scope that does not parse is refused at the door as
failed with ambient-invalid — never discovered downstream.
A supplied root envelope is schema-checked, must name the ambient thread,
and must fit the configured request-size ceiling (measured in UTF-8 bytes).
A thread admits one active root turn at a time. A concurrent root on that
thread receives failed with admit-refused.
That one-root-per-thread claim lives in the store — claimThread and
releaseThread on the Store interface. The default MemoryStore keeps it
in-process; a durable store adapter that makes the claim atomic serializes
root turns across multiple runtime instances behind a load balancer too. A
mismatched envelope instead fails with envelope-invalid, before model
execution.
Use a brief for programmatic work
runTurn also accepts brief without an envelope. This can open a root turn
for application-initiated work; delegation uses a brief for its child input.
The runtime does not append a user conversation message for a brief-only
invocation.
When both are provided, brief supplies the model request while the envelope's
message is stored as the user message. Prefer one clear input path unless
your application deliberately needs those different values. Use asTool for
delegation rather than manufacturing parent identifiers yourself.
Open a root turn under a deliverable contract
Deliverable contracts are not only for delegation. A root turn can be opened
under one, and the typed overload narrows the result: completed then carries
Delivered<T>, not Delivered<T> | null.
const outcome = await runtime.runTurn({
agent: researcher,
ambient,
brief: "Summarize decree 42",
budget: { steps: 6 },
deliverable: { part: findingsPart, nudges: 1 },
});
if (outcome.kind === "completed") {
outcome.deliverable.data; // typed by findingsPart's schema
}The overload returns DeliveredTurnOutcome<T>, inferred from the part's
schema. The contract's part must be registered in the runtime's
PartRegistry, or runTurn throws a KeelError before the turn opens. See
Completion and deliverables for
how the contract is enforced.
Observe output without inferring success
Use hooks.onEvent for parts emitted by this invocation and await the returned
outcome to learn how it ended. Child output is private by default. Cancellation
uses hooks.signal; see Budgets and cancellation.
The last part your hook sees is not on the payload
After the turn settles, hooks.onEvent receives one final,
framework-reserved outcome part carrying the outcome kind. It is a wire
signal for live consumers and is not included in
payload.parts — apps can neither emit nor forge it.
Inspect in Studio
Run the example request and check TURNS for its gates and grant, then
CONTEXT for the assembled request. Test an invalid envelope separately
and confirm admission fails before a model step appears. Studio's sidebar
edits the ambient scope as JSON — it is validated at the admit gate on every
run, so an invalid scope shows you ambient-invalid live.
Next: Inside a turn.
