> This page is for NeMo Relay, version dev.
> For other versions, use one of these documentation indexes:
> - Latest (v0.10.3) (default): https://docs.nvidia.com/nemo/relay/latest/llms.txt
> - dev: https://docs.nvidia.com/nemo/relay/dev/llms.txt
> - v0.10.3: https://docs.nvidia.com/nemo/relay/v0.10.3/llms.txt
> - v0.10.2: https://docs.nvidia.com/nemo/relay/v0.10.2/llms.txt
> - v0.9.4: https://docs.nvidia.com/nemo/relay/v0.9.4/llms.txt
> - v0.8.4: https://docs.nvidia.com/nemo/relay/v0.8.4/llms.txt
> - v0.7.3: https://docs.nvidia.com/nemo/relay/v0.7.3/llms.txt

> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.nvidia.com/nemo/relay/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.nvidia.com/nemo/relay/_mcp/server.

# LangGraph Quick Start

> Run one LangGraph workflow with NeMo Relay observability.

## Goal

Run one credential-free LangGraph workflow and write its lifecycle to a local
Agent Trajectory Observability Format (ATOF) file.

Use the recorded parent/child scopes to follow a workflow through its nodes
when debugging execution.

## Prerequisites

Before you start, make sure you have:

* Python 3.11 or newer
* `uv`

## Install

Create a project and install the NeMo Relay version for these docs with the
LangGraph extra.
For an existing `uv` project, run only the `uv add` command from its directory.

```bash
mkdir relay-langgraph-quickstart
cd relay-langgraph-quickstart
uv init
uv add "nemo-relay[langgraph]==0.11.0"
```

## Configure

`plugins.toml` enables Relay's observability component and writes its events
to `logs/events.jsonl`. In your own project, use the same plugin configuration
with the integration code below; the marker task and success checks are only
the quick start's validation example.

Save the following configuration as `plugins.toml` in your project directory:

```toml title={"plugins.toml"}
# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

version = 1

[policy]
unknown_component = "error"
unknown_field = "error"
unsupported_value = "error"

[[components]]
kind = "observability"
enabled = true

[components.config]
version = 4
enable_full_payloads = false

[components.config.atof]
enabled = true

[[components.config.atof.sinks]]
type = "file"
output_directory = "logs"
filename = "events.jsonl"
mode = "overwrite"

```

> **Warning**
>
> ATOF output can contain sensitive prompts, responses, tool arguments, and tool
> results. Protect `logs/events.jsonl` even when full payload export is disabled.

## Run

The example runs one local `emit_marker` graph node, with no inference
request or credentials. `NemoRelayCallbackHandler` records the graph lifecycle,
and the explicit `langgraph-request` scope groups its node activity. Plugin
activation loads `plugins.toml`; flushing finishes writing queued events before
verification. In your project, replace the marker graph with your workflow and
retain the callback, request scope, and activation lifecycle.

Save this file as `quickstart.py` beside `plugins.toml`:

```python title={"quickstart.py"}
# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

import asyncio
import json
from pathlib import Path

from langgraph.graph import END, START, StateGraph
from typing_extensions import TypedDict

import nemo_relay
from nemo_relay.integrations.langgraph import NemoRelayCallbackHandler


class State(TypedDict):
    """Carry the deterministic graph and response markers between nodes."""

    graph_marker: str
    response_marker: str


def emit_marker(_: State) -> State:
    """Produce local validation markers without an inference request."""
    return {
        "graph_marker": "RELAY_QUICKSTART_GRAPH_OK",
        "response_marker": "RELAY_QUICKSTART_RESPONSE_OK",
    }


builder = StateGraph(State)
builder.add_node("emit_marker", emit_marker)
builder.add_edge(START, "emit_marker")
builder.add_edge("emit_marker", END)
graph = builder.compile()


async def main() -> None:
    """Run the graph and verify that Relay exported the completed marker node."""
    async with nemo_relay.plugin.activate({}, "plugins.toml"):
        try:
            with nemo_relay.scope.scope("langgraph-request", nemo_relay.ScopeType.Agent):
                result = await graph.ainvoke(
                    {"graph_marker": "", "response_marker": ""},
                    config={"callbacks": [NemoRelayCallbackHandler()]},
                )

            print(result["graph_marker"])
            print(result["response_marker"])
        finally:
            await nemo_relay.subscribers.flush_async()

    events = [json.loads(line) for line in Path("logs/events.jsonl").read_text().splitlines()]
    if not events or any(
        not isinstance(event, dict) or event.get("atof_version") != "0.1" or event.get("kind") not in {"scope", "mark"}
        for event in events
    ):
        raise RuntimeError("Expected a nonempty ATOF 0.1 event stream")
    starts = {
        event["uuid"]: event for event in events if event["kind"] == "scope" and event.get("scope_category") == "start"
    }
    closed = [
        event
        for event in events
        if event["kind"] == "scope"
        and event.get("scope_category") == "end"
        and event.get("uuid") in starts
        and all(event.get(key) == starts[event["uuid"]].get(key) for key in ("name", "category", "parent_uuid"))
    ]
    # Both scopes are part of this guide's contract; an unrelated completed node is insufficient.
    for name in ("langgraph-request", "emit_marker"):
        if not any(event.get("name") == name and event.get("category") == "agent" for event in closed):
            raise RuntimeError(f"Expected the completed {name} scope")
    print("RELAY_QUICKSTART_EVENTS_OK")


asyncio.run(main())

```

Run it from the project directory:

```bash
uv run python quickstart.py
```

## Verify

The program checks the event file for matching start and end records from the
`emit_marker` graph node. A successful run prints these markers in order:

```text
RELAY_QUICKSTART_GRAPH_OK
RELAY_QUICKSTART_RESPONSE_OK
RELAY_QUICKSTART_EVENTS_OK
```

> **Tip**
>
> This deterministic path needs no model credentials. After it works, add a model
> and tool node to observe a full agent workflow.

### Inspect the Recorded Activity

Print one completed graph node from the event file:

```bash
uv run python -c "
import json
from pathlib import Path
events = [json.loads(line) for line in Path('logs/events.jsonl').read_text().splitlines()]
call = next(event for event in events if event.get('scope_category') == 'end' and event.get('category') == 'agent' and event.get('name') == 'emit_marker')
fields = ('name', 'category', 'category_profile', 'uuid', 'parent_uuid', 'timestamp', 'metadata')
print(json.dumps({key: call.get(key) for key in fields}, indent=2))
"
```

The node is an `agent` scope named `emit_marker`. Its `parent_uuid` links to
the immediate `LangGraph` scope, whose parent is the outer `langgraph-request`
scope. The node's `uuid` pairs its start and end records. `timestamp` records
when each event occurred. This local graph makes no model request; the records
demonstrate workflow structure.

## For More Details

For more details, refer to:

* [LangGraph integration guide](/supported-integrations/langgraph)
* [ATOF exporter](/configure-plugins/observability/atof)