OpenTelemetry GenAI semantic conventions: a practical guide to agent and tool spans

OpenTelemetry GenAI semantic conventions, shown on a real agent run: the three span types, the attributes that matter, and where the conventions stop.

On this page

The OpenTelemetry GenAI semantic conventions name the spans an AI agent produces: invoke_agent for the run, chat for each model call, execute_tool for each tool. Every span also gets standard gen_ai.* attributes, so any backend can read an agent’s trace.

That’s the definition. Below is what it looks like on a real run, and the one place where the conventions stop.

TL;DR

  • Three operations cover most agent traces: invoke_agent, chat and execute_tool. The gen_ai.* names are still in Development.
  • gen_ai.tool.call.id joins the model’s tool request, the tool span and the next model call.
  • Message content should be opt-in. The AI SDK records it by default, so check yours.

What are they, and where do they stand in 2026?

Semantic conventions are shared names. If every library calls a model call chat and puts the model in gen_ai.request.model, one dashboard works for all of them.

The GenAI set has lived in its own repository, open-telemetry/semantic-conventions-genai, since May 2026. Every page in it is marked Development, so the gen_ai.* names can still change. Shared attributes such as error.type come from the stable core conventions. The repository has no releases, so this post pins the commit it was checked against: e07f4eb, 2 October 2026.

Which spans does an agent run produce?

I ran the example agent from the hashspan repository. It’s built with the AI SDK and has two tools: pay_vendor and withdraw_from_vault. A scripted model makes the tool calls, so it runs offline. These are the GenAI spans it exported (hashspan’s own spans come in the last section):

Span tree of one agent run: invoke_agent contains step 1, 2 and 3; each step has a chat span (kind CLIENT) and, in steps 1 and 2, an execute_tool span (kind INTERNAL). The step spans use the custom operation name agent_step.Span tree of one agent run: invoke_agent contains step 1, 2 and 3; each step has a chat span (kind CLIENT) and, in steps 1 and 2, an execute_tool span (kind INTERNAL). The step spans use the custom operation name agent_step.

Three things to notice:

  1. The span kind follows where the work happens. chat is CLIENT because it calls a model API. execute_tool is INTERNAL because your code runs it. invoke_agent has two variants in the spec: CLIENT for a remote agent service, INTERNAL for an agent in your process, like this one.
  2. step N isn’t in the conventions. The AI SDK adds it with gen_ai.operation.name = agent_step. That’s allowed: when no well-known value fits, a library may use its own.
  3. The root span is named invoke_agent scripted-model. The spec says invoke_agent {gen_ai.agent.name}, which would be invoke_agent treasury-agent. It’s a SHOULD, not a MUST, but it means a query on span names won’t find this agent by its name. Filter on the gen_ai.agent.name attribute instead.

Here are the same spans laid out in time:

treasury-agent · one run on a local chaindurations in ms
invoke_agent scripted-model47.047.0
step 125.625.6
chat scripted-model2.02.0
execute_tool pay_vendor21.121.1
step 217.217.2
chat scripted-model0.90.9
execute_tool withdraw_from_vault15.415.4
step 30.30.3
chat scripted-model0.10.1
GenAI spans only. ai 7.0.127 and @ai-sdk/otel 1.0.127, Node 24, local Anvil chain. The scripted model answers in about a millisecond; a real one takes seconds.

Which attributes matter in practice?

The spec lists dozens. These are the ones you’ll query. All of them except error.type were set in the run; “If available” and “If it failed” are the spec’s Conditionally Required.

Attribute · span Level
gen_ai.operation.name · all three Required
gen_ai.provider.name · chat Required
gen_ai.request.model · chat If available
gen_ai.usage.*_tokens · chat Recommended
gen_ai.agent.name · invoke_agent If available
gen_ai.tool.name · execute_tool Required
gen_ai.tool.call.id · execute_tool Recommended
error.type · any If it failed

Attributes like these are what make “tokens per model” or “slowest tool” a one-line query in your backend.

The attribute I’d query first is gen_ai.tool.call.id. The model asks for a tool by id, the tool span records the same id, and the result goes back to the model with it:

