> ## Documentation Index
> Fetch the complete documentation index at: https://platform.atlan.com/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> To act on Atlan objects, use the Atlan MCP server at https://api.atlan.com/mcp or the atlanai CLI; `atlanai --map json` prints its command map. Run a read-only identity check before any write.
> The docs MCP server at /mcp searches these docs only. It cannot read or change Atlan objects.
> SDK packages: Python `atlanai` (PyPI) and TypeScript `@atlanai/sdk` (npm). Show Python first, then TypeScript.

# Trace your agent

> Create spans, record model usage and cost, wrap functions with observe, and derive deterministic trace IDs.

A trace is one agent run. Spans are the steps inside it. The SDK adds Atlan's
observation semantics on top of ordinary OpenTelemetry spans, so a step reads
as an LLM call or a tool call rather than an untyped span.

## Start with one logger

`initLogger` / `init_logger` creates one process-wide tracing pipeline. Ended
spans enter a bounded background batch by default, including spans emitted by
supported OpenTelemetry instrumentors. Call `flush()` before a script exits or
before an evaluation verifies traces and finalizes.

<CodeGroup>
  ```typescript TypeScript theme={null}
  import { initLogger, wrapTraced } from "@atlanai/tools/sdk/how-tos/tracing";

  const logger = initLogger({ projectName: "support-agent" });
  const answer = wrapTraced(callAgent, { name: "answer", type: "task" });

  try {
    await answer("How do I reset my password?");
  } finally {
    await logger.flush();
  }
  ```

  ```python Python theme={null}
  from atlanai.tracing import init_logger, traced

  logger = init_logger(project_name="support-agent")

  @traced(name="answer", type="task")
  def answer(question: str) -> str:
      return call_agent(question)

  try:
      answer("How do I reset my password?")
  finally:
      logger.flush()
  ```
</CodeGroup>

The default connection reads `ATLAN_API_KEY`, `ATLAN_WORKSPACE_ID`, and
`ATLAN_BASE_URL`. `projectName` / `project_name` becomes the trace service
name. The logger captures all OpenTelemetry spans by default so provider and
framework spans share the same trace tree.

See [Tracing integrations](/tools/sdk/how-tos/integrations) for Vercel AI SDK,
LangChain, LangGraph, Python auto-instrumentation, and direct OTLP paths.

For evaluation traces, the experiment-scoped span endpoint returns `core,io`
by default. The first read therefore includes the recorded question and answer:

```text theme={null}
GET /eval/v1/experiments/{experiment_id}/traces/{trace_id}/spans
```

Use `?fields=core` for a structure-only list. Other projection groups are
`metadata`, `attributes`, `events`, `usage`, `cost`, and `error`; combine them
as a comma-separated `fields` value. Experiment statistics and summaries are
scoped by the promoted `experiment_id`, so score counts and means are computed
from only that run's spans.

## Observation types

Set `as_type` / `asType` to classify a span. Agent Registry uses the type to
decide how to render the step and which metrics apply.

| Type | Use it for |
| - | - |
| `task` | A unit of work, and the usual choice for a root span. The default. |
| `llm` | A model call. Carries model, provider, usage, and cost. |
| `tool` | A call out to a tool or external system. |
| `function` | An internal function worth showing as its own step. |
| `session` | A conversation or long-running interaction. |
| `turn` | One exchange inside a session. |
| `score` | A span whose purpose is recording an evaluation. |

<Frame>
  <img className="block dark:hidden" src="https://mintcdn.com/atlan-602e2b74/h03yFRYof65Y69MX/assets/diagrams/trace-anatomy-light.svg?fit=max&auto=format&n=h03yFRYof65Y69MX&q=85&s=dc6d3c2403f41f7debc3d626f2456650" alt="Trace anatomy: a session identified by session_id groups traces. One trace is a task root span, summarize-thread, with llm, tool, function, and score child spans. propagate_attributes stamps session_id, user_id, and tags on every span in its scope, and a zoomed llm span shows model, provider, usage tokens, and cost." width="850" height="616" data-path="assets/diagrams/trace-anatomy-light.svg" />

  <img className="hidden dark:block" src="https://mintcdn.com/atlan-602e2b74/h03yFRYof65Y69MX/assets/diagrams/trace-anatomy-dark.svg?fit=max&auto=format&n=h03yFRYof65Y69MX&q=85&s=d7114ac767265fab871b7012a38848b8" alt="Trace anatomy: a session identified by session_id groups traces. One trace is a task root span, summarize-thread, with llm, tool, function, and score child spans. propagate_attributes stamps session_id, user_id, and tags on every span in its scope, and a zoomed llm span shows model, provider, usage tokens, and cost." width="850" height="616" data-path="assets/diagrams/trace-anatomy-dark.svg" />
</Frame>

## Create spans

`start_as_current_span` / `startAsCurrentSpan` opens a span, makes it the
active parent for anything started inside, and closes it on exit.

<CodeGroup>
  ```python Python theme={null}
  with client.start_as_current_span("summarize-thread", as_type="task") as span:
      span.update(input={"thread_id": "ticket-4821"})
      result = summarize(thread)
      span.update(output=result)
  ```

  ```typescript TypeScript theme={null}
  await client.startAsCurrentSpan("summarize-thread", { asType: "task" }, async (span) => {
    span.update({ input: { threadId: "ticket-4821" } });
    const result = await summarize(thread);
    span.update({ output: result });
  });
  ```
</CodeGroup>

Use `start_span` / `startSpan` for a detached span when the parent is not the
active context — a framework tracking its own run tree, a retroactive span with
an explicit `start_time`, or a child of a remote parent. Detached spans must be
ended explicitly.

## Record model usage and cost

