Skip to content
API Blog

Memory

Most agent memory systems are flat files scoped to one user. The agent appends notes, then re-scans them every turn to recall context. That works for a personal assistant. It breaks the moment two agents — or an automation, or another teammate — need to converge on the same fact.

Lobu’s memory layer is entity-based: facts attach to typed entities (Company, Project, Member), so any agent in your org reads the same record.

For a category-level overview of the problem and tradeoffs, see Shared Memory for AI Agents.

Workspace filesystem Entity memory
───────────────────── ─────────────────────
One user, one session Shared across agents
Ephemeral (dies with sandbox) Durable (survives sessions)
Drafts, scratch files, PDFs Companies, projects, people
No schema enforcement Typed with properties + keys
Agent A's DM workspace Company:Acme
├── draft-report.csv ├── CTO departed 2026-03
├── scratch.py ├── 3 open Linear bugs
└── downloaded.pdf ├── DSCR 1.1× (Q1 review)
└── Renewal due May 2026

A note on a Slack DM filesystem dies with the sandbox. A fact written to Company:Acme survives and shows up the next time any agent — billing, support, CSM — touches Acme.

For the longer argument and where to draw the line between the two, see Filesystem vs Database for Agent Memory.

Entity types define the schema for what you can store. Each type declares a name, description, required fields, and optional metadata properties. You define them in lobu.config.ts:

const company = defineEntityType({
key: "company",
name: "Company",
description: "A customer or prospect organization",
required: ["name"],
properties: {
name: { type: "string", "x-table-label": "Name", "x-table-column": true },
stage: { type: "string", enum: ["prospect", "active", "churned"] },
arr: { type: "number" },
},
});

When an agent writes a fact about Acme, Lobu validates the metadata against the Company schema. Required fields must be present. Properties surface in the admin UI and are queryable through the API.

Entity types Entity instances
───────────────────── ─────────────────────
defineEntityType({key:"company"}) Company:acme
defineEntityType({key:"project"}) Project:checkout-v4
defineEntityType({key:"member"}) Member:daniel
Each type owns the metadata Each instance holds actual data
schema (properties, required, conforming to the type's schema
eventKinds, measures)

Entity types can be derived — read-only SQL views over your internal tables or an external database connection. Use derived types when the data already lives somewhere and you want to expose it as a queryable entity without copying it:

const invoice = defineEntityType({
key: "invoice",
name: "Invoice",
backing: {
sql: "SELECT id, amount, status, customer_id FROM invoices WHERE status = 'open'",
connection: "stripe-db", // optional: run live against external DB
},
});

Relationships are typed, directed links between entities. They let you express structure like “Acme owns Project X” or “Daniel is the CSM for Acme”:

const owns = defineRelationshipType({
key: "owns",
name: "Owns",
description: "A company owns a project.",
rules: [{ source: company, target: project }],
});

rules constrain which entity types can be source and target. This prevents accidental links (e.g. linking two Members to each other as “owns”).

Company:acme ──owns──▸ Project:checkout-v4
Company:acme ──owns──▸ Project:billing-api
Member:daniel ──csm-for──▸ Company:acme

Relationships are first-class in the graph — you can query them, attach events to them, and use them to scope what an agent sees.

Events are the durable facts written to the append-only log. Every event has a semantic type (also called eventKinds) that describes what kind of fact it is. Entity types declare which event kinds are valid for them:

const company = defineEntityType({
key: "company",
eventKinds: {
"support-ticket": {
description: "A customer support interaction",
metadataSchema: {
type: "object",
properties: {
ticket_id: { type: "string" },
severity: { type: "string", enum: ["low", "medium", "high", "critical"] },
subject: { type: "string" },
},
},
},
"renewal-update": {
description: "Changes to renewal status or terms",
},
},
});

Built-in kinds like observation, decision, and metric are always available. Custom kinds declared on an entity type become subscribable by automations and queryable through the API.

Event log (append-only)
──────────────────────────────────────────────────────────
│ id │ semantic_type │ entity_link │ metadata │
│─────│──────────────────│──────────────│────────────────│
│ 101 │ support-ticket │ Company:acme │ severity: high │
│ 102 │ renewal-update │ Company:acme │ stage: active │
│ 103 │ observation │ Company:acme │ namespace: risk│
│ 104 │ decision │ Company:acme │ chose: extend │

