A trace is one end-to-end agent execution — from the user’s input to the final response. It is the unit Lemma searches, debugs, and monitors. Everything your agent does inside that execution (LLM calls, tool calls, retrieval, app logic) becomes a child span of the trace’s root.
One agent execution = one trace. Create the root span first, then do your work inside its callback so child spans nest automatically.
The root span
Open the trace with startActiveObservation. The callback is the active context: any span you create inside it becomes a child of this root.
import { startActiveObservation } from "@langfuse/tracing" ;
await startActiveObservation ( "support-agent" , async ( root ) => {
root . update ({ input: userMessage });
const answer = await runAgent ( userMessage );
root . update ({ output: answer });
});
from langfuse import get_client
langfuse = get_client()
with langfuse.start_as_current_span( name = "support-agent" ) as root:
root.update( input = user_message)
answer = run_agent(user_message)
root.update( output = answer)
This already produces a useful trace: a named execution with input, output, and duration.
The root input and output are what make a trace debuggable. Record the input as soon as you have it, and the output (or error) before the trace closes.
await startActiveObservation ( "support-agent" , async ( root ) => {
root . update ({ input: userMessage });
try {
const answer = await runAgent ( userMessage );
root . update ({ output: answer });
return answer ;
} catch ( error ) {
root . update ({
level: "ERROR" ,
statusMessage: error instanceof Error ? error . message : String ( error ),
});
throw error ;
}
});
with langfuse.start_as_current_span( name = "support-agent" ) as root:
root.update( input = user_message)
try :
answer = run_agent(user_message)
root.update( output = answer)
except Exception as error:
root.update( level = "ERROR" , status_message = str (error))
raise
Name the agent
Give the trace a stable agent name so traces are groupable and filterable by workflow. Set trace-level attributes once and they apply to the whole trace.
import { propagateAttributes , startActiveObservation } from "@langfuse/tracing" ;
await startActiveObservation ( "support-agent" , async ( root ) => {
root . update ({ input: userMessage });
await propagateAttributes (
{
traceName: "support-agent" ,
metadata: { "gen_ai.agent.name" : "support-agent" },
},
async () => {
const answer = await runAgent ( userMessage );
root . update ({ output: answer });
},
);
});
with langfuse.start_as_current_span( name = "support-agent" ) as root:
root.update( input = user_message)
langfuse.update_current_trace(
name = "support-agent" ,
metadata = { "gen_ai.agent.name" : "support-agent" },
)
answer = run_agent(user_message)
root.update( output = answer)
For threads, users, and other context, see Threads & context .
Add the work inside
The root on its own gives you input/output/duration. Add detail by nesting child spans inside the root callback :
Generations for LLM calls (model, tokens, prompt, completion).
Tool calls for tool invocations (name, args, result).
Spans for everything else (retrieval, ranking, app logic).
support-agent ← trace root (input, output)
├─ draft-reply ← generation
├─ search_docs ← tool call
└─ final-answer ← generation
Next steps
Generations Capture LLM calls with model and token usage.
Tool calls Record tool arguments and results.
Threads & context Group conversations and attach users, sessions, and metadata.
Trace contract The exact shape Lemma reads.