Flow Studio
OpenTelemetry Traces
Each node execution is an OpenTelemetry span, nested under the parent workflow execution span. Trace context is propagated via HTTP headers when executors make outbound calls. Spans are exported to BizFirst Observe (Tempo) for distributed trace visualization.
Span Hierarchy
[Trace: exec-abc-123]
└─ [Span: WorkflowExecution] ← parent span for the full run
├─ [Span: ApprovalNode/node-1] ← per-node span
│ └─ [Span: HILSuspend] ← sub-span for suspension
└─ [Span: EmailNode/node-2] ← per-node span
└─ [Span: HTTP POST /send] ← outbound HTTP call span
Accessing the Current Span
// ctx.Observability.Span gives you the active Activity (OTel span)
public override async Task<NodeExecutionResult> ExecuteAsync(
NodeExecutionContext ctx, CancellationToken ct)
{
// Add custom span attributes
ctx.Observability.Span?.SetTag("order.id", orderId);
ctx.Observability.Span?.SetTag("order.amount", amount.ToString());
// Make an HTTP call — trace context is propagated automatically
var response = await _httpClient.PostAsync("/api/payments", content, ct);
// The HTTP call becomes a child span under the node span
}
Trace Context Propagation
When an executor makes an outbound HTTP call, the OpenTelemetry SDK automatically injects the trace context into the request headers (traceparent, tracestate). Downstream services that support OTel will create child spans linked to the node's span.
// Headers added automatically by OTel HttpClientInstrumentation:
// traceparent: 00-{traceId}-{spanId}-01
// tracestate: bfai={tenantId}
// Any HttpClient registered with AddHttpClient() automatically participates
// No manual header injection needed
Spans in BizFirst Observe (Tempo)
# Find traces for a specific execution in Tempo (TraceQL):
{ resource.nodeId = "node-approval-1" && resource.executionId = "exec-abc-123" }
# Find all slow node executions (> 5s) across all executions:
{ resource.nodeType = "ApprovalNode" } | duration > 5s
ctx.Observability.Span may be null if OTel is not configured in the environment (e.g., local development without Tempo). Always use the null-conditional operator:
ctx.Observability.Span?.SetTag(...) — never assume the span exists.