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 ...  |        |                                    |
  +------------------------------------+        +------------------------------------+
ToRead
Call a stackCalling
Answer a callAnswering
Read the resultThe result
Declare what a stack takes and answersDeclaring an entry
Know what a call may and may not doThe boundary, What a call refuses
Bound a callDeadline and cancellation, Depth and fuel
See it in a traceIn the trace
Choose between a call and a jumpA 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 inputWhat the callee is given, as JSON (default {}). It is the only part of the caller’s envelope the callee sees.
WITH intoWhere the result object lands (default _call).
WITH timeoutThe call’s own limit, in ms or as a duration (default --call-default-timeout, 60 s). See the deadline.
WITH stackReplaces 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 grantA 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:

FieldValue
@src"call"
@call.inputthe resolved WITH input, read-only like all of _txc
@call.caller{stack, stage, op} of the calling rule
@call.depth1 for a call from a request, one more per nested call
@tenant, @rid, @principal, _tsthe request’s
@stackthe 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.outputAny 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 nameThe file’s stem, [a-z][a-z0-9_-]*. An entry is the stack’s own: two stacks may each have an orient.
entryThe 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.
timeoutMilliseconds. It can only shorten the call’s own timeout.
input, outputThe 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 .entry names the entry to the callee. Naming an entry and a scope together (call://app/_orient/100 with WITH entry) is txco_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, with output still carrying what the callee did write.
  • An entry the stack does not declare is txco_call_no_entry. txco apply warns when a literal WITH entry names 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
}
okThe 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.
outputThe callee’s ._call.output, or {}. Still carried when ok is false, with whatever the callee did write.
errornull, the callee’s ._call.error, or the engine’s own (below).
stack, versionWhich stack answered, and its active version number in the request’s pinned snapshot (0 when it cannot be told).
msThe 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:

CodeWhen
txco_call_no_stackNo stack of that name in this tenant.
txco_call_targetWITH stack is not a stack name, WITH entry is not an entry name, or an entry was named beside a scope.
txco_call_no_entryThe callee declares no such entry (declaring an entry).
txco_call_invalid_input, txco_call_invalid_outputThe 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_boundaryThe callee jumped to another stack (the boundary).
txco_call_unsupportedThe callee tried to suspend or stream (what a call refuses).
txco_call_timeout, txco_call_cancelledThe deadline, or the request was cancelled; stopped and running say what the callee’s ops did (deadline).
txco_call_depthCalls nested past the limit (depth).
txco_fuel_exhausted, txcl_scope_ttl_exhausted, txco_allowance_exhaustedThe request’s budget ran out inside the callee; the request ends at its next charge.
txco_call_failedThe callee’s run failed without an answer (a message says how).
txco_call_errorThe 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/onboard has nothing at or above the next scope, the lookup falls back to app at 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 unschemed EXEC "<stack>/<scope>", txco://route — ends the call with txco_call_boundary. To use another stack from inside a call, call it.
MechanismRuns the parent’s scopes too?
goto://app/onboard/0 (and @goto, unschemed EXEC)Yes: the overlay, unchanged
call://app/onboardNo
call://app/_onboardNo

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 without join_at_scope;
  • WITH mode = "continuable";
  • @web.res.body written in a scope that is not the callee’s last (the explicit streaming idiom);
  • workspace://… WITH stream = true refuses 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-max can lower that, never raise it. Over the limit, the result is txco_call_depth. Recursion is allowed within the limit.
  • Fuel is the request’s: the call pays the EXEC dispatch (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 with txcl_scope_ttl_exhausted as 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
Namesa stack, by placea capability, by name; the catalogue says which stack
Enters atthe 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 getsone result object under intocap://‘s shape: the output under into, cap.name, cap.status, cap.ms, failures at cap.error
Bounded byWITH timeoutWITH timeout, and the declared timeout when shorter
No answerok: 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 stageMoves the identity (@stack)ReturnsRuns the parent’s scopesIsolationAuthority
@goto, goto://, EXEC "<stack>/<scope>"yesnonoyes (overlay)nonethe request’s
txco://routeyesyesnoyes (overlay)nonethe request’s
call://for the calleefor the calleeyesnoenvelope onlythe request’s; WITH grant: the named run grant, for the callee’s cap:// calls
cap://, declared herefor the calleefor the calleeyesnoenvelope onlythe request’s (above); a run under a run grant is decided by _grant first (capabilities)
cap:// (up to a parent)another chassis—yes—envelopethe 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.

Edit this page · View as markdown