Docs/Lanes Link/Built-ins/Entities

Entities

Who everyone else is: the people, companies and projects you deal with, and how to reach each of them. Looked up rather than remembered, and never guessed.

Entities holds who everyone else is: the people, companies, projects and accounts you deal with, with the addresses and handles that reach each of them.

Identity says who you are so an agent can write as you. This is the same idea pointed outward. Without it, "email Jan about the invoice" makes an agent reach for an address it saw in a thread, and that guess does not look like a guess: an address that appeared in a mailbox your endpoint serves looks exactly like an address you use for that person.

What your agent can do

ReadWrite
entities.find, entities.getentities.write, entities.link, entities.forget

Entities are also readable as an MCP resource at a stable address, entities://entity/{id}, for clients that read those.

It never picks for you

This is the part worth understanding, because it is the whole point.

entities.find returns every match. One match is an answer. More than one is a question, and the answer shows what separates the candidates so it can usually be settled from context instead of asked:

Code
2 entities match "Jan" on `entities.main`.

  jan-de-vries  Jan de Vries  alias "Jan"  ·  email jdv@meridian.test  ·  works_at → meridian
  jan-bakker    Jan Bakker    alias "Jan"  ·  email jan@acme.test      ·  works_at → acme-bv

If the context does not make it clear which is meant, ask before acting. Nothing here
chooses between them, and the order is not a ranking.

Two matches is not an error. An assistant handed two people called Jan asks which one you meant, it does not fail, and a lookup that returned an error there would teach a client to route around it.

What the tool will not do is choose. Candidates are ordered so the list reads well, but the order is never a ranking, and there is deliberately no tiebreak inside a rank: nothing quietly promotes one of two exact matches. Only the fields that actually differ are shown, because two rows of identical detail bury the one column that would have decided it.

Nothing matching is not an error either. It means you have not written that person down, so the answer says to ask rather than to use an address from somewhere else.

Looking one up

Every criterion you give is combined, and a bare lanes link entities lists everything.

console
$ lanes link entities find Jan --profile personal --workspace local
$ lanes link entities find --type person --tag client --profile personal --workspace local
$ lanes link entities find --related works_at=acme-bv --profile personal --workspace local
$ lanes link entities find --attr github --profile personal --workspace local

--attr github means "has a GitHub handle at all". --attr github=janb means that one. Both are repeatable, and so is --related.

Declaring one

A flag you leave out keeps what is already stored, so this is also how you correct a single field without restating the rest. Notes come from stdin and are optional.

console
$ lanes link entities write "Acme B.V." --name acme-bv --type company \
    --attr domain=acme.test --profile personal --workspace local

$ printf 'Prefers email over calls.' | lanes link entities write "Jan Bakker" \
    --type person --alias Jan --attr email=jan@acme.test --attr github=janb \
    --related works_at=acme-bv --profile personal --workspace local

type is yours to choose. person, company and project are conventions rather than a fixed set, so vessel or client need no release.

Attributes are a list, not a map, and the order is the ranking: the first of a kind is the default, and a note says when to prefer another. That is the same rule identity follows, and it is why two email addresses are expressible at all.

The id is derived from the name the way a memory id is derived from a title, so Acme B.V. becomes acme-b-v. Pass --name acme-bv when that matters.

Relationships

An edge is written on the entity that declares it, once, and the reverse direction is derived:

console
$ lanes link entities link jan-bakker knows=marta-silva --profile personal --workspace local
$ lanes link entities get acme-bv --profile personal --workspace local

Acme's own file never mentions Jan, and get acme-bv still shows ← works_at Jan Bakker. Writing both sides would be two files for one fact, and a run interrupted between them would leave half an edge that nothing detects.

The other end does not have to exist yet. An edge to something undeclared is kept and shown as such rather than hidden, and you are told when you create one. forget does not clean up edges pointing at what it removes, because a delete that rewrote five other files could not be reviewed as one change. It names them before asking.

The files are yours to edit

One Markdown file per entity, frontmatter above your notes, exactly as memory works. A file you drop in by hand is an entity named after its filename, and an edit you make in an editor is what the next lookup returns.

YAML
type: person
name: Jan Bakker
aliases: [Jan, JB]
attributes:
  - { kind: email, value: jan@acme.test, note: work }
  - { kind: email, value: j.bakker@example.net, note: personal }
relations:
  - { predicate: works_at, entity: acme-bv, note: since 2023 }

There is one thing beside them that memory does not have: a derived _index.json, so a lookup opens no entity files at all. It is a cache, not a second copy of the truth. It carries a fingerprint of the files it was built from, so editing one by hand invalidates it and the next read rebuilds. A corrupt, truncated or stale index is never an error, only a rebuild.

console
$ lanes link entities reindex --profile personal --workspace local

You need that only when a write is not coming: a bulk edit made in an editor, or a pull from a repository. It says whether it had to do anything, and why.

At a thousand entities on a bucket a lookup is one listing and one read, whatever the count. At ten thousand this is the wrong shape and the index stops paying for itself.

Making it read-only

Three rules, not one, because three capabilities change something:

console
$ lanes link policy deny entities.write entities.link entities.forget \
    --profile personal --workspace local

Worth considering for the same reason as memory, and a little more sharply. An entry injected into memory is re-served as something true. An address injected here is used: it is what the next message is sent to.

Keeping it in a repository

Entity files can live in a private GitHub repository along with memory and skills, so they have a history and follow you between machines:

console
$ lanes link knowledge use github --repo <owner/name> --migrate --profile personal

The derived index travels with them, because it is derived from what travels. That means it appears in a commit that otherwise changed one attribute, which is a real cost of keeping it beside the documents. Tasks and assets stay where they are, and the vault, the credential store and the audit log are never eligible.

What is recorded

Ids are kept. Values are not, and neither is your lookup: a query here is frequently somebody else's address, typed by whoever was asking. What the log keeps instead is which entities came back, which is more useful to read later and is stable across spellings. A write records the kinds that changed, email and github, without the addresses themselves. See the audit log.


Next: the audit log, or Built-ins for how the stores divide up.