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

# Filters

> Expose a slice of a Brain as a read-only, addressable view.

A filter is a saved narrowing expression over one Brain. It owns no documents, no index, and no configuration of its own — it is a named view that restricts what searches and reads can see. Filters are addressed by a **qualified reference**: `{brain-slug}:{filter-slug}`, for example `company-knowledge:support-docs`.

## Brain vs. filter

Reach for a **separate Brain** when the content is a genuinely different corpus — different documents, a different embedding model or language, or a different set of sync workflows feeding it. Reach for a **filter** when it's the *same* corpus and you want different audiences or agents to see different slices of it.

|                                                | Brain                           | Filter                                                             |
| ---------------------------------------------- | ------------------------------- | ------------------------------------------------------------------ |
| Owns documents and an index                    | Yes                             | No — reads through the parent Brain's index                        |
| Has its own embedding/chunking/language config | Yes                             | No                                                                 |
| Writable (`uploadDocument`, deletes)           | Yes, with a `write` grant       | **Never** — qualified references are read-only                     |
| Costs extra storage/indexing                   | Yes                             | No — a filter is just a stored expression                          |
| Addressed as                                   | `company-knowledge`             | `company-knowledge:support-docs`                                   |
| Access control                                 | Shared with projects explicitly | Governed by the parent Brain's project access — no separate grants |

A practical rule: if you're about to create a second Brain and a sync workflow would upload the *same documents* to both, you want one Brain and a filter.

## The filter expression

A filter combines an optional `source` condition with optional `metadata` conditions:

```json theme={null}
{
  "source": ["linear", "notion"],
  "metadata": { "team": "support", "labels": ["faq", "how-to"] }
}
```

Semantics are simple and uniform:

* **Entries AND together** — a document must match `source` *and* every metadata key.
* **Arrays mean any-of** — `"source": ["linear", "notion"]` matches documents from either source; `"labels": ["faq", "how-to"]` matches if the document's `labels` contains either value.
* **Scalars mean equals** — `"team": "support"` matches documents whose `team` metadata equals `"support"` (or contains it, when the stored attribute is an array).

This is why disciplined `source` values and metadata on upload matter: filters can only slice on what your sync workflows stored. If you anticipate a "just the support docs" view, make sure the sync writes a `team` or `docType` attribute worth filtering on.

## Creating filters

In the dashboard: **Brains → your Brain → Filters → Create filter**. Or with the CLI:

```bash theme={null}
keystroke brains filters create company-knowledge \
  --name "Support docs" \
  --slug support-docs \
  --description "Support-facing articles and FAQs only." \
  --filter '{"source":["notion"],"metadata":{"team":"support"}}'

keystroke brains filters list company-knowledge
keystroke brains filters update company-knowledge support-docs --filter '{"source":["notion","linear"]}'
keystroke brains filters delete company-knowledge support-docs
```

The description is load-bearing for agents: like a Brain's description, it's how a model decides which attached knowledge source to search, so describe *what the slice contains and when to use it*.

## Using filters

**Attach to an agent** — the agent's `brain_search` / `brain_read` tools only ever see matching documents:

```ts src/agents/support-bot.ts theme={null}
export default defineAgent({
  slug: "support-bot",
  // ...
  brains: ["company-knowledge:support-docs"],
});
```

**Open from a workflow** — a qualified reference returns a read-only handle. `search`, `read`, and `list` are narrowed by the filter; `uploadDocument`, `updateMetadata`, and deletes are rejected:

```ts src/workflows/support-digest.ts theme={null}
import { brain } from "@keystrokehq/keystroke/brain";

const supportDocs = brain("company-knowledge:support-docs");
const hits = await supportDocs.search({ query: "refund policy" });
```

**Narrow further at query time** — search requests accept their own `filters` expression, which ANDs with the persisted filter:

```ts theme={null}
await supportDocs.search({
  query: "refund policy",
  filters: { metadata: { locale: "en" } }, // support docs AND locale=en
});
```

**Filter the document table** — the dashboard's document list and `keystroke brains documents list <brain> --filter <filter-slug>` apply a saved filter server-side, which is also a quick way to sanity-check what a filter matches before pointing an agent at it.

## Behavior to know

* **Live, not snapshotted** — a filter evaluates against current documents on every query. New documents that match appear immediately; editing the expression changes what every consumer sees on their next search.
* **Deleting a filter** doesn't touch documents, but agents and workflows addressing the qualified reference stop resolving — treat filter slugs referenced in deployed code as API contracts.
* **No document can hide from its Brain** — filters narrow reads through the *qualified* reference only. Anything with `read` access to the base Brain still sees everything, so a filter is a scoping tool for agents and views, not a security boundary between projects.
