---
title: "Add attributes and context"
description: "Add queryable metadata, session and customer context, LLM usage, and handled errors to traced work."
seo:
  title: "Add Trace Attributes and Context | Judgment Docs"
  description: "Add Judgment trace attributes, session and customer context, LLM metadata, and handled error status."
---

Add context after the trace structure is healthy. Keep input, output, model
usage, session identity, customer identity, and error state on the span that
owns them so Judgment can display, query, and evaluate the run correctly.

## Add queryable attributes

`Tracer.observe` captures function arguments and return values automatically.
Setting them again inside an observed function overwrites the captured values
unless automatic capture is disabled. Lower-level span APIs do not capture I/O,
so set input and output explicitly when you use them.

Use custom attributes for metadata that is not already part of the function's
input or output:

**Python**

```python
@Tracer.observe(span_type="agent", span_name="handle")
def handle(user_id: str, question: str) -> str:
    Tracer.set_attribute("app.channel", "web")
    Tracer.set_attributes({"app.plan": "pro"})
    Tracer.set_customer_user_id(user_id)
    return run(question)
```

**TypeScript**

```typescript
const handle = Tracer.observe(async function handle(
  userId: string,
  question: string,
): Promise<string> {
  Tracer.setAttribute("app.channel", "web");
  Tracer.setAttributes({ "app.plan": "pro" });
  Tracer.setCustomerUserId(userId);
  return run(question);
}, { spanType: "agent", spanName: "handle" });
```

Use your own namespace for custom keys. The `judgment.*` namespace is reserved
for the public keys in [Attribute keys](/documentation/tracing/attribute-keys).

In Python, use
[`Tracer.scoped_context()`](/sdk-reference/python/trace/tracer#scoped_context)
when session, customer, or custom attributes must apply before a span starts.
For context that must cross process boundaries, use the
[Python baggage API](/sdk-reference/python/trace/baggage) or the TypeScript
baggage APIs in the [SDK reference](/sdk-reference).

## Name the work consistently

Use spans for the model calls, tool calls, retrievals, and decisions that the
selected integration does not capture. Name every span for the work it performs
and keep its kind consistent:

| Span kind | Use it for |
| --- | --- |
| `agent` | The app root or a step where an agent decides or delegates. |
| `llm` | One model call and its model, token, and cost metadata. |
| `tool` | A tool, function, or retrieval call. |

## Record handled and returned failures

An uncaught exception marks an observed span as an error. A returned failure
does not, so mark it when the application recognizes a failure signal:

- A non-zero exit code or HTTP status of 400 or higher
- An error payload
- An exhausted retry or iteration limit
- A cancellation or timeout
- A non-empty denial list

Decide failure from the application's status field, result type, or explicit
success rule. A child can fail while the root succeeds when the agent still
reaches a valid result. If the final result fails, mark the root as an error
even when the function returns normally.

**Python**

```python
from opentelemetry.trace import Status, StatusCode

@Tracer.observe(span_type="tool", span_name="fetch_record")
def fetch_record(record_id: str) -> dict:
    result = client.fetch(record_id)
    if result.get("error"):
        Tracer.get_current_span().set_status(
            Status(StatusCode.ERROR, result["error"])
        )
    return result
```

**TypeScript**

```typescript
const fetchRecord = Tracer.observe(async function fetchRecord(
  recordId: string,
) {
  const result = await client.fetch(recordId);
  if (result.error) {
    Tracer.setError(new Error(result.error));
  }
  return result;
}, { spanType: "tool", spanName: "fetch_record" });
```

## Record each LLM call once

Prefer a supported framework or model-provider integration. When none exists,
create one `llm` span and pass the available fields to `recordLLMMetadata()`:

- `model`
- `provider`
- `non_cached_input_tokens`
- `output_tokens`
- `cache_read_input_tokens`
- `cache_creation_input_tokens`
- `total_cost_usd`

```typescript
Tracer.recordLLMMetadata({
  model: "gpt-5.6-luna",
  provider: "openai",
  output_tokens: 150,
});
```

Judgment can calculate cost from the provider, model, and token counts. Set
`total_cost_usd` only when the provider or subprocess reports a real cost; an
invented zero makes paid work look free.

Keep model metadata on the model span. Do not copy it to a parent span, which
would count the same usage twice. If a tool calls a model, keep both spans: the
tool span represents the action and its `llm` child owns model usage.

See [Attribute keys](/documentation/tracing/attribute-keys) for the exact
`judgment.*` keys written by this helper.

## Group traces into sessions

Set the session ID on each trace's root while it is active. Child spans inherit
it.

- Use a stable application identity such as a chat, workflow, or job ID.
- Keep the same ID across retries, replays, and worker replacement.
- If a CLI, subprocess, or upstream service owns the conversation, use its
  conversation ID and keep the application's request ID as a custom attribute.
- Record values you will query as attributes. Values buried only inside input
  or output payloads are not directly queryable.

**Python**

```python
@Tracer.observe(span_type="agent", span_name="chat_turn")
def chat_turn(chat_id: str, user_message: str) -> str:
    Tracer.set_session_id(chat_id)
    return run(user_message)
```

**TypeScript**

```typescript
const chatTurn = Tracer.observe(async function chatTurn(
  chatId: string,
  userMessage: string,
) {
  Tracer.setSessionId(chatId);
  return run(userMessage);
}, { spanType: "agent", spanName: "chat_turn" });
```

Use `set_customer_id` or `setCustomerId` for a tenant or account, and
`set_customer_user_id` or `setCustomerUserId` for an individual end user. The
Sessions view then groups traces by the stable session while customer fields
remain available for filters and breakdowns.

## Verify

Run one normal request and one handled failure. In Judgment, confirm:

1. The root has the expected session and customer identities.
2. Custom attributes are searchable and do not use the reserved
   `judgment.*` namespace.
3. Each model call appears once with its provider, model, usage, and cost.
4. The handled failure span is marked as an error while a successfully
   recovered root remains successful.

## Troubleshooting

| Symptom | Check |
| --- | --- |
| Session or customer ID is missing | Call its setter while the intended root is active. |
| Values exist but are not searchable as Judgment fields | Use a public Judgment key directly or [configure an attribute mapper](/documentation/tracing/attribute-mappers). |
| Model usage or cost is doubled | Remove the duplicate manual wrapper, provider integration, or copied parent metadata. |
| Returned failures remain successful | Mark the active span from the application's explicit failure signal. |
| Input or output changed unexpectedly | Do not overwrite automatic `Tracer.observe` capture unless that is intentional. |

## Related pages

- [Attribute keys](/documentation/tracing/attribute-keys)
- [Configure attribute mappers](/documentation/tracing/attribute-mappers)
- [Subprocess models](/documentation/tracing/subprocess-models)
- [Trace conventions](/documentation/reference/trace-conventions)
