Runs: how an operation executes
When you invoke an element operation, the platform creates a run: a tracked execution with inputs, status, output, logs, and a cost debit. Runs are how you see what happened and why.
The execution model
Every run starts the same way: a POST to an operation URL.
POST /api/{circle}/{slug}/ops/{operation}

The operation and its input schema come from the element’s chemistry YAML and flow through generated code, so the runtime, the docs, and the agent tools all agree on what a given operation accepts. The runtime never branches on element type to decide how to run it — dispatch is driven by the element’s category metadata, the same element-agnostic contract every other surface follows. That is why one URL shape and one run model cover Python, SQL, an LLM call, and a browser action alike.
Where the work actually happens depends on the element family, and this is the part worth understanding — a run is a uniform envelope around very different machinery:
- Code actions (
python,javascript,go-fn, …) run in a sandboxed Firecracker microVM on the isolation fleet. The sandbox is the security boundary: isolation workers have zero internet egress, so untrusted code can compute but cannot phone home. How that boundary actually holds — the microVM, the snapshot restore, the no-egress network — is its own concept (see isolation). - Intelligence operations (a
lab/braincall) go through the in-process LLM gateway, which fronts multiple providers behind one interface. - Data queries (
sql,vector, and the rest of the data category) execute against Postgres inside the circle’s own schema (circle_{uuid}) — never another circle’s.
Same envelope, different physics underneath. The dispatch layer that routes each one lives in the element runtime engine; the substrates it dispatches to are the compute, storage, and network force providers.
A run carries the four things you reason about after the fact:
- Inputs — the JSON body you posted, validated against the operation’s schema.
- Status — whether the run is in flight, finished, or failed. The live view of this transition is its own concept (see live-events); a run is the execution, the event stream is how you watch it move.
- Output — the operation’s result, or a structured error. Errors follow the
platform’s typed
_suggestioncontract, so a 4xx tells you why and how to fix it, not just what broke. - Logs and diagnostics — every run emits an OpenTelemetry trace span
(
ops.execute), tagged withtriform.circle,triform.element, andtriform.operation. That is the durable record of a run: filter on those tags in SigNoz to find exactly what one invocation did, how long it took, and where it failed.
Async invokes: the envelope is not the result
Not every invoke returns its output inline. Sandboxed code actions (python,
javascript) are asynchronous by design: the microVM boot and execution
take real time, so ops/invoke answers immediately with a run envelope —
{"status": "running", "run_id": "..."} — and the actual output arrives
later. Other action families (go-fn, ruby, rust-fn, csharp) currently
respond synchronously, which is exactly why this trap bites: a caller tested
against a sync element “just works”, then silently breaks when pointed at a
Python action.
The contract for any caller — a frontend, an agent, a script:
- If the response carries a terminal
status(completed/failed), use it. - If it carries a
run_idwith a non-terminal status, pollGET /api/{circle}/{slug}/ops/runs/{run_id}until the status is terminal, then readoutput(or the structured error). - Never render “no data”, “empty”, or “saved” off the envelope itself. The envelope contains no output; treating it as the result is how a working backend gets reported broken — or how a page claims success while the write is still in flight or about to fail.
Pages served on the platform get a helper for free:
window.triform.invokeAndPoll(circle, slug, payload, opts) does the
invoke-then-poll dance (inline fast-path included) and resolves with the real
output. Frontends that call actions should use it — or implement the same
poll loop — rather than a bare fetch().then(render).
Cost
A run is also the unit that gets billed. Metered operations debit AU from the circle’s wallet when they execute; how much depends on per-element pricing, and the full cascade — what’s free versus metered, and what happens when the balance runs out — is owned by billing-au. The point for the execution model is simply that the run is the thing the debit attaches to: no run, no charge.
Related
- Concept: live-events — watching runs happen in real time
- Concept: billing-au — what a run costs
- Concept: isolation — how a code-action run is sandboxed
- Concept: elements