Add manual spans
Add spans for custom steps that are not auto-captured: tools, LLM calls, retrieval, and arbitrary logic.
A span is one step inside a run. Auto-instrumentation captures calls to supported frameworks for you; add a manual span for anything else, like your own tools, a custom LLM client, a retrieval step, or any block you want to see on the run's waterfall.
When to use it
Use a manual span when a step won't show up on its own: a database tool, an internal API call, a RAG retriever, or a model client Trodo doesn't auto-instrument. If you're calling OpenAI, Anthropic, LangChain, and the like, those are already captured. See Frameworks.
Two ways to add a span
Helpers (declare once)
Wrap a function once at its definition site and every call becomes a span. Arguments are captured as the span's input, the return value as its output, and a thrown exception as an error.
const fetchUser = trodo.tool('fetch_user', async (id) => db.users.findById(id));
const searchKb = trodo.retrieval('kb.search', async (q) => kb.search(q, { k: 8 }));
const prepareCtx = trodo.trace('prepare_ctx', async (u, t) => ({
history: await loadHistory(u),
topic: t,
}));
const answer = trodo.llm(
'answer',
async (messages) => client.chat.completions.create({ model: 'gpt-4o-mini', messages }),
{ model: 'gpt-4o-mini', provider: 'openai' },
);
// Call them like normal functions:
const user = await fetchUser('u_123');@trodo.tool("fetch_user")
def fetch_user(id): return db.users.find(id)
@trodo.retrieval("kb.search")
def search_kb(q): return kb.search(q, k=8)
@trodo.trace("prepare_ctx")
def prepare_ctx(u, t): return {"history": load_history(u), "topic": t}
@trodo.llm("answer", model="gpt-4o-mini", provider="openai")
def answer(messages):
return client.chat.completions.create(model="gpt-4o-mini", messages=messages)
# Or without the decorator: wrap an existing function.
run_funnel = trodo.tool("run_funnel_query", run_funnel_query)withSpan (wrap a block)
When you'd rather instrument inline, wrap a block and set fields on the span it hands you.
await trodo.withSpan('search_kb', async (span) => {
span.setTool('search_kb');
span.setInput({ query });
const results = await kb.search(query);
span.setOutput({ count: results.length });
return results;
}, { kind: 'tool' });with trodo.span("search_kb", kind="tool") as span:
span.set_tool("search_kb")
span.set_input({"query": query})
results = kb.search(query)
span.set_output({"count": len(results)})Don't wrap calls to frameworks Trodo already auto-instruments (OpenAI, Anthropic, LangChain, and others) with the llm helper or withSpan — you'll get duplicate spans. Manual spans are for your own code.
Span kinds
| Kind | For |
|---|---|
llm | Model calls |
tool | Functions the agent invokes (search, DB, API) |
retrieval | Vector search, KB lookup, RAG retriever |
agent | A nested sub-step rendered as an agent stage |
chain | A chain or graph step (LangChain, LangGraph) |
function | A generic instrumented function |
Setting fields on a span
With withSpan you set fields directly; the helpers capture input/output/errors for you.
| Setter (Node / Python) | Sets |
|---|---|
setInput / set_input | The step's input (always manual) |
setOutput / set_output | The step's output (always manual) |
setLlm / set_llm | model, provider, inputTokens, outputTokens, cost, temperature |
setTool / set_tool | The tool name |
setAttribute / set_attribute | A custom key/value (see Metadata) |
setError / set_error | type + message, and marks the span status='error' |
LLM spans and token usage
The llm helper auto-extracts tokens from three response shapes: OpenAI (usage.prompt_tokens), Anthropic (usage.input_tokens), and Google (usageMetadata.promptTokenCount). For any other client, pass your own extractor:
const generate = trodo.llm('cohere.generate', callCohere, {
model: 'command-r',
provider: 'cohere',
extractUsage: (r) => ({
inputTokens: r?.meta?.tokens?.input_tokens,
outputTokens: r?.meta?.tokens?.output_tokens,
}),
});generate = trodo.llm(
"cohere.generate", call_cohere,
model="command-r", provider="cohere",
extract_usage=lambda r: {
"input_tokens": r["meta"]["tokens"]["input_tokens"],
"output_tokens": r["meta"]["tokens"]["output_tokens"],
},
)Cost is computed server-side from (provider, model) and the token counts; see Pricing to add custom model prices. If you already made the call and just want to record it, use trackLlmCall.
Structure LLM input for deeper analysis
When you set the input of an LLM span yourself (via setInput / set_input, the llm helper, or trackLlmCall), pass it as an object with query, context, and system_instruction where those parts are meaningful — instead of one blob:
span.setInput({
system_instruction: systemPrompt, // system / developer prompt
context: retrievedDocs, // RAG context, tool results, history
query: userQuestion, // the actual user turn
});span.set_input({
"system_instruction": system_prompt, # system / developer prompt
"context": retrieved_docs, # RAG context, tool results, history
"query": user_question, # the actual user turn
})Trodo embeds the input as a whole and each field separately (query, context, system_instruction), so LLM-node analysis — retrieval quality, context grounding, prompt drift, and the context-loss / hallucination detectors — can reason about each part independently.
It's optional and non-breaking: a plain string still works and is embedded as one vector. Use the structured form only where the parts actually exist (RAG, tool-augmented, or multi-part prompts) — don't invent fields for a one-line prompt. Each field accepts a string or a message array ([{ role, content }, …]).
Errors
A thrown exception inside a helper or withSpan sets status='error' plus error_type and error_message from the exception, then re-raises. The span is recorded either way, and it increments the run's error count. Wrap with try/catch only if you want to recover.
Next
- Long-running & background runs for spans emitted outside the wrap
- Raw HTTP / non-OTel clients for
trackLlmCall - Metadata for custom attributes and filtering