orbit: a personal CRM as Markdown notes, kept by an MCP server
orbit keeps a personal CRM as an Obsidian vault of Markdown notes: the people in your network, their organizations, your interactions with them, the tasks that come out of those, and the topics that keep coming back. You read and edit the notes in Obsidian or any editor; AI agents read and change them through orbit's MCP tools, which know what a person, an organization or an interaction is, and keep the vault trustworthy:
- One note per person, no duplicates. Agents resolve before they write:
resolve_entityfinds candidates by email address, name in any order or spelling (Müller, Mueller, "Müller, Hans", H. Müller), nickname, sound-alike surname and company domain, each with its evidence. The agent decides; orbit refuses a "new" person whose address is known, and unsure cases become questions you answer in Obsidian, where a duplicate decidedsameis merged. - No invented fields. Every write is checked against the schema of its note type. Fields you add by hand are kept.
- Every fact with its source. Observations carry the words that state them and the interaction they come from; field changes are dated, so an older email can't overwrite a newer fact.
- The notes are the truth. A SQLite index beside the vault makes lookups fast and guarantees uniqueness, but it is derived from the notes alone: delete it, and orbit builds it again. Edits made in Obsidian are indexed within a second, and on 50,000 interactions tools still answer in milliseconds (benchmarks).
- Upkeep that keeps links intact. Merging two notes, renaming one, or creating the notes that links point to rewrites every link that needs it, in front matter and text, as Obsidian does.
Everything runs locally: no LLM inside, no network access, no cloud.
Install
Requires Python 3.11 or newer. With uv:
uv tool install orbit-crm # installs the `orbit` command
Or run it without installing, uvx orbit-crm serve ~/crm, or install it with pip into a virtual environment:
pip install orbit-crm. The package is called orbit-crm and installs the command twice, as orbit and as
orbit-crm.
Create a vault, with your own note, Me.md, and your addresses, so that agents never take you for a contact:
orbit init ~/crm --name "Tobias" --email tobias@example.com
Open the folder as a vault in Obsidian. orbit init also writes three Bases views
to 95_Bases/, tables you can sort, filter and edit in place: People (everyone, by organization, not contacted
for a year, to confirm), Follow-ups (open tasks, tasks others owe you, people to get back to within two weeks)
and Review (questions waiting for your answer). Run orbit init on an existing vault to add what it lacks; it
never overwrites a file.
Setting up the MCP server
-
Start the server, and leave it running:
orbit serve ~/crm # prints: orbit MCP server on http://127.0.0.1:8775/mcp
It brings the index up to date, then watches the vault, so edits made in Obsidian count at once. To have it always there, start it at login with your system's scheduler (Task Scheduler, launchd, systemd).
-
Register it with your agent, once. With Claude Code, for all your projects:
claude mcp add --scope user --transport http orbit http://127.0.0.1:8775/mcp
Keep
--scope user: without it, the server is registered for the current folder only, and sessions started anywhere else don't see it. Other MCP clients connect the same way, to the streamable HTTP endpointhttp://127.0.0.1:8775/mcp; a client configured with JSON typically takes{"mcpServers": {"orbit": {"type": "http", "url": "http://127.0.0.1:8775/mcp"}}}. -
Start a new agent session: running sessions don't pick up new servers.
claude mcp listshould show orbit as connected, and an agent asked who is in your CRM answers from it. Connected agents receive orbit's rules in the server's instructions: resolve before writing, write only what a source states, and name the source.
One server serves any number of agents. Alternatively, a client can start orbit itself and speak to it over stdio:
claude mcp add --scope user orbit -- orbit serve --stdio ~/crm
But only one orbit process may open a vault at a time, so with stdio a second session's orbit is refused while the first runs: use HTTP when several sessions work at once. Web connectors (in claude.ai, for example) can't use orbit: they reach servers from the provider's cloud, which can't reach one on your computer.
Updating
uv tool upgrade orbit-crm # with pip: pip install -U orbit-crm
orbit --version
Then restart orbit serve (Ctrl+C, and start it again; a stdio server starts anew with each session), and
start new agent sessions: a client reads the tools and their parameters when a session starts, and keeps that
list, so a running session misses new tools and parameters. When a new version changes the index's layout, orbit
rebuilds the index from the notes on its first start, by itself. The changelog lists what changed.
The vault
crm/
├── 01_People/ Hans Müller.md, and Me.md for you
├── 02_Organizations/ Acme AG.md
├── 03_Interactions/
│ └── 2026/2026-10/ 2026-10-14 Email – AW Offerte Wartungsvertrag 2027.md
├── 04_Tasks/
├── 05_Topics/ projects, deals in progress, recurring themes
├── 90_Review/ questions for you: possible duplicates, job changes
├── 95_Bases/ Obsidian Bases views: People, Follow-ups, Review
└── 99_System/ orbit.toml, nicknames.txt, AGENTS.md
A person, as orbit writes them:
---
type: person
id: per_01M4WNN540CGMAHYXV1R90AA5V
name: Hans Müller
emails:
- hans.mueller@acme-ag.example
org: "[[Acme AG]]"
job_title: Leiter Einkauf
colleague_of:
- "[[Anna Keller]]"
last_contact: 2026-10-14
review_status: confirmed
created: 2026-10-14
updated: 2026-10-14
---
## Notes
## Observations
- 2026-10-14 · Decides on the budget for the 2027 maintenance contract · «Das Budget für 2027 ist freigegeben.» · [[2026-10-14 Email – AW Offerte Wartungsvertrag 2027]] · medium
## History
- 2026-10-14 · created: no candidate matched hans.mueller@acme-ag.example · [[2026-10-14 Email – AW Offerte Wartungsvertrag 2027]]
The file name is the note's name and link target, unique across the vault; its identity is the id, which never
changes. Front matter is flat (key: value lines and block lists), so YAML parsers, Obsidian Bases and line-based
readers all read it the same way. ## Notes is yours: orbit writes to it only to carry it over when two notes are
merged, and to keep its links pointing at renamed notes. An example vault with a note of every type is in
tests/vaults/example; the format is specified in docs/vault-format.md.
Tools
| Tool | What it does |
|---|---|
resolve_entity |
Candidates for a person, organization or topic, ranked, with the evidence for each. |
get_entity_context |
A note's fields, observations, last field changes, links in and out, recent interactions, open tasks and pending reviews. |
search_entities |
Full-text and fuzzy search over names, aliases, addresses, domains, job titles and notes. |
list_entities |
Queries over front-matter fields: everyone at an organization, open tasks due before a date, people not contacted since. |
log_interaction |
An email, meeting, call or message, idempotent on its message id; updates last_contact. |
upsert_person, upsert_organization, upsert_topic |
Create a record, or add to one: lists merged, empty fields filled, conflicts reported. |
link_entities |
Set a relation from a fixed vocabulary: org, reports_to, colleague_of (both ways), an interaction's people, … |
link_document |
Record a document outside the vault: a URL, a path or an id. |
add_observation |
A fact with its evidence quote, source and confidence. |
set_fields |
Change fields with a dated history line; a change older than the stored value becomes an observation instead. |
create_task, update_task |
What you promised, what was asked of you, and what others promised you. |
propose_review |
A question for you, answered in Obsidian: a duplicate decided same is merged, one decided different is recorded on both notes. |
merge_entities |
Two notes about the same person, organization or topic become one; links follow, differing values go to review. |
rename_entity |
A new name and file name; every link to the note follows. |
create_missing_references |
Notes for links to notes that don't exist yet: mapped to an alias, or a stub of the type the link's field asks for. |
lint_vault |
Schema conformance, duplicate ids and addresses, clashing file names, broken links, unknown fields; fix repairs the safe cases. |
Every write names its source, returns the ids and absolute paths of the notes it changed, and does nothing when
called again with the same arguments, where it can. Errors say what to do next. Every parameter is described in the
tools' schemas, for the agents that read them.
Commands
| Command | What it does |
|---|---|
orbit init [VAULT] |
Create a vault, or add what one lacks: its folders, Me.md, 99_System and the Bases views. |
orbit serve [VAULT] |
Serve the vault over MCP (--stdio, --host, --port), watching it for edits made elsewhere. |
orbit lint [VAULT] |
Check the vault (--fix, --json); exits with 1 if errors remain. |
orbit reindex [VAULT] |
Build the index again from the notes. |
orbit status [VAULT] |
Notes per type, pending reviews, clashes and the index, also while a server runs. |
VAULT defaults to the current folder. Results go to stdout; everything else orbit says goes to stderr.
Configuration
99_System/orbit.toml, written by orbit init:
| Setting | Default | |
|---|---|---|
me |
the id of Me.md |
Your own note, which your addresses resolve to. |
server.host, server.port |
127.0.0.1, 8775 |
Where orbit serve listens. It warns when told to listen beyond this machine. |
identity.free_mail_domains |
gmail.com, outlook.com, gmx.ch, bluewin.ch, … | Domains that say nothing about an employer. |
identity.role_addresses |
info, noreply, team, sales, support, … | Mailboxes that never belong to a person. |
99_System/nicknames.txt lists first names that stand for one another, one group per line: Hans = Hansi = Hansruedi. The one environment variable, ORBIT_HOME (default ~/.orbit/), says where the indexes live: outside
the vault, one database per vault, so sync tools never see a live database.
How it works
- Reading and writing. Anything may read the vault. Agents write through orbit's tools, which check the index, read the note again (an edit made by hand may not be indexed yet), write it atomically (a temporary file, then a rename), and update the index in the same call. Tools run one at a time, and one orbit process at a time may open a vault, so nothing writes concurrently.
- The index holds ids, paths and front matter; addresses, domains, name keys and sound-alike codes; links both ways;
field changes from the History sections; and full-text search. Its unique indexes on ids, file names, people's
addresses and message ids make duplicates impossible through orbit; a hand edit that breaks one is recorded as a
clash, which
lint_vaultreports, instead of stopping the index. - Duplicates. Names are compared without case, accents or word order (ä as ae, titles dropped), with initials, nicknames and Kölner Phonetik; addresses exactly; organizations by domain, Swiss UID and name without legal form. orbit ranks the candidates; the agent decides, or asks you through a review.
Scope
In scope: one owner's network, in Markdown files on their machine; any number of local agents; Linux, macOS and Windows. Out of scope, by design: an LLM inside orbit, network access and cloud services, reading mailboxes or the web (agents that do so use orbit's tools), authentication, and legal or compliance features.
Versioning
orbit is in early development (0.x): the vault format, the index, the tools and the Bases views work and are tested,
and minor versions may still change the tools and the format. From 1.0, it follows
semantic versioning: within a major version, the vault format, the MCP tools and their
parameters, the commands and their JSON output, and orbit.toml stay compatible. The index is derived and rebuilt
when its layout changes, so it never needs migrating. The changelog lists every change.
Development
git clone https://github.com/tobiaswuerth/orbit
cd orbit
uv sync # the virtual environment, with the development tools
uv run pre-commit install # lint, format and config checks on every commit
uv run pytest --cov # 100% line and branch coverage is required
The tests use temporary vaults and a temporary ORBIT_HOME, and run the MCP server for real, in memory. See
CONTRIBUTING.md for the code layout and how changes are made, and SECURITY.md for
reporting vulnerabilities.
License
Metadata
Release files for orbit-crm 0.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| orbit_crm-0.1.1.tar.gz | 70.7 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| orbit_crm-0.1.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 150.9 kB
Release files / orbit_crm-0.1.1.tar.gz
| Download URL | orbit_crm-0.1.1.tar.gz |
|---|---|
| Size | 70.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
c9c0a194179872a180d6cdfb7be3ecd5613f811b39c3ab4431d6b8bcd34e3332
|
|
BLAKE2b-256 checksum How to use checksums |
ebb4a7dba72f1f9e26232b74f56c8b42dbb059d34aeacfdfe358c0c2e01358a4
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 22, 2026.
Transparency logRelease files / orbit_crm-0.1.1-py3-none-any.whl
| Download URL | orbit_crm-0.1.1-py3-none-any.whl |
|---|---|
| Size | 80.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
2a1229b433f5d5e6342ec0cdf69d7c8fa1d24a40e168262b64286126991da413
|
|
BLAKE2b-256 checksum How to use checksums |
d4e4c61f1229db23400fc6fae194939f0e7fda1bf4dd2ad74b6bac46463d70af
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 22, 2026.
Transparency logSecurity for orbit-crm 0.1.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for orbit_crm-0.1.1.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 22, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.