The GenAI conventions, and how far you can lean on them
OpenTelemetry’s generative-AI semantic conventions are the closest thing to a shared vocabulary for agent telemetry, and they are still explicitly in development — which makes them a schema to design against and a schema to version-pin.
The span model gives you a tree with timings and a status. It does not tell you which span is a model call, which is a tool, what model, or how many tokens. That vocabulary is a separate layer, and the closest thing to a shared one is OpenTelemetry’s semantic conventions for generative AI.
This lesson is about how much weight that layer can bear. The short version: it is the right thing to design against, it is genuinely useful as an interchange format, and treating it as settled will cost you.
First, the conventions moved, and the old URLs are stubs
Anything written before roughly this year points at opentelemetry.io/docs/specs/semconv/gen-ai/. That URL still resolves. It now contains no attribute tables at all — only a notice, rendered under “Semantic conventions 1.44.0”:
“Moved: Generative AI semantic conventions… GenAI semantic conventions have moved to the OpenTelemetry GenAI semantic conventions repository. This page has moved and is no longer maintained in this repository.” (opentelemetry.io)
The live content is at open-telemetry/semantic-conventions-genai, under docs/gen-ai/. Three facts about that, all checked in early September 2026, all uncomfortable and all worth knowing before you cite it in a design doc:
- There is no rendered documentation site for the moved conventions. The citable form is raw Markdown in a Git repository.
- The repository has no tagged release. So every URL you can write points at
main, which is a moving target by construction. If a lesson, a design doc or a code comment needs to stay true, cite a specific commit rather than a branch. - The status is Development. Every document in the directory opens with
**Status**: [Development][DocumentStatus], and each span definition carries the same badge. That is the project’s current word for not-yet-stable — note that it is not“Experimental,” which is the older vocabulary you will find in secondhand write-ups.
For contrast, the general tracing spec from the previous lesson is marked Stable, and the attributes on a GenAI span that are stable are the ones borrowed from core semantic conventions — error.type, server.address, server.port. Every gen_ai.* attribute is Development.
The vocabulary, as it stands today
With that said: it is a good vocabulary, and it is the one the ecosystem is converging on. The pieces you will use most:
Operation names. gen_ai.operation.name is required on essentially every GenAI span, and has eighteen well-known values — if one applies it MUST be used. The ones relevant to an agent run are chat, execute_tool, invoke_agent, create_agent, invoke_workflow, plan, retrieval, embeddings, generate_content, text_completion and fetch_response; the remainder are memory operations.
Span names. Prescribed by formula. Inference spans: {gen_ai.operation.name} {gen_ai.request.model}. Tool execution: execute_tool {gen_ai.tool.name}. Agent invocation: invoke_agent {gen_ai.agent.name}, falling back to bare invoke_agent when the name is not available.
The attributes worth memorising. gen_ai.provider.name (required), gen_ai.request.model, gen_ai.response.model, gen_ai.usage.input_tokens, gen_ai.usage.output_tokens, and on a tool span gen_ai.tool.name (required), gen_ai.tool.call.id, gen_ai.tool.type, gen_ai.tool.call.arguments, gen_ai.tool.call.result. For agents, gen_ai.agent.name, gen_ai.agent.id and gen_ai.conversation.id. The registry currently holds 71 gen_ai.* attributes in total, including a caching breakdown — gen_ai.usage.cache_read.input_tokens and gen_ai.usage.cache_write.input_tokens — which is the convention’s answer to the cost problem the end-to-end lesson gets into.
One caution on gen_ai.tool.type: it has no closed enum. The values function, extension and datastore appear as described examples in a footnote, not as a fixed list. Do not write a TypeScript union over them.
Where people get burned
The conventions say nothing normative about whether a tool execution nests under the model call that requested it. The only explicit parent-child statement is about the plan span — the LLM call that generates the plan should be a child of it — and the non-normative examples say tool and chat spans are likely to be siblings under an encompassing span. If your component assumes nesting, it is assuming something the specification declines to promise.
What has already moved, which is the real argument
The best evidence that these names are not settled is the deprecation list, which the registry publishes openly. Every one of these was current advice recently enough that you will still find it in blog posts, sample code and shipped instrumentation:
| Was | Now |
|---|---|
gen_ai.system | gen_ai.provider.name |
gen_ai.usage.prompt_tokens | gen_ai.usage.input_tokens |
gen_ai.usage.completion_tokens | gen_ai.usage.output_tokens |
gen_ai.prompt, gen_ai.completion | Removed, no replacement at this time |
Source: the deprecated GenAI attributes table, which is one of the few GenAI pages still rendered on opentelemetry.io.
That last row is the interesting one, and the next section is about why. But note the detail in the first: even the values churned. The deprecated gen_ai.system listed xAI as xai; the current gen_ai.provider.name uses x_ai. A component switching on a provider string would have silently fallen through to its default branch across that change, rendering an unknown-provider state for a provider that is very much known.
You probably cannot show the prompt in production
This is the section with the largest consequence for your component, and it is a design constraint arriving from the specification rather than from a designer. The conventions are explicit:
“Model instructions, user messages, and model outputs are considered sensitive and are often large in size… OpenTelemetry instrumentations SHOULD NOT capture them by default, but SHOULD provide an option for users to opt in.” (gen-ai-spans.md)
Three patterns are named. The default is to record none of it. The second is to record it on the span, via gen_ai.system_instructions, gen_ai.input.messages and gen_ai.output.messages — all marked Opt-In — which the document scopes to “situations where telemetry volume is manageable… for example, in pre-production environments.” The third, and the one it recommends for production, is to store content externally and record only references on the span.
Read as a component author: in the environment where your panel matters most, the message content is most likely not in the trace. It is behind a reference, in a different store, with a different access policy and quite possibly a different retention window. A trace view that treats the prompt as a field it can render is a view that works in development and degrades to empty boxes in production — which is the worst possible place to discover the constraint.
The right response is not to fight it. It is to make “content is not in this trace” a first-class state in the schema, distinct from “content was empty” and from “content was redacted,” and to render all three differently. Those are three genuinely different facts about the world and a component that collapses them is lying in the specific direction that matters most for trust.
Even the followers diverge
A small, concrete illustration that this is not theoretical. The conventions say an agent-invocation span name should be invoke_agent {gen_ai.agent.name}. Vercel’s AI SDK — vendor documenting its own product — describes its recommended integration as following the GenAI conventions, and documents its root span as invoke_agent {modelId} — the model, not the agent name. Both are defensible; they are different strings, and a component that keys off span names rather than the gen_ai.operation.name attribute will disagree with itself across those two sources.
This is a specific instance of the general rule from the span-model lesson: read the attribute, not the name. The name is a display label whose format varies by publisher. The attribute is the contract.
Retrieval check
Your component reads gen_ai.usage.input_tokens and finds nothing. Name three different things that could be true, and say how you would tell them apart.
Check your answer
One: the instrumentation is older and emitted gen_ai.usage.prompt_tokens, the deprecated name. You tell by looking for the old key before concluding anything.
Two: the span is not an inference span at all. Token usage is Recommended on model-call spans; a tool-execution span has no reason to carry it. You tell by reading gen_ai.operation.name first — which is why that attribute, not the span name, is the thing your translation layer branches on.
Three: the runtime does not emit it. Token usage is Recommended, not Required, and Recommended means an instrumentation that omits it is still conformant. You cannot tell this apart from the others by inspecting one span; you find it by checking whether any inference span in the trace carries the attribute.
The point of the exercise: three different causes, three different correct UI behaviours — translate, don’t show, and show unavailable-for-this-runtime — and no way to pick between them unless the schema was built to ask. A component that renders a zero has chosen the one option that is wrong in all three cases.
Hands on
Build the translation table for your captured payload
Done when: ARTIFACT.md carries a row-per-field table mapping your runtime’s emitted field names to gen_ai.* attribute names, each row noting the convention’s requirement level, plus a dated note recording the spec commit or fetch date and the runtime version.
- Open the span conventions document and the attribute registry alongside your captured payload. Do not work from this lesson’s summary — the whole point is that the summary has a shelf life.
- For every field in list one from the capture exercise, find the
gen_ai.*attribute it corresponds to, if any. Some will map exactly, some approximately, and some not at all. Record all three outcomes; the “not at all” rows are the ones that tell you where your schema has to be richer than the convention. - Add the requirement level to each row — Required, Conditionally Required, Recommended or Opt-In — straight from the tables. Then look at what is left after you delete every Recommended and Opt-In row. That short remainder is what your component is allowed to assume exists.
- Write the three content states into the schema explicitly: content present, content stored externally and referenced, content not captured. Give each a distinct representation. Do not let
undefinedstand for all three. - Date the table. Record the date you read the spec and, better, the commit hash of the file you read — the repository has no tagged release, so a date and a hash are the only provenance available. Put the runtime’s package version on the same line.
- Bring the table into the chat, especially the rows that mapped to nothing. Those are where the interesting design decisions are hiding, and they are the rows most likely to have been quietly skipped.
What this does not cover
This lesson stayed at the level of the vocabulary: what the attributes are called, how much they are promised, and how fast they move. It did not walk a single run through them. That is the closing lesson of this module, which takes one tool-calling loop — request, tool call, error, retry, answer — and names every record it produces at both the protocol level and the span level, so the schema you write at the end has something concrete underneath every field.
It also left the metrics side alone entirely. The conventions define a parallel set of instruments — gen_ai.client.token.usage, gen_ai.client.operation.duration, gen_ai.invoke_agent.tool_calls among them — and metrics are the right substrate for the executive roll-up in the final module, where a question about a population of runs replaces the question about one run.
Read this next — primary source
Semantic conventions for generative AI spansThe OpenTelemetry Authors / CNCF, in the semantic-conventions-genai repository — the specification itself, not a vendor; free. Marked Status: Development, and the repository has no tagged release, so this is a main-branch document that can change under you.
This lesson takes the attribute names and the requirement levels. The document is worth reading whole for the tables — every attribute has a requirement level (Required, Conditionally Required, Recommended, Opt-In) and a condition attached to it, and those conditions are the real specification. Reading them in bulk gives you something a summary cannot: a feel for how little is actually Required, which is the honest basis for deciding what your component may assume is present. Read the “Capturing instructions, inputs, and outputs” section twice — it is short, and it constrains what a production trace panel is allowed to display more than any design decision you will make.
Stuck, curious, or think this lesson is wrong? Ask your teaching agent. The lessons are the scaffold; the conversation is where the learning gets unstuck.