Skip to main content
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 and open a trace.
  2. Select the Thread view.
    In the trace viewer, only the original thread view uses preprocessors and shows the preprocessor picker. 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.

Enable Topics

Once your traces pass the check above, enabling Topics turns on the built-in facets (Task, Sentiment, Issues) for the current project and starts the daily pipeline.
Only members of the Owners permission group can enable Topics. If you aren’t an owner, ask one to enable Topics for this project.
  1. Go to 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 to track progress or Common issues if results don’t appear.
Sessions and multi-turn conversations: By default, Topics classifies each trace independently. If your application produces one trace per turn (typical with auto-instrumentation), 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 so Topics classifies the full session as a unit.

When traces don’t work

If the check above 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 role/content messages: an array of messages on input, and the assistant message object on output:
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 so spans match a recognized shape, or 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 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. To manage all of the project’s saved preprocessors in one place, use its 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 and can be pasted directly into the preprocessor editor.
Trace shape:
Preprocessor:
Trace shape:
Preprocessor:
Trace shape:
Preprocessor:
Trace shape:
Preprocessor:
Trace shape:
Preprocessor:
To save a preprocessor and apply it across your project:
  1. Go to Logs and open any trace.
  2. Select Thread to switch to the Thread view.
    In the trace viewer, only the original thread view uses preprocessors and shows the preprocessor picker. 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.
    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 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 and select your preprocessor under Default preprocessor. Topics uses this 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 above to start the daily pipeline. If you already enabled Topics with the built-in default, rewind history after switching the default so past traces are reprocessed.

Next steps