> ## 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.

# Use custom preprocessor to fix missing Topics facet

> Restore missing Topics facet summaries by adding a custom preprocessor when your trace structure is not recognized by the default Thread preprocessor.

export const plans_0 = "Any"

export const deployments_0 = "Any"

export const data_plane_version_0 = undefined

export const use_case_0 = "Use case - Restore missing Task, Sentiment, or Issues facet summaries when a custom or reconstructed trace does not use the conversation structure recognized by Braintrust's default Thread preprocessor."

<Note>
  **Applies to:**

  * Plan - {plans_0}
  * Deployment - {deployments_0}
  * {data_plane_version_0}
  * {use_case_0}
</Note>

## Summary

When a built-in **Task**, **Sentiment**, or **Issues** facet processes traces but produces no matched facet summaries, check whether the Thread view contains the complete user and assistant conversation. If you send a custom or reconstructed trace that stores messages outside the recognized structure, Braintrust's default Thread preprocessor cannot transform it into the complete message sequence used by Topics. Topics then receives incomplete context and can return `no_match` even when the raw trace contains a clear signal. Adding a custom preprocessor requires fewer ingestion changes and can remain the project default. Alternatively, format model calls as `llm` spans with recognized `input` and `output` messages so the default preprocessor can reconstruct the conversation.

## What is happening

Before running a Topics facet's prompt, Braintrust preprocesses the trace into a formatted conversation. By default, facets use Braintrust's built-in Thread preprocessor. If you create a custom preprocessor and set it as the project default, facets use it unless configured to use another preprocessor.

Braintrust's default Thread preprocessor reads `input` and `output` from `llm` or untyped spans, converts recognized message formats into `role`/`content` pairs, and deduplicates messages across spans. A typical trace records the messages sent to the model on the LLM span's `input` and the returned assistant message on its `output`.

Custom ingestion and post-processing pipelines do not always produce this structure. As a result, the default preprocessor may return an incomplete conversation, causing Topics to evaluate only part of the interaction.

An incomplete conversation can cause a facet to return its exclusion result even when the raw trace contains a clear signal:

* **Sentiment** results that start with `NEUTRAL` become `no_match`.
* **Task** or **Issues** results that start with `NONE` become `no_match`.

Results stored as `no_match` are processed successfully, but they are excluded from the facet's **Matched** count and from Topics clustering. This means the underlying problem is not necessarily the facet prompt or Topics automation. First confirm that the facet's effective preprocessor returns the complete conversation. To check this, open a trace in **Logs**, select **Thread**, and confirm that the expected user and assistant messages appear.

## Fix

### Option 1: Add a custom Topics preprocessor

Use a custom preprocessor as a compatibility layer when you want to retain the existing trace structure. This is usually quicker than changing ingestion and is especially useful for pipelines that reconstruct traces after the model call or must preserve a custom schema. It can be a durable solution as long as you maintain and retest it when the source trace structure changes.

1. Go to [**<Icon icon="activity" /> Logs**](https://www.braintrust.dev/app/~/logs), open a representative trace, and select the **Thread** tab.
2. Select the preprocessor picker, then select **+ Custom preprocessor**.
3. Add a preprocessor that explicitly transforms your root-span or custom trace fields into the complete conversation as an array of `role`/`content` messages. For example, emit user turns from `input.messages` or `turn_N` fields and assistant text from `output.answer`.
4. Test the preprocessor against several representative traces. Confirm the Thread preview includes the complete user and assistant conversation, then save it with a clear name.
5. Go to **<Icon icon="settings-2" /> Settings** > [**<Icon icon="ellipsis" /> Advanced**](https://www.braintrust.dev/app/~/configuration/advanced) and select it under **Default preprocessor**. Topics facets use this default unless a facet explicitly selects another preprocessor.
6. For traces that already contain `no_match`, use [`bt topics rewind`](/docs/observe/topics/manage#rewind-history) to replay the affected historical window through the Topics pipeline with the updated preprocessor. Newly processed traces use the updated project default automatically.

Re-generating Topics only rebuilds topics from the existing facet summaries. It does not rerun the facets or apply the updated preprocessor to traces that have already been processed.

For additional trace shapes and example preprocessors, see [Write a custom preprocessor](/docs/observe/topics/enable#write-a-custom-preprocessor).

For traces reconstructed after the model call, changing the span type alone does not add missing LLM runtime metrics. A custom preprocessor can preserve the existing trace schema while formatting its conversation for the Thread view, Topics facets, and other functions that inherit the project default.

### Option 2: Format traces for the default Thread preprocessor

If you control how traces are created, use the standard structure so Braintrust's default preprocessor can transform them without project-specific logic:

1. Record each model call as an `llm` span.
2. Store the messages sent to the model on `input` as `role`/`content` objects.
3. Store the returned response on `output` as an assistant message.
4. Test the trace with the built-in Thread preprocessor and confirm it reconstructs the complete user and assistant conversation.

For supported providers and frameworks, [Braintrust auto-instrumentation](/docs/instrument) produces a recognized structure automatically. See [Update your instrumentation](/docs/observe/topics/enable#update-your-instrumentation) for other supported trace shapes and fixes.

## How to confirm it worked

* In [**<Icon icon="activity" /> Logs**](https://www.braintrust.dev/app/~/logs), open representative traces and select **Thread**. Confirm that the selected preprocessor displays every expected user and assistant message.
* Open [**<Icon icon="triangle" /> Scorers**](https://www.braintrust.dev/app/~/scorers) and test the affected **Task**, **Sentiment**, or **Issues** facet. Confirm a trace with a clear signal returns a usable facet summary rather than `no_match`.
* If you used a custom preprocessor, set it as the project default, then test the same trace again to confirm the facet uses it without a manual override.
* On [**<Icon icon="pentagon" /> Topics**](https://www.braintrust.dev/app/~/topics), open the affected facet's menu and select **View matched logs**. Confirm that matched facet summaries now appear.
* Check the automation status after reprocessing. Topics needs at least 100 facet summaries before it can generate clusters for a facet.

## Notes

* The `no_match` status is different from `Skipped`. `no_match` means the facet ran but its output matched the facet's exclusion pattern. `Skipped` means the preprocessor did not return usable content. See [Topics facets empty due to Skipped status](/docs/kb/topics-facets-empty-due-to-skipped-status) for that troubleshooting path.
* A legitimate neutral interaction or a trace with no identifiable task or issue can correctly return `no_match`. Test with traces that contain an unambiguous signal before treating the result as an extraction problem.
* Preprocessors are project-scoped. Configure the preprocessor separately in every project that uses the same reconstructed trace shape.
* A custom preprocessor is coupled to the trace schema. Retest the Thread view whenever the upstream API payload or reconstruction pipeline changes.
* A custom preprocessor does not need to be temporary. It can remain the project default when preserving the existing trace structure is preferable to changing ingestion.
* If multiple projects share the same reconstruction pipeline, maintain and test one common preprocessor implementation even though it must be configured separately in each project.
