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

# Enable Topics

> Confirm your traces render with the Thread preprocessor, enable Topics, and adapt traces that need a custom preprocessor.

Topics classify your traces by running an LLM over a normalized text representation of each trace. That representation comes from a **preprocessor**, and the built-in **Thread** preprocessor (the default) handles most traces out of the box. This guide covers confirming your traces render correctly, enabling Topics, and adapting traces that need a custom preprocessor.

## Check your traces

Before enabling Topics, confirm a representative trace renders correctly through the Thread preprocessor:

1. Go to [**<Icon icon="activity" /> Logs**](https://www.braintrust.dev/app/~/logs) and open a trace.
2. Select the <Icon icon="messages-square" /> **Thread** view.
3. Select <Icon icon="settings-2" /> and choose the **Thread** preprocessor.
4. Confirm the conversation renders with the user turns, assistant turns, and tool calls you expect.

If the conversation looks right, your traces are ready for Topics. If the Thread view is empty or missing content, see [When traces don't work](#when-traces-dont-work).

## Enable Topics

Once your traces pass the [check above](#check-your-traces), enabling Topics turns on the [built-in facets](/observe/topics#built-in-facets) (Task, Sentiment, Issues) for the current project and starts the daily pipeline:

1. Go to [**<Icon icon="pentagon" /> Topics**](https://www.braintrust.dev/app/~/topics).
2. Choose whether to **Apply to existing traces** or only to new traces.
3. Click **Enable topics**.

Topics is enabled per project. Repeat these steps in each project where you want classifications.

Each facet extracts a short summary from each trace and stores it in the background. Once at least 100 summaries are collected, the daily pipeline clusters them into topics that classify your logs. See [Check automation status](/observe/topics/manage#check-automation-status) to track progress or [Common issues](/observe/topics/manage#common-issues) if results don't appear.

<Tip>
  **Sessions and multi-turn conversations**: By default, Topics classifies each trace independently. If your application produces one trace per turn (typical with [auto-instrumentation](/instrument)), classifications can fragment across a conversation. For example, an early frustrated turn might be classified as **FRUSTRATED** and a later resolution turn as **POSITIVE**, so filtering on **FRUSTRATED** then surfaces every conversation that started badly, even the ones that resolved well. [Group traces into conversations](/observe/topics/manage#group-traces-into-conversations) so Topics classifies the full session as a unit.
</Tip>

<h2 id="when-traces-dont-work">
  When traces don't work
</h2>

If the [check above](#check-your-traces) didn't produce a conversation, the Thread preprocessor couldn't extract messages from your trace. The preprocessor walks every non-scorer span, reads `input` and `output`, converts recognized message data into role/content pairs, and deduplicates across spans.

Each span uses [OpenAI-style](https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create) role/content messages: an array of messages on `input`, and the assistant message object on `output`:

```json theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
{
  "input": [{"role": "user", "content": "Hi"}],
  "output": {"role": "assistant", "content": "Hello"}
}
```

When extraction fails, the trace typically falls into one of these patterns:

* **Conversation in metadata**: Messages stored in `metadata` instead of `input` or `output`.
* **Custom field names without `role`**: Objects using names like `user_question` and `assistant_reply` instead of `role` and `content`.
* **Flattened strings**: A turn collapsed into a single string like `"USER: ... \nASSISTANT: ..."`.
* **Deeply nested wrappers**: Messages buried below the recognized wrapper keys (`messages`, `prompt`, `input`, `output`, `choices`, `result`, `response`), like `data.payload.messages`.
* **LangGraph state without message arrays**: Graph state that doesn't expose a recognized message array.

You have two options: [update your instrumentation](#update-your-instrumentation) so spans match a recognized shape, or [write a custom preprocessor](#write-a-custom-preprocessor) that reshapes the data. Updating instrumentation is the better long-term fix because every downstream tool benefits from clean span data. Writing a custom preprocessor is often faster because you don't ship instrumentation changes or backfill data, and the preprocessor can apply to existing traces.

### Update your instrumentation

If you control how spans are logged, adjust `input` and `output` so they match a recognized shape. Common fixes for the patterns above:

* **Conversation in metadata**: Move messages from `metadata` to `input` and `output`.
* **Custom field names without `role`**: Rename fields to `role`/`content`, or wrap them under a recognized key like `messages`.
* **Flattened strings**: Emit a message array instead of a single concatenated string.
* **Deeply nested wrappers**: Unwrap so one of the recognized keys sits at the top level.
* **LangGraph state without message arrays**: Include the underlying LangChain or OpenAI-style messages on the spans you want Topics to see.

For supported providers and frameworks (OpenAI, Anthropic, Google Gemini, Bedrock, Vercel AI SDK, Pydantic AI, LangChain), switching to [Braintrust auto-instrumentation](/instrument) gets you a recognized shape without further work.

### Write a custom preprocessor

A custom preprocessor is a function named `handler` that receives a span's fields (`input`, `output`, `metadata`, `error`, `span_attributes`) and returns a message array Topics can use. Each example below addresses one of the patterns listed above and can be pasted directly into the preprocessor editor.

<AccordionGroup>
  <Accordion title="Conversation in metadata">
    Trace shape:

    ```json theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
    {
      "input": null,
      "output": null,
      "metadata": {
        "conversation": [
          {"role": "user", "content": "Hi"},
          {"role": "assistant", "content": "Hello"}
        ]
      }
    }
    ```

    Preprocessor:

    ```typescript theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
    function handler({
      metadata,
    }: {
      input: any;
      output: any;
      error: any;
      metadata: Record<string, any>;
      span_attributes: { name?: string; type?: string };
    }): any {
      return metadata?.conversation ?? [];
    }
    ```
  </Accordion>

  <Accordion title="Custom field names">
    Trace shape:

    ```json theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
    {
      "input": {"user_question": "How do I cancel my subscription?"},
      "output": {"assistant_reply": "From Settings > Billing > Cancel."}
    }
    ```

    Preprocessor:

    ```typescript theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
    function handler({
      input,
      output,
    }: {
      input: any;
      output: any;
      error: any;
      metadata: Record<string, any>;
      span_attributes: { name?: string; type?: string };
    }): any {
      const messages = [];
      if (input?.user_question) {
        messages.push({ role: "user", content: input.user_question });
      }
      if (output?.assistant_reply) {
        messages.push({ role: "assistant", content: output.assistant_reply });
      }
      return messages;
    }
    ```
  </Accordion>

  <Accordion title="Flattened strings">
    Trace shape:

    ```json theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
    {
      "input": "USER: Hi\nASSISTANT: Hello\nUSER: How are you?",
      "output": "I'm doing well, thanks!"
    }
    ```

    Preprocessor:

    ```typescript theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
    function handler({
      input,
      output,
    }: {
      input: any;
      output: any;
      error: any;
      metadata: Record<string, any>;
      span_attributes: { name?: string; type?: string };
    }): any {
      const messages = [];
      if (typeof input === "string") {
        for (const line of input.split("\n")) {
          const match = line.match(/^(USER|ASSISTANT):\s*(.*)$/i);
          if (match) {
            messages.push({ role: match[1].toLowerCase(), content: match[2] });
          }
        }
      }
      if (typeof output === "string") {
        messages.push({ role: "assistant", content: output });
      }
      return messages;
    }
    ```
  </Accordion>

  <Accordion title="Deeply nested wrappers">
    Trace shape:

    ```json theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
    {
      "input": {
        "data": {
          "payload": {
            "messages": [{"role": "user", "content": "Hi"}]
          }
        }
      },
      "output": {
        "data": {
          "payload": {"role": "assistant", "content": "Hello"}
        }
      }
    }
    ```

    Preprocessor:

    ```typescript theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
    function handler({
      input,
      output,
    }: {
      input: any;
      output: any;
      error: any;
      metadata: Record<string, any>;
      span_attributes: { name?: string; type?: string };
    }): any {
      const inputMessages = input?.data?.payload?.messages ?? [];
      const outputMessage = output?.data?.payload;
      return outputMessage ? [...inputMessages, outputMessage] : inputMessages;
    }
    ```
  </Accordion>

  <Accordion title="LangGraph state">
    Trace shape:

    ```json theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
    {
      "output": {
        "state": {
          "messages": [
            {"type": "human", "content": "Hi"},
            {"type": "ai", "content": "Hello"}
          ]
        }
      }
    }
    ```

    Preprocessor:

    ```typescript theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
    function handler({
      output,
    }: {
      input: any;
      output: any;
      error: any;
      metadata: Record<string, any>;
      span_attributes: { name?: string; type?: string };
    }): any {
      return (output?.state?.messages ?? []).map((m: { type: string; content: string }) => {
        const role = m.type === "human" ? "user" : m.type === "ai" ? "assistant" : "system";
        return { role, content: m.content };
      });
    }
    ```
  </Accordion>
</AccordionGroup>

To save a preprocessor and apply it across your project:

1. Go to [**<Icon icon="activity" /> Logs**](https://www.braintrust.dev/app/~/logs) and open any trace.
2. Select <Icon icon="messages-square" /> **Thread** to switch to the Thread view.
3. Select <Icon icon="settings-2" /> to open the preprocessor picker, then choose **+ Custom preprocessor (advanced)** at the bottom of the dropdown.
4. Paste or write your function, enter a name, and click **Save**.
5. Verify the Thread view now renders the conversation correctly using your preprocessor. Iterate on the function until it does.
6. Go to **<Icon icon="settings-2" /> Settings** > [**<Icon icon="ellipsis" /> Advanced**](https://www.braintrust.dev/app/~/configuration/advanced) and select your preprocessor under **Default preprocessor**. Topics will use it for every facet that doesn't override the preprocessor, including the built-in Task, Sentiment, and Issues facets.

Once the Thread view renders your traces correctly, return to [Enable Topics](#enable-topics) above to start the daily pipeline. If you already enabled Topics with the built-in default, [rewind history](/observe/topics/manage#rewind-history) after switching the default so past traces are reprocessed.

## Next steps

* [Review insights](/observe/topics/review-insights) once classifications appear.
* [Manage Topics](/observe/topics/manage) by checking pipeline status, re-generating topics, and adjusting sampling.
* [Create custom facets](/observe/topics/custom-facets) for domain-specific patterns beyond the built-in facets.
