All docs
Docs · Reference
Calls
a stack runs another stack and gets an answer
A call runs another stack of the same tenant in process, inside the same request, on a fresh envelope, and brings back one result object. It isolates data, not authority: the callee runs with the request's tenant, principal, secrets and run grant. A call is not a security boundary. Not calling a stack protects nothing that stack can reach, and calling it grants nothing a jump would not.
ONE REQUEST, shared by all its frames: tenant, principal, rid, opstack
snapshot, fuel and TTL, deadline, trace, secrets, run grant
FRAME 0: app (the inlet's) FRAME 1: app/_orient (a call)
+------------------------------------+ +------------------------------------+
| 100 load the job | | boundary: app/_orient only |
| | | |
| 200 EXEC "call://app/_orient" |-input->| 100 WHEN @src == "call" |
| WITH input = ..., | | reads @call.input |
| into = "_orient" | | 200 ... |
| : | | 300 EMIT ._call.output = ... |
| : the op waits | | |
| v | | ends on: @halt, no scope left, |
| ._orient = { ok, output, |<-result| an error, or the deadline |
| error, stack, version, | | |
| ms } | | never runs app's scopes; |
| | | a jump out ends the call |
| 300 WHEN ._orient.ok == true ... | | |
+------------------------------------+ +------------------------------------+ | To | Read |
|---|---|
| Call a stack | Calling |
| Answer a call | Answering |
| Read the result | The result |
| Declare what a stack takes and answers | Declaring an entry |
| Know what a call may and may not do | The boundary, What a call refuses |
| Bound a call | Deadline and cancellation, Depth and fuel |
| See it in a trace | In the trace |
| Choose between a call and a jump | A call, a jump, a capability |
Calling
WHEN ._job.id =~ /./
WITH input = &object("job", ._job, "text", ._msg.text),
into = "_orient",
timeout = 20000
EXEC "call://app/_orient" call://<stack> | Enter the stack at its first scope. |
call://<stack>/<scope> | Enter it at a floor, as a jump would. The last segment is the scope when it is all digits. |
WITH input | What the callee is given, as JSON (default {}). It is the only part of the caller’s envelope the callee sees. |
WITH into | Where the result object lands (default _call). |
WITH timeout | The call’s own limit, in ms or as a duration (default --call-default-timeout, 60 s). See the deadline. |
WITH stack | Replaces the ref’s stack from data, as WITH name does for cap://. A value that is not a stack name answers txco_call_target. |
WITH grant | A run grant id (rgr_…) this stack minted. The callee runs under it: the gateway signs it a token of its own, the callee’s run holds the token in its context (as a node’s web head would have put it there, never in an envelope), and every cap:// the callee makes is decided at the gateway first — the hard checks, the proposal, _grant, the verdict, the budget — before a name declared here answers it in process (capabilities). A grant that is not this stack’s, not live, or that no gateway here can sign for answers txco_call_grant as data, and the callee never runs. |
A literal target is checked at txco apply, like goto://. Whether the
callee exists is not: it deploys separately, and a call to a stack the
chassis does not have answers txco_call_no_stack as data. txco apply warns when the callee is not in the same apply.
Answering
A callee starts from a fresh envelope the chassis writes:
| Field | Value |
|---|---|
@src | "call" |
@call.input | the resolved WITH input, read-only like all of _txc |
@call.caller | {stack, stage, op} of the calling rule |
@call.depth | 1 for a call from a request, one more per nested call |
@tenant, @rid, @principal, _ts | the request’s |
@stack | the callee’s own stack, so its kv, notebook, read-file and dataset defaults are its own |
Rules cannot write _txc, so a callee cannot forge its input or its
caller. Gate the entry scope on the source, as a capability’s gates on @cap.name, so no other route into the stack can pose as a call:
# OPS/app/_orient/100_COMPILE/compile.txcl
WHEN @src == "call" AND @call.input.job.id =~ /./
...
EMIT ._call.output = &object("prompt", ._prompt, "sources", ._sources) The callee answers by writing, at the top level of its own envelope:
._call.output | Any JSON. What the caller gets as output. |
._call.error | {code, message}, or a string (the message). What the caller gets as error, with ok: false. |
Nothing else of the callee’s envelope comes back: not its working fields,
not its @web.res, not its _txc. The callee ends on @halt, when it has
no scope left, on an error, or on the deadline; the chassis then returns to
the caller in every case. A @halt ends the callee, never the request.
A callee that makes a call of its own with the default into leaves that
inner result at its _call, which is also what its caller reads back; a
callee that wants to answer for itself after such a call overwrites ._call.output (and clears ._call.error with @delete if it handled the
inner failure).
Declaring an entry
A stack that is called from several places, or by rules someone else
maintains, declares its entries: one small file per entry under the
reserved CALLS/ directory, beside CAPS/:
# OPS/app/_orient/CALLS/orient.yaml
description: Compile the context needed for a job.
entry: 100
timeout: 20000
input:
job:
description: the job record
required: true
text:
description: the message to answer
output:
prompt:
description: the compiled prompt
required: true
sources:
description: the documents it drew on | The name | The file’s stem, [a-z][a-z0-9_-]*. An entry is the stack’s own: two stacks may each have an orient. |
entry | The scope the call enters at (default 0, the stack’s start). It must name a scope the stack has; txco apply refuses one that doesn’t. |
timeout | Milliseconds. It can only shorten the call’s own timeout. |
input, output | The fields the call carries and the answer carries: <field>: {description, required}, each [a-z][a-z0-9_]*, at most 32. Not a schema: the chassis checks names and required, nothing about shape. |
The shape is the capability declaration’s, so a stack that answers callers inside the tenant and outside it writes the same contract twice under two names.
A rule names the entry when it calls:
WITH input = &object("job", ._job, "text", ._msg.text), entry = "orient", into = "_orient"
EXEC "call://app/_orient" - The call enters at the declared scope, and
.entrynames the entry to the callee. Naming an entry and a scope together (call://app/_orient/100withWITH entry) istxco_call_target. - A missing required input fails before the callee runs:
txco_call_invalid_input, naming the fields. - A missing required output fails after it ends:
txco_call_invalid_output, withoutputstill carrying what the callee did write. - An entry the stack does not declare is
txco_call_no_entry.txco applywarns when a literalWITH entrynames one its callee, in the same apply, lacks. - A call that names no entry is checked for nothing, as before.
txco calls list and txco://calls/list show every entry the tenant’s
active stacks declare, with what each takes and answers, so a stack that
composes others reads the contracts instead of carrying a copy.
examples/call-hello is all of this in two stacks: a declared entry, a
capability answered here, both called from one scope, and a refusal
answered as data.
The result
When the call ends, the caller finds one object under into, and nothing
else:
"_orient": {
"ok": true,
"output": { "prompt": "…", "sources": ["…"] },
"error": null,
"stack": "app/_orient",
"version": 12,
"ms": 41
} ok | The chassis’s verdict: the callee ended normally and wrote no ._call.error. A callee that wrote neither answers ok: true, output: {} — a call made for its effects is normal. |
output | The callee’s ._call.output, or {}. Still carried when ok is false, with whatever the callee did write. |
error | null, the callee’s ._call.error, or the engine’s own (below). |
stack, version | Which stack answered, and its active version number in the request’s pinned snapshot (0 when it cannot be told). |
ms | The call’s wall clock. |
Since nothing outside into is written, two calls in one scope cannot
collide. The caller gates as it does on an outlet:
WHEN ._orient.ok == true EMIT ._prompt = ._orient.output.prompt
WHEN ._orient.ok == false EMIT ._fallback = true, ._why = ._orient.error.code The engine’s own error codes:
| Code | When |
|---|---|
txco_call_no_stack | No stack of that name in this tenant. |
txco_call_target | WITH stack is not a stack name, WITH entry is not an entry name, or an entry was named beside a scope. |
txco_call_no_entry | The callee declares no such entry (declaring an entry). |
txco_call_invalid_input, txco_call_invalid_output | The call lacks an input the entry requires (checked before the callee runs), or the answer lacks an output it promises (checked after; output carries what was written). |
txco_call_boundary | The callee jumped to another stack (the boundary). |
txco_call_unsupported | The callee tried to suspend or stream (what a call refuses). |
txco_call_timeout, txco_call_cancelled | The deadline, or the request was cancelled; stopped and running say what the callee’s ops did (deadline). |
txco_call_depth | Calls nested past the limit (depth). |
txco_fuel_exhausted, txcl_scope_ttl_exhausted, txco_allowance_exhausted | The request’s budget ran out inside the callee; the request ends at its next charge. |
txco_call_failed | The callee’s run failed without an answer (a message says how). |
txco_call_error | The callee wrote a ._call.error with no code. |
The boundary
A call runs the named stack, and only it.
- Lookups never peel. Outside a call, a slash-named stack is an overlay:
when
app/onboardhas nothing at or above the next scope, the lookup falls back toappat the same scope and runs on there. Inside a call the lookup resolves the callee’s own stack exactly, and when it has no scope left the call ends. A_-named child (app/_onboard) and a slot (app/onboard) are called alike. - Jumps stay inside. A bare-scope jump (
EMIT @goto = "300") resolves in the callee as it does anywhere. A jump to another stack —@goto,goto://, an unschemedEXEC "<stack>/<scope>",txco://route— ends the call withtxco_call_boundary. To use another stack from inside a call, call it.
| Mechanism | Runs the parent’s scopes too? |
|---|---|
goto://app/onboard/0 (and @goto, unschemed EXEC) | Yes: the overlay, unchanged |
call://app/onboard | No |
call://app/_onboard | No |
With app = {100, 200} and app/onboard = {100, 150}, from app/100: a
jump to app/onboard/0 runs onboard’s 100 and 150 and then app’s 200, as
part of onboard, and nothing comes back; a call of app/onboard runs 100
and 150 only and returns, and the caller’s next scope runs with the result
in hand.
What a call refuses
A call cannot suspend and cannot stream; the chassis’s continuation model
is tail-call only, so nothing could return into the caller’s op afterwards.
Inside a call these end it with txco_call_unsupported, decided before
anything is dispatched, the same on every chassis whether or not it has a
continuation store:
WITH mode = "async", with or withoutjoin_at_scope;WITH mode = "continuable";@web.res.bodywritten in a scope that is not the callee’s last (the explicit streaming idiom);workspace://… WITH stream = truerefuses itself, as it does on any run with no client to stream to.
What a callee may use: every synchronous op, LOOP and LOOP EVERY, txco://schedule, outlets, kv, ai://, websocket send and reply, and
calls of its own.
Deadline and cancellation
The call’s timeout is what it asks for. Its deadline is the least of:
the caller's remaining deadline
the call's timeout (WITH timeout, default --call-default-timeout = 60 s)
the chassis maximum (--op-timeout-max; a call over it is clamped, not dropped) A callee can never extend its caller’s lifetime.
On cancellation, the call returns once the callee has stopped. The
chassis cancels the callee’s context and waits up to --call-stop-grace (default 2 s) for the callee’s ops to return, then answers txco_call_timeout (or txco_call_cancelled) with:
"error": { "code": "txco_call_timeout", "message": "app/_orient ran past its deadline",
"stopped": false, "running": ["app/_orient/300/fetch"] } Ops that honour their context — http, ai://, outlets, kv — stop well
inside the grace, and stopped is true. An op that ignores cancellation
cannot be stopped by the chassis: a write already handed to a database, a workspace:// command already running, mail already submitted. If such an
op outlives the grace, stopped: false says so and running names it; its
side effects may land after the call has returned.
Depth and fuel
- Depth. Calls nest to 8;
--call-depth-maxcan lower that, never raise it. Over the limit, the result istxco_call_depth. Recursion is allowed within the limit. - Fuel is the request’s: the call pays the
EXECdispatch (25), and the callee’s scopes and ops charge the same ledger at the usual rates (fuel). Exhaustion inside the callee ends the callee with the exhaustion as its error, and the request at its next charge. - The hop countdown is the call’s own. Each call starts at
--op-scope-ttl-max, as the request did, so a sub-flow pays for its own loops and the request never for the sub-flow’s hops — a capability answered in process walks its whole declaring stack each time. A callee that uses its countdown up ends withtxcl_scope_ttl_exhaustedas the call’s error; the caller’s own countdown is untouched and it goes on. Depth bounds the total. - The transition history is the call’s own. Each call starts with an empty one, so a sub-flow called twice in one request pays no repeat penalty; a tight loop inside one call still does.
In the trace
The callee’s stages and steps append to the caller’s request trace: it is
the same request. Two events bracket them, call.enter and call.return,
carrying the stack, the callee’s resolved version, the depth, the wall
clock and the outcome, so a trace always says which code answered. A jump
out of the callee records call.boundary before the call ends. In
OpenTelemetry the callee’s spans are children of the calling op’s exec-call://… span.
Redaction hints of every stack a request visits apply to the whole trace, so a callee’s hints also cover the caller’s later steps.
Capabilities answered here
The same engine has a second way in. EXEC "cap://<name>" on a chassis
where a stack of the request’s tenant declares <name> (CAPS/, capabilities) runs that stack here, in
process, as a frame on this request — not up to a parent, and not through
the bus. The two ways in differ only in what they stamp and how they shape
the answer:
call:// | cap://, declared here | |
|---|---|---|
| Names | a stack, by place | a capability, by name; the catalogue says which stack |
| Enters at | the ref’s scope (default 0) | the declaration’s entry |
| The callee sees | @src == "call", @call.input, @call.caller, @call.depth | @src == "cap", @cap.name, @cap.input, @cap.caller, @cap.depth, @cap.impl |
| The callee answers at | ._call.output / ._call.error | ._cap.output / ._cap.error, as it would a call from a node |
| The caller gets | one result object under into | cap://‘s shape: the output under into, cap.name, cap.status, cap.ms, failures at cap.error |
| Bounded by | WITH timeout | WITH timeout, and the declared timeout when shorter |
| No answer | ok: true, output: {} | txco_cap_unknown (the inlet’s contract: no rule answered) |
Everything else is the same: the boundary, the refusals, the deadline and
the wait on cancellation, the depth, the shared budget, the per-frame
history, the trace events (marked via: cap). A capability may call a
capability, or a stack, and the depth counts across both.
A run under a run grant — a request that arrived with the grant’s token
at the web head, or a callee under WITH grant — is one difference: its cap:// call to a name declared here is decided at the grant gateway
before the declaring stack runs, and that stack is stamped with the run
that asked (@cap.run, @cap.grant, …), as it would be at the inlet. A
rule of the tenant’s own code presents no grant and runs the declaring
stack with the request’s authority, as before.
A call, a jump, a capability
| Moves the stage | Moves the identity (@stack) | Returns | Runs the parent’s scopes | Isolation | Authority | |
|---|---|---|---|---|---|---|
@goto, goto://, EXEC "<stack>/<scope>" | yes | no | no | yes (overlay) | none | the request’s |
txco://route | yes | yes | no | yes (overlay) | none | the request’s |
call:// | for the callee | for the callee | yes | no | envelope only | the request’s; WITH grant: the named run grant, for the callee’s cap:// calls |
cap://, declared here | for the callee | for the callee | yes | no | envelope only | the request’s (above); a run under a run grant is decided by _grant first (capabilities) |
cap:// (up to a parent) | another chassis | — | yes | — | envelope | the run grant, decided by _grant (capabilities) |
Use a jump to hand a request over for good (the boot → service handoff, a channel’s entry). Use a call for a sub-flow whose answer you need: compiling a context, validating a record, a step several stacks share. Use a capability when the work runs on a node and must ask this chassis to do something under a grant.
Rolling out. A chassis that predates calls does not refuse call:// cleanly: call://x/100 matches the unschemed stage pattern and is run as a
jump to a stack literally named call://x, which ends the request there.
Do not push a stack that uses call:// until the fleet runs a release that
has it.