Skip to main content
Raindrop is an AI-native observability platform for production agents. Its model is event-centric: you emit an AI event per interaction, group related events into a conversation, and Raindrop surfaces quality signals, individual failures (stumbles), and recurring failures (issues) on top of that stream. Lemma models the same reality differently. It is trace-centric: one agent execution is one trace — a nested tree of typed spans — and Lemma runs automated issue detection on complete, correctly typed traces. Migrating is mostly a re-instrumentation: instead of emitting an event (and optional spans) per interaction, you wrap each execution in lemma.trace() and record its LLM calls and tools as typed children.
One agent execution = one trace. In Raindrop you often emit one event per interaction and group by convoId; in Lemma you emit one trace per execution and group multi-turn conversations by threadId. See the trace contract.

How the two models differ

Raindrop starts from the event — a single tracked AI interaction created with trackAi() (single-shot) or begin() / finish() (the interaction API) — and layers tracing spans, signals, and feedback onto it. Conversations are formed by sharing a convoId across events. Lemma starts from the trace — one end-to-end execution — and requires its structure to be explicit:
  • One root trace per agent execution carrying the current user input and the final output or error.
  • Typed children — generations, tools, and generic spans, not undifferentiated records.
  • Per-generation message history — the full ordered message list sent to each LLM call.
Because Lemma is an opinionated sink that reads a specific trace shape, the migration is to produce that shape explicitly with the Lemma SDK. See Overview for how Lemma reads your traces.

Concept mapping

The structural intuition transfers cleanly. What changes is that Lemma needs its children typed and its root complete.
Raindrop conceptClosest Lemma primitiveWhat Lemma needs
AI event / interaction (trackAi, begin + finish)Lemma trace record (root)One agent execution = one trace, with input and final output or error
Event name (event)Trace nameA stable agent / workflow name for grouping and filtering
Event input / outputTrace input / outputCurrent user turn on the root; final answer or error on the root
convoIdThread (threadId / thread_id)Groups multi-turn conversation traces
userIdUser (userId / user_id)Per-user slicing
model on the eventGeneration model (type: "generation")Model lives on a typed generation span, plus the full per-call message history
Tracing span (withSpan)Span (type: "span")input and output (or error)
Tool tracking (withTool, trackTool, startToolSpan)Tool (type: "tool")Tool name, arguments, and result or error
Auto-instrumented LLM spansGeneration (type: "generation")model plus the full ordered message list
Span / tool nestingparent_id nestingChildren under the step that caused them
Event properties, feature flagsTrace or span metadataArbitrary JSON attached to the root or a child
Stumbles / issuesLemma issue detectionComplete, correctly typed trees improve automated issue extraction
type is a discriminator. Setting a model on an untyped span does not make it a generation — you must set type: "generation". The SDK typed helpers (recordGeneration / recordTool / recordSpan and their Python equivalents) do this for you.
For per-interaction context such as feature flags or arbitrary properties, attach it as trace or span metadata.
Do not hand-build attribute names. Prefer native SDK props and first-class fields; use raw metadata only for context that has no dedicated field. See Client fields vs ingest normalization.

Migration path

1

Install the Lemma SDK

Install the SDK and set LEMMA_API_KEY and LEMMA_PROJECT_ID. See Setup.
npm install @uselemma/tracing
2

Wrap each execution in a trace

Replace the Raindrop begin() / finish() (or trackAi()) that bracketed one interaction with a single lemma.trace() around the whole execution. Put the current user turn on input and let the callback return the final answer.
3

Record generations and tools as typed children

Emit each LLM call with recordGeneration / record_generation and each tool call with recordTool / record_tool. This is the Lemma equivalent of Raindrop’s auto-instrumented LLM spans, withTool, and trackTool.
4

Add thread and user context

Map convoId to threadId / thread_id and userId to userId / user_id on the trace so multi-turn conversations group correctly. See Threads & context.
5

Confirm the shape

Turn on debug mode (LEMMA_DEBUG=1) and check the sending trace log’s spanCount (span_count in Python) to confirm your generations and tools were recorded. Cross-check fields against the trace contract.

Re-instrumenting an interaction

The example below is the Lemma equivalent of a Raindrop begin() → tool tracking → finish() flow: one trace, a typed tool, and a generation carrying the full message list. Install and configure the SDK first — see Setup.
import { Lemma } from "@uselemma/tracing";

const lemma = new Lemma();

// Raindrop: raindrop.begin({ event, userId, convoId, input, model })
// Lemma: one trace per execution.
const answer = await lemma.trace(
  {
    name: "support-agent",        // Raindrop `event`
    input: userMessage,           // Raindrop event `input`
    threadId: conversationId,     // Raindrop `convoId`
    userId: user.id,              // Raindrop `userId`
  },
  async (trace) => {
    const docs = await searchDocs(userMessage);
    trace.recordTool({
      name: "search_docs",        // Raindrop `withTool` / `trackTool`
      input: { query: userMessage },
      output: docs,
    });

    const messages = [
      { role: "system", content: "You are a support agent." },
      ...priorMessages,
      { role: "user", content: userMessage },
      { role: "tool", content: JSON.stringify(docs) },
    ];

    const response = await callModel(messages);
    trace.recordGeneration({
      name: "answer",
      model: "gpt-4o",             // Raindrop event `model`
      input: messages,
      output: response.text,
      llmInputMessages: messages,
    });

    return response.text;          // Raindrop finish({ output })
  },
);
Pass the full ordered message list for each call as both input and llmInputMessages / llm_input_messages. The root trace input is only the current user turn. For live handles that measure real duration, error recording, and nesting a tool under a measured parent span, see Generations and Tool calls. If your run spans streaming callbacks or helpers rather than one function, use a TypeScript trace handle and end it from the terminal callback. See Traces.

Pitfalls

Record complete inputs and outputs only when safe. Redact secrets, credentials, and sensitive user data before tracing them.
PitfallFix
Emitting one Lemma trace per LLM call, mirroring per-event trackingWrap the whole execution in one lemma.trace(); record each LLM call as a generation child.
Using convoId semantics as the trace identityconvoId maps to threadId / thread_id, which groups turns — it is not the per-execution trace id.
Putting model on the root instead of a generationModel belongs on a typed generation span; a model on an untyped span does not become a generation.
Missing message historyPut the full ordered message list on each generation’s input and llmInputMessages / llm_input_messages, not just the latest user string.
Untyped childrenRecord LLM calls with recordGeneration and tools with recordTool so they render and filter as generations and tools.

Setup

Install the SDK and point it at Lemma.

Trace contract

The exact shape and fields Lemma reads.

Building high-quality traces

Bad → better → best examples for the ideal path.

Instrument an agent

Build a complete Lemma trace one piece at a time.