> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.livingbrain.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.livingbrain.com/_mcp/server.

# Customer feedback

Feedback about one problem can sit in a support tool, a folder of interview recordings and a CRM, in
different words each time. When you capture all of it into one brain, each piece compiles onto the page
for its problem, so a product manager can read one page and follow a link to every source.

## How different sources reach one page

A support ticket, an interview transcript and a sales note describe the same problem in different words. All three compile into one customer problem page, which is connected to a related page.

Tickets, interviews and sales notes go to your app, which redacts and labels them and captures them to the brain. The brain compiles pages. A webhook updates your tracker, the team reads a daily brief, and a contested page goes to a person to resolve.

A ticket, an interview and a sales note can describe the same problem without sharing any words. Routing
compares each extracted fact with existing pages by meaning, so all three can land on the same page, and
each one updates the page and adds a source.

The line to the related page comes from a separate check on new pages. A model judges whether two pages
are worth reading together and stores its reason, so a connection can show something none of the captures
say.

## Try it from an AI tool

You can test this before writing any code. Connect a brain to [your AI tool](/docs/use-cases/one-brain-every-tool),
paste in a few pieces of feedback and ask for them back by problem.

![Hermes Agent capturing a note about a customer into a Research brain with the Living Brain CLI](/_fern-files/_img/55ece85b8d2042abdc21cc53256240f66d246587635f02aae29a3eb55227dba9.webp)

A capture is one command, and the response comes back as `pending` while the brain compiles it. This
example session comes from livingbrain.com.

Once it has compiled, try prompts such as:

```text
What do customers struggle with?

Show me the sources behind that page.
```

## How the parts fit

A support ticket, an interview transcript and a sales note describe the same problem in different words. All three compile into one customer problem page, which is connected to a related page.

Tickets, interviews and sales notes go to your app, which redacts and labels them and captures them to the brain. The brain compiles pages. A webhook updates your tracker, the team reads a daily brief, and a contested page goes to a person to resolve.

1. **Sources.** Your app collects feedback from the tools where it already lives, so customers and
   support agents keep working as they do now.
2. **Redact and label.** Your app removes personal details and sets a label and an `originRef`.
3. **Capture.** The call returns straight away with an ingest source. Save its `id` against the
   customer in your own database.
4. **Compile.** A background worker extracts facts, routes them to pages and records a new version of
   each page it changes.
5. **Pages to people.** A webhook updates your tracker, a daily brief goes to the team, and contested
   pages wait for someone to decide.

**`TypeScript`**

```typescript TypeScript
const source = await client.captures.capture({
  brainId,
  body: {
    kind: "note",
    content: redact(ticket.body),
    label: `Ticket ${ticket.id}`,
    originRef: `tickets:${ticket.id}`,
  },
});

// Your own table. The brain doesn't need to know who the customer is.
await db.feedback.insert({
  sourceId: source.id,
  customerId: ticket.customerId,
  plan: ticket.plan,
});
```

**`Python`**

```python Python
source = client.captures.capture(
    brain_id=brain_id,
    kind="note",
    content=redact(ticket.body),
    label=f"Ticket {ticket.id}",
    origin_ref=f"tickets:{ticket.id}",
)

# Your own table. The brain doesn't need to know who the customer is.
db.feedback.insert(
    source_id=source.id,
    customer_id=ticket.customer_id,
    plan=ticket.plan,
)
```

A capture returns straight away, before any compiling happens:

**`Example response`**

```json Example response
{
  "id": "6a2f1c9e-4d1b-4a57-9c0e-2f7b8e1d3a44",
  "kind": "note",
  "label": "Ticket 1042",
  "originRef": "tickets:1042",
  "status": "pending",
  "affectedPageIds": []
}
```

Once compiling finishes, the status becomes `completed` and `affectedPageIds` lists the pages it changed.

## Group by problem

