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

KindFor
llmModel calls
toolFunctions the agent invokes (search, DB, API)
retrievalVector search, KB lookup, RAG retriever
agentA nested sub-step rendered as an agent stage
chainA chain or graph step (LangChain, LangGraph)
functionA 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_inputThe step's input (always manual)
setOutput / set_outputThe step's output (always manual)
setLlm / set_llmmodel, provider, inputTokens, outputTokens, cost, temperature
setTool / set_toolThe tool name
setAttribute / set_attributeA custom key/value (see Metadata)
setError / set_errortype + 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

On this page