Skip to content

Observability

Two layers: run traces (execution telemetry you export to your own backend) and a governance audit log (decisions, kept server-side and queryable).

Run traces

Every operation emits an OperationTrace — the operation → agent → llm/tool tree with token usage and full prompt/response content — to a pluggable sink you own. Built-ins: LoggingTraceSink, JsonlFileTraceSink, and OTelTraceSink, which maps onto OpenTelemetry GenAI semantic conventions and ships spans over OTLP to any backend (Jaeger, Tempo, Langfuse, Phoenix, …). All operations of one run share a trace_id.

from boundflow import BoundFlowWorker
from boundflow.trace import OTelTraceSink

worker = BoundFlowWorker(llm=..., trace_sink=OTelTraceSink(tracer))

See sdk/python/examples/otel/ for a runnable OTLP → Jaeger setup.

Approval audit

Approval decisions are governance, not telemetry, so the decision / actor / timing live in a durable server-side audit log — the trace carries only the approval_id (on the await_approval span) as the correlation key. Look the record up by that id:

records = await cp.get_approval_audit(approval_id="…")
# -> decision (approved | rejected | timed_out), actor, opened_at, decided_at

Inventory & run history

# Every workflow with its current lifecycle / workflow state:
workflows = await cp.list_workflows()

# The tenants in your tenant group:
tenants = await cp.list_tenants()

# Per-workflow run history, with each run's outcome:
runs = await cp.list_workflow_runs(workflow_id)

# The status/outcome of a single run, by the request id invoke returned:
info = await cp.get_request_info(request_id)

Each run reports a run_outcomesuccessful, customer_marked_failure, uncaught_operation_exception, operation_timeout, or interrupted — plus a failure reason where applicable.