# Introduction

A Rust SDK, terminal UI, and MCP server for [Roam Research](https://roamresearch.com).

The `roam-sdk` crate provides three things in one package:

* **SDK** — a Rust client for the Roam Research API. Use it to build your own tools, scripts, or integrations.
* **TUI** — a terminal-based interface for navigating and editing your Roam graph.
* **MCP Server** — expose your Roam graph to AI assistants via the Model Context Protocol.

## Quick links

| I want to...                 | Go to                                                               |
| ---------------------------- | ------------------------------------------------------------------- |
| Use the terminal app         | [TUI Installation](/tui/installation)                               |
| Configure the app            | [Configuration](/tui/configuration)                                 |
| Learn keybindings            | [Keybindings](/tui/keybindings)                                     |
| Export notes                 | [Export](/export)                                                   |
| Select multiple blocks       | [Multi-Block Selection](/tui/multi-select)                          |
| Set up MCP for AI assistants | [MCP Setup](/mcp-server/setup)                                      |
| See MCP tools                | [Tools Reference](/mcp-server/tools)                                |
| Use the Rust SDK             | [SDK Getting Started](/sdk/getting-started)                         |
| See API reference            | [Client](/sdk/client), [Types](/sdk/types), [Queries](/sdk/queries) |

## Architecture

```
roam-sdk (crate)
├── lib.rs          → SDK: RoamClient, types, queries, errors
├── main.rs         → TUI: terminal interface using the SDK
├── mcp.rs          → MCP: stdio server with 18 tools
└── export.rs       → Export: markdown and JSON formatters
```

The TUI is built on [Ratatui](https://ratatui.rs) + [Tokio](https://tokio.rs) and uses the SDK internally. The MCP server uses [rmcp](https://crates.io/crates/rmcp) for the Model Context Protocol implementation. Both ship from the same crate — install the binary with `cargo install roam-sdk`, or add the library with `cargo add roam-sdk`.


# Export

Export daily notes and pages to Markdown or JSON format. Available both as a CLI subcommand (without launching the TUI) and as an in-app keybinding.

## CLI export

### Daily note

```bash
# Today's note as markdown (stdout)
roam export

# Specific date
roam export --date 2026-03-15

# As JSON
roam export --date 2026-03-15 --format json

# Write to file
roam export --date 2026-03-15 --output notes.md
```

### Page

```bash
# Export a page by title
roam export --page "Project Alpha" --format md

# As JSON to file
roam export --page "Meeting Notes" --format json --output meeting.json
```

### Options

| Flag             | Description                   | Default |
| ---------------- | ----------------------------- | ------- |
| `--date`         | Date in `YYYY-MM-DD` format   | Today   |
| `--page`         | Page title (overrides --date) | —       |
| `--format`       | `md` or `json`                | `md`    |
| `--output`, `-o` | File path                     | stdout  |

## In-TUI export

Press the export keybinding while viewing any page or daily notes:

| Preset | Keybinding |
| ------ | ---------- |
| Vim    | `Ctrl+E`   |
| Emacs  | `Alt+E`    |
| VSCode | `Ctrl+E`   |

The current view is exported as markdown to `~/roam-export/<title>.md`. A status message shows the file path.

## Markdown format

The exported markdown preserves Roam's block hierarchy using indented bullet points:

```markdown
# March 15th, 2026

- Meeting with [[John]]
  - Discussed Q1 roadmap
  - Action items
    - Follow up on budget
- **Deep Work** chapter 5
  - Key insight: time blocking works
```

* Indentation: 2 spaces per depth level
* Roam syntax is preserved: `[[links]]`, `((block refs))`, `{{TODO}}`, `**bold**`, etc.
* Code blocks keep their fenced syntax
* Multiple daily notes are separated by `---`

## JSON format

```json
[
  {
    "title": "March 15th, 2026",
    "uid": "03-15-2026",
    "date": "2026-03-15",
    "blocks": [
      {
        "uid": "abc123",
        "string": "Meeting with [[John]]",
        "order": 0,
        "open": true,
        "children": [
          {
            "uid": "def456",
            "string": "Discussed Q1 roadmap",
            "order": 0,
            "open": true,
            "children": []
          }
        ]
      }
    ]
  }
]
```

Each block includes `uid`, `string`, `order`, `open`, and nested `children`.


# Commands Reference

The `roam` binary exposes all SDK operations as CLI subcommands. No subcommand launches the TUI.

```bash
roam                    # launch TUI
roam journal            # view today's daily note
roam search "meeting"   # search pages by title
roam get page "Books"   # get a page with all blocks
```

Output is JSON for structured data (pages, blocks, queries) and plain text for status messages.

## Commands

### journal (alias: j)

View or add to your daily note.

```bash
# View today's daily note
roam journal
roam j

# View a specific date
roam journal view --date 2026-03-10

# Add a block to today's note
roam journal add "Meeting with [[John]]"

# Add to a specific date, at the top
roam journal add "First block" --date 2026-03-10 --order first

# Add with children blocks
roam journal add "Parent block" --children '["child 1", "child 2"]'
```

| Flag         | Description                                 |
| ------------ | ------------------------------------------- |
| `--date`     | Date in YYYY-MM-DD format (default: today)  |
| `--order`    | Position: `first`, `last`, or numeric index |
| `--children` | JSON array of child block strings           |

### search

Search pages by title or blocks by content.

```bash
# Search page titles
roam search "project"

# Search inside block content
roam search "action item" --blocks

# Limit results
roam search "meeting" --limit 5
```

| Flag             | Description                                 |
| ---------------- | ------------------------------------------- |
| `-b`, `--blocks` | Search block content instead of page titles |
| `-l`, `--limit`  | Maximum number of results                   |

### get

Read pages, blocks, daily notes, backlinks, refs, and stats.

```bash
# Get a page with all blocks
roam get page "Books"

# Get a block by UID
roam get block "abc123def"

# Get today's daily note
roam get daily

# Get a specific date
roam get daily --date 2026-03-10

# Get backlinks (blocks referencing a page)
roam get backlinks "Project Alpha"

# Get outbound references from a block
roam get refs "abc123def"

# Get graph statistics
roam get stats
```

### query

Run raw Datalog queries against your graph.

```bash
# Find all pages
roam query '[:find ?title :where [?e :node/title ?title]]'

# Query with arguments
roam query '[:find ?uid ?s :where [?e :block/uid ?uid] [?e :block/string ?s]]' \
  --args '["some-uid"]'
```

| Flag     | Description                   |
| -------- | ----------------------------- |
| `--args` | JSON array of query arguments |

### export

Export daily notes or pages to Markdown or JSON.

```bash
# Export today's daily note as markdown (stdout)
roam export

# Export a specific date as JSON
roam export --date 2026-03-10 --format json

# Export a page to a file
roam export --page "Books" --output books.md

# Export as JSON to file
roam export --page "Books" --format json --output books.json
```

| Flag             | Description                                     |
| ---------------- | ----------------------------------------------- |
| `--date`         | Date in YYYY-MM-DD format (default: today)      |
| `--page`         | Page title (exports page instead of daily note) |
| `--format`       | Output format: `md` or `json` (default: `md`)   |
| `-o`, `--output` | Output file path (default: stdout)              |

### create

Create pages and blocks.

```bash
# Create a page
roam create page "New Project"

# Create a page with a specific UID
roam create page "New Project" --uid "custom-uid"

# Create a block under a parent
roam create block --parent "parent-uid" "Block content"

# Create at a specific position with children
roam create block --parent "parent-uid" "Parent" --order first \
  --children '["child 1", "child 2"]'
```

### update

Update block content.

```bash
roam update block "block-uid" "New content for this block"
```

### delete

Delete blocks or pages.

```bash
roam delete block "block-uid"
roam delete page "page-uid"
```

### move

Move a block to a new parent.

```bash
# Move to a new parent (appends at end)
roam move block "block-uid" --parent "new-parent-uid"

# Move to a specific position
roam move block "block-uid" --parent "new-parent-uid" --order first
```

| Flag       | Description                                 |
| ---------- | ------------------------------------------- |
| `--parent` | New parent block/page UID                   |
| `--order`  | Position: `first`, `last`, or numeric index |

### batch

Execute multiple write operations from a JSON file or stdin.

```bash
# From a file
roam batch operations.json

# From stdin
echo '[{"action": "create-block", ...}]' | roam batch

# Pipe from another command
cat operations.json | roam batch -
```

The input is a JSON array of [WriteAction](/sdk/types) objects.

### sync

Bidirectional sync between Roam and local markdown files. See [Sync](/cli/sync) for full documentation.

```bash
# Pull changes from Roam
roam sync

# Push local edits to Roam
roam sync --direction push

# Full bidirectional sync
roam sync --direction both

# Preview without writing
roam sync --dry-run
```

| Flag            | Default           | Description                 |
| --------------- | ----------------- | --------------------------- |
| `--direction`   | `pull`            | `pull`, `push`, or `both`   |
| `-d`, `--dir`   | config `sync.dir` | Output directory            |
| `--daily`       | false             | Include daily notes         |
| `--dry-run`     | false             | Preview mode                |
| `--concurrency` | `5`               | Parallel fetches            |
| `--filter`      | none              | Page title prefix filter    |
| `--history`     | none              | Show history for a page UID |

### mcp

Start the MCP server over stdio (used by AI assistants).

```bash
roam mcp
roam --mcp  # legacy flag, same behavior
```

See [MCP Server docs](/mcp-server/mcp) for configuration details.

## Composing with other tools

The CLI outputs JSON, making it easy to pipe into `jq`, scripts, or other tools:

```bash
# Get all page titles containing "project"
roam search "project" | jq -r '.[].title'

# Export today's note and send to clipboard
roam export | pbcopy

# Add a timestamped entry
roam journal add "$(date +%H:%M) — Started deep work session"

# Batch create from a list
cat <<'EOF' | jq -c '[.[] | {action: "create-block", location: {parent_uid: "daily-uid", order: "last"}, block: {string: .}}]' | roam batch
["Task 1", "Task 2", "Task 3"]
EOF
```


# Sync

Bidirectional sync between your Roam graph and local markdown files. Edit in Roam or in your editor — changes flow both ways.

```bash
roam sync                       # pull changes from Roam
roam sync --direction push      # push local edits to Roam
roam sync --direction both      # pull then push
```

## How it works

### Pull (remote → local)

1. Queries the Roam API for the full page list (1 API call)
2. Queries pages modified since last sync using `:page/edit-time` (1 API call)
3. Compares against locally tracked pages in [ChronDB](https://github.com/avelino/chrondb)
4. Pulls only new or modified pages (1 API call per page)
5. Writes markdown files to the sync directory
6. Commits and pushes to git remote (if configured)

Daily notes are synced first (most recent first), then regular pages.

### Push (local → remote)

1. Reads all local `.md` files
2. Parses markdown back into Roam's block tree structure
3. Pulls current state from Roam to get block UIDs
4. Diffs local blocks against remote blocks by position in the tree
5. Applies changes to Roam via API:
   * **Updated blocks** → content changed in place
   * **New blocks** → created with parent UID and children
   * **Deleted blocks** → removed from Roam

### Bidirectional (`--direction both`)

Runs pull first, then push. Safe ordering — remote changes are downloaded before local changes are pushed, avoiding blind overwrites.

## Output structure

```
~/.config/roam-tui/sync/
  pages/
    Project Alpha.md
    Meeting Notes.md
    C++ _ Rust.md          # special chars sanitized
  daily/
    03-30-2026.md          # daily notes by UID
    03-29-2026.md
```

Page titles with `/ \ : * ? " < > |` are replaced with `_` in filenames.

## Markdown format

Files use a simple format that round-trips cleanly with Roam:

```markdown
# Page Title

- First block
  - Child block
    - Grandchild
- Second block
- [[Page Reference]] and ((block-ref)) preserved as-is
```

Each `-` line is a block. Indentation (2 spaces per level) represents nesting. Roam syntax (`[[links]]`, `((refs))`, `{{commands}}`) is preserved verbatim.

## Flags

| Flag            | Default           | Description                               |
| --------------- | ----------------- | ----------------------------------------- |
| `--direction`   | `pull`            | Sync direction: `pull`, `push`, or `both` |
| `-d`, `--dir`   | config `sync.dir` | Output directory for markdown files       |
| `--daily`       | false             | Include daily notes                       |
| `--dry-run`     | false             | Show what would change, don't write       |
| `--concurrency` | `5`               | Parallel page fetches                     |
| `--filter`      | none              | Only sync pages matching this prefix      |
| `--history`     | none              | Show version history for a page UID       |

## Examples

```bash
# Pull everything from Roam
roam sync

# Edit a file locally, then push changes back
vim ~/.config/roam-tui/sync/pages/Project\ Alpha.md
roam sync --direction push

# Full bidirectional sync
roam sync --direction both

# Only pages starting with "Project/"
roam sync --filter "Project/"

# Preview without writing
roam sync --dry-run

# Custom output directory
roam sync --dir ~/notes/roam
```

## Configuration

```toml
[sync]
dir = "~/.config/roam-tui/sync"               # markdown output
db_dir = "~/.config/roam-tui/.chrondb"         # ChronDB storage
remote = "git@github.com:user/notes.git"       # git remote for markdown files
```

Override via environment variables:

| Variable            | Config equivalent |
| ------------------- | ----------------- |
| `ROAM_SYNC_DIR`     | `sync.dir`        |
| `ROAM_SYNC_DB__DIR` | `sync.db_dir`     |
| `ROAM_SYNC_REMOTE`  | `sync.remote`     |

## Git remote

When `remote` is configured, `roam sync` automatically commits and pushes markdown files after each sync. This gives you a versioned backup of your notes on GitHub, GitLab, or any git remote.

The git repo is initialized automatically in the sync directory on first run. SSH keys from your system are used for authentication.

## Change detection

Pull uses Roam's `:page/edit-time` attribute to detect which pages changed since the last sync. This catches direct edits and changes to blocks referenced on the page.

Push compares local markdown against the current Roam state by position in the block tree. Blocks are matched by their order — content differences generate updates, missing blocks generate deletions, extra blocks generate creations.

## Storage

Sync state is stored in [ChronDB](https://github.com/avelino/chrondb), a git-based key/value database. It tracks which pages have been synced, the last sync timestamp, and provides version history.

Default location: `~/.config/roam-tui/.chrondb`

## Graceful interruption

Press `Ctrl+C` during sync to stop gracefully. The current page finishes, all progress is saved to ChronDB, and markdown files already written remain on disk. Run `roam sync` again to continue from where you left off.

If ChronDB's index gets corrupted (e.g., from a hard kill), it's rebuilt automatically from git data on the next run.

## API usage

| Scenario                  | API calls                     |
| ------------------------- | ----------------------------- |
| First sync (500 pages)    | 501 (1 list + 500 pulls)      |
| Re-sync (nothing changed) | 2 (list + modified check)     |
| Re-sync (3 pages changed) | 5 (list + modified + 3 pulls) |
| Push (10 files changed)   | 10 pulls + N writes           |


# Overview

`roam` is a terminal-based client for Roam Research. It lets you browse daily notes, navigate pages, edit blocks, and search — all from your terminal.

## Features

* Browse daily notes with infinite scroll (older days load on demand)
* Navigate to any page via `[[links]]` or the search popup
* Edit blocks with optimistic updates (changes apply instantly, sync in background)
* Undo/redo for text edits, block creation, deletion, and moves
* Indent/dedent blocks with Tab/Shift+Tab
* Block references `((uid))` resolve inline
* Linked references section per day/page
* Syntax highlighting for fenced code blocks (14 languages)
* Vim, Emacs, and VSCode keybinding presets
* [Multi-block selection](/tui/multi-select) with batch delete, indent, and dedent
* [Export](/export) current view to markdown (Ctrl+E)
* Dark and light themes
* Auto-refresh every 30 seconds

## Modal interface

The TUI uses a modal state machine:

| Mode             | Description                                                    |
| ---------------- | -------------------------------------------------------------- |
| **Normal**       | Navigate blocks, open pages, trigger search                    |
| **Insert**       | Edit block text, cursor movement, paired brackets              |
| **Search**       | Filter blocks by text, jump to result                          |
| **Autocomplete** | Type `((` in insert mode to search and insert block references |

Press `Esc` to return to Normal mode from any other mode.


# Installation

## From crates.io

```bash
cargo install roam-sdk
```

This installs the `roam` binary.

## From source

```bash
git clone https://github.com/avelino/roam-tui.git
cd roam-tui
cargo install --path .
```

## From GitHub releases

Pre-built binaries are available for every release:

| Platform                    | Target                            |
| --------------------------- | --------------------------------- |
| Linux x86\_64               | `roam-x86_64-unknown-linux-gnu`   |
| Linux ARM64                 | `roam-aarch64-unknown-linux-gnu`  |
| macOS x86\_64               | `roam-x86_64-apple-darwin`        |
| macOS ARM64 (Apple Silicon) | `roam-aarch64-apple-darwin`       |
| Windows x86\_64             | `roam-x86_64-pc-windows-msvc.exe` |

Download from [Releases](https://github.com/avelino/roam-tui/releases), make executable (`chmod +x roam-*`), and move to your PATH.

## First run

On first run, `roam` creates a default config file and tells you where it is:

```
Created default config at: ~/.config/roam-tui/config.toml
Please edit it with your Roam graph name and API token, then run again.
```

Edit the file with your graph name and API token, then run `roam` again.

## Getting an API token

1. Open your Roam graph in the browser
2. Go to **Settings** → **Graph** → **API tokens**
3. Create a new token with read/write permissions
4. Copy the token into your config file or set the `ROAM_GRAPH_API__TOKEN` environment variable


# Configuration

Config file location: `~/.config/roam-tui/config.toml`

## Full example

```toml
[graph]
name = "my-graph"
api_token = "roam-graph-token-..."

[ui]
theme = "dark"
sidebar_default = true
sidebar_width_percent = 35

[keybindings]
preset = "vim"

[keybindings.bindings]
quit = "Ctrl+q"
search = "Ctrl+f"

[sync]
dir = "~/.config/roam-tui/sync"
db_dir = "~/.config/roam-tui/.chrondb"
remote = "git@github.com:user/notes.git"
```

## Options

### `[graph]` — required

| Key         | Type   | Description                                |
| ----------- | ------ | ------------------------------------------ |
| `name`      | string | Your Roam graph name (as shown in the URL) |
| `api_token` | string | API token with read/write access           |

### `[ui]` — optional

| Key                     | Type   | Default  | Description                             |
| ----------------------- | ------ | -------- | --------------------------------------- |
| `theme`                 | string | `"dark"` | Color theme: `"dark"` or `"light"`      |
| `sidebar_default`       | bool   | `true`   | Show sidebar on startup                 |
| `sidebar_width_percent` | u16    | `35`     | Sidebar width as percentage of terminal |

### `[keybindings]` — optional

| Key      | Type   | Default | Description                                    |
| -------- | ------ | ------- | ---------------------------------------------- |
| `preset` | string | `"vim"` | Base preset: `"vim"`, `"emacs"`, or `"vscode"` |

### `[keybindings.bindings]` — optional

Override individual keys from the preset. Keys are action names, values are key strings.

```toml
[keybindings.bindings]
quit = "Ctrl+q"
search = "Ctrl+f"
move_up = "Ctrl+k"
```

See [Keybindings](/tui/keybindings) for all available actions and key format.

### `[sync]` — optional

| Key      | Type   | Default                       | Description                                                               |
| -------- | ------ | ----------------------------- | ------------------------------------------------------------------------- |
| `dir`    | string | `~/.config/roam-tui/sync`     | Directory for synced markdown files                                       |
| `db_dir` | string | `~/.config/roam-tui/.chrondb` | ChronDB storage directory                                                 |
| `remote` | string | `""`                          | Git remote URL for markdown backup (e.g. `git@github.com:user/notes.git`) |

See [Sync](/cli/sync) for usage details.

## Environment variables

Every config option can be set via environment variables with the `ROAM_` prefix. Use `__` (double underscore) for nesting.

| Variable                          | Config equivalent          |
| --------------------------------- | -------------------------- |
| `ROAM_GRAPH_NAME`                 | `graph.name`               |
| `ROAM_GRAPH_API__TOKEN`           | `graph.api_token`          |
| `ROAM_UI_THEME`                   | `ui.theme`                 |
| `ROAM_UI_SIDEBAR__DEFAULT`        | `ui.sidebar_default`       |
| `ROAM_UI_SIDEBAR__WIDTH__PERCENT` | `ui.sidebar_width_percent` |
| `ROAM_KEYBINDINGS_PRESET`         | `keybindings.preset`       |
| `ROAM_SYNC_DIR`                   | `sync.dir`                 |
| `ROAM_SYNC_DB__DIR`               | `sync.db_dir`              |
| `ROAM_SYNC_REMOTE`                | `sync.remote`              |

Environment variables override file values. This is useful for keeping tokens out of config files:

```bash
export ROAM_GRAPH_API__TOKEN="roam-graph-token-..."
roam
```


# Keybindings

## Presets

Set the preset in your config:

```toml
[keybindings]
preset = "vim"  # or "emacs" or "vscode"
```

## Vim (default)

### Normal mode

| Action              | Keys                              |
| ------------------- | --------------------------------- |
| Move up             | `k` / `Up`                        |
| Move down           | `j` / `Down`                      |
| Collapse block      | `h`                               |
| Expand block        | `l`                               |
| Enter / toggle      | `Enter`                           |
| Edit block          | `i`                               |
| Create block below  | `o`                               |
| Delete block        | `dd`                              |
| Undo                | `u`                               |
| Redo                | `Ctrl+R`                          |
| Indent              | `Tab`                             |
| Unindent            | `Shift+Tab`                       |
| Search              | `/`                               |
| Quick switcher      | `Ctrl+P`                          |
| Next day            | `N` / `PageDown`                  |
| Previous day        | `P` / `PageUp`                    |
| Go to today         | `G`                               |
| Toggle sidebar      | `b`                               |
| Navigate back       | `Ctrl+O` / `Shift+Left` / `Alt+[` |
| Navigate forward    | `Shift+Right` / `Alt+]`           |
| Export current view | `Ctrl+E`                          |
| Select block up     | `Shift+Up`                        |
| Select block down   | `Shift+Down`                      |
| Help                | `?`                               |
| Quit                | `q`                               |

### Insert mode

| Action                 | Keys                                  |
| ---------------------- | ------------------------------------- |
| Exit to normal         | `Esc`                                 |
| Move cursor            | Arrow keys                            |
| Word left/right        | `Ctrl+Left` / `Ctrl+Right`            |
| Home / End             | `Home` / `End` or `Ctrl+A` / `Ctrl+E` |
| Toggle TODO            | `Ctrl+Enter` or `Alt+Enter`           |
| Indent block           | `Tab`                                 |
| Dedent block           | `Shift+Tab`                           |
| Block ref autocomplete | Type `((`                             |

## Emacs

| Action              | Keys                    |
| ------------------- | ----------------------- |
| Move up             | `Ctrl+P` / `Up`         |
| Move down           | `Ctrl+N` / `Down`       |
| Collapse block      | `Ctrl+B`                |
| Expand block        | `Ctrl+F`                |
| Enter / toggle      | `Enter`                 |
| Edit block          | `Enter`                 |
| Create block below  | `Alt+Enter`             |
| Undo                | `Ctrl+/`                |
| Redo                | `Ctrl+Shift+/`          |
| Search              | `Ctrl+S`                |
| Next day            | `Alt+N` / `PageDown`    |
| Previous day        | `Alt+P` / `PageUp`      |
| Go to today         | `Ctrl+D`                |
| Navigate back       | `Shift+Left` / `Alt+[`  |
| Navigate forward    | `Shift+Right` / `Alt+]` |
| Export current view | `Alt+E`                 |
| Select block up     | `Shift+Up`              |
| Select block down   | `Shift+Down`            |
| Help                | `Ctrl+H`                |
| Quit                | `Ctrl+Q`                |

## VSCode

| Action              | Keys                    |
| ------------------- | ----------------------- |
| Move up             | `Up`                    |
| Move down           | `Down`                  |
| Collapse block      | `Ctrl+Left`             |
| Expand block        | `Ctrl+Right`            |
| Enter / toggle      | `Enter`                 |
| Edit block          | `Enter`                 |
| Create block below  | `Ctrl+Enter`            |
| Undo                | `Ctrl+Z`                |
| Redo                | `Ctrl+Shift+Z`          |
| Search              | `Ctrl+Shift+F`          |
| Quick switcher      | `Ctrl+P`                |
| Next day            | `Alt+Up` / `PageDown`   |
| Previous day        | `Alt+Down` / `PageUp`   |
| Go to today         | `Ctrl+D`                |
| Toggle sidebar      | `Ctrl+B`                |
| Navigate back       | `Shift+Left` / `Alt+[`  |
| Navigate forward    | `Shift+Right` / `Alt+]` |
| Export current view | `Ctrl+E`                |
| Select block up     | `Shift+Up`              |
| Select block down   | `Shift+Down`            |
| Help                | `F1`                    |
| Quit                | `Ctrl+Q`                |

## Custom overrides

Override any action from the preset:

```toml
[keybindings.bindings]
quit = "Ctrl+q"
search = "Ctrl+f"
move_up = "Ctrl+k"
move_down = "Ctrl+j"
```

### Key format

Modifiers: `Ctrl`, `Alt`, `Shift` (case-insensitive), combined with `+`.

Special keys: `Enter`, `Esc`, `Tab`, `Backspace`, `Delete`, `Up`, `Down`, `Left`, `Right`, `Home`, `End`, `PageUp`, `PageDown`, `F1`–`F12`.

Examples: `Ctrl+k`, `Alt+Enter`, `Shift+Left`, `Ctrl+Shift+Z`.

### Available actions

`quit`, `move_up`, `move_down`, `cursor_left`, `cursor_right`, `collapse`, `expand`, `enter`, `exit`, `edit_block`, `create_block`, `indent`, `unindent`, `undo`, `redo`, `search`, `quick_switcher`, `next_day`, `prev_day`, `go_daily`, `toggle_sidebar`, `nav_back`, `nav_forward`, `export`, `select_up`, `select_down`, `help`


# Multi-Block Selection

Select multiple blocks at once and perform batch operations: delete, indent, or dedent all selected blocks in one step.

## Selecting blocks

Extend the selection with `Shift+Up` and `Shift+Down` from the current cursor position. The selection is always a contiguous range.

| Action      | Vim          | Emacs        | VSCode       |
| ----------- | ------------ | ------------ | ------------ |
| Select up   | `Shift+Up`   | `Shift+Up`   | `Shift+Up`   |
| Select down | `Shift+Down` | `Shift+Down` | `Shift+Down` |

All selected blocks are visually highlighted.

### Resetting selection

Any regular movement key (`j`/`k`, arrows, page up/down) collapses the selection back to a single block cursor.

## Batch operations

With multiple blocks selected:

| Operation | Key         | Effect                                             |
| --------- | ----------- | -------------------------------------------------- |
| Delete    | `dd`        | Deletes all selected blocks                        |
| Indent    | `Tab`       | Indents all selected blocks under previous sibling |
| Dedent    | `Shift+Tab` | Dedents all selected blocks to parent level        |

### Delete

All blocks in the selection range are removed. The API call uses batch-actions to delete them in a single request.

### Indent / Dedent

Each block in the selection is indented (or dedented) individually, preserving relative order. All moves are sent as a batch API call.

## Undo

Batch operations create a single undo entry. Pressing undo (`u` in Vim, `Ctrl+/` in Emacs, `Ctrl+Z` in VSCode) reverts the entire batch operation in one step — all deleted blocks are restored, or all indent/dedent moves are reverted.

## Limitations

* Selection is always a **contiguous range** (no non-contiguous multi-select like Cmd+Click in Roam web)
* Selection does not span across **linked refs** sections
* Entering **insert mode** resets the selection
* Selection works only in **Normal mode**


# Overview

The roam-sdk includes a built-in [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) server that exposes your Roam graph to AI assistants like Claude, Cursor, and other MCP-compatible clients.

## Features

* **18 tools** covering read, write, search, and export operations
* Runs over **stdio transport** — works with any MCP client
* **Batch writes** — execute multiple operations in a single API call
* **Full-text search** — search inside block content, not just page titles
* **Daily note access** — retrieve notes by date or today's note
* **Markdown export** — convert pages to clean markdown
* **Graph statistics** — page and block counts at a glance

## Quick start

```bash
# Run directly
roam --mcp

# Or via npx (no install needed)
npx roam-tui@latest --mcp
```

The server reads config from `~/.config/roam-tui/config.toml` or environment variables.

## Next steps

* [Setup & Configuration](/mcp-server/setup) — configure your MCP client
* [Tools Reference](/mcp-server/tools) — all 18 tools with parameters and examples


# Setup

## Configuration

The MCP server needs your Roam graph name and API token. Configure via environment variables or config file.

### Environment variables

```bash
export ROAM_GRAPH_NAME="your-graph-name"
export ROAM_GRAPH_API__TOKEN="roam-graph-token-..."
```

### Config file

Same config file as the TUI (`~/.config/roam-tui/config.toml`):

```toml
[graph]
name = "your-graph-name"
api_token = "roam-graph-token-..."
```

## Client configuration

### Claude Desktop / Claude Code

Add to your MCP settings:

```json
{
  "mcpServers": {
    "roam": {
      "command": "npx",
      "args": ["-y", "roam-tui@latest", "--mcp"],
      "env": {
        "ROAM_GRAPH_NAME": "your-graph-name",
        "ROAM_GRAPH_API__TOKEN": "roam-graph-token-..."
      }
    }
  }
}
```

### Using a local binary

If you installed via `cargo install roam-sdk`:

```json
{
  "mcpServers": {
    "roam": {
      "command": "roam",
      "args": ["--mcp"],
      "env": {
        "ROAM_GRAPH_NAME": "your-graph-name",
        "ROAM_GRAPH_API__TOKEN": "roam-graph-token-..."
      }
    }
  }
}
```

### Cursor

Add to `.cursor/mcp.json` in your project or `~/.cursor/mcp.json` globally:

```json
{
  "mcpServers": {
    "roam": {
      "command": "npx",
      "args": ["-y", "roam-tui@latest", "--mcp"],
      "env": {
        "ROAM_GRAPH_NAME": "your-graph-name",
        "ROAM_GRAPH_API__TOKEN": "roam-graph-token-..."
      }
    }
  }
}
```

## Supported platforms

The npm package includes pre-built binaries for:

| Platform | Architecture                   |
| -------- | ------------------------------ |
| macOS    | ARM64 (Apple Silicon), x86\_64 |
| Linux    | x86\_64, ARM64                 |
| Windows  | x86\_64                        |

## Verifying

Once configured, ask your AI assistant to search your Roam graph:

> "Search my Roam graph for pages about 'project'"

It should use the `search` tool and return matching page titles.


# Tools Reference

All 18 tools available in the Roam MCP server.

## Read operations

### search

Search pages by title in the Roam graph.

| Parameter | Type   | Required | Description                                          |
| --------- | ------ | -------- | ---------------------------------------------------- |
| `query`   | string | yes      | Case-insensitive substring match against page titles |
| `limit`   | number | no       | Maximum results to return (default: no limit)        |

Returns an array of `{title, uid}` objects.

### search\_blocks

Full-text search inside block content across the entire graph.

| Parameter | Type   | Required | Description                                         |
| --------- | ------ | -------- | --------------------------------------------------- |
| `query`   | string | yes      | Case-insensitive substring match against block text |
| `limit`   | number | no       | Maximum results (default: 50)                       |

Returns an array of `{uid, string, page_title}` objects.

### get\_page

Get a page by title with its full block tree.

| Parameter | Type   | Required | Description      |
| --------- | ------ | -------- | ---------------- |
| `title`   | string | yes      | Exact page title |

Returns the raw Roam pull result with `:node/title`, `:block/uid`, `:block/children`, `:block/string`, `:block/refs`.

### get\_block

Get a block by UID with its full subtree.

| Parameter | Type   | Required | Description |
| --------- | ------ | -------- | ----------- |
| `uid`     | string | yes      | Block UID   |

Returns the block with children, refs, order, and open/collapsed state.

### get\_daily\_note

Get a daily note by date.

| Parameter | Type   | Required | Description                                     |
| --------- | ------ | -------- | ----------------------------------------------- |
| `date`    | string | no       | Date in `YYYY-MM-DD` format. Defaults to today. |

Returns the full block tree for that day's note.

### get\_backlinks

Get all blocks across the graph that reference a page by title.

| Parameter | Type   | Required | Description                      |
| --------- | ------ | -------- | -------------------------------- |
| `title`   | string | yes      | Page title to find backlinks for |

Returns results grouped by source page: `[{page_title, blocks: [{uid, string}]}]`.

### get\_block\_refs

Get all outbound references from a block.

| Parameter | Type   | Required | Description |
| --------- | ------ | -------- | ----------- |
| `uid`     | string | yes      | Block UID   |

Returns referenced entities with their UIDs and titles (pages via `[[]]` and blocks via `(())`).

### get\_graph\_stats

Get graph statistics. No parameters.

Returns `{pages: number, blocks: number}`.

### export\_page\_as\_markdown

Export a page as formatted markdown.

| Parameter | Type   | Required | Description          |
| --------- | ------ | -------- | -------------------- |
| `title`   | string | yes      | Page title to export |

Returns a markdown string with `# Title` heading and indented bullet list preserving block hierarchy.

### roam\_query

Run a raw Datalog query against the Roam graph.

| Parameter | Type   | Required | Description                   |
| --------- | ------ | -------- | ----------------------------- |
| `query`   | string | yes      | Datalog query string          |
| `args`    | string | no       | JSON array of query arguments |

**Example queries:**

Find all pages:

```
[:find ?title ?uid :where [?e :node/title ?title] [?e :block/uid ?uid]]
```

Find blocks containing text (with args):

```
[:find ?uid ?s :in $ ?search :where [?b :block/string ?s] [?b :block/uid ?uid] [(clojure.string/includes? ?s ?search)]]
```

Args: `["search term"]`

## Write operations

### create\_page

Create a new page.

| Parameter | Type   | Required | Description                            |
| --------- | ------ | -------- | -------------------------------------- |
| `title`   | string | yes      | Page title                             |
| `uid`     | string | no       | Custom UID (auto-generated if omitted) |

### create\_block

Create a single block under a parent.

| Parameter    | Type   | Required | Description                                               |
| ------------ | ------ | -------- | --------------------------------------------------------- |
| `parent_uid` | string | yes      | UID of parent block or page                               |
| `content`    | string | yes      | Block content (Roam markdown)                             |
| `order`      | string | no       | `"first"`, `"last"`, or numeric index (default: `"last"`) |

### create\_block\_with\_children

Create a block with nested children in a single operation.

| Parameter    | Type   | Required | Description                       |
| ------------ | ------ | -------- | --------------------------------- |
| `parent_uid` | string | yes      | UID of parent block or page       |
| `content`    | string | yes      | Block content                     |
| `order`      | string | no       | Position (default: `"last"`)      |
| `uid`        | string | no       | Custom UID for the parent block   |
| `children`   | string | no       | JSON array of child block strings |

**Example children:** `["Child 1", "Child 2", "Child 3"]`

### update\_block

Update the text content of an existing block.

| Parameter | Type   | Required | Description |
| --------- | ------ | -------- | ----------- |
| `uid`     | string | yes      | Block UID   |
| `content` | string | yes      | New content |

### delete\_block

Delete a block and all its children.

| Parameter | Type   | Required | Description |
| --------- | ------ | -------- | ----------- |
| `uid`     | string | yes      | Block UID   |

### delete\_page

Delete a page and all its blocks.

| Parameter | Type   | Required | Description |
| --------- | ------ | -------- | ----------- |
| `uid`     | string | yes      | Page UID    |

### move\_block

Move a block to a new parent.

| Parameter    | Type   | Required | Description                  |
| ------------ | ------ | -------- | ---------------------------- |
| `uid`        | string | yes      | Block UID to move            |
| `parent_uid` | string | yes      | UID of the new parent        |
| `order`      | string | no       | Position (default: `"last"`) |

### batch\_write

Execute multiple write operations in sequence. Each action is sent as an individual API request, stopping on the first error.

| Parameter | Type   | Required | Description                  |
| --------- | ------ | -------- | ---------------------------- |
| `actions` | string | yes      | JSON array of action objects |

Each action follows the Roam write API format:

```json
[
  {"action": "create-page", "page": {"title": "New Page"}},
  {"action": "create-block", "location": {"parent-uid": "abc", "order": "last"}, "block": {"string": "Content"}},
  {"action": "update-block", "block": {"uid": "xyz", "string": "Updated text"}},
  {"action": "delete-block", "block": {"uid": "old-uid"}},
  {"action": "move-block", "block": {"uid": "b1"}, "location": {"parent-uid": "new-parent", "order": 0}}
]
```

## Suggested workflows

### Read a page and summarize

1. `search` to find the page title
2. `get_page` or `export_page_as_markdown` to get content
3. Process the content

### Add structured notes

1. `create_page` to create a new page
2. `create_block_with_children` to add content with nested structure
3. Or use `batch_write` for complex multi-level structures

### Daily review

1. `get_daily_note` (no date = today)
2. `get_backlinks` to see what references today's note
3. `search_blocks` to find related content

### Graph exploration

1. `get_graph_stats` to understand graph size
2. `search` to find pages by topic
3. `get_backlinks` to discover connections
4. `get_block_refs` to follow outbound links from specific blocks


# Overview

`roam-sdk` is a Rust client for the [Roam Research API](https://roamresearch.com). It provides an async HTTP client, typed data structures, and query builders.

## What you get

| Module       | What's inside                                                              |
| ------------ | -------------------------------------------------------------------------- |
| `RoamClient` | Async HTTP client with `pull`, `query`, `write`, and `write_batch` methods |
| `types`      | `Block`, `DailyNote`, `WriteAction`, `LinkedRefGroup`, and more            |
| `queries`    | Helpers to build Datalog queries and pull selectors                        |
| `RoamError`  | Typed errors for API, network, and parsing failures                        |

## Design

* **Async-first** — built on `reqwest` + `tokio`
* **rustls** — no OpenSSL dependency
* **Typed mutations** — `WriteAction` enum covers create, update, delete, move, and batch operations
* **Raw results where needed** — pull responses return `serde_json::Value` for flexibility with Roam's dynamic schema

## Example

```rust
use roam_sdk::{RoamClient, queries, types};

#[tokio::main]
async fn main() -> roam_sdk::Result<()> {
    let client = RoamClient::new("my-graph", "my-token");

    // Fetch a page
    let (eid, selector) = queries::pull_page_by_title("Projects");
    let resp = client.pull(eid, &selector).await?;
    println!("{}", resp.result);

    // Update a block
    client.write(types::WriteAction::UpdateBlock {
        block: types::BlockUpdate {
            uid: "block-uid".into(),
            string: "New content".into(),
        },
    }).await?;

    Ok(())
}
```


# Getting Started

## Add the dependency

```bash
cargo add roam-sdk
```

Or add to `Cargo.toml`:

```toml
[dependencies]
roam-sdk = "0.3"
tokio = { version = "1", features = ["full"] }
```

## Create a client

```rust
use roam_sdk::RoamClient;

let client = RoamClient::new("your-graph-name", "your-api-token");
```

The graph name is the one that appears in your Roam URL. The API token can be generated in Roam under **Settings > Graph > API tokens**.

## Read a daily note

```rust
use roam_sdk::{RoamClient, queries, types::DailyNote};
use chrono::NaiveDate;

#[tokio::main]
async fn main() -> roam_sdk::Result<()> {
    let client = RoamClient::new("my-graph", "my-token");

    let date = NaiveDate::from_ymd_opt(2026, 2, 21).unwrap();
    let uid = queries::daily_note_uid_for_date(2, 21, 2026); // "02-21-2026"
    let (eid, selector) = queries::pull_daily_note(&uid);

    let resp = client.pull(eid, &selector).await?;
    let note = DailyNote::from_pull_response(date, uid, &resp.result);

    println!("Title: {}", note.title);
    for block in &note.blocks {
        println!("  - {}", block.string);
    }

    Ok(())
}
```

## Read any page

```rust
let (eid, selector) = queries::pull_page_by_title("My Page");
let resp = client.pull(eid, &selector).await?;
let note = DailyNote::from_pull_response(
    NaiveDate::from_ymd_opt(2000, 1, 1).unwrap(), // dummy date for non-daily pages
    resp.result.get(":block/uid")
        .and_then(|v| v.as_str())
        .unwrap_or("")
        .to_string(),
    &resp.result,
);
```

## Find linked references

```rust
use roam_sdk::{queries, types};

let query = queries::linked_refs_query("Projects");
let resp = client.query(query, vec![]).await?;
let groups = types::parse_linked_refs(&resp.result, "Projects");

for group in &groups {
    println!("From page: {}", group.page_title);
    for block in &group.blocks {
        println!("  - {}", block.string);
    }
}
```

## Write operations

```rust
use roam_sdk::types::*;

// Create a block
client.write(WriteAction::CreateBlock {
    location: BlockLocation {
        parent_uid: "page-uid".into(),
        order: OrderValue::Position("last".into()),
    },
    block: NewBlock {
        string: "Hello from Rust!".into(),
        uid: None,
        open: None,
    },
}).await?;

// Update a block
client.write(WriteAction::UpdateBlock {
    block: BlockUpdate {
        uid: "block-uid".into(),
        string: "Updated text".into(),
    },
}).await?;

// Delete a block
client.write(WriteAction::DeleteBlock {
    block: BlockRef { uid: "block-uid".into() },
}).await?;

// Move a block
client.write(WriteAction::MoveBlock {
    block: BlockRef { uid: "block-uid".into() },
    location: BlockLocation {
        parent_uid: "new-parent-uid".into(),
        order: OrderValue::Index(0),
    },
}).await?;
```

## Error handling

```rust
use roam_sdk::RoamError;

match client.pull(eid, &selector).await {
    Ok(resp) => println!("{}", resp.result),
    Err(RoamError::Api { status: 429, .. }) => {
        eprintln!("Rate limited — wait and retry");
    }
    Err(RoamError::Api { status: 401, .. }) => {
        eprintln!("Invalid token — check your credentials");
    }
    Err(e) => eprintln!("Error: {}", e),
}
```


# Client

`RoamClient` is the async HTTP client for the Roam Research API.

## Creating a client

```rust
use roam_sdk::RoamClient;

let client = RoamClient::new("graph-name", "api-token");
```

The client is `Clone` — you can share it across async tasks.

## Methods

### `pull`

Fetch an entity by ID or lookup reference using a [pull expression](https://www.roamresearch.com/#/app/developer-documentation/page/eb8OVhaFC).

```rust
pub async fn pull(
    &self,
    eid: serde_json::Value,
    selector: &str,
) -> Result<PullResponse>
```

**Parameters:**

* `eid` — entity identifier. Either a direct eid or a lookup ref as an EDN string:
  * `json!("[:block/uid \"02-21-2026\"]")` — lookup by block UID
  * `json!("[:node/title \"My Page\"]")` — lookup by page title
* `selector` — EDN pull expression selecting which attributes to return

**Returns:** `PullResponse { result: serde_json::Value }`

**Example:**

```rust
use serde_json::json;

let eid = json!("[:node/title \"Projects\"]");
let selector = "[:block/uid :node/title :block/string {:block/children ...}]";

let resp = client.pull(eid, selector).await?;
let title = resp.result.get(":node/title").and_then(|v| v.as_str());
```

### `query`

Run a [Datalog query](https://www.roamresearch.com/#/app/developer-documentation/page/eb8OVhaFC) against the graph.

```rust
pub async fn query(
    &self,
    query: String,
    args: Vec<serde_json::Value>,
) -> Result<QueryResponse>
```

**Parameters:**

* `query` — Datalog query string with `:find` and `:where` clauses
* `args` — arguments for `:in` clause bindings (pass `vec![]` if none)

**Returns:** `QueryResponse { result: Vec<Vec<serde_json::Value>> }`

Each inner `Vec` is one result row, with values matching the `:find` variables.

**Example:**

```rust
let query = r#"[:find ?uid ?s
                :where [?b :block/string ?s]
                       [?b :block/uid ?uid]
                       [(clojure.string/includes? ?s "TODO")]]"#;

let resp = client.query(query.into(), vec![]).await?;
for row in &resp.result {
    let uid = row[0].as_str().unwrap_or("");
    let text = row[1].as_str().unwrap_or("");
    println!("{}: {}", uid, text);
}
```

### `write`

Execute a write operation (create, update, delete, or move a block).

```rust
pub async fn write(&self, action: WriteAction) -> Result<()>
```

**Parameters:**

* `action` — a `WriteAction` variant describing the mutation

**Example:**

```rust
use roam_sdk::types::*;

client.write(WriteAction::UpdateBlock {
    block: BlockUpdate {
        uid: "abc123".into(),
        string: "Updated content".into(),
    },
}).await?;
```

See [Types](/sdk/types) for all `WriteAction` variants.

### `write_batch`

Execute multiple write operations in sequence. Each action is sent as an individual API request, stopping on the first error.

```rust
pub async fn write_batch(&self, actions: Vec<WriteAction>) -> Result<()>
```

**Parameters:**

* `actions` — a list of `WriteAction` variants to execute atomically

**Example:**

```rust
use roam_sdk::types::*;

client.write_batch(vec![
    WriteAction::CreatePage {
        page: PageCreate {
            title: "New Page".into(),
            uid: None,
        },
    },
    WriteAction::CreateBlock {
        location: BlockLocation {
            parent_uid: "page-uid".into(),
            order: OrderValue::Position("last".into()),
        },
        block: NewBlock {
            string: "First block".into(),
            uid: None,
            open: None,
        },
    },
]).await?;
```

Sends each action as a separate API request in order. Stops and returns the error if any request fails.

## Authentication

The client sends the API token as a Bearer token in the `X-Authorization` header on every request. All communication goes over HTTPS via rustls (no OpenSSL needed).

## Base URL

Requests go to `https://api.roamresearch.com/api/graph/{graph_name}/`:

| Endpoint | Method    |
| -------- | --------- |
| `/pull`  | `pull()`  |
| `/q`     | `query()` |
| `/write` | `write()` |


# Types

All types are in `roam_sdk::types` (re-exported from `roam_sdk::api::types`).

## Data structures

### `Block`

A block in the Roam graph. Blocks form a tree via `children`.

```rust
pub struct Block {
    pub uid: String,
    pub string: String,
    pub order: i64,
    pub children: Vec<Block>,
    pub open: bool,
    pub refs: Vec<RefEntity>,
}
```

* `uid` — unique block identifier
* `string` — the block's text content (Roam markdown)
* `order` — sort position among siblings
* `children` — nested child blocks (recursive tree)
* `open` — whether children are expanded or collapsed
* `refs` — page/block references contained in this block (not serialized to JSON)

### `RefEntity`

A reference target found in a block.

```rust
pub struct RefEntity {
    pub uid: String,
    pub title: Option<String>,   // page title, if it's a page ref
    pub string: Option<String>,  // block text, if it's a block ref
}
```

### `DailyNote`

A daily note page with its blocks.

```rust
pub struct DailyNote {
    pub date: chrono::NaiveDate,
    pub uid: String,
    pub title: String,
    pub blocks: Vec<Block>,
}
```

Parse from a pull response:

```rust
let note = DailyNote::from_pull_response(date, uid, &pull_response.result);
```

Blocks are automatically sorted by `order`. Nested children are parsed recursively.

### `LinkedRefBlock` / `LinkedRefGroup`

Results from a linked references query, grouped by source page.

```rust
pub struct LinkedRefBlock {
    pub uid: String,
    pub string: String,
    pub page_title: String,
}

pub struct LinkedRefGroup {
    pub page_title: String,
    pub blocks: Vec<LinkedRefBlock>,
}
```

Parse from a query response:

```rust
let groups = parse_linked_refs(&query_response.result, "Current Page");
```

Self-references (blocks from the current page) are automatically filtered out. Groups are sorted alphabetically by page title, blocks within each group sorted by text.

## Write actions

### `WriteAction`

An enum representing mutations to the graph.

```rust
pub enum WriteAction {
    CreateBlock { location: BlockLocation, block: NewBlock },
    UpdateBlock { block: BlockUpdate },
    DeleteBlock { block: BlockRef },
    MoveBlock { block: BlockRef, location: BlockLocation },
    CreatePage { page: PageCreate },
    BatchActions { actions: Vec<WriteAction> },
}
```

Serializes with a `"action"` tag: `"create-block"`, `"update-block"`, `"delete-block"`, `"move-block"`, `"create-page"`, `"batch-actions"`.

`WriteAction` implements both `Serialize` and `Deserialize`, so you can parse action JSON:

```rust
let actions: Vec<WriteAction> = serde_json::from_str(json_string)?;
```

### `PageCreate`

Data for creating a page.

```rust
pub struct PageCreate {
    pub title: String,
    pub uid: Option<String>,
}
```

### `BlockLocation`

Where to place a block.

```rust
pub struct BlockLocation {
    pub parent_uid: String,
    pub order: OrderValue,
}
```

Serializes `parent_uid` as `"parent-uid"`.

### `OrderValue`

Position within siblings.

```rust
pub enum OrderValue {
    Index(i64),           // specific position (0-based)
    Position(String),     // "last", "first"
}
```

Serializes as either a number (`0`) or a string (`"last"`).

### `NewBlock`

Data for creating a block.

```rust
pub struct NewBlock {
    pub string: String,
    pub uid: Option<String>,
    pub open: Option<bool>,
}
```

`uid` and `open` are omitted from JSON when `None`.

### `BlockUpdate`

Data for updating a block's text.

```rust
pub struct BlockUpdate {
    pub uid: String,
    pub string: String,
}
```

### `BlockRef`

A block reference (for delete and move).

```rust
pub struct BlockRef {
    pub uid: String,
}
```

## API request/response types

### `PullResponse`

```rust
pub struct PullResponse {
    pub result: serde_json::Value,
}
```

The `result` is a raw JSON value matching the pull selector shape. Use `.get(":attribute")` to access fields.

### `QueryResponse`

```rust
pub struct QueryResponse {
    pub result: Vec<Vec<serde_json::Value>>,
}
```

Each inner `Vec` is a result row. Values correspond to `:find` variables in order.

## Error types

### `RoamError`

```rust
pub enum RoamError {
    Api { status: u16, message: String },
    Http(reqwest::Error),
    Config(String),
    Io(std::io::Error),
    Json(serde_json::Error),
    TomlDe(toml::de::Error),
}
```

Implements `std::error::Error` and `Display`. Conversions from `reqwest::Error`, `std::io::Error`, `serde_json::Error`, and `toml::de::Error` via `From`.

### `Result<T>`

```rust
pub type Result<T> = std::result::Result<T, RoamError>;
```


# Queries

Helper functions for building Roam API queries. Available at `roam_sdk::queries`.

## Daily notes

### `daily_note_uid_for_date`

Generate the UID for a daily note from a date.

```rust
pub fn daily_note_uid_for_date(month: u32, day: u32, year: i32) -> String
```

```rust
let uid = queries::daily_note_uid_for_date(2, 21, 2026);
assert_eq!(uid, "02-21-2026");
```

### `pull_daily_note`

Build a pull request for a daily note by its UID.

```rust
pub fn pull_daily_note(uid: &str) -> (serde_json::Value, String)
```

Returns `(eid, selector)` ready to pass to `client.pull()`.

```rust
let uid = queries::daily_note_uid_for_date(2, 21, 2026);
let (eid, selector) = queries::pull_daily_note(&uid);
let resp = client.pull(eid, &selector).await?;
```

The selector includes: `:block/uid`, `:node/title`, `:block/string`, `:block/children` (recursive), `:block/order`, `:block/open`, `:block/refs`.

## Pages

### `pull_page_by_title`

Build a pull request for any page by title.

```rust
pub fn pull_page_by_title(title: &str) -> (serde_json::Value, String)
```

Uses `[:node/title "..."]` as the entity lookup. Same selector as daily notes.

```rust
let (eid, selector) = queries::pull_page_by_title("Projects");
let resp = client.pull(eid, &selector).await?;
```

### `all_page_titles_query`

Build a Datalog query to fetch all page titles and UIDs.

```rust
pub fn all_page_titles_query() -> String
```

Returns rows of `[title, uid]`.

### `search_blocks_query`

Build a Datalog query to fetch all blocks with their text and parent page title.

```rust
pub fn search_blocks_query() -> String
```

Returns rows of `[uid, block_string, page_title]`. Useful for full-text search when combined with client-side filtering.

## Graph statistics

### `graph_page_count_query`

Count total pages in the graph.

```rust
pub fn graph_page_count_query() -> String
```

Returns `[[count]]`.

### `graph_block_count_query`

Count total blocks in the graph.

```rust
pub fn graph_block_count_query() -> String
```

Returns `[[count]]`.

## Linked references

### `linked_refs_query`

Build a Datalog query that finds all blocks referencing a page.

```rust
pub fn linked_refs_query(page_title: &str) -> String
```

Returns a query string for `client.query()`. Double quotes in the title are escaped.

```rust
let query = queries::linked_refs_query("My Project");
let resp = client.query(query, vec![]).await?;
```

The query returns rows of `[uid, block_string, source_page_title]`. Parse the results with `types::parse_linked_refs()`:

```rust
let groups = types::parse_linked_refs(&resp.result, "My Project");
```

## Writing your own queries

You can pass any Datalog query string directly to `client.query()`:

```rust
// Find all blocks containing "TODO"
let query = r#"[:find ?uid ?s
                :where [?b :block/string ?s]
                       [?b :block/uid ?uid]
                       [(clojure.string/includes? ?s "TODO")]]"#;

let resp = client.query(query.into(), vec![]).await?;
```

Query format notes:

* Use `:find` with simple variable bindings (not pull expressions)
* `:where` clauses use Datomic-style pattern matching
* `args` must always be provided (use `vec![]` for no arguments)
* Results are `Vec<Vec<serde_json::Value>>` with values in `:find` variable order