Customers often describe the solution they have in mind, and a product team usually learns more from the
problem behind it. Use the [profile](/docs/concepts#brain-profile) to tell the brain to organize feedback
by problem:

**`TypeScript`**

```typescript TypeScript
await client.workspace.updateProfile({
  brainId,
  focus: "What customers struggle with in our product, and when it happens",
  vaultOrganization: "One page per customer problem, named after the problem",
  directives: {
    flagContradictions: true,
    custom: "Keep the customer's own words as quotes. Record requested features under the problem they would solve.",
  },
});
```

**`Python`**

```python Python
client.workspace.update_profile(
    brain_id=brain_id,
    focus="What customers struggle with in our product, and when it happens",
    vault_organization="One page per customer problem, named after the problem",
    directives={
        "flag_contradictions": True,
        "custom": "Keep the customer's own words as quotes. Record requested features under the problem they would solve.",
    },
)
```

Profile changes only apply to later captures, so set this before the first import.

## Count customers yourself

Source counts don't measure demand, because ten tickets from one customer and one ticket each from ten
customers both give ten sources. To count customers, join a page's sources to the table you saved at
capture time:

**`TypeScript`**

```typescript TypeScript
const page = await client.workspace.getPage({ brainId, slug: problemSlug });
const sourceIds = page.sources.map((s) => s.ingestSourceId);

const rows = await db.feedback.whereSourceIn(sourceIds);
const customers = new Set(rows.map((r) => r.customerId)).size;
const enterprise = new Set(rows.filter((r) => r.plan === "enterprise").map((r) => r.customerId)).size;
```

**`Python`**

```python Python
page = client.workspace.get_page(brain_id=brain_id, slug=problem_slug)
source_ids = [s.ingest_source_id for s in page.sources]

rows = db.feedback.where_source_in(source_ids)
customers = len({r.customer_id for r in rows})
enterprise = len({r.customer_id for r in rows if r.plan == "enterprise"})
```

**`Example output`**

```text Example output
customers: 7
enterprise: 3
```

Keep plan, revenue and region in that table too. You can join them to any page's sources without
sending them to the brain.

## Keep other systems in step

To link pages to issues in your tracker, subscribe to `brain.page.updated` and key the link on the page
ID, because a renamed page gets a new slug and keeps its ID. Read the [change feed](/docs/changes) as
well. After a `merged` event several pages have become one, and after a `split` event one page has become
several, so your links need updating.

For a weekly review, read everything that changed since the last one:

**`TypeScript`**

```typescript TypeScript
for await (const ev of await client.workspace.changes({ brainId, since: lastReview })) {
  if (ev.action === "created" || ev.action === "updated") review.add(ev.pageId);
}
```

**`Python`**

```python Python
for ev in client.workspace.changes(brain_id=brain_id, since=last_review):
    if ev.action in ("created", "updated"):
        review.add(ev.page_id)
```

To send pages to the team on a schedule, turn on [daily briefs](/docs/changes#daily-briefs) and
[bind a Slack channel](/docs/slack#bind-a-channel) with `notifyBriefs` on.

## What a contested page can mean

When new feedback contradicts a page, the brain marks it `contested` and keeps both claims until someone
resolves it.

| The conflict                                                     | What might be behind it                         | What to do                                                          |
| ---------------------------------------------------------------- | ----------------------------------------------- | ------------------------------------------------------------------- |
| A release note says it's fixed, and a later ticket says it isn't | The fix was partial, or only reached some plans | Reopen the issue and resolve the page with what's now true          |
| One customer wants it simpler, another wants more options        | Two segments want different things              | Check your table for the plan behind each claim and record both     |
| An old interview disagrees with recent tickets                   | The product changed in between                  | [Resolve](/docs/pages#contested-pages) in favor of the recent claim |

## Personal data

Redact before you capture. Text from a capture can end up in pages and their earlier versions, and
removing it from there is much harder.

If a customer asks you to delete their data, start with your table of source IDs, which shows the sources
that belong to them. Deleting an ingest source leaves the pages derived from it in place, so use each
source's `affectedPageIds` to find those pages and [edit them](/docs/pages#editing). An edit keeps the
previous text in the page's history, and only [permanent deletion](/docs/pages#deleting) removes the
versions too.

## Limits to plan for

| Limit                                | What it means here                                                   | What to do                                       |
| ------------------------------------ | -------------------------------------------------------------------- | ------------------------------------------------ |
| 50,000 characters per inline capture | A long interview transcript can exceed this                          | Split it and give each part its own `originRef`  |
| Compiling is asynchronous            | A ticket isn't searchable as soon as it's captured                   | React to `brain.page.updated`                    |
| Batches process items independently  | A failed item doesn't stop the others, so a batch can partly succeed | List sources with status `failed` and retry them |
| Profile changes aren't retroactive   | An import before the profile is set follows the old guidance         | Set the profile first                            |

## When to use something else

Use a brain for written feedback that people need to read. The rate of an error or the share of users on
a feature belongs in your analytics. A survey with fixed answers fits a spreadsheet, though its free-text
comments are worth capturing.

[How to build with Living Brain](/docs/build) covers the write and read paths in more detail.