An event can link to several entities. A single support email can update a company, a contact, and a renewal project while keeping one source event as evidence.

Every entity instance is identified by a key — one to four fields whose values form a stable identity across automation runs. This is how Lobu knows that “Company:acme” written by the CSM automation is the same record as “Company:acme” written by the billing automation.

outputs: {
accounts: {
entity: company,
key: ["domain"], // stable identity — use source IDs, not labels
name: ["name"], // optional: readable name fields
},
}
Key fields: ["domain"] Identity across runs
───────────────────── ─────────────────────
Run 1: { domain: "acme.com" } ──▸ Company:acme.com
Run 2: { domain: "acme.com" } ──▸ Company:acme.com (same record)
Run 3: { domain: "initech.com" } ──▸ Company:initech.com (new record)

Use durable source IDs, not editable labels. Changing the key fields, their order, or the entity type changes identity and starts a new chain.

For event outputs, key fields work the same way — they determine when a new event supersedes the previous one with the same identity:

outputs: {
risk_observations: {
event: "observation",
key: ["namespace", "entity_domain"], // per-entity risk namespace
},
}

A single inbound fact lands on the right entity and accumulates alongside everything else known about that customer:

Inbound fact
──────────────────────────────────────────────────────
"Acme's CTO left in March, no exec champion since"
Company:Acme
├── CTO departed 2026-03 — no exec champion
├── Automation: 3 open Linear bugs this week
├── DSCR 1.1× (Q1 review)
├── Renewal due May 2026
└── Decision: declined $2M extension — DSCR below 1.2×
Company:Initech
├── ...

Different sources, same destination. A CSM’s note in Slack, an automation ingesting Linear bugs, and a credit decision from a separate agent all converge on Company:Acme. The next agent that reads Acme — through search, recall, or an explicit lookup — sees the full picture without re-deriving it from scratch.

Events land in memory through connectors — built-in integrations for GitHub, Gmail, Linear, Stripe, and more, or custom ones your agent writes in TypeScript against the Connector SDK. Connectors run in an isolated V8 sandbox, pull data from APIs, OAuth services, browsers, or webhooks, and produce typed events that automations shape into entity memory.

Automations are versioned jobs that run on schedules, connector events, or on demand. They separate activation (when to run) from instructions (what to do) from outputs (what to write).

Automation run
──────────────────────────────────────────────────────────
Activation: schedule / connector event / workspace event / manual
Read: pending window + named sources + bound entities
Decide: agent follows prompt + pinned skills
Complete: structured output against the window
Promote: entity writes, event writes, reactions, notifications

The prompt is the judgment filter — it tells the agent which signals from a noisy event stream actually matter. Sources decide what it can read; they do not wake it by themselves.

Automation: track-acme-incidents
Prompt: "Track changes to active incidents, blockers, and pending PRs.
Skip OOO and personal chatter."
Schedule: every 1h
9:02 Dan picking up INC-4421, rolling back checkout-v43
9:05 Priya still blocked on checkout cluster admin creds
9:11 Jay caching layer PR ready for review, needs to land by EOD
9:18 Sam OOO today — family thing
9:27 Nina writing INC-4378 postmortem, sharing draft at lunch
▼ consolidate
Company:Acme — new memory:
├── Incident INC-4421 — checkout-v43 rollback (Dan)
├── Caching layer PR pending merge by EOD (Jay)
└── INC-4378 postmortem drafting (Nina)

Sam’s OOO and Nina’s lunch plan drop out — they don’t match the prompt. The agent isn’t online, but the memory is moving. By the time someone asks “what’s going on with Acme?”, the answer is already in the entity.

For the full automation model, see Automations.

Reach for entity memory when you need:

  • multiple agents (or automations) to write into and read from the same record
  • decisions and context that survive sandboxes and channel resets
  • ingested external signal (Linear, HubSpot, Stripe, email) attached to the right customer or project
  • operator-visible memory that can be audited and corrected in the UI

If you only need a working directory for one session, the filesystem is enough.

The fastest path is to install the Lobu skill into your coding agent and let it do the wiring — it knows the lobu.config.ts API, plugin install, and CLI flow:

Terminal window
npx skills add lobu-ai/lobu --skill lobu

For the step-by-step manual setup, see the Getting Started guide and the Lobu Memory CLI reference.