Build on it

Build vector + graph RAG on one engine. No new query language.

  • Vectors and graph on one copy of the data: retrieval and traversal over the same stored records.
  • Write relationships as you write the data; afterwards every read is a get, a walk or a scan.
  • No query planner to coax and no indexes to declare.
  • Four doors answer today: the REST API, the WebSocket, the binary wire and clients for the languages you already use. Each door reads AND writes; the Elixir and Rust clients carry every data operation, the others carry the memory calls and the core reads and writes.
  • Admin console: Coming soon.
  • The binary wire is the engine's own compact frame protocol, and it reads and writes: a node takes an ingest over the wire as well as over HTTP, and a passage written on either is found by a search on the other. The clients encode and decode those frames today; the published wire specification follows.

Evaluating Semurg for a team? The evaluation guide takes you from a running node to your own data and the published benchmarks in an afternoon.

Four doors, one engine

REST API request and response, reads and writes WebSocket live cluster state pushed every second Binary wire the engine's own compact frames Clients eleven languages, HTTP and the wire ONE ENGINE The same answer whichever door asked
Four doors reach the same engine and give the same answer: the REST API, the WebSocket, the binary wire and the clients. One capability reaching one door and not the others is a defect we fix, not a roadmap item.

The first five minutes

Install, prove the token, write, read. Every command below is one the installer prints or the release's own install test runs against a node you just installed. There is no account.

1. Install. The installer ends with a SUCCESS banner that prints your API token. Keep it.

curl -fsSL https://one.semurg.io/install | sudo bash

One prerequisite, and the installer checks it. The data directory must sit on a real disk filesystem, ext4 or xfs. A network share, a container overlay or a shared folder from a desktop virtual machine cannot take the writes this engine makes, and the installer stops and says so rather than running on top of one. On a normal Linux machine this is already true and there is nothing to do.

What it runs on today. Linux on x86-64: every release is installed and tested on Ubuntu 24.04, and the same release runs in Docker on any x86-64 Linux host. Any x86-64 processor from roughly the last ten years works: the release carries builds for processors with and without AVX-512 and picks the right one when it starts. To try it, 2 cores and 4 GB of memory are enough (the Docker quickstart below runs comfortably in a 4 GB container); the data directory needs a few GB free and grows with what you store. No GPU is needed. Apple Silicon Macs and Windows: native releases are planned, built from the same Ubuntu release; today, run the node on a Linux machine or VM you can reach and point your agents at it. On Windows, WSL2 and the Docker image in Docker Desktop are being tested now.

Already running a node? Take the new release with semurg update --apply (the node checks the signed manifest, downloads, verifies, swaps and restarts, and rolls back if the new release does not come up). That line is for a node installed with step 1: the Docker image carries no semurg command. Re-running the install command on a node that already has a completed install is REFUSED on purpose, because a plain re-run would rewrite its environment from a fresh hardware scan and could leave your stored bytes unserved; say what you mean with SEMURG_INSTALL_MODE=upgrade if you want the installer to do it.

2. Put the token in your shell. It is stored on the node, so you can read it back at any time.

TOKEN=$(sudo grep '^SEMURG_API_TOKEN=' /etc/semurg/semurg.env | cut -d= -f2-)

1b. Or run it in Docker. The same node, from the published image at one.semurg.io/semurg. Every /v1 call needs the token exactly as on bare metal; Docker prints it in the container log at first start and keeps it on the node.

docker run -d --name semurg -p 127.0.0.1:4000:4000 \
  --ulimit memlock=-1 --ulimit nofile=1048576:1048576 --security-opt seccomp=unconfined \
  -v semurg-data:/var/lib/semurg one.semurg.io/semurg
until docker logs semurg 2>&1 | grep -q 'Your API token'; do sleep 1; done
TOKEN=$(docker exec semurg cat /var/lib/semurg/.api_token)

--security-opt seccomp=unconfined is not optional: Docker's default profile blocks the system calls the engine reads and writes with, and without it the container exits at start with a message naming this flag. --ulimit memlock=-1 is what lets it register its buffers. Binding to 127.0.0.1 keeps the node on the host until you decide otherwise. The image is pulled from Semurg's own read-only registry at one.semurg.io, never from Docker Hub.

No licence key, ever, on one node. A single node runs the full engine, writes included, with nothing to obtain. A stale or foreign key file on the node changes nothing. A key exists only to join a second node to a cluster.

3. Prove the gate is on. Without the token the API answers 401 on purpose. With it, 200. A node needs a few seconds to start: if you pasted the steps above back to back, wait until curl -s http://127.0.0.1:4000/api/health answers before you run these two lines.

