Memory
--- title: "Building a filesystem-based agent memory on SurrealDB | Blog | SurrealDB" meta: description: "Give an AI agent persistent memory as a filesystem in SurrealDB: computed file paths, fused full-text and vector search, and RBAC in a single PERMISSIONS clause" "og:description": "Give an AI agent persistent memory as a filesystem in SurrealDB: computed file paths, fused full-text and vector search, and RBAC in a single PERMISSIONS clause" "og:title": "Building a filesystem-based agent memory on SurrealDB | Blog | SurrealDB" "twitter:description": "Give an AI agent persistent memory as a filesystem in SurrealDB: computed file paths, fused full-text and vector search, and RBAC in a single PERMISSIONS clause" "twitter:title": "Building a filesystem-based agent memory on SurrealDB | Blog | SurrealDB" --- [Skip to content](-content) NEW Introducing SurrealDB Studio, the new official app of SurrealDB [Learn more](https://surrealdb.com/blog/introducing-surrealdb-studio) 1 / 3 [](/) DatabaseAgent MemoryResources [Use cases](https://surrealdb.com/solutions) [Docs](https://surrealdb.com/docs) [Pricing](https://surrealdb.com/pricing) [763 online](https://discord.gg/surrealdb) [SurrealDB on Discord](https://discord.gg/surrealdb) [33k](https://github.com/surrealdb/surrealdb) [SurrealDB on GitHub](https://github.com/surrealdb/surrealdb) [Sign in](https://studio.surrealdb.com/?signin=true) [Get started](https://studio.surrealdb.com/?signin=true) [](/) [Get started](https://studio.surrealdb.com/?signin=true) # Building a filesystem-based agent memory on SurrealDB Tutorial AI Aug 27, 2026 · 9 min read  Martin Schaer [Show all posts](https://surrealdb.com/blog) Give a model no memory API, just filesystem tools like `mkdir`, `cat` and `grep`, and it will naturally use them to store memories. That is what "keep notes" looks like in the billions of shell transcripts it trained on. This post builds that filesystem in [SurrealDB](https://surrealdb.com): one table, five bash-shaped tools, computed paths, and full-text plus vector search over the same field. Ask an agent framework what "memory" means and you'll get a different answer every time: a vector store, a summarization pass, a key-value blob the framework manages for you and won't show you. Every serious coding agent already works the other way. It writes a scratch file, greps for something it wrote three steps ago, edits a todo list in place. Nobody trained it to do that specifically for memory. Putting that filesystem in SurrealDB instead of on disk makes the same data full-text searchable, semantically searchable, and shareable across processes and users, without leaving SurrealQL. If you want this off the shelf, it already exists: [surrealfs](https://github.com/surrealdb/surrealfs) is a virtual filesystem for AI agents built on SurrealDB, with the rough edges already handled. This post builds a smaller version from scratch anyway. The useful part is seeing *why* the pattern works and the SurrealQL that makes it short: computed paths, one field carrying two indexes, and per-user isolation in a `PERMISSIONS` clause. Our newsletter Get tutorials, AI agent recipes, webinars, and early product updates in your inbox every two weeks Subscribe --- ## What is "agent memory" actually for? Link to this section A conversation ends and the context window empties with it. Memory is whatever survives that: the user's preferences, the state of a project, a decision made two sessions ago and never revisited. Concretely, it is always one of three shapes: - • **notes**: free text an agent re-reads to remind itself what's going on ( `/notes.md`, `/projects/<name>/todo.md`). - • **facts**: small structured statements about the user ( `/preferences/editor.md`). - • **an index into both**: so "what did we decide about the release" finds the right note without the agent re-reading everything. A folder of Markdown files covers the first two natively. The third stops being a separate system once that folder lives in a database that can search its own contents, as this post shows. --- ## Step 1: start a server Link to this section ``` surreal start --user root --pass root memory ``` `memory` is SurrealDB's in-memory storage: everything is gone on restart. Fine for following along, useless for an agent that's meant to remember things. To keep notes across runs, point the server at a file on disk instead: ``` surreal start --user root --pass root rocksdb://agent.db ``` That creates (or reopens) `agent.db` in the current directory and every query below works unchanged. --- ## Step 2: one table, self-referential Link to this section A file tree is a table where every row can point at another row of the same table as its parent. `path` is never stored, it's **computed** from the parent chain, so renaming or moving a folder is one `UPDATE` and every descendant's path follows for free. Let's create the schema file `schema.surql`: ``` DEFINE TABLE OVERWRITE file SCHEMAFULL; DEFINE FIELD OVERWRITE name ON file TYPE string; DEFINE FIELD OVERWRITE parent ON file TYPE option<record<file>> DEFAULT NONE; -- A composite UNIQUE index on (parent, name) would let every root-level file -- through unchecked: SurrealDB skips a unique index when one of its fields is -- NONE. `parent_key` mirrors `parent` with the root spelled 'root' so the -- index has something to compare. It cannot be COMPUTED -- SurrealDB refuses -- to index a computed field -- so it is a stored VALUE, re-evaluated on write. DEFINE FIELD OVERWRITE parent_key ON file TYPE string VALUE IF $this.parent IS NONE { 'root' } ELSE { <string>$this.parent }; -- <string> guards against $this.name being NONE mid-delete. DEFINE FIELD OVERWRITE path ON file COMPUTED ( ($this.parent.path || '') + '/' + <string>$this.name ) TYPE string; -- A folder is a row with no content and no embedding -- nothing else marks it. DEFINE FIELD OVERWRITE is_folder ON file COMPUTED ( $this.content IS NONE ); DEFINE FIELD OVERWRITE content ON file TYPE option<string> DEFAULT NONE; DEFINE FIELD OVERWRITE embedding ON file TYPE option<array<float>> DEFAULT NONE; DEFINE FIELD OVERWRITE updated_at ON file TYPE datetime VALUE time::now(); -- Serves both the uniqueness constraint and "everything under this parent" -- -- the hottest lookup here (ls, path resolution). The reverse field order -- would force a table scan for that query. DEFINE INDEX OVERWRITE child_unique ON TABLE file FIELDS parent_key, name UNIQUE; -- Full-text search over file contents. HIGHLIGHTS is what makes -- search::highlight/search::offsets return real snippets instead of the -- untouched content -- it stores term positions, so it's opt-in. DEFINE ANALYZER OVERWRITE file_analyzer TOKENIZERS blank, class, camel, punct FILTERS snowball(english); DEFINE INDEX OVERWRITE file_content_fts ON file FIELDS content FULLTEXT ANALYZER file_analyzer BM25(1.2, 0.75) HIGHLIGHTS; -- Semantic search over the same field. Real embedders return 1536+ dimensions -- (e.g. OpenAI's text-embedding-3-small); this tutorial uses toy 4-dimensional -- vectors so the numbers stay readable -- the index and the query are identical -- either way, just change DIMENSION to match your embedder. DEFINE INDEX OVERWRITE file_embedding_hnsw ON file FIELDS embedding HNSW DIMENSION 4 DIST COSINE TYPE F32; ``` q(80)/zl4kmr5p3fppmmu0i9pohkp4.auto>) Four choices in there are worth calling out: - • **`is_folder` is computed, not stored.** A folder is defined as "has no content", one less field to keep in sync. - • **`parent_key` exists only because of an index quirk.** A `UNIQUE` index skips any row where one of its fields is `NONE`, so indexing `(parent, name)` directly would let two root-level `/notes.md` both get created. `parent_key` stores the same thing as a string, with the root spelled `'root'` instead of `NONE`, so the index has something to compare on every row. - • **`path` cannot be indexed.** SurrealDB refuses to index a `COMPUTED` field, so `WHERE path = $p` is a table scan, fine at the size an agent's notes actually reach (dozens to low thousands of files), and simpler to read than the alternative. A production version resolves a path by walking its segments through the `(parent_key, name)` index instead, as [`surrealfs`](https://github.com/surrealdb/surrealfs/blob/main/surrealfs/fs.py) does in its `_resolve()`. - • **One field, two search engines.** `content` carries a `FULLTEXT` index for keyword search and `embedding` carries an `HNSW` index for vector search. Nothing about the schema treats these as separate subsystems. Now let's load the schema: ``` surreal sql --endpoint http://localhost:8000 \ --user root --pass root --ns agent --db memory \ --multi < schema.surql ``` --- ## Step 3: seed a small tree Link to this section There is no `mkdir` here, and no directory type. Every row below is a plain `CREATE file`, folders included. Two things turn six flat rows into a tree: `parent` points a row at another row, and leaving `content` unset is what makes a row a folder. ``` LET $notes = (CREATE ONLY file SET name = 'notes.md', content = "# Working notes\n- Building an internal tool to track deploys.\n" + "- User prefers terse commit messages, no emoji."); LET $preferences = (CREATE ONLY file SET name = 'preferences'); LET $editor = (CREATE ONLY file SET name = 'editor.md', parent = $preferences.id, content = "# Editor preferences\nUses Neovim with a 2-space indent for everything. " + "Reviews diffs before every commit, never squashes without asking."); LET $projects = (CREATE ONLY file SET name = 'projects'); LET $launch = (CREATE ONLY file SET name = 'launch', parent = $projects.id); LET $todo = (CREATE ONLY file SET name = 'todo.md', parent = $launch.id, content = "# Launch checklist\n- [x] Write the changelog\n- [ ] Cut the release branch\n" + "- [ ] Announce in -updates"); -- Toy embeddings standing in for a real embedder -- see the note in schema.surql. UPDATE $editor.id SET embedding = [0.9, 0.1, 0.0, 0.0]; UPDATE $todo.id SET embedding = [0.0, 0.1, 0.9, 0.2]; UPDATE $notes.id SET embedding = [0.6, 0.3, 0.3, 0.1]; ``` No statement above wrote a path. Ask for one and the `COMPUTED` field walks the parent chain at read time: ``` SELECT path, is_folder FROM file ORDER BY path; ``` ``` [ { path: '/notes.md', is_folder: false }, { path: '/preferences', is_folder: true }, { path: '/preferences/editor.md', is_folder: false }, { path: '/projects', is_folder: true }, { path: '/projects/launch', is_folder: true }, { path: '/projects/launch/todo.md', is_folder: false } ] ``` Which is this tree, drawn the way an agent expects to see it: ``` / ├── notes.md ├── preferences/ │ └── editor.md └── projects/ └── launch/ └── todo.md ``` --- ## Step 4: bash, in SurrealQL Link to this section Every tool a filesystem-shaped agent needs maps to one statement. Run these against the seeded tree (`queries.surql` has all of them): **`ls /`** ``` SELECT path, is_folder FROM file WHERE parent_key = 'root' ORDER BY path; ``` ``` [ { path: '/notes.md', is_folder: false }, { path: '/preferences', is_folder: true }, { path: '/projects', is_folder: true } ] ``` **`cat /notes.md`** ``` SELECT content FROM file WHERE path = '/notes.md'; ``` ``` [{ content: '# Working notes\n- Building an internal tool to track deploys.\n- User prefers terse commit messages, no emoji.' }] ``` **`grep -r commit`** full-text search, ranked: ``` SELECT path, search::score(1) AS score FROM file WHERE content @1@ 'commit' ORDER BY score DESC; ``` ``` [ { path: '/notes.md', score: 0.4417339265346527 }, { path: '/preferences/editor.md', score: 0.4106069505214691 } ] ``` Real `grep` shows you the matching line, not just the filename. That's `search::highlight`, and it needs the `HIGHLIGHTS` clause on the index, without it the function silently returns the untouched content (and `search::offsets` returns `NONE`). The clause is why `file_content_fts` above ends in `BM25(1.2, 0.75) HIGHLIGHTS`; it stores the term positions the highlighter needs, so it costs index space you only pay for if you want snippets. ``` SELECT path, search::score(1) AS score, search::highlight('**', '**', 1) AS snippet FROM file WHERE content @1@ 'commit' ORDER BY score DESC; ``` ``` [ { path: '/notes.md', score: 0.4417339265346527, snippet: '# Working notes\n- Building an internal tool to track deploys.\n- User prefers terse **commit** messages, no emoji.' }, { path: '/preferences/editor.md', score: 0.4106069505214691, snippet: '# Editor preferences\nUses Neovim with a 2-space indent for everything. Reviews diffs before every **commit**, never squashes without asking.' } ] ``` Here we surrounded the highlight with `**` because the content is in markdown format, and `**` makes it bold. `search::offsets(1)` gives the same information as `{ s, e }` character ranges instead of inline markers, if your agent wants to build its own snippet window. **`mv /projects/launch /projects/v2-launch`**, the payoff for a computed path: ``` LET $launch = (SELECT VALUE id FROM ONLY file WHERE path = '/projects/launch'); UPDATE $launch SET name = 'v2-launch'; SELECT path FROM file WHERE parent_key = <string>$launch OR id = $launch; ``` ``` [ { path: '/projects/v2-launch' }, { path: '/projects/v2-launch/todo.md' } ] ``` One `UPDATE` on the folder, and `todo.md` (never touched) now resolves to `/projects/v2-launch/todo.md`. A real filesystem needs a recursive rename to get this; here it's a side effect of `path` never having been stored. **Edit a file in place**, the way you'd `sed -i` one: ``` LET $todo = (SELECT VALUE id FROM ONLY file WHERE path = '/projects/v2-launch/todo.md'); UPDATE $todo SET content = string::replace( content, '[ ] Cut the release branch', '[x] Cut the release branch' ); ``` *(`sed -i` is a common Unix command to edit files in place)* **Semantic search**, same field, the other index, rank by *meaning* instead of shared words: ``` SELECT path, vector::distance::knn() AS distance FROM file WHERE embedding <|3, 40|> [0.1, 0.1, 0.8, 0.3] ORDER BY distance ASC; ``` ``` [ { path: '/projects/v2-launch/todo.md', distance: 0.0163 }, { path: '/notes.md', distance: 0.4395 }, { path: '/preferences/editor.md', distance: 0.8725 } ] ``` The query vector above stands in for an embedded question like *"what's left before we ship?"*, it never shares a word with the todo list, and still ranks it first, well ahead of a note about editor preferences. --- ## Step 5: wiring it to an agent Link to this section An LLM tool-calling loop needs each of the above as a named function that takes arguments and returns text. `fs_tools.py` (see [Example files]() below) is that, in about 130 lines, `ls`, `cat`, `write_file`, `edit`, `search`, each one SurrealQL statement wrapped in a Python function: ``` async def search(db: AsyncSurreal, query: str) -> str: """Full-text search over every file's content, best match first.""" rows = await _q( db, "SELECT path, search::score(1) AS score, " "search::highlight('**', '**', 1) AS snippet FROM file " "WHERE content @1@ $query ORDER BY score DESC", {"query": query}, ) if not rows: return "(no matches)" return "\n".join( f"{r['path']}: {next((l for l in r['snippet'].splitlines() if '**' in l), r['snippet'])}" for r in rows ) ``` Run it: `uv run --with surrealdb python fs_tools.py`, and it exercises a short session against a real server: ``` >>> write_file('/preferences/editor.md', ...) Wrote 50 bytes to /preferences/editor.md >>> ls('/') 87 notes.md - preferences/ >>> cat('/preferences/editor.md') Uses Neovim. Reviews every diff before committing. >>> edit('/preferences/editor.md', 'Uses Neovim', 'Uses Neovim with a 2-space indent') --- a/preferences/editor.md +++ b/preferences/editor.md @@ -1 +1 @@ -Uses Neovim. Reviews every diff before committing. +Uses Neovim with a 2-space indent. Reviews every diff before committing. >>> search('changelog') /notes.md: # Working notes ``` Handing these to a model is deliberately straightforward: a dict of `{name: function}`. There is nothing SurrealDB-specific about the wiring, that's the point of building memory this way. The system prompt that makes an agent actually *use* it is one paragraph: ``` You have a persistent filesystem; treat it as your memory. Record what you learn about the user's preferences under /preferences/. Give each project a folder under /projects/<name>/. Search before you create — the file you want may already exist. ``` That's [surrealfs](https://github.com/surrealdb/surrealfs)'s actual instructions block, close to verbatim. What it adds over the code above: binary files, `glob`, three ready-made integrations (a plain async API, a [pydantic-ai](https://ai.pydantic.dev) toolset, raw Anthropic/OpenAI tool schemas), and error handling that turns a wrong path into a retry instead of a crash. Its [`examples/anthropic_loop.py`](https://github.com/surrealdb/surrealfs/blob/main/examples/anthropic_loop.py) is the same idea as `fs_tools.py` plumbed into a real tool-use loop, in about 40 lines. --- ## Step 6: why put this in SurrealDB, and not a real disk Link to this section Nothing above needed a database instead of a folder, `ls`, `cat` and `edit` work identically over `open()` and `os.listdir()`. Three things change once the "filesystem" is a table: **Search comes for free, and it's already fused.** A real filesystem gives you `grep`. It does not give you "rank these results by both shared keywords *and* similar meaning," which is what makes an agent's memory usable once it holds more than a handful of files. Here that's reciprocal-rank fusion over two indexes on one field, no separate vector database to keep in sync, no second write path. (`surrealfs.search()` does exactly this fusion; see `fs.py` in the repo linked above.) **RBAC is a `PERMISSIONS` clause, not an authorization layer you build.** Add an `owner` field and one clause, and every read and write is scoped for free: ``` DEFINE FIELD OVERWRITE owner ON TABLE file TYPE option<record<user>> DEFAULT $auth.id; DEFINE TABLE OVERWRITE file SCHEMAFULL PERMISSIONS FOR select, create, update, delete WHERE owner = $auth.id; ``` Under [record access](https://surrealdb.com/docs/surrealql/statements/define/access/record), `$auth.id` is the signed-in user, so this one clause is the entire multi-tenancy story: agent A's memory is invisible to agent B's queries, not because your application code checks a `user_id` column, but because the database refuses the row. `surrealfs` ships this as an optional [`user.surql`](https://github.com/surrealdb/surrealfs/blob/main/surrealfs/schema/user.surql) you apply only if you need it. **The memory is a database, not a directory on one machine.** A file written mid-conversation by an agent running on one host is immediately queryable from another (the web UI in Step 7, a nightly job, a teammate's agent) because it was never local to begin with. "Where does the agent's memory live" stops being a filesystem-path question and becomes a connection string. --- ## Step 7: seeing it for real Link to this section Two examples ship in [`surrealfs`](https://github.com/surrealdb/surrealfs): Note The examples uses [just](https://github.com/casey/just) to simplify the commands. If you don't have it, you can run the commands in the [Justfile](https://github.com/surrealdb/surrealfs/blob/main/Justfile) directly. - • **`examples/chat_agent.py`**: the pydantic-ai agent whose instructions you read in Step 5, as a full web chat ( `just agent`, `:7932`). It writes to its own memory mid-conversation and rereads it in the next one. - • **`surrealfs/browser`**: a tree view of the same `file` table on the left, the selected file open and editable on the right, and a chat panel wired to the same agent on the far side ( `just browser`, `:7933`). It's the fastest way to *see* what "memory as a filesystem" means: watch a folder appear under `/projects/` while you talk to the agent, open it, edit a line by hand, ask the agent about it in the same breath. Both need only `just db` running underneath and the schema applied, no separate memory subsystem to stand up. --- ## What to consider before production Link to this section - • **Path lookups are a table scan.** Fine at hundreds of files; past a few thousand, resolve paths by walking segments through the `(parent_key, name)` index instead of scanning the computed field, `surrealfs._resolve()` is the reference. - • **This schema has no owner.** Every file is visible to every session. Layer on Step 6's `owner` field and `PERMISSIONS` clause before more than one person's agent touches the same database. - • **Deletes here are hard deletes.** If you need an undo, that's a `deleted_at` field with matching read filters, not a place this schema currently has an opinion. --- ## Get started Link to this section - • [Create a free cloud instance](https://surrealdb.com/cloud) - • [surrealfs on GitHub](https://github.com/surrealdb/surrealfs): the production version of everything in this post - • [`DEFINE INDEX ... HNSW` docs](https://surrealdb.com/docs/surrealql/statements/define/indexes) - • [`DEFINE INDEX ... FULLTEXT` docs](https://surrealdb.com/docs/surrealql/statements/define/indexes) - • [Record access docs](https://surrealdb.com/docs/surrealql/statements/define/access/record) - • [Join our Discord server](https://discord.gg/surrealdb) --- ## Example files Link to this section ### `fs_tools.py`Link to this section ``` """The smallest version of agent memory: bash-shaped tools over one SurrealDB table. An LLM has seen millions of `ls`, `cat`, `mkdir -p` and `grep -r` sessions, so a tool named `ls` that returns text needs no explanation in a system prompt -- the shape is already in the weights. This module is that idea with nothing else in it: five functions, each one SurrealQL statement against the `file` table from schema.surql, each returning plain text a model can read. Wiring these into an actual tool-calling loop is small -- hand a dict of {name: function} to whatever provider SDK you use, one branch per tool call. `surrealfs` (github.com/surrealdb/surrealfs) is the version of this with error recovery, glob, binary files, and three ready-made integrations (pydantic-ai, raw Anthropic/OpenAI schemas, a plain async API); its `examples/anthropic_loop.py` is ~40 lines end to end. surreal start --user root --pass root memory # in one terminal uv run --with surrealdb python fs_tools.py # runs the self-check below """ from __future__ import annotations import asyncio import difflib from pathlib import Path, PurePosixPath from typing import Any from surrealdb import AsyncSurreal SCHEMA = (Path(__file__).parent / "schema.surql").read_text() async def _q(db: AsyncSurreal, sql: str, vars: dict[str, Any] | None = None) -> Any: """Run one or more statements, return the last one's result.""" return await db.query(sql, vars or {}) async def _resolve(db: AsyncSurreal, path: str) -> dict[str, Any] | None: if path == "/": return None row = await _q(db, "SELECT * FROM ONLY file WHERE path = $path LIMIT 1", {"path": path}) return row async def ls(db: AsyncSurreal, path: str = "/") -> str: """List a folder: one line per entry, `-` size for folders.""" if path != "/": folder = await _resolve(db, path) if folder is None or not folder["is_folder"]: return f"Error: not a directory: {path}" key = str(folder["id"]) else: key = "root" rows = await _q( db, "SELECT name, is_folder, " "IF content IS NOT NONE THEN string::len(content) ELSE 0 END AS size " "FROM file WHERE parent_key = $key ORDER BY name", {"key": key}, ) if not rows: return "(empty)" return "\n".join( f"{'-' if r['is_folder'] else r['size']:>6} {r['name']}{'/' if r['is_folder'] else ''}" for r in rows ) async def cat(db: AsyncSurreal, path: str) -> str: """Read a file's full content.""" entry = await _resolve(db, path) if entry is None: return f"Error: no such file: {path}" if entry["is_folder"]: return f"Error: is a directory: {path}" return entry["content"] async def write_file(db: AsyncSurreal, path: str, content: str) -> str: """Create or replace a file, creating missing parent folders.""" parts = PurePosixPath(path).parts[1:] # drop the leading "/" if not parts: return "Error: cannot write to the root directory" parent_id = None for segment in parts[:-1]: existing = await _q( db, "SELECT * FROM ONLY file WHERE parent_key = $key AND name = $name LIMIT 1", {"key": "root" if parent_id is None else str(parent_id), "name": segment}, ) if existing is None: existing = await _q( db, "CREATE ONLY file SET name = $name, parent = $parent", {"name": segment, "parent": parent_id}, ) elif not existing["is_folder"]: return f"Error: not a directory: {existing['path']}" parent_id = existing["id"] name = parts[-1] existing = await _resolve(db, path) if existing is not None: if existing["is_folder"]: return f"Error: is a directory: {path}" await _q(db, "UPDATE $id SET content = $content", {"id": existing["id"], "content": content}) return f"Wrote {len(content)} bytes to {path}" await _q( db, "CREATE file SET name = $name, parent = $parent, content = $content", {"name": name, "parent": parent_id, "content": content}, ) return f"Wrote {len(content)} bytes to {path}" async def edit(db: AsyncSurreal, path: str, old: str, new: str) -> str: """Replace the first occurrence of `old` with `new`, returning a diff.""" entry = await _resolve(db, path) if entry is None: return f"Error: no such file: {path}" current = entry["content"] or "" if old not in current: return f"Error: text not found in {path}: {old!r}" updated = current.replace(old, new, 1) await _q(db, "UPDATE $id SET content = $content", {"id": entry["id"], "content": updated}) diff = difflib.unified_diff( current.splitlines(), updated.splitlines(), fromfile=f"a{path}", tofile=f"b{path}", lineterm="" ) return "\n".join(diff) async def search(db: AsyncSurreal, query: str) -> str: """Full-text search over every file's content, best match first.""" rows = await _q( db, "SELECT path, search::score(1) AS score, " "search::highlight('**', '**', 1) AS snippet FROM file " "WHERE content @1@ $query ORDER BY score DESC", {"query": query}, ) if not rows: return "(no matches)" return "\n".join( f"{r['path']}: {next((l for l in r['snippet'].splitlines() if '**' in l), r['snippet'])}" for r in rows ) async def demo() -> None: """Self-check: a short bash-shaped session against a scratch namespace.""" db = AsyncSurreal("ws://127.0.0.1:8010/rpc") await db.signin({"username": "root", "password": "root"}) await db.use("tutorial", "fs_tools_demo") for statement in SCHEMA.split(";\n\n"): if statement.strip(): await db.query(statement) assert "Wrote" in await write_file(db, "/preferences/editor.md", "Uses tabs.") assert await cat(db, "/preferences/editor.md") == "Uses tabs." assert "preferences/" in await ls(db, "/") diff = await edit(db, "/preferences/editor.md", "tabs", "2-space indent") assert "2-space indent" in diff hit = await search(db, "indent") assert "editor.md" in hit assert "**indent**" in hit # fails if the index loses HIGHLIGHTS assert await cat(db, "/missing.md") == "Error: no such file: /missing.md" print("all checks passed") await db.close() if __name__ == "__main__": asyncio.run(demo()) ``` On this page [What is "agent memory" actually for?](-is-agent-memory-actually-for) [Step 1: start a server](-1-start-a-server) [Step 2: one table, self-referential](-2-one-table-self-referential) [Step 3: seed a small tree](-3-seed-a-small-tree) [Step 4: bash, in SurrealQL](-4-bash-in-surrealql) [Step 5: wiring it to an agent](-5-wiring-it-to-an-agent) [Step 6: why put this in SurrealDB, and not a real disk](-6-why-put-this-in-surrealdb-and-not-a-real-disk) [Step 7: seeing it for real](-7-seeing-it-for-real) [What to consider before production](-to-consider-before-production) [Get started](-started) [Example files](-files) [fs\_tools.py]() ## Related posts [ Aug 5, 2026Tutorial · AI<h3>Chat with your meeting notes: CocoIndex graph with SurrealQL</h3>](https://surrealdb.com/blog/chat-with-your-meeting-notes-a-cocoindex-knowledge-graph-and-a-text-to-surrealql-agent) [ Aug 5, 2026Release · Tutorial · AI<h3>Empower your agents with the new SurrealDB MCP</h3>](https://surrealdb.com/blog/surrealmcp-a-managed-mcp-server-for-ai-agents) [ Jul 29, 2026AI · Tutorial<h3>One database for the whole claim: agentic claims on SurrealDB</h3>](https://surrealdb.com/blog/one-database-for-the-whole-claim-agentic-claims-triage-on-surrealdb) [ Aug 25, 2026Company<h3>How Cobrainer built graph-based agent memory on one engine</h3>](https://surrealdb.com/blog/how-cobrainer-built-graph-based-agent-memory-on-one-engine) [ Sep 1, 2026Release · Company<h3>SurrealDB Cloud is now available in São Paulo, Brazil</h3>](https://surrealdb.com/blog/surrealdb-cloud-is-now-available-in-so-paulo) Our newsletter Get tutorials, AI agent recipes, webinars, and early product updates in your inbox every two weeks Subscribe  The unified data layer for AI Graph, vector, document, and relational in one engine. Agent Memory that connects and retrieves context wherever your data lives. Explore with AI [Explore SurrealDB with ChatGPT](https://chatgpt.com/?q=Summarise%20and%20analyse%20the%20key%20insights%20from%20https%3A%2F%2Fsurrealdb.com%2F%20as%20the%20primary%20source.%20SurrealDB%20is%20the%20multi-model%20database%20that%20unifies%20documents%2C%20graphs%2C%20vectors%2C%20time-series%2C%20full-text%2C%20and%20relational%20data%20in%20one%20ACID%20transaction%20-%20the%20unified%20data%20layer%20for%20AI.) [Explore SurrealDB with Claude](https://claude.ai/new?q=Summarise%20and%20analyse%20the%20key%20insights%20from%20https%3A%2F%2Fsurrealdb.com%2F%20as%20the%20primary%20source.%20SurrealDB%20is%20the%20multi-model%20database%20that%20unifies%20documents%2C%20graphs%2C%20vectors%2C%20time-series%2C%20full-text%2C%20and%20relational%20data%20in%20one%20ACID%20transaction%20-%20the%20unified%20data%20layer%20for%20AI.) [Explore SurrealDB with Perplexity](https://www.perplexity.ai/search?q=Summarise%20and%20analyse%20the%20key%20insights%20from%20https%3A%2F%2Fsurrealdb.com%2F%20as%20the%20primary%20source.%20SurrealDB%20is%20the%20multi-model%20database%20that%20unifies%20documents%2C%20graphs%2C%20vectors%2C%20time-series%2C%20full-text%2C%20and%20relational%20data%20in%20one%20ACID%20transaction%20-%20the%20unified%20data%20layer%20for%20AI.) [Explore SurrealDB with Grok](https://grok.com/?q=Summarise%20and%20analyse%20the%20key%20insights%20from%20https%3A%2F%2Fsurrealdb.com%2F%20as%20the%20primary%20source.%20SurrealDB%20is%20the%20multi-model%20database%20that%20unifies%20documents%2C%20graphs%2C%20vectors%2C%20time-series%2C%20full-text%2C%20and%20relational%20data%20in%20one%20ACID%20transaction%20-%20the%20unified%20data%20layer%20for%20AI.) Stay in the loop Tutorials, AI agent recipes, and product updates, every two weeks. Subscribe Independently verified SOC 2 Type 2 GDPR Cyber Essentials Plus ISO 27001 [Trust Centre](https://trust.surrealdb.com) DATABASE [SurrealDB](https://surrealdb.com/surrealdb) [Cloud](https://surrealdb.com/cloud) [Studio](https://surrealdb.com/studio) [Extensions](https://surrealdb.com/surrealdb/extensions) [All features](https://surrealdb.com/surrealdb/features) [Technical deep-dive](https://surrealdb.com/surrealdb/deep-dive) [Performance benchmarks](https://surrealdb.com/surrealdb/benchmarks) [Install](https://surrealdb.com/surrealdb/install) [Pricing](https://surrealdb.com/pricing) [Use cases](https://surrealdb.com/solutions) [vs. Postgres](https://surrealdb.com/comparison/postgres) [vs. MongoDB](https://surrealdb.com/comparison/mongodb) [vs. Neo4j](https://surrealdb.com/comparison/neo4j) [vs. Elasticsearch](https://surrealdb.com/comparison/elasticsearch) [vs. vector databases](https://surrealdb.com/why/vs-vector-databases) [vs. traditional databases](https://surrealdb.com/why/vs-traditional-databases) [vs. data platforms](https://surrealdb.com/why/vs-data-platforms) AGENT MEMORY [Agent Memory](https://surrealdb.com/agent-memory) [Cloud](https://surrealdb.com/cloud) [Studio](https://surrealdb.com/studio) [Get started](https://surrealdb.com/agent-memory/get-started) [Technical deep-dive](https://surrealdb.com/agent-memory/deep-dive) [MCP server](https://surrealdb.com/mcp) [Connectors and integrations](https://surrealdb.com/integrations) [vs. memory middleware](https://surrealdb.com/why/vs-memory-middleware) [vs. agent databases](https://surrealdb.com/why/vs-agent-databases) [vs. vector databases](https://surrealdb.com/why/vs-vector-databases) RESOURCES [Labs](https://surrealdb.com/docs/labs) [University](https://surrealdb.com/learn) [Connect your agent](https://surrealdb.com/docs/build/ai-agents) [Blog](https://surrealdb.com/blog) [Case studies](https://surrealdb.com/casestudies) [Whitepapers](https://surrealdb.com/whitepapers) [Events and webinars](https://surrealdb.com/events) [Newsletter](https://surrealdb.com/newsletter) [Enterprise](https://surrealdb.com/enterprise) [Ambassador programme](https://surrealdb.com/ambassador-programme) [Releases](https://surrealdb.com/releases) [Roadmap](https://surrealdb.com/roadmap) DOCUMENTATION [Overview](https://surrealdb.com/docs) [SurrealDB](https://surrealdb.com/docs/what-is-surrealdb) [Agent Memory](https://surrealdb.com/docs/agent-memory) [SurrealQL](https://surrealdb.com/docs/reference/query-language) [Cloud](https://surrealdb.com/docs/manage/instances) [Studio](https://surrealdb.com/docs/explore/studio) [SDKs](https://surrealdb.com/docs/languages/javascript) COMPANY [Contact us](https://surrealdb.com/contact) [Brand assets](https://surrealdb.com/brand) [Careers](https://surrealdb.com/careers) [Open source](https://surrealdb.com/opensource) [Store](https://surrealdb.store/) [Status](https://status.surrealdb.com) LEGAL [Overview](https://surrealdb.com/legal) [Terms](https://surrealdb.com/legal/category/terms) [Privacy](https://surrealdb.com/legal/category/privacy) [Trust and security](https://surrealdb.com/legal/category/security) [Compliance](https://surrealdb.com/legal/category/compliance) [Service Level Agreements](https://surrealdb.com/legal/category/sla) [Subscribe to updates](https://surrealdb.com/legal?subscribe) [Licence FAQs](https://surrealdb.com/license) [security.txt](https://surrealdb.com/.well-known/security.txt) COMMUNITY [Discord763 online](https://discord.gg/surrealdb) [GitHub](https://github.com/surrealdb/surrealdb) [X](https://twitter.com/surrealdb) [YouTube](https://www.youtube.com/@surrealdb) [LinkedIn](https://www.linkedin.com/company/surrealdb) [Reddit](https://www.reddit.com/r/surrealdb) [Instagram](https://www.instagram.com/surrealdb) [Stack Overflow](https://stackoverflow.com/questions/tagged/surrealdb) [Medium](https://medium.com/surrealdb) [Dev](https://dev.to/surrealdb) Copyright © 2026 SurrealDB Ltd. Registered in England and Wales. Company no. 13615201 Registered address: 3rd Floor 1 Ashley Road, Altrincham, Cheshire, WA14 2DT, United Kingdom Trading address: Huckletree Oxford Circus, 213 Oxford Street, London, W1D 2LG, United Kingdom
Tags: surrealdb, agent-memory, filesystem, reference, future-plans, project:docuclear, url:https://surrealdb.com/blog/agent-memory-is-a-filesystem-building-one-on-surrealdb, main, eng, files, what, step, get, example, fs_toolspy, kind:semantic — Source: web — 2026-09-03 04:53:32 UTC