Download all docs

Live events

Triform pushes live events as things happen — element created, run progressed, run completed, state changed. The UI updates in real time, and your own code can subscribe to the same stream.

This is the real-time, watching lens. A run is the execution itself — its inputs, status, output, and cost. Live events are the notifications that execution emits as it moves. The two are separate concerns: you read a run to see what happened; you subscribe to live events to watch it happen.

The real-time model

Every operation a circle runs emits a lifecycle stream without anyone writing emit code. The contract is fixed: one Started before the work dispatches, exactly one terminal at the end, and zero or more Progress events in between. Each kind carries a different payload because each answers a different question:

KindCarriesAnswers
startedinputwhat was asked, and by whom
progressprogresshow far along (op-specific JSON)
completedoutputthe result
failederrorwhy it failed
cancelledthat it was stopped

Every event also carries an invocation_id (stable across the whole lifecycle of a single operation, so progress and the terminal correlate back to their start), an actor describing who triggered it (user / agent / system / external), and a trace_id that matches the operation’s span in observability. The live stream and the trace history are two views of one event.

The terminal is the load-bearing one — unique per invocation, so a watcher always knows exactly when an operation is done and how it ended. It is emitted after the underlying state commits, never before, so a watcher never acts on a result a concurrent reader cannot yet see. Progress, by contrast, is best-effort: human- meaningful checkpoints, not a per-iteration firehose, and a dropped one never changes the outcome the terminal reports.

How the UI stays coherent

When the portal receives a Completed event it does one of two things, decided per operation by generated policy — never by hand-matching on element type:

  • Apply — for operations whose output is the element’s full new state, the portal writes that output straight into its cache. No refetch, no flicker.
  • Invalidate — otherwise, the portal drops its cached copy and lets the next reader refetch. This is the safe default, and Failed / Cancelled always invalidate.

The same discipline that protects the cache is what you honor when authoring an operation: if you declare that an operation’s output replaces the element, that output must be the element’s complete shape, never a partial or a child object — otherwise the cache silently corrupts. When in doubt, let the portal invalidate.

Subscribing

The authenticated stream is one Server-Sent Events endpoint, scoped to a single circle:

GET /api/events/{circle}?subjects=<pattern,pattern>
Accept: text/event-stream

Subjects on the wire are {element_type}.{op_name}.{kind} — for example automation.execute.completed or linkedin.ingest-search-url.progress. Patterns are NATS-style: * matches one segment, > matches any tail, and commas separate multiple patterns. Subscribe to exactly the slice you care about:

subjects=automation.*.completed     every successful automation operation
subjects=*.update.completed         every successful update across the circle
subjects=sales-board.>              every event on sales-board

Subscribing requires an authenticated caller with access to that circle (or platform-admin) — the stream is filtered to the circle’s events and never leaks another circle’s activity. The reference consumer is the portal itself, which drives every live update in the workbench from this one endpoint.

The public token route

For sharing a live view outside the circle — a branded public face, an embedded widget — there is a separate, narrower door:

GET /api/public/events/{circle}?t=<share-token>

This stream is gated by a share token rather than a session, minted per element for a specific embed purpose (POST /api/{circle}/_share/mint). The public stream is default-deny: events are filtered before they leave the server, so a public viewer sees only what the token authorizes and only the fields the public face is allowed to expose. Where the authenticated stream trusts the caller and streams the full event, the token stream trusts nothing and strips the payload down first — and when a payload has been stripped that way, the portal’s reconciler treats it as invalidate-only rather than applying a partial state. Same event vocabulary, far tighter blast radius.

Related

  • Concept: runs — what the events are about
  • Reference: /docs/api — the event payload schemas
  • Element: websocket, platform-trigger