Skip to content

Observing your Flows

Orvanta can export a distributed trace for every job it runs — a script, a flow step, or a BPMN process instance — over the OpenTelemetry Protocol (OTLP). This page describes what those traces actually look like once you’ve wired Orvanta to a backend. For the environment variables and instance settings that turn tracing on, see Tracing & Logging with OpenTelemetry. For the exact span and attribute names, see Orvanta Telemetry Standards.

Every job’s trace identity is derived from the job’s own UUID, not generated randomly and not stored anywhere. A UUID is 16 bytes and a W3C trace id is 16 bytes, so the job id is the trace id, byte for byte. The job’s root span, full_job, is exported under that derived id when the job completes.

This has a practical consequence: you don’t need to copy a trace id out of a log line. Take the job’s UUID from the Orvanta UI or the CLI, strip the dashes, and paste the 32 hex characters straight into your trace backend’s “search by trace ID” box (Tempo, Jaeger, whatever you’ve pointed Orvanta at).

Orvanta also exposes GET /w/{workspace}/jobs/get_otel_traces/{id}, which is designed to look spans up locally by this same derivation. As of this writing that endpoint has no local writer behind it in the Teams/Enterprise builds — it will return an empty list. Treat your configured OTLP backend as the source of truth for trace data; don’t rely on this endpoint yet.

A regular flow’s trace is what you’d expect: one full_job root span for the flow job, with each step’s own job span (and its job_postprocessing completion span) nested underneath, because every step job resolves its span parent back to the flow’s root job.

A BPMN process instance is different in one important way: it’s long-lived, and the engine executes it through many separate pulls of the same queue row rather than one continuous execution. Every dispatched task parks the process; the task’s completion wakes it back up, possibly on a different worker, for another pull. A flowable:async boundary is the same mechanism — it’s a re-pull of the same job, not a second job.

Because the trace id is derived from the process job’s UUID — the one thing every re-pull preserves — every pull, on every worker, reconstructs the identical trace id with no coordination. The result is one trace per instance, shaped like this:

full_job the process instance root
├── bpmn.step one per pull
├── bpmn.step another pull — e.g. across a flowable:async boundary
│ ├── bpmn Task_1 one span per element execution that closed during that pull
│ └── job a dispatched Script/Service task's own execution span
│ └── job_postprocessing
└── bpmn.step

A few things follow directly from that shape:

  • One instance can show many bpmn.step spans. Each is a pull of the process job, not a step in the BPMN diagram. A flowable:async boundary always forces at least one extra pull, so it always adds at least one extra bpmn.step sibling.
  • Element spans (bpmn <elementId>) appear only once the element closes, with the real [entered, closed] interval — even when the element was entered several pulls, or an async boundary, earlier. An element that’s currently in flight (entered but not yet closed) produces no span until whichever later pull closes it.
  • A dispatched Script or Service task’s job reports into the same trace as its own job span, not as a separate trace linked from the element span. So a BPMN task’s script execution nests naturally under the element that dispatched it.

Reading bpmn.element.entry_observed = false

Section titled “Reading bpmn.element.entry_observed = false”

Element history is capped at 2,000 entries per instance. On a long-running process, the opening Entered transition for an element can be rotated out of that history before the matching Completed or Failed arrives. When that happens, the emitted span still gets a start time — but it’s a floor (the earliest point the engine could still see), not the element’s true entry time. bpmn.element.entry_observed = false is how the span tells you that. true means the interval is exact.

If a script wants to join the trace its job is running under — to emit its own spans, or just to log a correlatable id — Orvanta hands it three environment variables when tracing is on:

VariableValue
TRACEPARENTThe W3C traceparent header value: 00-<32 hex trace id>-<16 hex span id>-01
OTEL_TRACE_IDThe 32-hex trace id alone
OTEL_SPAN_IDThe 16-hex span id alone

This only works for languages that run as a separate OS process per job. That covers most of what Orvanta supports — Bun, Bunnative, Deno, Python, Go, Bash, PowerShell, PHP, Rust, Ansible, C#, Nu, Java, Kotlin, Groovy, Ruby, R, Dart, and MongoDB (which shells out to mongosh). It does not cover native TypeScript, which runs inside the worker’s own in-process deno_core runtime, or the SQL and GraphQL family (Postgres, MySQL, MSSQL, Oracle, BigQuery, Snowflake, DuckDB, GraphQL), which are executed by in-process Rust drivers. In both cases there’s no per-job OS process to set an environment variable on — that gap is real, not an oversight, and closing it needs an in-band mechanism (a runtime global, a SQL comment) that doesn’t exist yet.

Dedicated workers get nothing either, for a different reason: a dedicated worker is one long-lived interpreter process reused across many jobs, so a per-job environment variable would be structurally wrong there — the process outlives any single trace.

Orvanta deliberately does not hand a script the OTLP endpoint itself. If a script wants to emit its own spans, it configures its own exporter — handing every sandboxed script the collector address would open an egress path the platform decided on rather than the operator.

The orvanta CLI can trace itself too: one span per invocation, named after the leading sub-command words (never the arguments — those routinely carry paths and secrets), exported over OTLP. When the CLI has a traceparent to send, it’s injected onto every API call the invocation makes, so the server’s spans join the CLI’s trace and the job it starts links back to it.

This is entirely separate from the platform-side toggle: it activates only when OTEL_EXPORTER_OTLP_ENDPOINT (or OTEL_EXPORTER_OTLP_TRACES_ENDPOINT) is set in the environment the CLI runs in on your machine or CI runner. With neither set, the CLI doesn’t even import the OpenTelemetry SDK — a plain, untraced orvanta invocation is unaffected.

The job the CLI starts still gets its own derived trace id — the CLI’s context isn’t allowed to overwrite it, because GET /jobs/get_otel_traces/:id looks up a job’s spans by that derived id. Instead you’ll see a job_requested span inside the job’s trace carrying a link back to the CLI’s span, and the CLI’s own trace carrying a link forward to the job — two traces, joined by links in both directions, neither one silently swallowing the other.

Orvanta doesn’t run a hosted trace collector today — a managed option is planned but not yet available. Until then, you configure OTEL_EXPORTER_OTLP_ENDPOINT (and friends) to point at an OTLP-compatible backend you operate — Tempo, Jaeger, or a collector in front of either. See Tracing & Logging with OpenTelemetry for the concrete environment variables, the Instance Settings toggle, and example collector configuration.