Generated banner for Dd Instrument Llmo
Generated logo for Dd Instrument Llmo

Dd Instrument Llmo

DatadogOfficialCreated by datadog-labs

  • Skill
  • Workflow
  • Cloud and DevOps
  • MIT
  • Low risk

From the author

Instrument the current project with Datadog LLM Observability for Python or Node.js/Next.js backends that call LLMs or run AI agents. Detects the runtime and LLM framework, provisions credentials, adds SDK init (ddtrace/dd-trace) with the correct kwargs, persists the dependency into the deploy manifest, and audits session-ID plumbing for gaps — fixing them when found. Use when the user says "instrument this project with LLM Observability", "add LLM Observability", "monitor my AI app in Datadog", "add LLM spans", "add agent session tracking", or "verify/repair my LLM Observability setup".
— the description by datadog-labs

AutoGineer summary

Written by AutoGineer from the skill’s own files, not by the author

This skill provides a structured workflow to instrument backend applications with Datadog LLM Observability. It detects the runtime and framework, handles credential provisioning, integrates the required SDKs (ddtrace or dd-trace), ensures dependencies are correctly persisted in the deployment manifest, and audits session-ID propagation to ensure accurate trace linking.

What it does

  • Detects backend runtime and LLM/agent frameworks.
  • Provisions Datadog API keys via secure local tools.
  • Integrates ddtrace or dd-trace SDKs with correct initialization parameters.
  • Persists required dependencies into the project's authoritative deployment manifest.
  • Audits and repairs session-ID plumbing for consistent trace correlation.
  • Verifies successful instrumentation through execution tests.

When to use it

  • When you need to add LLM Observability to a Python or Node.js/Next.js backend.
  • When you need to monitor AI agents or LLM calls in Datadog.
  • When you need to verify or repair existing LLM Observability instrumentation.
  • When you need to ensure session tracking is correctly implemented for LLM spans.

Advantages

  • Ensures correct SDK initialization and import paths.
  • Prevents common deployment errors by updating the authoritative dependency manifest.
  • Maintains clean code by avoiding redundant instrumentation.
  • Provides a systematic audit of session-ID flow for reliable observability.

Usage examples

“Instrument this project with LLM Observability.”
The agent will analyze the project structure, identify the backend runtime, add the necessary Datadog SDK, and configure it for LLM monitoring.
“Add agent session tracking to my Node.js app.”
The agent will audit the current session-ID plumbing and implement the necessary logic to ensure session IDs propagate correctly to LLM spans.

Install

  1. Download the skill as a ZIP:

    https://mp-staging.autogineer.com/agent-skills/datadog-labs/dd-instrument-llmo/download
  2. Unzip it into the cross-client skills folder of your project:

    mkdir -p .agents/skills && unzip dd-instrument-llmo.zip -d .agents/skills

Documentation by datadog-labs

Datadog LLM Observability Instrumentation

This skill assesses the current project (backend runtime, LLM/agent framework, existing instrumentation) and routes you to the right reference file under references/ for the actual setup steps. It is fully self-contained — it does not call any Datadog MCP server. Credential provisioning uses the local CreateApiKey tool, and all code changes are made by you, directly, using your own file-editing tools.

Stay within LLM Observability scope. Do not add RUM, APM application instrumentation, or unrelated Datadog products. RUM and a Datadog Agent are relevant only because they change what session/trace linking is achievable (see the "Beyond SDK init" section in the reference files).

Do NOT invent tool names. Use only CreateApiKey as described in references/common-credentials.md; every other step is done with your normal file-editing/search tools.

Do NOT write anything to memory during or after this skill. Project paths, frameworks, credentials, and ML app names are project-specific and must not be stored in persistent memory.

