Skip to Content
Getting StartedConceptsData Model

Data model

Everything in Morse AI is built from three nested objects: a thread holds many traces, and a trace holds many spans. Spans are typed from the moment they’re recorded — and that one decision is why cost, context, memory, and eval data all show up consistently no matter what instrumented the run.

Span

A span is one unit of work inside a run: a single LLM call, a tool call, a database query, an HTTP request, a retrieval, or a memory operation. Every span carries a type drawn from a fixed, canonical set — eight of them:

agent · llm · tool · http · db · custom · memory · retriever

The type isn’t a label you attach for display — it’s enforced at ingest. The valid set is derived directly from the backend enum, so both the SDK-batch and OTLP ingestion paths accept exactly the same types and reject anything else.

Alongside its type, a span records timing (started_at, ended_at, duration_ms), status (running / completed / failed / timed_out), and — where the type calls for it — LLM fields (model, input_tokens, output_tokens, cost_usd, cache tokens), context-profile aggregates, and a type-dependent input/output payload.

Trace

A trace is one complete agent run, assembled from its spans. It carries the run’s status, success/outcome, duration, and denormalized rollups (total_tokens, total_cost_usd, span_count) so the product never has to re-aggregate spans on every page load.

Spans link to their trace by value, not by foreign key: a span’s trace_id is matched to the trace’s trace_id. This is deliberate — the schema is designed to migrate to ClickHouse without rewriting, and column stores don’t do FK joins. The only foreign key in the whole data plane is a trace’s link to its agent.

Thread

A thread groups the traces of a single conversation or session — a multi-turn chat, a long-running workflow — so you can look at “every run in thread Y” as one unit. thread_id lives on the trace; a related user_id is your application’s own end-user identifier (a string like usr_123), not a Morse account.

So the hierarchy reads top-down: thread → traces → spans.

Why typed-from-day-one matters

A generic tracer sees a db span as raw SQL and an http span as a URL. Because Morse AI types spans at the source, every downstream capability reads one normalized model instead of a bespoke schema per feature:

  • Cost is attributed by span type. llm spans price per-token; memory spans classify into rag (vector search, per-query) or memory (per-call); tool/http/db/agent/custom spans have no model cost and are excluded from coverage rather than charged a fake zero.
  • Context & memory profiling keys on type — context-segment processing gates on llm, memory operations gate on memory.
  • Semantic enrichment comes from type: a typed db span can be recognized as a LangGraph checkpoint, a vector search, or a memory store — distinctions a raw-SQL view can’t make.

That’s the payoff of typing once, at ingest: cost, context, memory, and evals all dock onto the same span without special-casing where the run came from.

Last updated on