# Metadata Search

> Filter and find notes by their YAML frontmatter — status, type, tags, priority, confidence, custom fields, and more.

Every note in Basic Memory has YAML frontmatter at the top. Metadata search lets you find notes by those fields — status, type, tags, custom fields, anything you put there. It's how you ask things like *"show me all in-progress specs tagged security"* or *"find decisions made in the last quarter with priority high or critical"* without scanning text.

Metadata search is a parameter on the same [`search_notes`](/reference/mcp-tools-reference#search_notes) tool you use for text and [semantic search](/concepts/semantic-search). You can use it on its own, or combine it with a text query and tag filters.

<tip>

Frontmatter is structured data. The richer your frontmatter, the sharper your search. A note with `type: meeting`, `status: open`, `priority: high`, and `attendees: [...]` is reachable from many more angles than one with just a title.

</tip>

---

## What's in your frontmatter

Anything you put in the YAML block at the top of a note becomes searchable:

```markdown
---
title: Auth Design
type: spec
tags: [security, oauth]
status: in-progress
priority: high
confidence: 0.85
---
```

A few fields have shortcuts in `search_notes` (`tags`, `status`, `type`), but all custom fields work the same way through `metadata_filters`. You can also reach into nested objects with **dot notation** — `schema.confidence` targets the `confidence` key inside a `schema` object.

<note>

Field **names** must match the chars `A-Z a-z 0-9 _ -` (with `.` for nesting). Field **values** can be strings, numbers, booleans, dates, or lists.

</note>

---

## Filter syntax

`metadata_filters` is a JSON object. Each key is a field name; each value is either a literal (for equality) or an operator object.

Multiple top-level keys combine with **AND** — all conditions must match.

### Equality

```json
{"status": "in-progress"}
```

### Array contains (all values present)

For list-valued fields like `tags`, an array means *"all of these"*:

```json
{"tags": ["security", "oauth"]}
```

### `$in` — any of

Match if the field equals **any** value in the list. This is how you express OR within a single field:

```json
{"priority": {"$in": ["high", "critical"]}}
```

### Comparison — `$gt`, `$gte`, `$lt`, `$lte`

Numeric or lexicographic comparison; boundaries on `$gte`/`$lte` are inclusive.

```json
{"confidence": {"$gt": 0.7}}
{"score": {"$lte": 100}}
```

### `$between` — inclusive range

Both ends included; the value must be exactly two items:

```json
{"confidence": {"$between": [0.5, 0.9]}}
```

### Nested fields (dot notation)

```json
{"schema.confidence": {"$gt": 0.7}}
```

### Operator reference

<table>
<thead>
  <tr>
    <th>
      Operator
    </th>
    
    <th>
      Value
    </th>
    
    <th>
      Behavior
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <em>
        (scalar)
      </em>
    </td>
    
    <td>
      string / number / bool / date
    </td>
    
    <td>
      Exact match
    </td>
  </tr>
  
  <tr>
    <td>
      <em>
        (array)
      </em>
    </td>
    
    <td>
      list of scalars
    </td>
    
    <td>
      All values must be present (for list fields)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        $in
      </code>
    </td>
    
    <td>
      non-empty list
    </td>
    
    <td>
      Any of
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        $gt
      </code>
      
      , <code>
        $gte
      </code>
      
      , <code>
        $lt
      </code>
      
      , <code>
        $lte
      </code>
    </td>
    
    <td>
      number
    </td>
    
    <td>
      Comparison (gte/lte inclusive)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        $between
      </code>
    </td>
    
    <td>
      <code>
        [min, max]
      </code>
    </td>
    
    <td>
      Inclusive range, exactly 2 values
    </td>
  </tr>
</tbody>
</table>

<note>

Each operator object holds **one** operator. `{"$gt": 0.5, "$lt": 1.0}` is invalid — use `{"$between": [0.5, 1.0]}` instead. There's no `$ne`, `$nin`, `$regex`, or `$exists`; combine multiple top-level keys with AND, and use `$in` for OR within a field.

</note>

---

## Convenience shortcuts

`search_notes` exposes a few common metadata filters as top-level parameters:

<table>
<thead>
  <tr>
    <th>
      Shortcut
    </th>
    
    <th>
      Equivalent filter
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        tags=["security"]
      </code>
    </td>
    
    <td>
      <code>
        {"tags": ["security"]}
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        status="draft"
      </code>
    </td>
    
    <td>
      <code>
        {"status": "draft"}
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        note_types=["spec", "decision"]
      </code>
    </td>
    
    <td>
      type filter (any of)
    </td>
  </tr>
</tbody>
</table>

You can also write a tag filter directly in the query string:

```python
# All equivalent
search_notes("tag:security")
search_notes(tags=["security"])
search_notes(metadata_filters={"tags": ["security"]})
```

Multiple tags in the shorthand mean *all of them*:

```python
search_notes("tag:tier1,alpha")   # comma-separated
search_notes("tag:tier1 alpha")   # space-separated
# Both require BOTH tags to be present.
```

If a key appears in both a shortcut and `metadata_filters`, the explicit filter wins.

---

## Combining with text and semantic search

Metadata filters compose with everything else `search_notes` does. The text query and the filters apply together (AND):

```python
# OAuth-related notes that are still in progress
search_notes("OAuth", metadata_filters={"status": "in-progress"})

# Pure metadata — no text query
search_notes(metadata_filters={"type": "spec", "status": "active"})

# Text + tag shortcut + metadata
search_notes("auth", tags=["security"], metadata_filters={"priority": {"$in": ["high", "critical"]}})
```

See [Semantic Search](/concepts/semantic-search) for how text and vector modes interact.

---

## From the CLI

`bm tool search-notes` mirrors the same surface:

```bash
# Simple equality
bm tool search-notes "" --meta status=draft

# Multiple --meta flags AND together
bm tool search-notes "" --meta status=active --meta priority=high

# Full JSON filter
bm tool search-notes "" --filter '{"confidence": {"$between": [0.3, 0.8]}}'

# Convenience shortcuts
bm tool search-notes "auth" --tag security --tag oauth --status draft --type spec
```

See [CLI Reference](/reference/cli-reference#bm-tool-search-notes) for every flag.

---

## Worked examples

Given a few notes like this:

```markdown
---
title: Auth Design
type: spec
status: in-progress
priority: high
tags: [security, oauth]
confidence: 0.85
---
```

```markdown
---
title: Search Redesign
type: spec
status: planning
priority: medium
tags: [search, performance]
confidence: 0.6
---
```

<table>
<thead>
  <tr>
    <th>
      You want…
    </th>
    
    <th>
      <code>
        metadata_filters
      </code>
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      All in-progress specs
    </td>
    
    <td>
      <code>
        {"status": "in-progress", "type": "spec"}
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      Specs with confidence above 0.7
    </td>
    
    <td>
      <code>
        {"type": "spec", "confidence": {"$gt": 0.7}}
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      Anything high or critical priority
    </td>
    
    <td>
      <code>
        {"priority": {"$in": ["high", "critical"]}}
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      Specs in confidence range <span>
        0.5, 0.9
      </span>
    </td>
    
    <td>
      <code>
        {"type": "spec", "confidence": {"$between": [0.5, 0.9]}}
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      Notes tagged both <code>
        security
      </code>
      
       AND <code>
        oauth
      </code>
    </td>
    
    <td>
      <code>
        {"tags": ["security", "oauth"]}
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      "OAuth" matches in still-open work
    </td>
    
    <td>
      use <code>
        search_notes("OAuth", metadata_filters={"status": "in-progress"})
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      Anything with nested <code>
        schema.confidence
      </code>
      
       ≥ 0.7
    </td>
    
    <td>
      <code>
        {"schema.confidence": {"$gte": 0.7}}
      </code>
    </td>
  </tr>
</tbody>
</table>

---

## Type handling

- **Strings** match exactly (case-sensitive).
- **Numbers** — comparison operators detect numeric values automatically, so `"0.5"` and `0.5` compare the same way.
- **Booleans** are compared as the strings `"True"` and `"False"`.
- **Dates and datetimes** are normalized to ISO strings.
- **Missing fields** never match — filtering on a key the note doesn't have returns no result for that note (not a null match).

---

## Limits and caveats

- **AND only at the top level.** Combine multiple keys to AND; use `$in` for OR within a single field. There's no top-level `$or`.
- **No full-text matching inside metadata.** Use exact, range, or `$in` matching — for free-text search across note bodies, use [text or semantic search](/concepts/semantic-search).
- **Lists in filters must be non-empty.** `{"tags": []}` is rejected.
- **$between requires exactly two values.**
- **No $exists, $ne, $nin, $regex.** They're not in the operator set.

---

## Related

- [Search Notes (MCP Tools Reference)](/reference/mcp-tools-reference#search_notes)
- [Semantic Search](/concepts/semantic-search) — text, vector, and hybrid modes
- [Schema System](/concepts/schema-system) — make required frontmatter fields explicit
- [Knowledge Format](/concepts/knowledge-format) — the structure of a note
- [CLI: `bm tool search-notes`](/reference/cli-reference#bm-tool-search-notes)