Ground rules while instrumenting

  • Verify before you assert. If you're not sure about file content or codebase structure, read the files — do not guess.

  • Check for existing instrumentation first. Before touching the backend, check whether ddtrace/dd-trace is already initialized with LLM Observability enabled. If it is, do not add a second, competing init/enable call. This only means skip re-init — it does not mean skip the work. SDK presence is not the same as a well-formed trace or a session ID that actually flows; run the session-ID plumbing audit in Phase 1d and close any gap it finds, even when the SDK is already present.

  • Only use packages/features you've explicitly been told to add. Don't decide on your own to enable an additional Datadog product or SDK feature beyond what this skill specifies.

  • Persist added dependencies into the deploy's manifest. Any Datadog package you add (ddtrace, dd-trace) must be written into the dependency manifest the build/deploy installs from — the one identified in Phase 1e — not just installed into the local environment. A clean deploy install reads only the manifest and will crash with a missing-module error (e.g. ModuleNotFoundError: No module named 'ddtrace') if the package isn't declared there.

  • Don't make stylistic changes to code you're not otherwise touching.

  • No package aliases when importing Datadog packages.

  • Copy Datadog SDK package names, import paths, and init keyword arguments verbatim from the applicable reference section — do not paraphrase them from memory. The Python LLM Observability import is exactly from ddtrace.llmobs import LLMObs — the module is ddtrace.llmobs, not ddtrace.llm_observability (that module does not exist and produces a deploy-time ModuleNotFoundError). The Node package is dd-trace, initialized with the form that matches the project: CommonJS require('dd-trace').init(...), an ESM import-based form for "type": "module"/.mjs projects, or Next.js's dd-trace/initialize.mjs in instrumentation.ts — all shown in references/llmobs-nodejs.md; never force require(...) into an ESM project. Use the exact LLMObs.enable(...) / .init({...}) keyword arguments shown; do not add, drop, or rename kwargs based on general Datadog knowledge.

  • Use a real edit tool for existing files (Edit/Write, not sed -i, awk, or scripted find/replace via Bash). If an edit-by-text-match fails, re-read the file first rather than retrying the identical edit.

  • Checklist discipline. Before starting the instrumentation steps, post a short checklist of the steps you're about to take. Check items off as you go, and review the checklist at the end of the run.

  • After any code change, check package.json for a lint:fix, fix, or format script (or eslint --fix / prettier --write config) and run it automatically — no need to ask permission.

  • Verify the app still builds/runs before declaring success (see references/common-verify-report.md).

  • LLM Observability session IDs propagate from the root span only. Never pass a session ID to a child span/decorator — see the "Adding spans" section in references/llmobs-python.md / references/llmobs-nodejs.md.

  • Don't promise what the environment doesn't support. If no RUM SDK is detected, don't claim RUM session linking; if no dd-agent is detected, don't claim navigable APM trace linking. State the gap and the upgrade path instead (see the "Beyond SDK init" section in the reference files).


Phase 1: Analysis

Inspect the relevant application directory before asking questions or editing files.

1a. Detecting the backend LLM/agent runtime
  • Python signals: requirements.txt, pyproject.toml, Pipfile, or *.py files → runtime python

  • Node.js signals: a backend entry point (server.js, index.js, an Express/Next.js/Fastify app) → runtime nodejs

  • If both exist (e.g. a Next.js app with a Python worker), instrument each backend runtime independently.

  • If neither is present, there is nothing to instrument — stop and tell the user.

1b. Detecting the LLM framework/SDK (optional, informational)

Check dependency files for signals of: openai, @anthropic-ai/sdk / anthropic, langchain, langgraph, ai (Vercel AI SDK), boto3 + Bedrock usage, google-generativeai / google-genai, crewai, litellm, pydantic-ai, an MCP SDK, google-adk. This doesn't change the init code (ddtrace/dd-trace auto-instruments these SDKs once the tracer is initialized) — it's only used for confirming the setup with the user and for special-cased frameworks noted in the reference files (Next.js, Vercel AI SDK).

1c. Detecting the backend application framework
  • Python: fastapi, flask, or django dependency

  • Node.js: express dependency, or next (Next.js API routes / server actions)

