Download all docs

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}
Lead Scorer Python action selected on the logged-in Triform hex canvas.
Action elements use the same canvas grammar as data and frontend elements, so code, data, automations, and UI stay visually comparable.Live triform.dev - 1920x1080 - 433914e4d138Open full size

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/brain call) 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 _suggestion contract, 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 with triform.circle, triform.element, and triform.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_id with a non-terminal status, poll GET /api/{circle}/{slug}/ops/runs/{run_id} until the status is terminal, then read output (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