> ## Documentation Index
> Fetch the complete documentation index at: https://docs.uselemma.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# From Raindrop to Lemma

> Map Raindrop's event- and signal-centric model onto Lemma's trace contract, and re-instrument your agent to emit conforming Lemma traces

[Raindrop](https://www.raindrop.ai) 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.

<Note>
  **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](/reference/trace-contract).
</Note>

## 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](/reference/trace-contract), the migration is to produce that shape explicitly with the Lemma SDK. See [Overview](/tracing/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 concept                                         | Closest Lemma primitive                                                           | What Lemma needs                                                               |
| -------------------------------------------------------- | --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ |
| AI event / interaction (`trackAi`, `begin` + `finish`)   | Lemma [trace record](/reference/trace-contract#trace-record) (root)               | One agent execution = one trace, with `input` and final `output` or `error`    |
| Event name (`event`)                                     | Trace `name`                                                                      | A stable agent / workflow name for grouping and filtering                      |
| Event `input` / `output`                                 | Trace `input` / `output`                                                          | Current user turn on the root; final answer or error on the root               |
| `convoId`                                                | [Thread](/tracing/instrumentation/context) (`threadId` / `thread_id`)             | Groups multi-turn conversation traces                                          |
| `userId`                                                 | [User](/tracing/instrumentation/context) (`userId` / `user_id`)                   | Per-user slicing                                                               |
| `model` on the event                                     | [Generation](/tracing/instrumentation/generations) `model` (`type: "generation"`) | Model lives on a typed generation span, plus the full per-call message history |
| Tracing span (`withSpan`)                                | [Span](/tracing/instrumentation/spans) (`type: "span"`)                           | `input` and `output` (or `error`)                                              |
| Tool tracking (`withTool`, `trackTool`, `startToolSpan`) | [Tool](/tracing/instrumentation/tool-calls) (`type: "tool"`)                      | Tool `name`, arguments, and result or error                                    |
| Auto-instrumented LLM spans                              | [Generation](/tracing/instrumentation/generations) (`type: "generation"`)         | `model` plus the full ordered message list                                     |
| Span / tool nesting                                      | `parent_id` nesting                                                               | Children under the step that caused them                                       |
| Event `properties`, feature flags                        | Trace or span [`metadata`](/reference/trace-contract#trace-record)                | Arbitrary JSON attached to the root or a child                                 |
| Stumbles / issues                                        | Lemma [issue detection](/reference/trace-contract#issue-detection)                | Complete, correctly typed trees improve automated issue extraction             |

<Note>
  `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.
</Note>

For per-interaction context such as feature flags or arbitrary properties, attach it as trace or span [`metadata`](/reference/trace-contract#trace-record).

<Warning>
  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](/reference/trace-contract#client-fields-vs-ingest-normalization).
</Warning>

## Migration path

<Steps>
  <Step title="Install the Lemma SDK">
    Install the SDK and set `LEMMA_API_KEY` and `LEMMA_PROJECT_ID`. See [Setup](/tracing/instrumentation/setup).

    <Tabs>
      <Tab title="TypeScript">
        ```bash theme={null}
        npm install @uselemma/tracing
        ```
      </Tab>

      <Tab title="Python">
        ```bash theme={null}
        pip install uselemma-tracing
        ```
      </Tab>
    </Tabs>
  </Step>

  <Step title="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.
  </Step>

  <Step title="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`.
  </Step>

  <Step title="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](/tracing/instrumentation/context).
  </Step>

  <Step title="Confirm the shape">
    Turn on [debug mode](/tracing/troubleshooting/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](/reference/trace-contract).
  </Step>
</Steps>

## 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](/tracing/instrumentation/setup).

<Tabs>
  <Tab title="TypeScript">
    ```typescript theme={null}
    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 })
      },
    );
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    import json

    from uselemma_tracing import Lemma

    lemma = Lemma()

    # Raindrop: raindrop.begin(event=..., user_id=..., convo_id=..., input=...)
    # Lemma: one trace per execution.
    def run(trace):
        docs = search_docs(user_message)
        trace.record_tool(
            name="search_docs",          # Raindrop withTool / trackTool
            input={"query": user_message},
            output=docs,
        )

        messages = [
            {"role": "system", "content": "You are a support agent."},
            *prior_messages,
            {"role": "user", "content": user_message},
            {"role": "tool", "content": json.dumps(docs)},
        ]

        response = call_model(messages)
        trace.record_generation(
            name="answer",
            model="gpt-4o",              # Raindrop event model
            input=messages,
            output=response.text,
            llm_input_messages=messages,
        )

        return response.text             # Raindrop finish(output=...)

    answer = lemma.trace(
        "support-agent",                 # Raindrop event
        run,
        input=user_message,              # Raindrop event input
        thread_id=conversation_id,       # Raindrop convo_id
        user_id=user.id,                 # Raindrop user_id
    )
    ```
  </Tab>
</Tabs>

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](/tracing/instrumentation/generations) and [Tool calls](/tracing/instrumentation/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](/tracing/instrumentation/traces).

## Pitfalls

<Warning>
  Record complete inputs and outputs only when safe. Redact secrets, credentials, and sensitive user data before tracing them.
</Warning>

| Pitfall                                                             | Fix                                                                                                                                            |
| ------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| Emitting one Lemma trace per LLM call, mirroring per-event tracking | Wrap the whole execution in one `lemma.trace()`; record each LLM call as a `generation` child.                                                 |
| Using `convoId` semantics as the trace identity                     | `convoId` maps to `threadId` / `thread_id`, which groups turns — it is not the per-execution trace id.                                         |
| Putting `model` on the root instead of a generation                 | Model belongs on a typed `generation` span; a `model` on an untyped span does not become a generation.                                         |
| Missing message history                                             | Put the full ordered message list on each generation's `input` and `llmInputMessages` / `llm_input_messages`, not just the latest user string. |
| Untyped children                                                    | Record LLM calls with `recordGeneration` and tools with `recordTool` so they render and filter as generations and tools.                       |

## Related pages

<CardGroup cols={2}>
  <Card title="Setup" icon="wrench" href="/tracing/instrumentation/setup">
    Install the SDK and point it at Lemma.
  </Card>

  <Card title="Trace contract" icon="file-check" href="/reference/trace-contract">
    The exact shape and fields Lemma reads.
  </Card>

  <Card title="Building high-quality traces" icon="circle-check" href="/reference/building-high-quality-traces">
    Bad → better → best examples for the ideal path.
  </Card>

  <Card title="Instrument an agent" icon="bot" href="/tracing/instrumentation/instrument-an-agent">
    Build a complete Lemma trace one piece at a time.
  </Card>
</CardGroup>
