POST /traces/ingest. A trace is one agent execution: the root record carries the current input, final output or error, agent name, optional thread/user context, and timing. All other work is child spans in trace.spans[], typed as span, generation, or tool, optionally nested via parent_id.
The SDK builds this payload for you. This page defines what Lemma accepts, what each field means, how ingest normalizes it, and which fields unlock which product behavior.
One agent execution = one trace. LLM calls, tool calls, retrieval, and app logic are child spans inside that trace, not separate top-level traces.
Mental model
The SDK sends a trace record plus child spans. At ingest, Lemma creates a synthetic root span (
{traceId}:root) that carries root-level fields for storage and display. Producers should target the trace record; they do not create the synthetic root themselves.
Requirement tiers
Ingest accepts a name-only trace. Semantic fields are not HTTP-required, but without them Lemma can only render timing and a sparse tree. For the pedagogical ideal path, see Building high-quality traces.
Ingest payload
Envelope and validation
Successful ingest returns 201. Invalid shape returns 400. Missing
trace.id or span id values are generated server-side. Missing or invalid spans[].type defaults to "span". Missing parent_id attaches the child under the synthetic root.
Delivery and retries
Deliver one complete trace when the execution (agent turn) finishes. This is required; patching a trace over time is not currently supported. Treatingest as a single delivery, not an incremental merge API.
- Required: one complete delivery per execution — root input/output, thread/user, and all child spans in one payload, sent when the turn is done.
- Retries: re-sending the same payload with the same span IDs is idempotent; spans whose IDs already exist are skipped.
- Not a merge API: omitted root fields do not preserve prior values. There is no field-level upsert, and patching a trace across calls is not supported.
- Processing: after an idle window Lemma processes the trace once; a later successful re-delivery does not restart
processTrace/extractSignals. Late new span IDs may still append for display. - Do not reuse one
trace.idacross conversation turns — see Instrumenting multi-turn agents.
Trace record
Semantic guidance: include root
input and either output or error. Without them, the dashboard can show timing but not what the user asked or whether the run succeeded.
Callback traces set output from the return value unless you override it with trace.output(...). Uncaught exceptions mark the root as failed and still send the trace.
Child spans
Every child is a named object intrace.spans[].
Typed children
Type is a discriminator. Setting
model alone does not make a generation; set type: "generation".
Nesting
Children nest throughparent_id. In TypeScript, recording from a span handle sets the parent automatically. In Python, pass parent_id explicitly on start_tool / start_generation / start_span when the child belongs under another span.
Generations and message history
trace.input is the current user turn. A generation should record the full ordered message list sent to that LLM call, including system prompt, prior turns, tool results, and the current user message.
Prefer both:
input— the prompt payload for that callllmInputMessages/llm_input_messages— structured messages flattened intollm.input_messages.{index}.message.*
model is the primary model identifier. Native generation props such as llmProvider, llmInvocationParameters, and llmOutputMessages populate OpenInference-style attributes. Pass usage (camelCase in TypeScript, snake_case kwargs in Python) when the provider returns token counts — see Token usage.
Token usage
Generations may include a top-levelusage object. Lemma does not invent token counts. Coverage semantics:
Healthy zero ≠ missing instrumentation. Analytics uses absence vs explicit zero to decide whether a metric is unsupported or genuinely empty.
Wire format (ingest JSON)
TypeScript DX
Python DX
gen_ai.usage.input_tokens, gen_ai.usage.output_tokens, gen_ai.usage.cache_read.input_tokens, gen_ai.usage.cache_creation.input_tokens, gen_ai.usage.reasoning.output_tokens) plus OpenInference compatibility keys (llm.token_count.prompt, llm.token_count.completion).
Do not derive user-facing aggregates or estimated cost in the SDK — the backend computes windowed Analytics quantities from this source telemetry.
Tools
Tool spans should carry:name— stable tool identifierinput— argumentsoutputorerror— result or failure- optional
toolParameters/tool_parametersfor the schema
tool_name when present, otherwise the span name, and normalizes tool I/O into tool-call aliases for display.
Errors
Record a failure on the exact span, generation, or tool where it happened:- Keep the input that led to the failure when it is safe to record.
- Pass
errorinstead of inventing an output. - End the failed child so timing is preserved.
- Rethrow when the whole execution should fail; leave the root successful when the agent recovers.
trace.fail(error). For completed work, pass error to recordSpan / recordGeneration / recordTool (or the Python equivalents). For live handles, call .end({ error }) / .end(error=...).
- TypeScript
- Python
Timing
Duration resolution order:- Explicit
duration_ms/durationMswhen provided - Otherwise
ended_at - started_atwhen both timestamps exist - Otherwise, for children with missing duration, Lemma allocates remaining parent time
- Starts from the synthetic root and walks the parent → child tree
- Explicit sibling durations claim time first
- Siblings with
duration_ms == nullsplit the remaining parent duration equally - If explicit siblings already exceed the parent duration, omitted siblings receive
0 - The same rule applies recursively when a child becomes a parent
startSpan / startTool / startGeneration) so .end(...) measures real elapsed time. One-shot record* helpers omit child duration when you do not pass it.
Client fields vs ingest normalization
Clients send first-class JSON fields. Ingest generates display and analysis aliases.
Do not hand-build the normalized aliases unless you are bypassing the SDK. Prefer native SDK props and first-class fields.
SDK mapping
- TypeScript
- Python
Native contract props
Use native props instead of hand-building flattened attribute names. In Python, use snake_case.- Common
- Generation
- Tool
- Embedding
- Reranker
model also populates llm.model_name for generation records. Object and array attribute values are serialized where Lemma expects string-valued attributes. For fields without a native prop, pass raw attributes.
Provenance attributes (Lemma extensions)
Every emitted span includes SDK provenance so Analytics can attribute coverage gaps to an integration or language without provider-specific frontend logic:
These are Lemma-specific extensions (not OpenTelemetry GenAI canonical names).
Semantic requirements
Issue detection
Complete, correctly typed trees improve classification and automated issue extraction. Root I/O, model fields, and tool payloads are not general ingest gates. Some environments may further restrict issue extraction to specific framework shapes; treat that as an environment policy, not as the base SDK contract.Privacy
Redact secrets, credentials, and sensitive user data before tracing them. Framework integrations always record inputs, outputs, and error messages: without them a trace cannot show what a run consumed, produced, or why it failed.Related pages
Analytics telemetry
Required attributes for Analytics widgets and upgrade behavior.
Building high-quality traces
Bad → better → best examples for the ideal instrumentation path.
Instrument an agent
Build a complete Lemma trace one piece at a time.
Generations
Capture model, full messages, and completion.
Common issues
Diagnose missing spans, flat trees, and blank I/O.