curl -si http://127.0.0.1:4000/v1/whoami | head -1
curl -si -H "Authorization: Bearer $TOKEN" http://127.0.0.1:4000/v1/whoami | head -1

4. Write something. A passage is the simplest thing to write: text goes in, and the node keeps one copy.

curl -s -H "Authorization: Bearer $TOKEN" -H 'content-type: application/json' \
  -d '{"passages":[{"text":"Semurg keeps one copy of your data and answers every kind of question about it."}]}' \
  http://127.0.0.1:4000/v1/ingest

5. Read it back. A search answers with three numbers: how many stored items matched, how many it looked at, and how many bytes that took. This search matches a needle of up to 20 bytes, so keep the needle to 20 bytes or fewer: a longer passage is stored in pieces it matches one by one.

curl -s -H "Authorization: Bearer $TOKEN" -H 'content-type: application/json' \
  -d '{"op":"search","needle":"Semurg keeps"}' \
  http://127.0.0.1:4000/v1/query

A new node starts empty. To try a traversal on data that is already there, ask for a small public sample graph (a Panama Papers subset from the ICIJ Offshore Leaks data) on the node's first boot: run the installer with SEMURG_PRELOAD_PANAMA=1, or add -e SEMURG_PRELOAD_PANAMA=1 to the docker run line; a search's "looked at" count then includes the sample. See the blog for worked examples on data that carries relationships of your own. Similarity search over your own passages (vector_search, which returns the matching text rather than a count) needs an embedding model on the node; a new install downloads one on its first boot unless you declined, and until one is installed the API says so plainly if you call it. Every call above works on a fresh node.

Your coding agent

Two things a coding agent can do with the node you just installed, in this order: remember across sessions, and build your app on it. The first is one command. The second is a short file you choose to add.

A. Give your agent a memory. One command connects Claude Code to the node's seven memory tools over MCP. The token stays an environment-variable reference in the agent's settings, so export it in the shell you start the agent from (TOKEN is the variable from step 2 or 1b above). claude mcp list should then show semurg as connected. Claude Code itself installs with the line on the Agents page.

export SEMURG_API_TOKEN="$TOKEN"
claude mcp add-json --scope user semurg '{"type":"http","url":"http://127.0.0.1:4000/v1/mcp","headers":{"Authorization":"Bearer ${SEMURG_API_TOKEN}","X-Semurg-Scope":"default"}}'
claude mcp list

What the agent stores lives on the node, not in the chat: a later session, or a different agent, that connects to the same node and the same scope reads it back. Replace the address if your node is not local. The Agents page records Claude Code as Connected: its own client listed the node's memory tools. Cursor and the OpenAI Codex CLI use the same node and the same seven tools; their configuration is on that page too, marked as results pending until we have run each one ourselves.

B. Check the node by hand first. Six short calls, with no agent and no model involved. They use their own scope, rules-check, so they do not touch your app's memory. Each prints the answer and then its HTTP status.

export SEMURG_API_TOKEN="$TOKEN"
NODE_URL=http://127.0.0.1:4000
mem() { curl -sS -w '\nHTTP %{http_code}\n' -H "Authorization: Bearer $SEMURG_API_TOKEN" -H 'content-type: application/json' "$NODE_URL/v1/memory/$1" ${2:+-d "$2"}; }
mem status
mem remember '{"scope":"rules-check","facts":{"entities":[{"name":"Acme","type":"Company"},{"name":"Dana","type":"Person"}],"relations":[{"source":"Dana","target":"Acme","type":"WORKS_AT"}]},"passages":[{"text":"Dana works at Acme.","mentions":["Dana","Acme"]}],"source":"rules check"}'
mem recall '{"scope":"rules-check","subject":"Dana"}'
mem context '{"scope":"rules-check","query":"Where does Dana work?"}'
mem recall '{"scope":"rules-check","subject":"Nobody"}'
mem remember '{"scope":"rules-check","content":"Dana works at Acme."}'

