Domain Model
LeftFold is built on three concepts: aggregates, events, and traits. Everything else — agents, the mailbox, the query engine — follows from these.
Overview
Instead of storing content in rows that get overwritten, LeftFold records every change as an immutable event. Events belong to aggregates — domain objects like articles, people, or recipes. Aggregates declare traits that determine what AI agents do with them.
This architecture means full audit history, automatic AI enrichment, and custom content types that work instantly — no migrations, no schema changes, no agent reconfiguration.
Aggregates
An aggregate is a domain object with a distinct lifecycle — an Article, a Person, a Recipe, a Brand. Each aggregate has:
- A type — what kind of thing it is (e.g.
article,person) - An event stream — its complete history of changes
- A set of traits — capabilities that determine how agents interact with it
You never read or write aggregates directly. You append events to record changes and fold the event stream to compute current state. See Event Sourcing for the full query model.
Events
An event is an immutable fact — something that happened. article.created, article.published, person.mentioned. Events are always:
- Past-tense — describes what happened, not what should happen
- Typed — follows the
aggregate_type.actionnaming convention - Immutable — once recorded, never modified or deleted
- Attributed — records who did it (user, agent, or system) and when
Event structure
{
"event_id": "f47ac10b-...",
"aggregate_id": "a1b2c3d4-...",
"aggregate_type": "article",
"event_type": "article.created",
"principal_type": "user",
"principal_id": "u1s2e3r-...",
"payload": {
"title": "ACL Recovery Timeline",
"body": "# Introduction\n\nThe anterior cruciate ligament...",
"slug": "acl-recovery-timeline",
"tags": ["sports", "rehabilitation"]
},
"sequence_number": 1
}Every aggregate type defines at minimum three events: .created, .updated, and .archived. Content types add .published, .enriched, and others.
Traits
Traits are capabilities an aggregate declares. They determine which agents enrich it. An article with the content-bearing trait automatically gets a summary, entity extraction, and vector embedding. A custom aggregate with the same trait gets the same enrichment — no configuration needed.
| Trait | Description | Subscribing agents |
|---|---|---|
| content-bearing | Contains long-form text suitable for NLP | Summarizer, Embedder, Entity Extractor |
| media-bearing | Has an associated binary file (image, video, audio, document) | Kind Detector, Image Analyzer |
| entity-like | A named entity that accumulates mentions from content | Entity Extractor, Profile Builder |
| publishable | Has a public-facing state (draft → published) | Structured Data Generator, Linker |
| time-bound | Has a temporal lifecycle (scheduled, occurs, past) | Reserved for future agents |
| operational | Infrastructure aggregate — agents ignore its events | None (exclusion sentinel) |
Traits decouple agents from aggregate types. When you create a custom Recipe aggregate with content-bearing and publishable traits, the summarizer, entity extractor, structured data generator, and linker all fire automatically — the same agents that handle articles.
Seeded Aggregates
Every workspace ships with 13 built-in aggregate types, organized into four categories:
Content types
| Type | Traits | Description |
|---|---|---|
| article | content-bearing, publishable | Long-form text content with title, body, slug, tags |
| image | media-bearing, content-bearing | Images with alt text, captions, dimensions |
| video | media-bearing, content-bearing | Video files with duration, title, description |
| audio | media-bearing, content-bearing | Audio files with duration, title, description |
| document | media-bearing, content-bearing | PDFs, Word docs, spreadsheets |
Entity types
| Type | Traits | Description |
|---|---|---|
| person | entity-like | People referenced in content — name, role, bio |
| place | entity-like | Locations — address, coordinates, description |
| organization | entity-like | Companies, institutions, teams |
System types
| Type | Traits | Description |
|---|---|---|
| brand | operational | Voice, glossary, taxonomy, and content guardrails for agents |
| workspace | operational | Tenant boundary — members, settings |
| project | operational | Content grouping within a workspace |
Workflow types
| Type | Traits | Description |
|---|---|---|
| task | supervisory | Human-in-the-loop workflow — A2A-aligned lifecycle with threaded messages |
| artifact | reviewable | Agent-produced output — persists in the artifact library beyond task completion |
Custom Aggregates
Beyond the 13 built-in types, you can define your own. Event storming is the primary way to do this: describe your business in plain language, and LeftFold's AI proposes aggregate types with appropriate traits.
How event storming works
- Describe your domain — "I run a physiotherapy clinic with four practitioners. We treat sports injuries and do post-surgical rehab."
- Review proposals — AI proposes 2-7 custom aggregate types (e.g. Practitioner, Treatment, Condition) with traits and field definitions
- Accept, edit, or reject — each proposal individually
- Immediately available — accepted types are queryable, creatable, and enriched by agents across all surfaces (MCP, CLI, SDK, API, dashboard)
Event storming is available after signup (onboarding), and can be re-run at any time from settings or via LeftFold storming start in the CLI. No migration needed — the fold-based query engine handles new types instantly.
Programmatic registration
You can also register aggregate types directly via the MCP tool or API:
// Via MCP tool: aggregate.define
{
"type_name": "treatment",
"display_name": "Treatment",
"plural_name": "Treatments",
"traits": ["content-bearing", "publishable"],
"fields": [
{ "name": "name", "type": "string", "required": true },
{ "name": "description", "type": "text" },
{ "name": "duration_minutes", "type": "number" }
]
}Knowledge Graph
Every aggregate automatically becomes a node in the workspace's knowledge graph. When content mentions entities — people, places, organizations — edges are created between them.
- Nodes — one per aggregate instance, with traits and a semantic embedding (1536-dimensional vector)
- Edges — created from
.mentionedevents, reference fields, and explicit relationships - Semantic search — query the graph by meaning, not just keywords, via pgvector
The entity extractor agent automatically creates mention edges when it finds people, places, or organizations in content. You can also create explicit relationships via the aggregate.relate MCP tool.