Preserving Trace Hierarchy with Manual RunTree Creation

Last updated: December 8, 2025

Problem

When manually creating RunTree objects for async/distributed workflows, client.read_run(run_id, load_child_runs=True) only returns one child run instead of all children, even though the UI displays the complete trace correctly.

Root Cause

Creating a RunTree without preserving dotted_order generates a new ordering value, breaking the trace hierarchy.

Solution

Always preserve dotted_ordertrace_id, and parent_run_id when reconstructing RunTree objects:

# WRONG - Breaks hierarchy
def get_run_tree(run_id: str) -> RunTree:
    return RunTree(id=run_id, client=client)

# CORRECT - Preserves hierarchy
def get_run_tree(run_id: str) -> RunTree:
    runs = list(client.list_runs(run_ids=[run_id], limit=1))
    run = runs[0]
    
    return RunTree(
        id=run_id,
        dotted_order=run.dotted_order,      # Critical
        trace_id=run.trace_id,
        parent_run_id=run.parent_run_id,
        client=client,
    )

Better Alternative: Distributed Tracing

Use to_headers() and from_headers() to avoid manual field management:

# Service A - Serialize context
run_tree = RunTree(name="Parent", project_name="my-project")
trace_headers = run_tree.to_headers()
redis.set(f"trace:{job_id}", json.dumps(trace_headers))

# Service B - Deserialize and continue
headers = json.loads(redis.get(f"trace:{job_id}"))
with tracing_context(parent=RunTree.from_headers(headers)):
    await process_job()  # Automatically preserves hierarchy

Requirements

  • Minimum SDK version: langsmith >= 0.4.56

  • Earlier versions have issues with malformed dotted_order

Related Documentation