All six answer HTTP 200. The fifth, a name that matches no stored entity, answers state: nothing_stored with no items and a note saying what was tried: an empty answer that says so, never an invented one. The sixth sends raw text as content: it is stored now, with no model, cut into passages and linked to the entities it names that are already stored; the reply says extraction: known_names_only and carries a hint: send the entities and relations you found in the text as facts in the same call, and they are stored at once (the node's own extraction model reads text sent without facts only when it is installed).

C. Let your agent build on it (optional). If you want a coding agent to build an app that keeps its data in the node, a short rules file helps it start with the right calls instead of guessing. It is a recommendation, not a requirement: nothing on the node depends on it, and you can edit or delete any line. Each recommendation carries its reason, so you can keep the ones that fit your app and drop the rest. Save it at the root of the repository as CLAUDE.md (Claude Code), AGENTS.md or .cursorrules (agents that read those names).

# Semurg for this app's memory and retrieval (recommended, not required)

This file recommends keeping what the app must remember in a Semurg node. Each
recommendation says why, so you can keep the ones that fit and drop the rest.

## Connection
- Node: $NODE_URL (a base URL, for example http://127.0.0.1:4000).
- Token: $SEMURG_API_TOKEN.
  Recommended: keep the token and the node address out of source files and out
  of commits. Why: anyone who can read the repository could then use the node.
- MCP: POST /v1/mcp on the node (streamable HTTP, bearer token).
  HTTP: POST /v1/memory/remember, /v1/memory/recall, /v1/memory/relate,
  /v1/memory/forget, /v1/memory/context; GET /v1/memory/status.
- Recommended: when a memory call fails, call memory_status first.
  Why: it says whether memory is on for this node and which scopes hold facts.

## The seven memory calls
memory_status, memory_remember, memory_recall, memory_relate, memory_forget,
memory_context, memory_document.

- One app, many end users. Recommended: send user_id (optional, on every
  memory call; no ':', at most 256 bytes) naming the end user.
  Why: each end user's memory is then their own inside your key, and two end
  users never meet.
- One connection per end user. Recommended: send the X-Semurg-User header with
  that end user's id (over MCP or HTTP).
  Why: the connection is then pinned to that user. A call that names a
  different user_id is refused with user_id_pinned_mismatch, so a mistake in
  your code cannot read or delete another user's memory.
- When an end user leaves. Recommended: POST /v1/memory/delete_user
  {"user_id": "..."} (over HTTP; it is not an MCP tool).
  Why: one call forgets everything that end user stored.

- Where the app's durable memory lives. Recommended: the node, and not a second
  store (another database file, cache or search index) for the same data.
  Why: a second copy is a second thing to keep in step, and the node keeps the
  relationships between things, which is what recall and context walk. If the
  app needs another store for a different reason, that is your call.
- remember. Recommended: you are the extractor. Whenever you store text as
  "content", also send the entities and relations you found in it as "facts"
  in the same call.
  Text with its facts: {"scope":"<app>","content":"Dana works at Acme.",
  "facts":{"entities":[{"name":"Dana","type":"Person"},{"name":"Acme","type":"Company"}],
  "relations":[{"source":"Dana","target":"Acme","type":"WORKS_AT"}]}}
  Facts alone: {"scope":"<app>","facts":{"entities":[{"name":"Acme","type":"Company"}],
  "relations":[{"source":"Dana","target":"Acme","type":"WORKS_AT"}]},
  "source":"<where it came from>"}
  Passages: {"scope":"<app>","passages":[{"text":"Dana works at Acme.",
  "mentions":["Dana","Acme"]}]}
  Raw text: {"scope":"<app>","content":"Dana works at Acme."}
  Why: facts you send are stored at once, and you extract better and faster
  than the node's own model, which reads text sent without facts only in the
  background and slowly on a CPU (or not at all when none is installed; the
  reply's model_extraction says which, and a reply to text without facts
  carries a hint). Raw text is stored now: it is cut into passages and each
  passage is linked to the entities it names that are already stored or that
  you send beside it as facts (the reply says extraction: agent_facts or
  known_names_only, and unlinked_passages counts the passages no entity reaches). Each mention of a
  passage must appear in its text (letter case aside), or the passage is
  refused by name. content and passages cannot ride in one call. A name is at
  most 1024 bytes and one call holds at most 1000 entities; more is refused
  with 422, so send several calls.
- recall. Recommended: pass the name of one entity, as stored or close to it:
  {"scope":"<app>","subject":"Dana","k":20}
  Why: recall finds the entity by its name (exact, a similar spelling or letter
  case, or one whole word of a longer name), walks the graph from it and
  returns the connected facts in both directions. A name that matches nothing
  answers HTTP 200 with state nothing_stored and no items: not an error, not a
  guess. Read state before you use the items.
- context. Recommended: ask it a question with the entity names in it:
  {"scope":"<app>","query":"Where does Dana work?"}
  Why: each word or phrase of the query that names a stored entity (exact, a
  similar spelling or letter case, one whole word of a longer name, or the
  start of a word) or a stored type becomes a seed. The graph is walked "hops"
  steps (default 2) and the ranked facts come back, with any passage that names
  those entities. When nothing matches, state is nothing_stored and items is
  empty. A stored name that shares one ordinary word with a longer question can
  still be a seed (a question about the capital of France can reach a stored
  "Capital One"). Recommended: read the reply's grounding before you answer.
  Why: grounded means the cited items cover your question; grounded_by_meaning
  means a passage found by meaning answers a reworded question, so answer from
  it when it does; related means they share only some of its words, so use only
  what really answers; nothing_stored means say you do not know.
- compact. Recommended: send "compact": true on recall and context.
  Why: the same answer, each item once, in a fraction of the bytes. Leave
  it off only when you need the diagnostics (how each seed was found and the
  walk it took).
- time. Recommended: when a fact changes, remember the new value with "date"
  set to when it became true; to ask about the past, send as_of (what the node knew then) or
  valid_at (what was true then) on recall or context, as an ISO 8601 date or
  instant.
  Why: a relation's values come back newest first, so the first is the current
  one, and a date that does not parse is refused by name, never ignored.
- scope. Recommended: one scope per app, sent in the body of every HTTP call.
  Why: a scope keeps one app's memory apart from another's, and the HTTP
  routes answer 422 "missing scope" without it. Over MCP, the X-Semurg-Scope
  header sets the connection's default.
- forget. Recommended: use it with the entity's exact name for an entity that
  should no longer be returned, and check with a recall afterwards.
  Why: the node marks the entity deleted, so recall and context should no
  longer return it or a fact with it at either end; the recall is how you
  confirm that on your node. A name that matches nothing is refused with 404
  and up to three close names to check. A later remember that names it brings
  it back. Every passage LINKED to it is hidden whole, with whatever else it
  says; a passage that names it without that link stays, and the reply counts
  it in passages_retained. To drop one fact use the fact form, to drop one text
  the passage form.
- ids. Recommended: treat ids as strings and send them back unchanged.
  Why: they are 64-bit numbers, and a JavaScript Number cannot hold one. A
  passage id sent to forget as a JSON number is refused with id_not_string,
  because the number may already have been rounded on the way.

## What to expect
- Recall and context find stored names by their spelling (exact, letter case,
  a similar spelling, one whole word of a longer name, the start of a word),
  and by MEANING once the node's embedding model is installed (it downloads on
  the first boot). Recommended: read the reply's meaning field before you rely
  on a paraphrase.
  Why: until the model is installed, replies say meaning: not_armed and a
  paraphrase that shares no name with the question is not found.
- Every memory tool and memory route, with its parameters, is listed in
  https://one.semurg.io/llms.txt (the data routes, /v1/ingest and /v1/query
  with its operations, are in the blog's API reference),
  which is generated from the source. Recommended: read it instead of guessing
  a route. Why: it is the one list that matches the running code.
- A refusal names what to change. Recommended: change that, and do not retry
  the same call unchanged. Why: the same call is refused the same way.

Your code's history, held in Semurg

Put a git repository's history into your node, and your coding agent can ask it who changed a file, when and why, and what the file looked like at any point. It reads the history; it never changes your repository.

Six tools for your agent. The MCP connection from step A carries them beside the memory tools:

  • code_status: what the node holds of a repository and how fresh it is. Call it first: every other answer is as of the last commit it took in.
  • code_at: a file as it was at a commit, a branch, a tag or a point in time.
  • code_history: a file's history, newest first: who, when, the message, and what each commit did to it.
  • code_blame: the commit that produced the file as it is at a revision, and the commits that changed it before that. Whole-file today: line-by-line blame is not built, and asking for it is refused by name.
  • code_diff: the files changed between two revisions, and for one file its text before and after.
  • code_changes_since: the commits after a revision, oldest first, each with the files it changed: what an agent reads to catch up.

Putting a repository in is one command on a node installed with step 1, and taking new commits in again later adds only those. Its how-to appears here with the next release, once we have run it from outside on that release. Files whose names or contents look like credentials are never stored. Read-only: the node holds the history and answers questions about it, and your commits still go through git.

How you work with it

Write the relationships

You say what connects to what as you write. That is the work, and it is done once, at write time.

Read three ways

Get one thing by id, walk from it to what it connects to, or scan a run of it. Those are the reads.

No query language

Ordinary code in the language you already use. There is nothing new to learn and nothing to tune.

The clients

A client points at the base URL of a node you operate, with a token if you set one. There is no Semurg-hosted control plane in the middle, and nothing about your data leaves your network.

  • Python: pip install semurg (PyPI, Apache-2.0, no dependencies, Python 3.9 or newer). Start a node with the Docker command above, then remember, recall and context in five lines: the example is on the PyPI page.
  • Eleven languages: C++, C#, Elixir, Erlang, F#, Go, Java, Julia, OCaml, Python and Rust, in one archive: one.semurg.io/dl/clients.tar.gz, with CONNECTING.md, the guide to pointing a client at your node. The Python client was installed from PyPI and run against a live node on the current release (remember, recall, context); the other ten have not yet been run against it, and the four verbs below are the ones each client carries. Elixir needs Elixir 1.18 or newer and Rust needs a 2024-edition toolchain (rustup), which the Ubuntu 24.04 packages do not meet; C++ needs libcurl4-openssl-dev, Erlang needs rebar3 and OCaml needs dune. The archive's fetch_batch, benchmarks, dashboard and bench methods call routes that no longer exist and are being removed; the four verbs below work in every client
  • What they cover today: health, status, ingest and query in all eleven; the cluster and licence reads in five (Python, Go, Elixir, C#, Julia). The retrieval, analytics and generate verbs are REST-only today and are being added to the clients now.
  • For a coding agent: the memory calls (status, remember, recall, relate, forget, context) are in the Python, Go, Java, C#, F#, Elixir, Erlang, Rust and OCaml clients; Julia has remember, recall and context; C++ reaches them over HTTP. The tools, routes and calls are in one plain-text file, generated from the source: one.semurg.io/llms.txt, and as a page with every command on it: one.semurg.io/commands.
  • The same small surface in each, written in that language's idiom, so the mental model carries across your stack.
  • A PostgreSQL wire door: the node also answers the PostgreSQL wire protocol on 127.0.0.1:5432, so an existing PostgreSQL driver can read from it. Today it is read-only, plus session control; any other statement is refused by name. When the node has an API token, the door asks for a token as the password (SCRAM-SHA-256); it admits without a password only on a node with no token, and it opens on an address other than loopback only when a token is set. It has no TLS yet, so reach it from another machine through a tunnel.

Beyond an app: an enterprise in a box

The biggest thing to build on Semurg is not one app: it is a digital twin of an organisation. Its databases, object stores and documents in one knowledge graph, and a swarm of persistent agents working on it, each an isolated, supervised process on the BEAM that writes its memories into the graph and reads the graph to act, on the same nodes it runs beside, calling a model on the node or one you choose outside it.

Build it yourself, today

Write the swarm in Elixir, with Jido or plain OTP: one supervised process and one memory scope per agent. Start from the complete Jido program on the agents page. Any other language reaches the same memory over MCP or HTTP, one connection per agent.

What the BEAM gives a swarm

Isolation: an agent that fails stops alone. Supervision: it restarts, with a ceiling, and reads its memory back from the graph. Concurrency: a node runs hundreds of thousands of processes across every core, so thousands of agents are an ordinary load.

Or build it with us

Sources synced, the graph and ontology built with your people, agents written for your organisation's own work, including agents running inside the node's own virtual machine. See consulting.

Tier-3 apps: your app on the node's data

A Tier-3 app is your own application, in any language and any web framework, that keeps its data in a node through the node's public API: the memory routes and MCP tools above, /v1/ingest and /v1/query, and the clients. It runs where you run it, and the node holds the one copy of its data. Apps that run inside the node itself, written in Elixir, come later as a private install signed by Semurg; that path is not open to third parties yet.

TIER 3 · integrations TIER 2 · domain packs TIER 1 all data Tier 1: the engine How bytes move: one storage format and one path to the disk, and the four operations over it: write, read, traverse, scan. It knows no domain. Tier 2: the domain packs What bytes mean: graph, vector, time series, documents, search, the knowledge graph. Built only on Tier 1, and shipped with the node. Tier 3: your application Your app or your integration, in any language. It reaches data only through the node's public API, so it inherits the same data path as everything else instead of building its own.
You develop at Tier 3. Everything inside the hexagon is built and ships with the node, and the wall between the rings is a composition check that runs on every release today: no ring may reach around the one inside it.

What a Tier-3 app is

Your own application, or an integration with a system you already run, in the language you already use. It keeps its data in the node through the public API, so it uses the domain packs the node ships with and cannot drift from them.

Where it runs

On your servers, beside the node or on another machine, reaching it over HTTP, MCP or a client. One copy of the data serves your app and every other door; there is no second store to keep in step and no export to schedule.

Inside the node, later

Apps that run inside the cluster, as supervised Elixir processes sharing the store, come later as a private install signed by Semurg. The composition wall will be their condition of install: an app that reaches around the primitives does not pass it, so nothing installed can quietly open its own path to the disk.

The same wall already holds what Semurg itself ships: the domain packs pass it on every release. An integration a customer writes and one we write are held to the same rule.