Skip to navigation

LangGraph Quick Start

View as Markdown

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
1

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.

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

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:

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"

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

3

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:

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:

uv run python quickstart.py
4

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:

RELAY_QUICKSTART_GRAPH_OK
RELAY_QUICKSTART_RESPONSE_OK
RELAY_QUICKSTART_EVENTS_OK

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:

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: