Skip to main content
A span represents a unit of work. It has a name, a start time, an end time, and optional attributes. Every trace is a tree of spans. Most of the time, the @observe decorator handles span creation automatically. But sometimes you need direct control:
  • Conditional tracing — Start a span only if certain conditions are met.
  • Dynamic naming — Determine the span name based on runtime data.
  • Long-running operations — Hold a span open across multiple function calls.
  • Non-function boundaries — Trace a block of code that isn’t a function.

Choosing the Right Method

It’s important to distinguish:
  • Creating a span (starts timing) vs activating a span (makes it the current parent for nesting)
  • Active spans (recommended) vs detached/manual spans (advanced)
Active spans automatically parent anything that runs inside them (including instrumented third-party libraries), because context propagation can attach child spans under the active span. Detached/manual spans are useful when you need to pass a span object around explicitly (or across async boundaries where call-stack parenting isn’t enough). Analogy:
  • Active span: create a folder and open it — new files go inside automatically.
  • Detached span: create a folder but don’t open it — new files won’t go inside unless you explicitly place them there.

Span Lifecycle

A manual span follows three steps:
  1. Start — Create the span with a name. It becomes the “current” span, and any child spans nest under it.
  2. Enrich — Add attributes, set input/output, record events or errors.
  3. End — Close the span. This records the end time and sends the span to Laminar.
If you don’t end a span, it won’t appear in your traces. Use context managers (with in Python) or try/finally blocks to ensure spans always close, even when errors occur.

Common Options (and When to Use Them)

MethodCreates spanActivates spanEnds spanUse when
observe()You’re tracing a function or request handler
Laminar.startActiveSpan()You need a parent span outside observe()
Laminar.startSpan()You want a detached span you’ll pass around
Laminar.withSpan()❌ (optional)You have a span object and want it to be current
import { Laminar } from '@lmnr-ai/lmnr';

const span = Laminar.startActiveSpan({ name: 'custom_operation' });
try {
  // Any spans created here become children of custom_operation
  await doWork();
} catch (error) {
  span.recordException(error as Error);
  throw error;
} finally {
  span.end();
}
See also: Laminar.startActiveSpan

In the Laminar UI

  • Manually-created spans look the same as auto-instrumented spans: a node in the trace tree with timing and attributes.
  • If a span is active, any work executed inside it is parented underneath it automatically.

Span Types

Spans can have a type that affects how they appear in the Laminar UI:
  • DEFAULT — General-purpose span.
  • LLM — Language model call (enables token counts, cost tracking).
  • TOOL — Tool or function call within an agent.
  • EXECUTOR — Orchestration or routing logic.
Set the type when creating the span. LLM spans expect specific attributes for cost calculation—see LLM Cost Tracking and the SDK reference for span creation options.