`update` sets fields on the span that spent them. Record usage and cost on the
`llm` span, not the root.

<CodeGroup>
  ```python Python theme={null}
  with client.start_as_current_span("chat", as_type="llm") as generation:
      generation.update(
          model="claude-sonnet-5",
          provider="anthropic",
          usage={"input_tokens": 1200, "output_tokens": 340},
          cost={"input": 0.0036, "output": 0.0051},
          input=[{"role": "user", "parts": [{"type": "text", "content": "Summarize this thread"}]}],
          output="Three open questions remain.",
      )
  ```

  ```typescript TypeScript theme={null}
  await client.startAsCurrentSpan("chat", { asType: "llm" }, async (generation) => {
    generation.update({
      model: "claude-sonnet-5",
      provider: "anthropic",
      usage: { input_tokens: 1200, output_tokens: 340 },
      cost: { input: 0.0036, output: 0.0051 },
      input: [{ role: "user", parts: [{ type: "text", content: "Summarize this thread" }] }],
      output: "Three open questions remain.",
    });
  });
  ```
</CodeGroup>

`cost` accepts `input`, `output`, and `total`. Provide `total` when your
provider bills a single figure you cannot split.

<Note>
  `usage` keys are `agent-sdk.v1` attribute names, so they stay `snake_case` in
  both languages — `input_tokens`, not `inputTokens`. That is what keeps the
  two SDKs' output identical. `cost` takes `input`, `output`, and `total` in
  both languages. If a trace shows no token counts, check these key names
  first.
</Note>

## Wrap a function with observe

`observe` traces a function without restructuring it. It handles sync
functions, async functions, and generators.

<CodeGroup>
  ```python Python theme={null}
  from atlanai.tracing import observe


  @observe(as_type="tool")
  def search_docs(query: str) -> list[str]:
      return index.search(query)


  @observe  # bare form: as_type defaults to "task", name from the function
  async def plan(goal: str) -> str:
      ...
  ```

  ```typescript TypeScript theme={null}
  import { observe } from "@atlanai/tools/sdk/how-tos/tracing";

  const searchDocs = observe(
    (query: string) => index.search(query),
    { asType: "tool", name: "search_docs" },
  );
  ```
</CodeGroup>

Input and output are captured by default. Turn either off per function with
`capture_input=False` / `capture_output=False` when the payload is large or
sensitive. For a blanket rule across every span, use
[`trace_content`](/tools/sdk/how-tos/privacy) instead.

## Carry identity across a run

`propagate_attributes` stamps association attributes onto every span started
in its scope — including spans created by third-party instrumentation you do
not control.

<CodeGroup>
  ```python Python theme={null}
  with atlan.propagate_attributes("ticket-4821",
      user_id="jordan@example.com",
      visitor_id=visitor.id if visitor else None,
      tags=["support", "escalated"],
      trace_name="zendesk:4821",
  ):
      run_agent(thread)
  ```

  ```typescript TypeScript theme={null}
  await atlan.propagateAttributes(
    {
      sessionId: "ticket-4821",
      userId: "jordan@example.com",
      visitorId: visitor?.id,
      tags: ["support", "escalated"],
      traceName: "zendesk:4821",
    },
    async () => {
      await runAgent(thread);
    },
  );
  ```
</CodeGroup>

`session_id` groups traces into one session in Agent Registry. `trace_name`
overrides the display name of the trace, which is useful when the root span
name is generic but the run has a meaningful external label.

## Deterministic trace IDs

`create_trace_id(seed)` derives a trace ID from a stable external key. The
same seed always produces the same trace ID, in both SDKs, byte-identical. Use
it when a run is triggered by something that already has an id — a webhook, a
ticket, a queue message — so retries and multi-service handling converge on one
trace instead of fragmenting.

<CodeGroup>
  ```python Python theme={null}
  trace_id = atlan.create_trace_id(seed="ISSUE-123")

  with client.start_as_current_span(
      "linear-webhook:ISSUE-123",
      trace_context={"trace_id": trace_id},
  ) as root:
      ...
  ```

  ```typescript TypeScript theme={null}
  const traceId = atlan.createTraceId("ISSUE-123");

  await client.startAsCurrentSpan(
    "linear-webhook:ISSUE-123",
    { traceContext: { traceId } },
    async (root) => {
      // ...
    },
  );
  ```
</CodeGroup>

Called with no seed, it returns a random ID. The derivation is
Langfuse-compatible, so a run already keyed by seed in Langfuse keeps the same
trace ID here.

## Record failures

`record_failure` / `recordFailure` marks the span as errored and records the
exception without re-raising it, so tracing never changes your control flow.

<CodeGroup>
  ```python Python theme={null}
  try:
      result = call_provider()
  except ProviderError as error:
      span.record_failure(error)
      raise
  ```

  ```typescript TypeScript theme={null}
  try {
    const result = await callProvider();
  } catch (error) {
    span.recordFailure(error);
    throw error;
  }
  ```
</CodeGroup>

## Next steps

<CardGroup cols={2}>
  <Card title="Connect an integration" icon="plug" href="/tools/sdk/how-tos/integrations">
    Capture framework and model calls without rebuilding their span trees.
  </Card>

  <Card title="Add scores" icon="star" href="/tools/sdk/how-tos/scores">
    Attach evaluation results to a trace.
  </Card>

  <Card title="Privacy controls" icon="shield" href="/tools/sdk/how-tos/privacy">
    Mask payloads and suppress content.
  </Card>

  <Card title="Inspect a trace" icon="magnifying-glass" href="/registry/traces/index">
    What a trace looks like in Agent Registry.
  </Card>

  <Card title="Configuration" icon="gear" href="/tools/sdk/references/configuration">
    Options, environment variables, and export modes.
  </Card>
</CardGroup>