1d. Detecting existing instrumentation, and auditing session-ID plumbing
  • Grep for ddtrace init (LLMObs.enable(, ddtrace-run) in Python, or dd-trace init (require('dd-trace').init(, dd-trace/initialize) in Node.js.

  • Also grep for a frontend RUM SDK (datadogRum.init(, @datadog/browser-rum) — not to set it up, but because its presence determines whether the opt-in RUM↔LLMObs pivot is available (that pivot reuses the RUM session ID as the LLMObs session ID).

  • If LLMObs init is already present, do not add a second enable/init call — but do not stop there. SDK presence only means "don't re-init"; it says nothing about whether a session ID actually flows. Run this audit whenever its prerequisite surfaces are present:

    • Session-ID plumbing — applicable whenever Phase 1a found an LLM/agent backend. Check whether a stable, conversation/operation-scoped session ID flows from the appropriate source: for web apps, the frontend sends a per-conversation ID and the backend request model/route reads it; for CLI/background jobs, the backend reuses an existing job/request/task ID or mints one UUID per invocation. A root agent/workflow span must then set it as session_id/sessionId (see "Beyond SDK init" and "Session ID intake by environment" in references/llmobs-python.md / references/llmobs-nodejs.md). LLMObs.enable()/dd-trace().init() alone does not establish this. If a RUM SDK is also present, the RUM↔LLMObs pivot is available as an opt-in — reusing the RUM session ID as the session_id — but that is a deliberate trade-off (the LLMObs session then spans the whole browser session), not the default; don't flag its absence as a gap.

    • If the check finds a gap, tell the user what's missing and fix it (same reference files, same ground rules) even though the SDK itself doesn't need re-initializing. If it passes, say so explicitly. Note the RUM pivot as not applicable rather than a gap when there's no RUM SDK.

1e. Detecting the dependency manifest to persist into

For each backend runtime found in 1a, identify the dependency manifest the build/deploy actually installs from. This is where any Datadog package you add (ddtrace/dd-trace) must be persisted so a clean deploy install includes it. It is not necessarily "whichever manifest file happens to exist": a repo can have several (e.g. an empty requirements.txt alongside a pyproject.toml, or multiple package.json files where only one is the deployed workspace), and editing a non-authoritative one is a silent no-op at deploy time.

Resolve it in this order:

  1. Deploy/build install command first (authoritative). Read the project's build/deploy configuration and use the install source it names: render.yaml (buildCommand), Procfile, Dockerfile (RUN … install …), Makefile, CI workflows, package.json scripts. Examples: pip install -r <file> → that requirements file; poetry install / uv sync / pdm install → pyproject.toml (+ its lockfile); pipenv install → Pipfile; npm ci / yarn install / pnpm i → package.json.

  2. Else, the highest-priority manifest present. Python: a lockfile-backed manager (uv.lock or poetry.lock → pyproject.toml) > pyproject.toml [project.dependencies]/[tool.poetry.dependencies] > requirements*.txt > Pipfile > setup.py/setup.cfg. Node.js: package.json (always).

Record the manifest path and the manager that owns it. Persisting commands (poetry add, uv add, pdm add, pipenv install, npm/yarn/pnpm/bun add) write the manifest. Non-persisting commands (pip install, uv pip install, conda install) touch only the current environment — with those you must also hand-edit the manifest. If requirements.txt is generated from a requirements.in (pip-tools), edit the .in and recompile. After hand-editing a manifest that has a lockfile, regenerate the lock so a frozen deploy install picks up the new package.


Phase 2: Instrumentation — routing table

Provision DD_API_KEY via references/common-credentials.md, then follow the reference for each backend runtime found in Phase 1a:

NeedRead
LLM Observability — Python (SDK init, spans, session ID, RUM/APM linking)references/llmobs-python.md
LLM Observability — Node.js / Next.js (SDK init, spans, session ID, RUM/APM linking)references/llmobs-nodejs.md

If Phase 1d found existing LLMObs instrumentation on a runtime, skip only that runtime's init/enable call and credential provisioning — still act on any gap the Phase 1d session-ID plumbing audit found, using the same reference files.


Phase 3: Verification and Reporting

See references/common-verify-report.md for the verify step and the JSON report shape.

Files

references/llmobs-nodejs.md

LLM Observability: Node.js

Provision DD_API_KEY first via common-credentials.md if you haven't already.

Install — and persist dd-trace into the dependency manifest the deploy installs from (the one identified in SKILL.md Phase 1e, normally package.json). npm install / yarn add / pnpm add / bun add all write it to package.json by default, which is what you want:

npm install dd-trace

(substitute yarn add / pnpm add / bun add per the detected package manager). Do not pass --no-save, and confirm dd-trace appears in package.json afterward — a package present only in node_modules is missing from a clean deploy install and the deploy will crash with Cannot find module 'dd-trace'.

In-code init (non-Next.js)

Copy the package name and .init({...}) option keys verbatim — the package is dd-trace and the options are exactly llmobs: { mlApp, agentlessEnabled } as shown below. Don't paraphrase the package name or invent option keys from memory. Adapt only the module mechanism to the project's module system: the CommonJS require('dd-trace').init(...) form is below; a project with "type": "module" in package.json or an .mjs entrypoint must use an ESM form instead (Node's --import dd-trace/initialize.mjs via NODE_OPTIONS, or import tracer from 'dd-trace' — both shown later in this file), because require is undefined in an ES module and forcing it there crashes at startup with require is not defined.

Must run before any other application code, typically as the very first line of the entry file or in a separate tracer.js required first:

require('dd-trace').init({
    llmobs: {
        mlApp: process.env.DD_LLMOBS_ML_APP,
        agentlessEnabled: true,
    },
    site: process.env.DD_SITE,
    env: process.env.DD_ENV || "dev",
});
// ... then require everything else

Env vars (.env/.env.local):

DD_API_KEY=<key from common-credentials.md>
DD_LLMOBS_ML_APP=<suitable app name, e.g. project folder name>
DD_SITE=<site, default datadoghq.com>

Next.js

Look for (or create) instrumentation.ts/.js at the project root and add:

export async function register() {
    if (process.env.NEXT_RUNTIME === 'nodejs') {
        const initializeImportName = 'dd-trace/initialize.mjs';
        await import(/* webpackIgnore: true */ initializeImportName as 'dd-trace/initialize.mjs')
    }
}

Also add serverExternalPackages: ["<llm-sdk-package>"] (e.g. "@anthropic-ai/sdk", "openai") to next.config.js. Additionally set DD_LLMOBS_ENABLED=true and DD_LLMOBS_AGENTLESS_ENABLED=true in the env file. After making these changes, run npx tsc --noEmit and add // @ts-ignore only for genuinely new errors introduced by the dd-trace types.

If the project already defines a register() export in instrumentation.ts (e.g. for other instrumentation), extend it rather than adding a second, competing register() export.

Vercel AI SDK (ai package)

The LLM Observability integration requires experimental_telemetry.isEnabled = true to be explicitly set (not omitted) on every call to generateText, streamText, generateObject, streamObject, embed, embedMany, and any tool.execute. Grep for these call sites and fix each one; if experimental_telemetry is missing entirely, add experimental_telemetry: { isEnabled: true }. For Next.js apps using this SDK, also add "ai" to serverExternalPackages.

Follow-up reading for the user: Vercel AI SDK integration docs.

Vercel-hosted projects: env var placement

On Vercel, add these to the project's env vars — Vercel dashboard for deployed environments, .env.local for local dev:

VariableValue
DD_API_KEYFrom common-credentials.md
DD_SITEe.g. datadoghq.com
DD_LLMOBS_ENABLEDtrue
DD_LLMOBS_AGENTLESS_ENABLEDtrue
DD_LLMOBS_ML_APPA suitable ML app name (e.g. the Vercel project name)

Alternative: no code change

Use NODE_OPTIONS in package.json scripts:

{ "scripts": { "start": "NODE_OPTIONS='--import dd-trace/initialize.mjs' node app.js" } }

For Next.js: "start": "NODE_OPTIONS='--import dd-trace/initialize.mjs' next start", "dev": "NODE_OPTIONS='--import dd-trace/initialize.mjs' next dev".

Note: when running an app inline for a one-off manual test (not via its normal start script), .env values won't be picked up automatically — pass them on the command line directly:

DD_LLMOBS_ENABLED=1 DD_LLMOBS_ML_APP=<app-name> DD_API_KEY=<key> DD_LLMOBS_AGENTLESS_ENABLED=1 DD_SITE=datadoghq.com pnpm dev

Beyond SDK init: a well-formed trace

Run this section even if dd-trace's init() already exists in the project. SDK init alone only turns on auto-instrumented llm spans — it does not by itself produce a session ID, a root span, or a RUM/APM link. Don't let Phase 1d's "SDK already present" finding stand in for this checklist.

Enabling the SDK produces auto-instrumented llm spans for supported providers, but a useful trace also needs a root span, session ID, and annotations. Work toward these four goals; the first two are always achievable, the last two depend on the environment:

#GoalAlways achievable?What it requires
1Well-formed LLMObs traceYesSDK initialized; a root span with the right kind; nested child spans; inputData/outputData annotated
2Agent Session IDYesA stable sessionId on the root span, scoped to one conversation/operation — it propagates to children automatically
3RUM session linkingOpt-in, needs RUM SDKCorrelating a RUM session with LLMObs works by forwarding the RUM session ID (datadogRum.getInternalContext().session_id) to the backend and using it as the sessionId. That is the only documented link, so it makes the LLMObs session span the whole browser session — several conversations in one browse collapse into one. Prefer the conversation-scoped sessionId from goal 2 by default; opt in only when that whole-browser grouping is acceptable
4APM trace linkingPartial — alwaysapm_trace_id is set on every span automatically. Full navigable linking in the UI needs a dd-agent; without one the tag is present but the APM trace won't exist

Detect what's achievable before instrumenting: check for @datadog/browser-rum on the frontend (goal 3, opt-in), and for DD_AGENT_HOST/DD_TRACE_AGENT_URL/NODE_OPTIONS=--require dd-trace/init/an agent sidecar (goal 4). Never promise RUM or navigable APM linking that the detected environment doesn't support — state the gap and the upgrade path instead.

Goal 4 config knob: the init snippets above hardcode agentlessEnabled: true. Flip it to false whenever a dd-agent is detected — leave it true otherwise (the apm_trace_id tag is still set either way, but only becomes a navigable APM trace with an agent present).

Adding spans

Obtain llmobs from the already-initialized tracer (the require('dd-trace').init(...) from "In-code init" above) — don't call .init() again:

import tracer from 'dd-trace';
const { llmobs } = tracer;

(CommonJS: same tracer instance returned by the require('dd-trace').init(...) call above — const { llmobs } = require('dd-trace');)

llmobs.trace() (preferred):

const result = await llmobs.trace(
  { kind: 'agent', name: 'my_agent', sessionId },   // set sessionId here only
  async (span) => {
    llmobs.annotate(span, { inputData: userMessage });
    const res = await doWork(userMessage);
    llmobs.annotate(span, { outputData: res });
    return res;
  }
);

llmobs.wrap() for named functions:

const instrumentedFn = llmobs.wrap({ kind: 'tool', name: 'my_tool' }, myFn);

Annotation:

llmobs.annotate(span, {
  inputData: 'user message',   // string or messages array
  outputData: 'agent response',
  metadata: { key: 'value' },
});

Session ID rule: pass sessionId only to whichever llmobs.trace()/llmobs.wrap() call is the root span of the trace — typically kind: 'agent', but kind: 'workflow' can be the root if the entry point has no direct LLM orchestration (see the LangChain.js/Vercel AI SDK notes below). Child spans created in the same async context inherit it automatically — never pass sessionId to a non-root span.

Session name (optional display title)

sessionId has no companion "display name" field in the SDK — the UI otherwise shows a truncated session ID. If the application already has (or can cheaply derive) a human-readable label for the operation — a job type, a conversation topic, a ticket title — set it as a session_name tag on the same root span that carries sessionId, via llmobs.annotate(span, { tags }):

const result = await llmobs.trace(
  { kind: 'agent', name: 'my_agent', sessionId },
  async (span) => {
    llmobs.annotate(span, { tags: { session_name: 'Playlist for Milo (cat)' } });
    return await doWork();
  }
);

Only set it once, on the root span — same rule as sessionId. Skip this if there's no natural label available; don't fabricate one from generic defaults (e.g. `session-${sessionId}`) since that's no more useful than the raw ID.

Session ID intake by environment

Scope the sessionId to one conversation/operation — that is the unit the LLM Obs UI groups on. Don't default it to the browser/RUM session (see the opt-in below for why).

Default — web app (conversation-scoped) — the frontend owns a conversation ID: mint one when a conversation starts (a chat thread, a task), reuse it across that conversation's turns, and start a fresh one when the user begins a new conversation. Send it with each request under whatever key fits this project's existing request-body naming; read it on the backend with the same request-parsing accessor the project already uses for its other fields — do not assume Express's req.body (Fastify uses request.body, Next.js Route Handlers / Hono use await req.json(), etc.; see Framework-specific notes below for the accessor per framework):

// Shown with Express's req.body — substitute the accessor your framework/route actually uses.
const sessionId = req.body.sessionId ?? req.headers['x-session-id'] ?? crypto.randomUUID();
const result = await llmobs.trace({ kind: 'agent', name: 'my_agent', sessionId }, async (span) => {
  llmobs.annotate(span, { inputData: req.body.message });
  const res = await doWork(req.body.message);
  llmobs.annotate(span, { outputData: res });
  return res;
});

Frontend side (show as instructions, never edit frontend files yourself in this reference):

body: JSON.stringify({
  message: userMessage,
  // one ID per conversation, regenerated when the user starts a new one — NOT the RUM/browser session ID
  sessionId: conversationId,
})

Opt-in — reuse the RUM session ID (enables the RUM↔LLMObs pivot) — the only documented way to pivot between a RUM session and its LLMObs traces is to forward the RUM session ID and use it as the sessionId. The trade-off: the LLMObs session then spans the whole browser session, so multiple conversations in one browse collapse into a single LLMObs session. Choose this only when that grouping is acceptable (setting up the RUM SDK itself is out of scope for this skill):

body: JSON.stringify({
  message: userMessage,
  sessionId: datadogRum.getInternalContext()?.session_id ?? crypto.randomUUID(),
})

No web frontend (CLI / background job) — reuse an existing per-operation identifier if the code already generates one before the pipeline runs (jobId, requestId, taskId, etc.) instead of minting a second ID; otherwise one UUID per invocation:

const sessionId = jobId ?? crypto.randomUUID();

Multi-agent pipelines and sub-agent orchestration

When one function invokes multiple distinct agents (or the same agent multiple times) as sequential stages of a single logical operation — a background job that runs a profiler agent, then a theme agent, then a scout agent, then a curator agent, for example — wrap the entire orchestrating function once. One root span per logical operation, not one per agent-run call:

async function runPipeline(jobId: string) {
  return llmobs.trace({ kind: 'workflow', name: 'playlist_pipeline', sessionId: jobId }, async (span) => {
    const vibe = await profilerAgent.run(...);
    const theme = await themeAgent.run(...);
    const candidates = await scoutAgent.run(...);
    const selections = await curatorAgent.run(...);
    llmobs.annotate(span, { inputData: jobId, outputData: selections });
    return selections;
  });
}

Use kind: 'workflow' here, not 'agent' — runPipeline doesn't call an LLM directly, it orchestrates other agents that do (see the decision table below). Each agent-run call auto-instruments as a nested agent span under this one root; all four land on a single trace and inherit sessionId through the same async context. This applies to any auto-instrumented agent framework (LangChain.js/LangGraph.js, Vercel AI SDK, OpenAI Agents JS) — the rule is about where you put the one enclosing span, not which framework is calling the LLM.

Anti-pattern — produces one trace/session per stage instead of one per operation: calling each stage's agent-run function directly inside the pipeline function with no enclosing llmobs.trace()/llmobs.wrap(). Without an active span in scope, there's nothing for the auto-instrumentation to attach each call to, so every stage gets its own root span — a separate trace, and (with no explicit sessionId) a separate session in the UI. This is easy to miss because the SDK is "enabled" and traces do show up — they're just fragmented one-per-agent instead of grouped one-per-operation. Symptom in the trace list: what should be one conversation instead shows up as N separate single-agent conversations (e.g. curator_agent, scout_agent, theme_agent, profiler_agent each listed on their own, instead of one session containing all four).

Detecting this while verifying: if a single logical operation (one job, one request, one pipeline run) produces more than one root span/trace in the LLM Obs UI, the fix is to add one enclosing kind: 'workflow'/'agent' span around the whole orchestrating function — not to sprinkle the same sessionId across each stage's own root span. Matching sessionIds across separate traces groups them into one session, but a single trace with nested children is the stronger, simpler, and intended fix; prefer it whenever the stages share one caller.

A deliberate split is fine — the anti-pattern is the accidental one. The failure above is unintentional fragmentation: no enclosing span and no shared sessionId, so the trace splits and the pieces never regroup. When the agents are genuinely independent or long-lived — not tight sequential stages of one operation — a separate trace per agent is a legitimate call; put the same sessionId on each root span and they still read as one session in the UI. One combined trace stays the default — it's simpler, and it's usually what a single operation is — so reach for per-agent traces when you actually want them, not because the enclosing span got dropped.

Span kind decision table

Code patternSpan kind
Top-level function orchestrating LLM calls and toolsagent
Multi-step pipeline not directly calling LLMsworkflow
Function calling an LLM directly (only if not auto-instrumented)llm (manual)
Function wrapping a capability (web search, calculator, DB)tool
Pure data transformation / business logictask
RAG / vector searchretrieval
Text embedding generationembedding

Auto-instrumented providers (no manual llm span needed): OpenAI JS SDK, Anthropic JS SDK, LangChain.js/LangGraph.js, Vercel AI SDK. Wrap the calling function with kind: 'tool' or kind: 'agent' instead of the provider call itself.

Never add spans to: web routing/middleware, data model/config classes, DB/cache clients, logging/metrics utilities, test files, pure utility functions.

Framework-specific notes

  • LangChain.js / LangGraph.js: auto-instrumented; wrap the top-level invoking function with llmobs.trace({ kind: 'workflow', sessionId }) — sessionId is valid here only because this call is the root span; don't add it to any nested kind: 'workflow' call.

  • Vercel AI SDK: auto-instrumented; wrap the calling function with llmobs.trace({ kind: 'workflow', sessionId }) (same root-span caveat) — do not wrap individual generateText()/streamText() calls, they're already traced. Also requires experimental_telemetry.isEnabled = true per call — see the Vercel AI SDK section above.

  • OpenAI Agents JS: auto-instrumented; wrap the function calling Runner.run() with llmobs.trace({ kind: 'agent', sessionId }). If that function calls more than one agent/Runner.run() (a multi-stage pipeline), wrap it once, enclosing every stage — see "Multi-agent pipelines and sub-agent orchestration" above; don't wrap each stage's call separately. Use kind: 'workflow' instead of 'agent' when the wrapper orchestrates multiple agents without calling an LLM itself.

  • Express / Fastify / Hono / Koa: session ID from req.body.sessionId or req.headers['x-session-id'] with crypto.randomUUID() fallback.

  • Next.js App Router: session ID from the request body in Route Handlers/API routes; the SDK itself still initializes in instrumentation.ts as shown above. Pages Router: same intake, init via NODE_OPTIONS — never in _app.tsx, which is a browser bundle.

  • CLI / background job: prefer an existing per-operation ID if the code already tracks one (const sessionId = jobId ?? crypto.randomUUID()); otherwise crypto.randomUUID() per run. RUM not applicable.

Reporting goal status

Alongside the standard report shape in common-verify-report.md, summarize goal status in the human-readable message, e.g.:

✓ Well-formed LLMObs trace
✓ Agent Session ID (conversation-scoped; UUID fallback)
~ RUM session linking: not applicable (no RUM SDK detected)
~ APM trace linking: apm_trace_id set, but not navigable (no dd-agent detected)

Requirements and compatibility

Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta

“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.

Needs: Needs an MCP server, Needs an API key, Needs Python, Needs Node.js

Security

What our scanner noticed· Low risk

  • Instructions that use the network

A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.

Attribution and licence

Created by
datadog-labs
Publisher
Datadog (Official)
Source
datadog-labs/agent-skills · dd-instrument-llmo · commit 8e1306a
Licence
MIT License (redistribution allowed)
  • LICENSE (added by AutoGineer from LICENSE)
  • ATTRIBUTION.md (written by AutoGineer)
Synced

AutoGineer is not affiliated with or endorsed by datadog-labs or Datadog.

Download ZIP

The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download dd-instrument-llmo.zip