The id call_1 appears in three spans: in the output messages of the step 1 chat span as a tool_call, as gen_ai.tool.call.id on execute_tool pay_vendor, and in the input messages of the step 2 chat span as a tool_call_response.The id call_1 appears in three spans: in the output messages of the step 1 chat span as a tool_call, as gen_ai.tool.call.id on execute_tool pay_vendor, and in the input messages of the step 2 chat span as a tool_call_response.

When an agent calls the same tool twice in one step, the id is what tells the two results apart.

What does the AI SDK record by default?

The conventions are careful with content. Prompts, messages and tool results are sensitive and large. The spec says instrumentations SHOULD NOT capture them by default, and describes three patterns:

Three content-capture patterns: don't record (the spec's default), record on the span as opt-in attributes (fine for development), or store elsewhere and keep a reference (for production). AI SDK 7 points at the second: recording is on by default.Three content-capture patterns: don't record (the spec's default), record on the span as opt-in attributes (fine for development), or store elsewhere and keep a reference (for production). AI SDK 7 points at the second: recording is on by default.

The AI SDK takes the second pattern unless you say otherwise: recordInputs and recordOutputs are both on by default. The example sets neither, and its spans carried the system prompt, the user’s message, the tool definitions, the tool arguments and the tool results.

For an agent that moves money, the tool results contain transaction hashes. Turn recording off where that matters:

const agent = new ToolLoopAgent({
// …model, tools
telemetry: {
functionId: 'treasury-agent',
recordInputs: false,
recordOutputs: false,
},
});

I reran the example with this change. The messages, instructions, tool definitions, arguments and results were gone; span names, gen_ai.tool.call.id and token counts stayed.

One more addition is worth knowing about. Since 30 September there’s a gen_ai.main_agent entity: a resource that names the top-level agent of a process. It needs a stable id from a provider, like a Bedrock agent ARN. The spec forbids generating one in memory, so a local agent like this one doesn’t emit it. That’s by design.

Where do the conventions stop?

The withdrawal in this run reverted on chain. Here’s how the trace answers “did it work?”:

execute_tool withdraw_from_vault has status unset because the tool returned an object, with no exception or error code; the word reverted only appears inside the opt-in tool result. Below the line where the GenAI conventions end, hashspan's send and confirm spans show the confirm span with status ERROR, blockchain.tx.status reverted, the revert reason and the fee that was still paid.execute_tool withdraw_from_vault has status unset because the tool returned an object, with no exception or error code; the word reverted only appears inside the opt-in tool result. Below the line where the GenAI conventions end, hashspan's send and confirm spans show the confirm span with status ERROR, blockchain.tx.status reverted, the revert reason and the fee that was still paid.

The tool span stays clean, and the instrumentation can’t do better. OpenTelemetry’s error rules count an operation as failed when it throws or returns an error code. The tool did neither: it returned an object, and the AI SDK has no way to know that "status": "reverted" inside it means the withdrawal never happened. The conventions describe the agent’s side of the call. What the call did out in the world is outside their scope.

That’s the gap hashspan fills for on-chain tools. It adds send and confirm as child spans of execute_tool, with names from its own semantic conventions:

confirm 31337error
blockchain.tx.status
reverted
blockchain.tx.revert.reason
WithdrawalLimitExceeded(100000000000000000, 1000000000000000000)
blockchain.tx.fee
40,616,290,500,000
gen_ai.agent.name
treasury-agent
The confirm span of the reverted withdrawal, from the same run (local chain 31337, fee in wei).

Note the last row. hashspan writes the agent’s gen_ai.agent.name on its own spans too, so “which agent spent what” doesn’t need a join. The full story of why the trace used to stop at the tool is in Your agent’s trace stops at the tool call.

Run it yourself

The run needs no API key. In a clone of the repository, with Docker running and Node.js 22.18 or later (see the local lab):

Terminal window
make lab-up # Jaeger on http://localhost:16686
make demo # the agent, a local chain, both transactions

To wire the same thing into your own AI SDK agent, follow the AI SDK recipe. The GenAI conventions will keep moving while they’re in Development. I’ll update this post when they do.

Selim Aytaç on GitHub

Selim Aytaç · Maintainer

Maintains hashspan. Writes about OpenTelemetry, agentic payments and on-chain transaction internals.

Discussion

Comments live in GitHub Discussions. Sign in with GitHub to join.

Type to search the docs, Learn topics and the blog.