> ## 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.

# LangGraph

> Instrument a LangGraph agent so each run is one nested trace in Lemma

[LangGraph](https://langchain-ai.github.io/langgraph/) builds stateful, multi-step agent graphs on top of LangChain. With OpenInference instrumentation it emits spans tagged with `openinference.span.kind`, which Lemma reads directly. Point Langfuse at Lemma, enable the instrumentation, and wrap each graph invocation in one root span to get a single nested trace.

<Note>
  **One agent execution = one trace.** Wrap the graph invocation in a single root span so every node, model, and tool call nests under it. See the [trace contract](/reference/trace-contract).
</Note>

LangGraph emits **OpenInference-style spans** (`openinference.span.kind`), which are a **fully supported shape** for Lemma's automated [issue detection](/reference/good-vs-bad-traces) (silent failures, bad tool calls, loops) today.

## Recipe

<Steps>
  <Step title="Install">
    ```bash theme={null}
    pip install langgraph langchain langfuse openinference-instrumentation-langchain opentelemetry-sdk opentelemetry-exporter-otlp
    ```
  </Step>

  <Step title="Register the Langfuse → Lemma exporter">
    Register the exporter once at startup, before any graph runs. This matches [Setup](/tracing/instrumentation/setup).

    ```python theme={null}
    # instrumentation.py — imported first, before your app code
    import os
    from opentelemetry import trace
    from opentelemetry.sdk.trace import TracerProvider
    from opentelemetry.sdk.trace.export import BatchSpanProcessor
    from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter

    provider = TracerProvider()
    provider.add_span_processor(
        BatchSpanProcessor(
            OTLPSpanExporter(
                endpoint=os.environ["LEMMA_BASE_URL"],
                headers={
                    "Authorization": f"Bearer {os.environ['LEMMA_API_KEY']}",
                    "X-Lemma-Project-ID": os.environ["LEMMA_PROJECT_ID"],
                },
            )
        )
    )
    trace.set_tracer_provider(provider)
    ```

    Set the environment variables. Lemma-only export needs no `LANGFUSE_*` credentials.

    ```bash theme={null}
    export LEMMA_BASE_URL="https://api.uselemma.ai/otel/v1/traces"
    export LEMMA_API_KEY="lma_..."
    export LEMMA_PROJECT_ID="proj_..."
    ```
  </Step>

  <Step title="Enable OpenInference instrumentation">
    LangGraph runs on LangChain, so the LangChain instrumentor captures graph nodes, model calls, and tool calls — emitting the `openinference.span.kind` spans Lemma reads. See the [Langfuse LangGraph guide](https://langfuse.com/integrations/frameworks/langgraph) for more detail.

    ```python theme={null}
    from openinference.instrumentation.langchain import LangChainInstrumentor

    LangChainInstrumentor().instrument()
    ```
  </Step>

  <Step title="Wrap the whole run in one root span">
    Wrap `graph.invoke` in a single Langfuse root span so every node and call nests under one trace. Record the input and final output on the root, and set a stable agent name.

    ```python theme={null}
    from langfuse import get_client

    langfuse = get_client()

    def run_support_agent(user_message: str, thread_id: str) -> str:
        with langfuse.start_as_current_span(name="support-agent") as root:
            root.update(input=user_message)
            langfuse.update_current_trace(
                name="support-agent",
                session_id=thread_id,
                metadata={"gen_ai.agent.name": "support-agent"},
            )

            result = graph.invoke({"messages": [{"role": "user", "content": user_message}]})
            final = result["messages"][-1].content

            root.update(output=final)
            return final
    ```

    Every span emitted inside the `with` block becomes a child of the root, producing one nested trace:

    ```text theme={null}
    support-agent              ← trace root (input, output)
    ├─ agent (llm)             ← generation (model, tokens)
    ├─ search_docs (tool)      ← tool call (args, result)
    └─ agent (llm)             ← generation (final answer)
    ```
  </Step>

  <Step title="Flush before the process exits">
    In short-lived runtimes, flush so the whole trace ships in one batch.

    ```python theme={null}
    from langfuse import get_client

    get_client().flush()
    ```
  </Step>
</Steps>

<Warning>
  If graph nodes show up as their own separate traces, the graph ran outside the root's active context. Keep `graph.invoke` inside the `start_as_current_span` block. See [Troubleshooting](/tracing/troubleshooting/common-problems#every-span-appears-as-a-separate-trace).
</Warning>

## Verify in Lemma

Open the [Lemma dashboard](https://platform.uselemma.ai) → **Traces** and confirm:

* **One trace per run** — a full graph invocation is one trace, not one per node.
* **Root has input and output** — the root span shows the user message and the final output.
* **Generations are nested** — each model call appears as a child generation with model and token usage.
* **Tools are nested** — each tool invocation appears as a child tool span with arguments and result.

## Next steps

<CardGroup cols={2}>
  <Card title="Trace contract" icon="file-check" href="/reference/trace-contract">
    The exact shape Lemma reads.
  </Card>

  <Card title="Setup" icon="plug" href="/tracing/instrumentation/setup">
    Wire the Langfuse → Lemma exporter.
  </Card>

  <Card title="Threads and sessions" icon="users" href="/tracing/instrumentation/context">
    Group multi-turn conversations with a thread id.
  </Card>

  <Card title="Good vs bad traces" icon="circle-check" href="/reference/good-vs-bad-traces">
    What issue detection looks for, per shape.
  </Card>
</CardGroup>
