> ## Documentation Index
> Fetch the complete documentation index at: https://docs.braintrust.dev/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 [** Logs**](https://www.braintrust.dev/app/~/logs) and open a trace.

2. Select the  **Thread** view.

   > [!NOTE]
   >
   > In the trace viewer, only the original thread view uses preprocessors and shows the preprocessor picker. The [new thread view](https://docs.braintrust.dev/observe/examine-traces.md#use-the-new-thread-view) is the default, so select  and choose **Go back to old thread view** first.

3. Select  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](https://docs.braintrust.dev/observe/topics.md#built-in-facets) (Task, Sentiment, Issues) for the current project and starts the daily pipeline.

> [!NOTE]
>
> Only members of the **Owners** [permission group](https://docs.braintrust.dev/admin/access-control.md) can enable Topics. If you aren't an owner, ask one to enable Topics for this project.

1. Go to [** 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](https://docs.braintrust.dev/observe/topics/manage.md#check-automation-status) to track progress or [Common issues](https://docs.braintrust.dev/observe/topics/manage.md#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](https://docs.braintrust.dev/instrument.md)), 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](https://docs.braintrust.dev/observe/topics/manage.md#group-traces-into-conversations) so Topics classifies the full session as a unit.

## When traces don't work

If the [check above](#check-your-traces) didn't produce a conversation, the Thread preprocessor couldn't extract messages from your trace. The preprocessor reads `input` and `output` on spans with type `llm` or untyped spans, 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](https://docs.braintrust.dev/instrument.md) gets you a recognized shape without further work.

### Write a custom preprocessor

A custom preprocessor is a function named `handler` that reshapes a span's fields into a message array Topics can use. Write it in the preprocessor editor in the Thread view, or write, preview, and push it from local files with [`bt preprocessors`](https://docs.braintrust.dev/reference/cli/preprocessors). To manage all of the project's saved preprocessors in one place, use its [Preprocessors](https://docs.braintrust.dev/admin/projects.md#manage-preprocessors) settings.

When you write the function, keep in mind:

* **Inputs**: The function receives `input`, `output`, `metadata`, `error`, and `span_attributes`. It doesn't receive `expected`, so log expected output under `metadata` if the preprocessor needs it.
* **Unavailable fields**: Braintrust can represent an unavailable field by omitting it or setting it to `null`. Treat both forms as unavailable.
* **Output**: Return a message array. Output that isn't a recognized message array is converted to text, and the thread layout renders that text labeled with the preprocessor's name instead of a role.
* **Runtime errors**: If the preprocessor throws an error on any span, the whole preprocessor run fails instead of skipping that span.
* **Syntax errors**: Braintrust rejects a preprocessor with a syntax error, such as an unclosed `/*` comment, when you save it. The error message starts with `Preprocessor code has a syntax error` and includes the parser's description of the problem. An existing preprocessor that already contains a syntax error fails every run with the same error until you fix the code and save it again.

Each of the following examples handles one of the [patterns above](#when-traces-dont-work) and can be pasted directly into the preprocessor editor.

> [accordion] 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] 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] 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] 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] 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 };
>   });
> }
> ```

To save a preprocessor and apply it across your project:

1. Go to [** Logs**](https://www.braintrust.dev/app/~/logs) and open any trace.

2. Select  **Thread** to switch to the Thread view.

   > [!NOTE]
   >
   > In the trace viewer, only the original thread view uses preprocessors and shows the preprocessor picker. The [new thread view](https://docs.braintrust.dev/observe/examine-traces.md#use-the-new-thread-view) is the default, so select  and choose **Go back to old thread view** first.

3. Select  to open the preprocessor picker, then choose **+ Custom preprocessor** at the bottom of the dropdown.

4. Paste or write your function, enter a name, and click **Save**.

   > [!TIP]
   >
   > To author the function with a coding agent, click **Copy prompt** and pass the copied instructions to your agent. The agent uses the [`bt` CLI](https://docs.braintrust.dev/reference/cli/quickstart) to inspect your real trace data and writes the preprocessor `handler` for you.

5. Verify the Thread view now renders the conversation correctly using your preprocessor. Iterate on the function until it does.

6. Go to ** Settings** > [** Project**](https://www.braintrust.dev/app/~/configuration/general) and select your preprocessor under **Default preprocessor**. Topics uses this [default preprocessor](https://docs.braintrust.dev/admin/projects.md#set-a-default-preprocessor) 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](https://docs.braintrust.dev/observe/topics/manage.md#rewind-history) after switching the default so past traces are reprocessed.

## Next steps

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


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.