Carry
A local-first semantic database for humans and machines.
Carry is a CLI tool for working with Dialog DB – a local-first, semantic database designed for structured data that both people and machines can read, write, and reason about.
carry init my-project
carry assert com.app.person name=Alice age=28
carry query com.app.person name age
What is Carry?
Carry gives you a private, local-first data repository to which both you and your tools can read and write. Data lives on your machine, not in a cloud service. Carry provides a shared, durable place for your data to live.
At its core, Carry stores data as claims – simple statements in the form (the X of Y is Z) – organized into domains and composable schemas. This structure is flexible enough to model anything from a personal profile to a recipe database, while remaining queryable and human-readable.
Key Properties
- Local-first. Your data lives on your machine. No cloud service holds your memory. Sync is optional and on your terms.
- Schema-on-read. You don’t need to design a schema before writing data. Define concepts when you need them, and Dialog interprets your data at query time.
- Composable. Attributes combine into concepts. Concepts combine into rules. Rules derive new knowledge from existing data. Each layer builds on the one below.
Who is Carry for?
- Developers using multiple AI tools who are tired of re-explaining context across Claude Code, Cursor, and others.
- Data modelers who want a local and lightweight tool for defining and querying structured data without standing up a database server.
- Anyone who wants to own their data, inspect it, and carry it with them.
What’s in these docs?
| Section | What you’ll find |
|---|---|
| Getting Started | Installation and first steps |
| Philosophy | Why Carry exists and what it believes |
| Core Concepts | Claims, entities, domains, and asserted notation |
| Domain Modeling | Defining attributes, concepts, and rules |
| CLI Reference | Every command, flag, and option |
| Use Cases | Concrete examples of what to build with Carry |
| Dialog DB | The database engine underneath |
Status
Caution
Carry is version 0.1 and under active development. The CLI and data model are stabilizing, but expect rough edges. Dialog DB itself is experimental – binary encoding and index construction may change between releases without a migration path.
That said, the data you write is yours. If the tooling changes, your data doesn’t disappear.
Installation
Quick Install (recommended)
The fastest way to install Carry is with the install script. It detects your platform, downloads the right binary, and installs shell completions automatically:
curl -fsSL https://raw.githubusercontent.com/tonk-labs/tonk/feat/carry/install.sh | sh
This installs the carry binary to /usr/local/bin (you may be prompted for your password) and sets up shell completions for your current shell (zsh, bash, or fish).
It’s always good practice to inspect a script before running it on your machine. You can view it here.
Uninstall
To remove Carry and its shell completions:
curl -fsSL https://raw.githubusercontent.com/tonk-labs/tonk/feat/carry/install.sh | sh -s -- uninstall
From Nix
If you have Nix installed, you can build Carry from the Tonk flake:
nix build github:tonk-labs/tonk#carry
This produces a standalone binary at ./result/bin/carry. Copy it to somewhere on your $PATH:
cp ./result/bin/carry ~/.local/bin/
From Source
Clone the repository and build with Cargo:
git clone https://github.com/tonk-labs/tonk.git
cd tonk
cargo build --release --package carry
The binary will be at target/release/carry.
Using the Nix development shell
If you’re working on the Tonk codebase, the Nix flake provides a complete development environment:
cd tonk
nix develop
cargo build --package carry
Verify Installation
carry --help
You should see the Carry help output describing available commands and key concepts.
Shell Completions
The install script sets up shell completions automatically for zsh, bash, and fish. If you installed via another method, Carry supports completions via clap_complete. Generate them by running:
COMPLETE=zsh carry # for zsh
COMPLETE=bash carry # for bash
COMPLETE=fish carry # for fish
Redirect the output to the appropriate completions directory for your shell.
Quick Start
This guide walks you through creating a repository, defining a schema, asserting data, and querying it – all in about five minutes.
1. Initialize a Repository
carry init my-project
Output:
Initialized my-project repository in /path/to/.carry/did:key:zAbc123
This creates a .carry/ directory in your current working directory. Carry looks for a repository in the current directory first, then seeks through parent directories until it finds one. Repositories hold all the data created when using Carry.
2. Assert Some Data
Let’s start with raw domain assertions – no schema needed:
carry assert com.app.person name=Alice age=28
Output:
did:key:zNewEntity123
The output is the DID of the newly created entity. Every piece of data in Carry is an entity with a globally unique, content-derived identity.
Add another person:
carry assert com.app.person name=Bob age=35
3. Query Your Data
carry query com.app.person name age
Output:
did:key:zAlice123:
com.app.person:
name: Alice
age: 28
did:key:zBob456:
com.app.person:
name: Bob
age: 35
Filter by a field value:
carry query com.app.person name="Alice" age
Output:
did:key:zAlice123:
com.app.person:
name: Alice
age: 28
4. Define a Schema
Raw domain assertions work fine, but attributes and concepts give you reusable, named structures. Let’s define some:
# Define attributes
carry assert attribute @person-name \
the=com.app.person/name as=Text cardinality=one \
description="Name of a person"
carry assert attribute @person-age \
the=com.app.person/age as=UnsignedInteger cardinality=one \
description="Age of a person"
# Define a concept grouping those attributes
carry assert concept @person \
description="A person" \
with.name=person-name \
with.age=person-age
Now you can query using the concept name instead of the domain:
carry query person
Output:
did:key:zAlice123:
person:
name: Alice
age: 28
did:key:zBob456:
person:
name: Bob
age: 35
5. Use a YAML File
For anything beyond a few fields, YAML files are more convenient. Create a file called schema.yaml:
task-title:
attribute:
description: Title of a task
the: com.app.task/title
as: Text
cardinality: one
task-status:
attribute:
description: Current status
the: com.app.task/status
as: Text
cardinality: one
task:
concept:
description: A task to be completed
with:
title: task-title
status: task-status
Assert it:
carry assert schema.yaml
Then add data:
carry assert task title="Write docs" status=in-progress
carry assert task title="Ship v0.1" status=todo
Query:
carry query task
6. Update and Retract
Update an existing entity by passing this=:
carry assert task this=did:key:zTask1 status=done
Retract a field entirely:
carry retract task this=did:key:zTask1 status
7. Pipe Between Commands
Query output is valid input for assert and retract. This enables Unix-style composition:
# Copy data by piping query output back as assertions
carry query person --format triples | carry assert -
# Retract all matching data
carry query person name="Alice" --format triples | carry retract -
Next Steps
- Core Concepts – understand claims, entities, and domains
- Domain Modeling – define attributes, concepts, and rules
- CLI Reference – every command and flag
- Use Cases:
- Persistent Memory for AI Tools – shared context across Cursor, Claude, and others
- Personal Knowledge Management – contacts, research notes, reading lists
- Structured Data Modeling – lightweight local database for any structured data
Why Carry
The Problem
Structured data tends to end up in one of two bad places: a cloud service you don’t control, or ad-hoc files you can’t query.
Cloud services are convenient but fragile. Your data lives on someone else’s machine, under their terms, queryable only through their API. If they change the product or shut it down, your data is gone or locked in an export format. If the data is sensitive, you’ve handed it to a third party by design.
Ad-hoc files – markdown notes, CSV exports, JSON dumps, per-tool config files like .cursorrules or CLAUDE.md – give you local control but sacrifice structure. They’re hard to query across, drift out of sync, can’t be written to by the tools that read them, and don’t scale as the amount of data grows.
Neither option is good if you want data that is private, durable, structured, and accessible to the tools you use.
Carry’s Answer
Carry starts from a few beliefs:
Your data should live where you put it
Carry stores everything on your filesystem in a .carry/ directory. There’s no server, no account, no cloud dependency. You can back it up however you like, and delete it by removing a directory.
Sync is optional. If you want it, you choose the remote – your own bucket, a peer, or a Tonk relay.
One repository, every tool
Instead of maintaining parallel copies of your data in every tool’s proprietary format, Carry provides a single repository to which any tool can read and write. The same claims are available to a CLI script, an AI coding assistant, a custom agent, or anything else that can speak YAML.
Human-readable means machine-readable
Carry presents your data as YAML or JSON – asserted notation. It looks like this:
did:key:zAlice:
com.app.person:
name: Alice
age: 28
There’s no binary blob to decode, no proprietary format to reverse-engineer. If you can read YAML, you can read your data. Any tool that can read YAML can read your data too. The same format is used for query output and data input, so piping between commands works naturally:
carry query person --format triples | carry assert -
Structure should be earned, not imposed
Many databases force you to define a schema before you can write anything. Carry inverts this. You can start by asserting raw claims in any domain you like:
carry assert com.my.notes title="Meeting notes" date="2026-03-18"
Later, when patterns emerge, you can define attributes and concepts to give your data structure. Dialog DB interprets schemas at read time, not write time. This means your data model can evolve without migrations.
Attribution matters
Each repository has its own cryptographic identity, providing a foundation for knowing who contributed what. By using separate repositories for different tools, agents, or collaborators, you can keep contributions isolated.
Knowing the provenance of data matters whether the source is a person, a script, or an AI agent. Per-claim attribution (tracking who made each individual claim and when) is a planned feature.
What Carry Is Not
- Not a replacement for your tools. Carry doesn’t compete with Cursor, Claude, Obsidian, or any application you use. It gives them a shared, durable place to read and write structured data.
- Not an application layer. Carry is the store and the protocol to access it. What you build on top is up to you.
- Not mandatory cloud. Local-only is a first-class path.
- Not a high-throughput database. Carry works well for hundreds to thousands of entities. It’s not designed for large-scale analytics or concurrent multi-user writes (sync is still developing).
The Bigger Picture
Carry is built by Tonk. The long-term vision is a world where your data is truly yours, where tools interoperate on your terms, and where the structure of your information is something you define and control, not something imposed by a vendor.
Core Concepts
Carry organizes data using a small set of composable primitives. Understanding these will help you make sense of everything else in the documentation.
The Data Model at a Glance
Claim The atomic unit: "the X of Y is Z"
Entity Anything with an identity (a DID)
Relation What kind of association: domain/name
Value The data: text, numbers, booleans, references to other entities
Domain A namespace that groups related relations
Attribute A relation with type and cardinality constraints
Concept A reusable schema: a named group of attributes
Rule Logic that derives new concepts from existing data
Everything in Carry reduces to claims. Schemas, concepts, and rules are themselves stored as claims. The system is self-describing.
How the Pieces Fit Together
-
You assert claims – like “the name of Alice is Alice” – into a domain like
com.app.person. -
When patterns emerge, you define attributes that refine relations with types (
Text,UnsignedInteger, etc.) and cardinality (oneormany). -
You compose attributes into concepts – named schemas like “person” that group related attributes together.
-
You write rules that derive new concept instances from existing data, like “a safe meal is one where no attendee has an allergy to any ingredient.”
-
All of this lives in a
.carry/repository with its own cryptographic identity.
Each of these is explained in its own section:
Claims and Entities
Claims
A claim is the atomic unit of data in Carry. Every claim is a triple:
(the: relation, of: entity, is: value)
For example:
- the: com.app.person/name
of: did:key:zAlice
is: Alice
This says: “the com.app.person/name of the entity did:key:zAlice is Alice.”
When you retract a claim, it is removed from the indexes and no longer appears in query results. Each claim records a Cause link to its predecessor for basic provenance. A full temporal index preserving complete assertion/retraction history is planned but not yet implemented.
Claim Components
| Component | What it is | Example |
|---|---|---|
the | The relation – identifies the kind of association. Composed of domain/name. | com.app.person/name |
of | The entity this claim is about. Always a DID. | did:key:zAlice |
is | The value being associated. Can be a scalar or a reference to another entity. | Alice |
Entities
An entity is anything with an identity. In Carry, entities are identified by DIDs (Decentralized Identifiers) in did:key:z... format.
Entities are not defined explicitly – they come into existence when claims are asserted about them. An entity is simply the set of claims that reference it.
Entity Identity
When you create a new entity via carry assert, its DID is derived deterministically from its content:
- The field values are sorted and hashed with BLAKE3.
- The hash is used as an Ed25519 signing key seed.
- The public key is encoded as a
did:key:z....
This means that asserting the same data twice produces the same entity DID. Identity is content-derived, not randomly assigned.
# Both produce the same entity DID because the content is identical:
carry assert com.app.person name=Alice age=28
carry assert com.app.person name=Alice age=28
Explicit Entity Targeting
You can target an existing entity with this=:
# Update Alice's age
carry assert com.app.person this=did:key:zAlice age=29
Named Entities (Bookmarks)
Entities can be given human-readable names using the @name syntax:
carry assert attribute @person-name \
the=com.app.person/name as=Text cardinality=one
The @person-name asserts dialog.meta/name on the entity, creating a bookmark. You can then reference this entity by name in other commands:
carry assert concept @person with.name=person-name
Names are shared across the repository and travel with synced data.
Value Types
Claims support the following value types:
| Type | Description | Example |
|---|---|---|
Text | UTF-8 string | "Alice" |
UnsignedInteger | Non-negative integer | 28 |
SignedInteger | Signed integer | -5 |
Float | Floating-point number | 3.14 |
Boolean | True or false | true |
Symbol | A namespaced symbol | carry.profile/work |
Entity | Reference to another entity (a DID) | did:key:zAlice |
Bytes | Raw bytes | (binary data) |
When asserting values via the CLI, Carry auto-detects the type: DIDs are recognized as entity references, numbers as integers or floats, true/false as booleans, and everything else as strings.
Domains
A domain is a namespace for grouping related relations. Domains use reverse-DNS notation, similar to Java package names or Android app identifiers.
Naming Convention
com.myapp.person
diy.cook
xyz.tonk.carry
Each relation is qualified by its domain:
com.app.person/name -- the "name" relation in the "com.app.person" domain
com.app.person/age -- the "age" relation in the same domain
diy.cook/quantity -- the "quantity" relation in the "diy.cook" domain
Reserved Domains
Domains starting with dialog. are reserved for Dialog DB internals:
| Domain | Purpose |
|---|---|
dialog.attribute | Stores attribute identity fields (/id, /type, /cardinality) |
dialog.concept.with | Stores required concept membership by field name |
dialog.concept.maybe | Stores optional concept membership by field name |
dialog.meta | Universal metadata: /name and /description for any entity |
Do not assert claims into dialog.* domains directly. Carry manages these when you use carry assert attribute, carry assert concept, etc.
The xyz.tonk.carry domain is used by Carry itself for repository-level metadata like labels and settings.
Using Domains
In Commands
When a target in carry assert or carry query contains a ., it’s treated as a domain:
# Assert into a domain
carry assert com.app.person name=Alice age=28
# Query from a domain
carry query com.app.person name age
In YAML Files
Domains appear as the second level in asserted notation:
did:key:zAlice:
com.app.person:
name: Alice
age: 28
Choosing Domain Names
Pick a domain name that represents your use case or organization:
com.mycompany.project– for company or project datame.myname.notes– for personal datadiy.recipes– for hobby projects
The domain name itself has no special meaning to Carry beyond namespacing. Two claims with the same field name but different domains are completely independent.
Asserted Notation
Asserted notation is the canonical YAML format that Carry uses for command output and file-based input. It represents data as a three-level hierarchy that expands unambiguously to a set of raw claims.
Because carry query output and carry assert - input share the same format, you can pipe query output directly back as input without any transformation.
Three-Level Structure
Every entry in asserted notation follows:
<entity-identifier>:
<context>:
<field>: <value>
Level 1: Entity Identifier
The outermost key identifies the entity being described.
| Form | Meaning | Example |
|---|---|---|
Contains : | Global identifier (a DID or URI) | did:key:zAlice |
No : | Local bookmark name | quantity, person |
Level 2: Context
The second key declares how the fields beneath it should be interpreted.
| Form | Meaning | Example |
|---|---|---|
Contains . | Domain context – fields expand to domain/field relation identifiers | com.app.person |
No . | Concept context – fields are named attributes of that concept | attribute, concept, bookmark |
Level 3: Fields
Named values within the context.
- Scalar value: A direct association –
name: Alice,age: 28 - Non-scalar value (a nested YAML map): Implies a nested entity
Data Assertions (Domain Context)
Under a domain context, each field expands to a claim:
did:key:zAlice:
com.app.person:
name: Alice
age: 28
Expands to:
- the: com.app.person/name
of: did:key:zAlice
is: Alice
- the: com.app.person/age
of: did:key:zAlice
is: 28
Schema Definitions (Concept Context)
Under a concept context, the pre-registered concept schema determines how fields are interpreted. See Attributes and Concepts for details.
person-name:
attribute:
description: The person's name
the: com.app.person/name
as: Text
cardinality: one
Anonymous Entities
Use _ as the entity identifier when you don’t care about the identity:
_:
diy.cook:
quantity: 2
ingredient: carrot
Each _ creates a fresh entity. If you need to reference the same anonymous entity in multiple places within one document, use a named variable like ?foo:
?meal:
diy.planner:
attendee: ?person
recipe: ?recipe
All occurrences of ?foo in the same document bind to the same generated entity.
Nested Entities
A non-scalar value under a domain context implies a nested entity:
did:key:zAlice:
com.app.person:
name: Alice
address:
city: San Francisco
zip: 94107
Expands to:
- the: com.app.person/name
of: did:key:zAlice
is: Alice
- the: com.app.person/address
of: did:key:zAlice
is: <address-entity>
- the: com.app.address/city
of: <address-entity>
is: San Francisco
- the: com.app.address/zip
of: <address-entity>
is: 94107
The nested entity’s domain is derived from the parent domain with the field name appended as a segment.
EAV Triple Format
For piping between commands, Carry also supports a flat triple format via --format triples:
- the: com.app.person/name
of: did:key:zAlice
is: Alice
- the: com.app.person/age
of: did:key:zAlice
is: 28
This format is accepted by both carry assert - and carry retract -.
JSON
JSON is supported as a structural equivalent to YAML. The same three-level hierarchy applies:
{
"did:key:zAlice": {
"com.app.person": {
"name": "Alice",
"age": 28
}
}
}
EAV triples in JSON:
[
{"the": "com.app.person/name", "of": "did:key:zAlice", "is": "Alice"},
{"the": "com.app.person/age", "of": "did:key:zAlice", "is": 28}
]
Round-Trip Property
Because query output is asserted notation and assert accepts it, the following always works:
carry query person name="Alice" | carry assert -
carry query person --format triples | carry retract -
This makes Carry composable in the Unix tradition: commands produce output that other commands can consume.
Attributes
An attribute is a relation elevated with type and cardinality constraints. Where a raw claim like com.app.person/name just associates a value with an entity, an attribute says: “this relation accepts Text values and each entity has exactly one.”
Attributes are the building blocks of concepts.
Defining Attributes
Via the CLI
carry assert attribute @person-name \
the=com.app.person/name \
as=Text \
cardinality=one \
description="Name of a person"
The @person-name creates a bookmark so you can reference this attribute by name later.
Via YAML
person-name:
attribute:
description: Name of a person
the: com.app.person/name
as: Text
cardinality: one
The top-level key (person-name) becomes the bookmark name.
Attribute Fields
| Field | Required | Description |
|---|---|---|
the | Yes | The relation identifier – domain/name format |
as | Yes | The value type (see below) |
cardinality | No | one (default) or many |
description | Yes | Human-readable description |
Value Types
| Type | Description | Example Values |
|---|---|---|
Text | UTF-8 string | "Alice", "hello world" |
UnsignedInteger | Non-negative integer | 0, 28, 1000 |
SignedInteger | Signed integer | -5, 0, 42 |
Float | Floating-point number | 3.14, -0.5 |
Boolean | True or false | true, false |
Symbol | A namespaced constant | carry.profile/work |
Entity | Reference to another entity | did:key:z... |
Bytes | Raw binary data | (binary) |
You can also specify an enumeration of allowed symbols:
task-status:
attribute:
description: Current status of a task
the: com.app.task/status
as: [":todo", ":in-progress", ":done"]
cardinality: one
Cardinality
one– Each entity has at most one value for this attribute. Asserting a new value replaces the old one.many– Each entity can have multiple values. Asserting adds to the set; retracting a specific value removes it.
# A person has one name
person-name:
attribute:
description: Name
the: com.app.person/name
as: Text
cardinality: one
# A recipe can have many ingredients
recipe-ingredient:
attribute:
description: An ingredient in the recipe
the: diy.cook/ingredient
as: Entity
cardinality: many
Attribute Identity
Two attributes with the same relation identifier (the) but different type or cardinality are distinct entities. The attribute’s DID is derived from the hash of (relation_id, type, cardinality). The description does not affect identity.
This means you can have, for example, both a Text version and an Entity version of the same relation, and they will be treated as different attributes.
Querying Attributes
# List all defined attributes
carry query attribute
# Find a specific attribute by name
carry query attribute the=com.app.person/name
Concepts
A concept is a composition of attributes that describes the shape of a thing. Think of it as a lightweight, named schema – like a type or class, but realized through schema-on-read rather than schema-on-write.
Concepts are the primary unit of domain modeling in Carry.
Defining Concepts
Via the CLI
First define the attributes, then compose them into a concept:
# Define attributes
carry assert attribute @person-name \
the=com.app.person/name as=Text cardinality=one \
description="Name of a person"
carry assert attribute @person-age \
the=com.app.person/age as=UnsignedInteger cardinality=one \
description="Age of a person"
# Compose into a concept
carry assert concept @person \
description="A person" \
with.name=person-name \
with.age=person-age
The with.name=person-name means: “the field called name in this concept uses the person-name attribute.” The left side of = is the field name; the right side is the attribute bookmark.
Via YAML (Separate Definitions)
person-name:
attribute:
description: Name of a person
the: com.app.person/name
as: Text
cardinality: one
person-age:
attribute:
description: Age of a person
the: com.app.person/age
as: UnsignedInteger
cardinality: one
person:
concept:
description: A person
with:
name: person-name
age: person-age
Via YAML (Inline Attributes)
For convenience, you can define attributes inline within a concept:
person:
concept:
description: A person
with:
name:
description: Name of a person
the: com.app.person/name
as: Text
cardinality: one
age:
description: Age of a person
the: com.app.person/age
as: UnsignedInteger
cardinality: one
Both forms produce identical claims.
Required vs. Optional Fields
Concepts distinguish between required fields (with) and optional fields (maybe):
task:
concept:
description: A task to be completed
with:
title:
description: Title of the task
the: com.app.task/title
as: Text
status:
description: Current status
the: com.app.task/status
as: [":todo", ":in-progress", ":done"]
maybe:
priority:
description: Priority level
the: com.app.task/priority
as: [":low", ":medium", ":high"]
An entity matches a concept if all with fields are present, regardless of which maybe fields exist. Optional fields are included in query output when present but don’t affect concept membership.
Concept Identity
A concept’s identity is derived from its complete set of required fields – the (field_name, attribute_entity) pairs. Both the field names and the attributes they point to participate in identity:
- A concept with a field named
namepointing at attributeAis distinct from one with a field namedfullnamepointing at the same attributeA. - Optional fields (
maybe) do not participate in concept identity.
Using Concepts
Assert Data Against a Concept
carry assert person name=Alice age=28
Fields are validated against the concept’s schema. If a required field is missing or a value doesn’t match the expected type, the assertion is rejected.
Query by Concept
carry query person
carry query person name="Alice"
Concept queries return all fields defined by the concept (both with and maybe), unlike domain queries where you must explicitly request each field.
Concept Queries vs. Domain Queries
| Domain Query | Concept Query | |
|---|---|---|
| Target | com.app.person (contains .) | person (no .) |
| Fields returned | Only those you request | All fields the concept defines |
| Schema validation | None | Validated against concept |
| Requires schema | No | Yes (concept must be defined) |
Referencing Attributes
In the CLI, with.<field>=<value> can reference attributes in two ways:
- By bookmark name:
with.name=person-name– looks up the attribute by itsdialog.meta/name. - By selector:
with.name=com.app.person/name– looks up (or auto-creates) the attribute by its relation identifier. If the value contains/, it’s treated as a selector.
Querying Concept Definitions
# List all defined concepts
carry query concept
# Show a specific concept
carry query concept description
Rules
A rule derives new concept instances from existing data. Rules are Carry’s mechanism for logic and inference – they let you express constraints, joins, and derived views without writing application code.
Rules are evaluated at query time by the semantic layer, not stored as materialized views. They are declarative: you describe what should be true, and Dialog figures out how to compute it.
Structure of a Rule
A rule has three parts:
| Part | Required | Description |
|---|---|---|
deduce | Yes | The concept being derived – what the rule produces |
when | Yes | Positive premises – conditions that must hold |
unless | No | Negative premises – conditions that must NOT hold |
Example: Finding Allergy Conflicts
Given a cooking domain with recipes and ingredients, and a health domain with allergies, you can write a rule that finds conflicts:
diy.planner:
find-allergy-conflicts:
description: Find conflicts between recipe ingredients and allergies
deduce:
AllergyConflict:
person: ?person
recipe: ?recipe
when:
- diy.cook/Recipe:
this: ?recipe
ingredient: ?ingredient
- diy.cook/Ingredient:
this: ?ingredient
name: ?substance
- diy.health/Allergy:
this: ?this
person: ?person
substance: ?substance
unless: []
This rule says: “An AllergyConflict exists when a recipe contains an ingredient whose name matches a substance someone is allergic to.” The ?variables unify across premises – ?substance must be the same value in both the Ingredient and Allergy matches.
Example: Safe Meals
Building on the allergy conflict rule, you can define meals that respect dietary restrictions:
diy.planner:
respect-dietary-restrictions:
description: A meal that respects dietary restrictions
deduce:
Meal:
attendee: ?person
recipe: ?recipe
occasion: ?occasion
when:
- diy.planner/Meal:
this: ?this
attendee: ?person
recipe: ?recipe
occasion: ?occasion
unless:
- diy.planner/AllergyConflict:
person: ?person
recipe: ?recipe
This rule says: “A meal is safe if it’s a planned meal AND there is no allergy conflict between the attendee and the recipe.” The unless clause acts as negation.
Variables
Variables in rules start with ? and unify by name across all when and unless clauses:
| Variable | Meaning |
|---|---|
?this | Binds to the entity being matched or derived |
?person | User-defined variable; unifies across premises |
?recipe | User-defined variable; unifies across premises |
All occurrences of the same variable name must bind to the same value for the rule to fire.
Cross-Domain Rules
Rules can join data across multiple domains:
user.rules:
plan-event-meal:
description: Suggest a meal for an event attendee using an available recipe
deduce:
diy.planner/Meal:
attendee: ?person
recipe: ?recipe
occasion: ?event
when:
- diy.planner/Event:
this: ?event
title: ?title
- diy.planner/Meal:
this: ?this
attendee: ?person
- diy.cook/Recipe:
this: ?recipe
title: ?recipe-name
This rule joins across the diy.planner and diy.cook domains to match events with meals and recipes.
Rules vs. Queries
Rules and queries are complementary:
- Queries ask “what data exists that matches this shape?”
- Rules define “given data of shape A, derive data of shape B.”
Rules extend the space of queryable data without materializing it. When you query a concept that has rules, the rules fire and produce derived results alongside concrete data.
Deductive vs. Inductive Rules
The rules described here are deductive: they interpret what is already in the database. Dialog also has a concept of inductive rules (inspired by Dedalus) that react to changes over time, producing new claims that get asserted back.
Note
Inductive rules are prototyped but not yet fully implemented in Carry.
Modeling by Example
This chapter walks through complete domain models from simple to complex, showing how attributes, concepts, and rules compose in practice.
Task Tracker (Minimal)
The simplest useful model – a task with a title, status, and optional priority:
app:
Task:
description: A task to be completed
with:
title:
description: Title of the task
as: Text
status:
description: Current status
as: [":todo", ":in-progress", ":done"]
maybe:
priority:
description: Priority level
as: [":low", ":medium", ":high"]
Usage:
carry assert app/schema.yaml
carry assert task title="Write documentation" status=todo priority=high
carry assert task title="Review PR" status=in-progress
carry query task
carry query task status=todo
Note the use of enumerated symbols (:todo, :in-progress, :done) to constrain allowed values.
Recipe Book (Multi-Concept)
A cooking domain with recipes, ingredients, and steps. This demonstrates cross-concept references and cardinality: many:
diy.cook:
Recipe:
description: Meal recipe
with:
title:
description: The name of this recipe
as: Text
ingredient:
description: Ingredients of the recipe
cardinality: many
steps:
description: Steps of the cooking process
cardinality: many
as: .RecipeStep
Ingredient:
description: The meal ingredient
with:
name:
description: Name of this ingredient
as: Text
quantity:
description: Quantity of the ingredient
as: Integer
unit:
description: The unit of measurement
as: [":tsp", ":mls"]
RecipeStep:
description: The cooking step
with:
instruction:
description: Instructions for this step
as: Text
maybe:
after:
description: Step to perform this after
as: .RecipeStep
Key patterns:
.RecipeStepreferences another concept within the same domain (the leading.means “relative to this domain”).cardinality: manyoningredientandstepsmeans a recipe can have multiple of each.- Self-reference:
RecipeStep.afterreferences anotherRecipeStep, enabling ordered sequences.
Meal Planner (Rules and Cross-Domain Joins)
A more complex model that spans two domains and uses rules for inference:
diy.health:
Allergy:
description: The allergy a person has
with:
person:
description: Person having an allergy
substance:
description: Substance the person is allergic to
as: Text
diy.planner:
Event:
description: Event being planned
with:
title:
description: Title of the event
as: Text
time:
description: Time of the event
as: Text
Meal:
description: The plan for the meal
with:
attendee:
description: The meal attendee
recipe:
description: The meal recipe
occasion:
description: The occasion for the meal
AllergyConflict:
description: A conflict between a recipe ingredient and an allergy
with:
person:
description: The person with the allergy
recipe:
description: The recipe with the allergenic ingredient
find-allergy-conflicts:
description: Find conflicts between recipe ingredients and allergies
deduce:
AllergyConflict:
person: ?person
recipe: ?recipe
when:
- diy.cook/Recipe:
this: ?recipe
ingredient: ?ingredient
- diy.cook/Ingredient:
this: ?ingredient
name: ?substance
- diy.health/Allergy:
this: ?this
person: ?person
substance: ?substance
respect-dietary-restrictions:
description: A meal that respects dietary restrictions
deduce:
Meal:
attendee: ?person
recipe: ?recipe
occasion: ?occasion
when:
- diy.planner/Meal:
this: ?this
attendee: ?person
recipe: ?recipe
occasion: ?occasion
unless:
- diy.planner/AllergyConflict:
person: ?person
recipe: ?recipe
This model demonstrates:
- Cross-domain references: The allergy conflict rule joins data from
diy.cook(recipes and ingredients) withdiy.health(allergies). - Derived concepts:
AllergyConflictdoesn’t store data directly – it’s derived by a rule from existing data. - Negation: The
respect-dietary-restrictionsrule usesunlessto exclude meals where an allergy conflict exists. - Variable unification:
?substancein thefind-allergy-conflictsrule must match in both the ingredient name and the allergy substance.
User Profile (Real-World EAV Data)
For highly structured, multi-faceted data like a user profile, you can use EAV triples directly. Here’s a condensed example from the carry.profile domain:
# Person
- the: carry.profile/name
of: keri-vasquez
is: "Keri Vasquez"
- the: carry.profile/role_title
of: keri-vasquez
is: "Independent consultant"
- the: carry.profile/location
of: keri-vasquez
is: "Amsterdam, NL"
# Expertise (cardinality: many through multiple entities)
- the: carry.profile/person
of: keri-exp-knowledge-mgmt
is: keri-vasquez
- the: carry.profile/topic
of: keri-exp-knowledge-mgmt
is: "Knowledge management theory"
- the: carry.profile/expertise_level
of: keri-exp-knowledge-mgmt
is: carry.profile/deep
# Communication preferences
- the: carry.profile/person
of: keri-comm-conclusion-first
is: keri-vasquez
- the: carry.profile/description
of: keri-comm-conclusion-first
is: "Lead with the conclusion, then the reasoning"
- the: carry.profile/direction
of: keri-comm-conclusion-first
is: carry.profile/inbound
This pattern uses satellite entities (like keri-exp-knowledge-mgmt) linked back to a central person entity. Each satellite carries its own fields, enabling rich, queryable structures without nested objects.
Tips for Modeling
-
Start with raw domain assertions. Don’t design a schema upfront. Assert data as you have it and let patterns emerge.
-
Use domains for namespacing, concepts for structure. Domains prevent name collisions. Concepts give you validation and named queries.
-
Prefer
cardinality: manyfor lists. Instead of comma-separated values in a text field, use a many-valued attribute. This makes each item independently queryable. -
Use entity references for relationships. Instead of embedding data, reference other entities. This keeps your model normalized and composable.
-
Rules are views, not triggers. Rules derive data at query time. They don’t materialize new claims into storage. Use them for joins, constraints, and derived aggregates.
CLI Reference
Carry provides a small set of commands for interacting with Dialog DB. Every command follows a consistent pattern and shares global options.
Commands
| Command | Alias | Description |
|---|---|---|
carry init | i | Create a new repository |
carry assert | a | Assert claims (add or update data) |
carry query | q | Query entities by domain or concept |
carry retract | r | Retract claims (remove data) |
carry status | st | Show repository info |
Global Options
Every command accepts:
| Flag | Description |
|---|---|
--repo <PATH> | Path to a specific .carry/ repository. Skips filesystem walk. |
--format <FORMAT> | Output format: yaml (default), json, or triples. |
Repo Resolution
When --repo is omitted, Carry walks up the filesystem tree from $PWD toward $HOME, looking for a .carry/ directory. The first one found is used. You can also set the CARRY_REPO environment variable.
Output Formats
| Format | Description | Best for |
|---|---|---|
yaml | Asserted notation (default) | Human reading, file round-trips |
json | Array of objects with id field | Programmatic consumption |
triples | Flat EAV YAML (the/of/is) | Piping between carry commands |
The preferred format can also be persisted as a setting:
carry assert xyz.tonk.carry output-format=json
Command-line --format always takes precedence over the persisted preference.
Target Syntax
Several commands accept a <TARGET> argument. The syntax is:
| Pattern | Interpretation | Example |
|---|---|---|
Contains . | Domain target | com.app.person |
No . | Concept target (resolved by bookmark name) | person |
- | Read from stdin | - |
Contains / or ends in .yaml/.yml/.json | File path | schema.yaml |
Field Syntax
Commands that accept fields use the format FIELD[=VALUE]:
| Syntax | Meaning |
|---|---|
name | Projection: include this field in output |
name="Alice" | Filter: only match entities where name is Alice |
this=did:key:z... | Target a specific entity |
@myname | Assert dialog.meta/name on the entity (bookmark) |
Value Auto-Detection
When asserting values via the CLI, Carry auto-detects the type:
| Input | Detected Type |
|---|---|
did:key:z... | Entity reference |
123 | Unsigned integer |
-5 | Signed integer |
3.14 | Float |
true / false | Boolean |
| Anything else | Text (string) |
carry init
Create a new Dialog DB repository.
Synopsis
carry init [LABEL] [--repo <PATH>]
Description
Creates a .carry/ directory. If --repo is not specified, the repository is created in the current working directory.
The command:
- Generates an Ed25519 keypair for the repository.
- Creates
.carry/<did>/with acredentialsfile andclaims/directory. - Bootstraps the builtin concepts (
attribute,concept,bookmark) so they can be used immediately. - If
LABELis provided, asserts it as the repository label.
If a .carry/ directory already exists at the target location, the command reports its status.
Arguments
| Argument | Description |
|---|---|
LABEL | Optional label for the repository (e.g., “my-project”) |
Options
| Flag | Description |
|---|---|
--repo <PATH> | Directory where .carry/ should be created. Defaults to $PWD. |
Examples
# Initialize in current directory
carry init
# Initialize with a label
carry init my-project
# Initialize in a specific directory
carry init --repo /path/to/project
# Initialize with label in specific directory
carry init my-project --repo /path/to/project
Output
Initialized my-project repository in /path/to/.carry/did:key:zAbc123
Notes
- Running
carry initinside a directory that is already within an existing repository creates a nested repository. Carry does not detect or warn about nesting. - The DID (e.g.,
did:key:zAbc123) is derived from the generated public key and is globally unique. - The private key at
.carry/<did>/credentialsis stored with mode0600(owner read/write only).
carry assert
Assert claims on entities – add or update data.
Synopsis
carry assert <TARGET|FILE|-> [this=<ENTITY>] [@name] [FIELD=VALUE ...] [--repo <PATH>] [--format <FMT>]
Description
Assert creates or updates claims. Claims are stored as (the: relation, of: entity, is: value).
Input Modes
| Mode | Syntax | Description |
|---|---|---|
| Target | carry assert <domain-or-concept> field=value ... | Assert from CLI arguments |
| File | carry assert <file.yaml> | Assert from a YAML or JSON file |
| Stdin | carry assert - | Assert from standard input |
Target Detection
-is always stdin- Contains
/or ends in.yaml,.yml,.json– file path - Contains
.– domain target - Otherwise – concept target (resolved by bookmark name)
Arguments
| Argument | Description |
|---|---|
TARGET | Domain (e.g., com.app.person) or concept name (e.g., person) |
FILE | Path to a YAML or JSON file |
- | Read from stdin |
Fields
| Syntax | Description |
|---|---|
field=value | Assert this field with this value |
this=<DID> | Target an existing entity instead of creating a new one |
@name | Assert dialog.meta/name on the entity (creates a bookmark) |
with.field=attr | (Concept assertions) Required field referencing an attribute |
maybe.field=attr | (Concept assertions) Optional field referencing an attribute |
Options
| Flag | Description |
|---|---|
--repo <PATH> | Path to .carry/ repository |
--format <FMT> | Output format: yaml, json, or triples |
Examples
Domain Assertions
# Create a new entity (DID printed to stdout)
carry assert com.app.person name=Alice age=28
# Update an existing entity
carry assert com.app.person this=did:key:zAlice age=29
Concept Assertions
# Assert using a defined concept
carry assert person name=Alice age=28
Builtin Concepts
# Define an attribute with a bookmark name
carry assert attribute @person-name \
the=com.app.person/name as=Text cardinality=one \
description="Name of a person"
# Define a concept
carry assert concept @person \
description="A person" \
with.name=person-name \
with.age=person-age
# Create a bookmark
carry assert bookmark this=did:key:zEntity name=my-entity
File and Stdin
# Assert from a YAML file
carry assert schema.yaml
# Assert from stdin
carry query person --format triples | carry assert -
# Assert from stdin (asserted notation also works)
carry query person | carry assert -
File Formats
Assert accepts two YAML formats (auto-detected):
Asserted notation (from default --format yaml):
did:key:zAlice:
com.app.person:
name: Alice
age: 28
EAV triples (from --format triples):
- the: com.app.person/name
of: did:key:zAlice
is: Alice
- the: com.app.person/age
of: did:key:zAlice
is: 28
JSON EAV triples are also accepted:
[{"the": "com.app.person/name", "of": "did:key:zAlice", "is": "Alice"}]
Output
When creating a new entity (no this=), the generated entity DID is printed to stdout:
did:key:zNewEntity123
Notes
- Without
this=, a new entity is created with a deterministic DID derived from the content. - With
this=, at least one field is required. - The
@namesyntax is shorthand for assertingdialog.meta/nameon the entity. - For concept assertions, if a
with/maybevalue contains/, it’s treated as an attribute selector (the attribute is looked up or auto-created). Without/, it’s treated as an attribute bookmark name. - Cardinality defaults to
onefor attribute assertions if not specified.
carry query
Query entities by domain or concept.
Synopsis
carry query <TARGET> [FIELD[=VALUE] ...] [--repo <PATH>] [--format <FMT>]
Description
Query returns matching entities in asserted notation. The target determines the kind of query:
- Domain query (target contains
.): Searches for entities with claims in that domain. You choose which fields to include in output. - Concept query (target has no
.): Resolves the named concept via bookmark, returns all fields the concept defines.
Arguments
| Argument | Description |
|---|---|
TARGET | Domain (e.g., com.app.person) or concept name (e.g., person) |
Fields
| Syntax | Description |
|---|---|
name | Projection – include this field in output |
name="Alice" | Filter – only return entities where name matches this value |
Filter fields narrow results. Projection fields expand what’s shown. For concept queries, all concept fields are always included in output; specify fields only to filter.
Options
| Flag | Description |
|---|---|
--repo <PATH> | Path to .carry/ repository |
--format <FMT> | Output format: yaml (default), json, or triples |
Examples
Domain Queries
# Get name and age for all entities in the domain
carry query com.app.person name age
# Filter: only entities where name is Alice
carry query com.app.person name="Alice" age
Concept Queries
# Get all fields of the 'person' concept
carry query person
# Filter by field value
carry query person name="Alice"
Piping
# Pipe to assert (copy data)
carry query person --format triples | carry assert -
# Pipe to retract (remove matching data)
carry query person name="Alice" --format triples | carry retract -
# Asserted notation also pipes correctly
carry query com.app.person name age | carry assert -
Querying Schema
# List all defined attributes
carry query attribute
# List all defined concepts
carry query concept
Output Formats
YAML (default)
did:key:zAlice:
com.app.person:
name: Alice
age: 28
did:key:zBob:
com.app.person:
name: Bob
age: 35
JSON (--format json)
[{"id": "did:key:zAlice", "name": "Alice", "age": 28}]
Triples (--format triples)
- the: com.app.person/name
of: did:key:zAlice
is: Alice
- the: com.app.person/age
of: did:key:zAlice
is: 28
Notes
- Domain queries require at least one field to be specified (projection or filter).
- Concept queries with no fields return all entities matching the concept with all of the concept’s fields.
carry retract
Retract claims from entities – remove data.
Synopsis
carry retract <TARGET|FILE|-> [this=<ENTITY>] [FIELD[=VALUE] ...] [--repo <PATH>] [--format <FMT>]
Description
Retract removes claims from the indexes. Retracted claims no longer appear in query results.
Input Modes
| Mode | Syntax | Description |
|---|---|---|
| Target | carry retract <domain-or-concept> this=<entity> field ... | Retract from CLI arguments |
| File | carry retract <file.yaml> | Retract from a YAML or JSON file |
| Stdin | carry retract - | Retract from standard input |
Arguments
| Argument | Description |
|---|---|
TARGET | Domain (e.g., com.app.person) or concept name (e.g., person) |
FILE | Path to a YAML or JSON file |
- | Read from stdin |
Fields
| Syntax | Description |
|---|---|
field | Retract this field regardless of its current value |
field=value | Retract only if the claim matches this exact value |
this=<DID> | The entity to retract from (required for target mode) |
Using field=value is useful for cardinality: many attributes where an entity has multiple values and you only want to remove one.
Options
| Flag | Description |
|---|---|
--repo <PATH> | Path to .carry/ repository |
--format <FMT> | Output format |
Examples
# Retract a field (any value)
carry retract person this=did:key:zAlice age
# Retract a specific value (for multi-valued fields)
carry retract person this=did:key:zAlice tag=urgent
# Retract using a domain
carry retract com.app.person this=did:key:zAlice name age
# Retract all claims on an entity
carry retract com.app.person this=did:key:zAlice
# Retract from a file
carry retract retractions.yaml
# Retract from stdin (pipe query output to remove matching data)
carry query person name="Alice" --format triples | carry retract -
Notes
- Unlike
assert, target-moderetractrequiresthis=to identify which entity to modify. - Other claims on the entity are unaffected – only the specified fields are retracted.
- Retracted claims are removed from the indexes and no longer appear in query results.
- If no fields are specified with
this=, all claims on that entity are retracted.
carry status
Display information about the current repository.
Synopsis
carry status [--repo <PATH>] [--format <FMT>]
Description
Shows the resolved .carry/ repository path and DID.
Options
| Flag | Description |
|---|---|
--repo <PATH> | Path to .carry/ repository |
--format <FMT> | Output format: yaml (default) or json |
Examples
# Show status
carry status
# Show status as JSON
carry status --format json
Output
Repo: /path/to/project/.carry
DID: did:key:zAbc123
With --format json:
{
"repo": "/path/to/project/.carry",
"did": "did:key:zAbc123"
}
Persistent Memory for AI Tools
The flagship use case for Carry: a shared, private memory layer that multiple AI tools can read from and write to, eliminating session amnesia and cross-tool silos.
The Problem
If you use more than one AI tool – Cursor, Claude Code, ChatGPT, Copilot – you’ve experienced this:
- You explain your project conventions to Claude. Next session, it’s forgotten.
- You set up rules in Cursor via
.cursorrules. Claude doesn’t know about them. - You build a useful pattern in ChatGPT. There’s no way to share it with your coding tools.
- You copy-paste context between tools manually. It’s fragile and tedious.
Each tool has its own memory silo. The workarounds – markdown files, per-tool configs, copy-paste – don’t scale.
How Carry Solves This
Carry provides a single .carry/ repository that any tool can access:
Your AI Tools Carry Repository
(.carry/)
Cursor ---read/write--->
Claude ---read/write---> Shared Memory
ChatGPT ---read/write---> (Dialog DB)
Ollama ---read/write--->
Step 1: Initialize
carry init my-context
Step 2: Add Your Context
Migrate existing context from tool-specific files:
# Assert your project conventions
carry assert com.me.conventions \
language=TypeScript \
style="functional, no classes" \
testing="vitest, co-locate tests"
# Assert your preferences
carry assert com.me.preferences \
tone="direct, no preamble" \
format="structured markdown" \
english_variant=british
Or assert from a YAML file for more complex context:
# context.yaml
_:
com.me.project:
name: "My App"
description: "A local-first task manager"
stack: "Rust + TypeScript + Leptos"
conventions: "Prefer composition over inheritance"
_:
com.me.rules:
rule: "Always write tests for new functions"
rule: "Use Result types, not exceptions"
rule: "Document public APIs with examples"
carry assert context.yaml
Step 3: Connect Your Tools
Carry can be exposed to any agentic AI tool with shell permissions and access to the Carry CLI.
Through agentic calls to Carry, your tools share the same context. Cursor knows your conventions. Claude knows your project structure. A new chat session can pick up with all the context built in the last one.
Step 4: Let Tools Write Back
When an AI tool discovers something useful – a pattern, a decision, a convention that emerged during a coding session – it can write that back to Carry:
carry assert com.app.decisions \
decision="Use SQLite for local storage" \
date="2026-03-15" \
reason="Simpler than PostgreSQL for single-user local-first"
These decisions are then available to every tool, creating a growing, shared knowledge base.
What Makes This Different
vs. .cursorrules / CLAUDE.md
These are static files that one tool reads. They can’t be written to by the tool, can’t be shared across tools, and have no structure or query capability.
Carry’s data is structured, queryable, writable by any connected tool, and can evolve over time without manual maintenance.
vs. Cloud Memory (Mem0, OpenAI Memory, Zep)
Cloud services lock your data in a vendor. You don’t control where it lives, who can access it, or how it’s used.
Carry stores everything locally. No account, no API keys for the storage layer, no data leaving your machine unless you explicitly sync.
vs. Copy-Paste / Manual Memory
Manual approaches don’t scale. They break when you forget, when the format changes, or when you switch tools.
Carry is persistent and structured. Once data is asserted, it stays until you retract it. The format is stable and machine-readable.
Separation: Who Wrote What?
Per-claim provenance tracking (recording who asserted each individual claim and when) is a planned feature. In the meantime, you can use separate repositories for different tools or agents, keeping contributions isolated.
Example: Developer Profile
A practical example of persistent AI context – your developer profile:
- the: carry.profile/name
of: dev-profile
is: "Jane Developer"
- the: carry.profile/preferred_language
of: dev-profile
is: "Rust"
- the: carry.profile/style
of: dev-profile
is: "Functional, minimal dependencies, explicit error handling"
- the: carry.profile/testing_preference
of: dev-profile
is: "Property-based tests where possible, integration tests for IO boundaries"
- the: carry.profile/communication_style
of: dev-profile
is: "Direct, conclusion first, flag uncertainty explicitly"
Every AI tool that reads from this repository knows your preferences from the first message.
Personal Knowledge Management
Carry can serve as a structured personal knowledge base – a place to organize notes, expertise, contacts, and project information in a format that’s both human-readable and machine-queryable.
Why Not Just Use Markdown?
Markdown files work well for short, declarative content (rules, patterns, quick notes). But they fall short when:
- You want to query across your knowledge: “Which contacts work in data engineering?”
- You need structure that’s consistent: Every person should have name, role, and context.
- You want your AI tools to reason over your data, not just read it.
- Your knowledge base grows beyond what fits in a single context window.
Carry’s fact-based model lets you store structured data that’s both inspectable in a text editor and queryable via the CLI.
Example: Personal CRM
Define the Schema
contacts:
Person:
description: A person in my network
with:
name:
description: Full name
as: Text
role:
description: Their role or title
as: Text
maybe:
company:
description: Where they work
as: Text
context:
description: How I know them
as: Text
last_contact:
description: When we last spoke
as: Text
notes:
description: Freeform notes
as: Text
Add Data
carry assert schema.yaml
carry assert person name="Alex Good" role="Core maintainer, Automerge" \
context="Collaborated on WASM implementation" \
last_contact="2026-02"
carry assert person name="Peter Van Hardenburg" role="Researcher, Ink & Switch" \
context="Shared trail-runner project" \
notes="Interested in local-first tools for thought"
Query
# Find everyone I know at a company
carry query person company="Ink & Switch"
# List all contacts with their roles
carry query person name role
# Find people I haven't talked to recently
carry query person name last_contact
Example: Research Notes
Track research topics, findings, and open questions:
research:
Topic:
description: A research topic I'm exploring
with:
title:
description: Topic name
as: Text
status:
description: Current status
as: [":active", ":paused", ":completed"]
maybe:
summary:
description: Current understanding
as: Text
open_questions:
description: Unresolved questions
as: Text
cardinality: many
Finding:
description: A specific finding or insight
with:
topic:
description: Related topic
claim:
description: The finding
as: Text
confidence:
description: How confident I am
as: [":high", ":medium", ":low", ":speculative"]
maybe:
source:
description: Where this came from
as: Text
Usage:
carry assert schema.yaml
carry assert topic @crdt-sync \
title="CRDT Synchronization" \
status=active \
summary="Exploring efficient sync for local-first databases"
carry assert finding \
topic=crdt-sync \
claim="Prolly trees enable efficient diff-based sync" \
confidence=high \
source="Dialog DB implementation"
Example: Reading List
carry assert com.me.reading \
title="Designing Data-Intensive Applications" \
author="Martin Kleppmann" \
status=finished \
rating=5 \
takeaway="Replication and partitioning fundamentals"
carry assert com.me.reading \
title="A Philosophy of Software Design" \
author="John Ousterhout" \
status=in-progress
# What have I finished reading?
carry query com.me.reading title author status=finished
Advantages Over PKM Tools
| Obsidian/Notion/Roam | Carry | |
|---|---|---|
| Data format | Proprietary or semi-structured markdown | Structured YAML claims |
| Query language | Limited (Dataview, formulas) | Full EAV queries + rules |
| AI access | Plugin-dependent | Direct via CLI |
| Schema | Informal, drifts over time | Explicit, validated |
| Offline | Varies | Always (local-first) |
Carry doesn’t replace your PKM tool for long-form writing and linking. It complements it by providing a structured, queryable layer for the data that benefits from consistency and machine access.
Structured Data Modeling
Carry can serve as a lightweight, local database for structured data that doesn’t warrant standing up PostgreSQL or managing a cloud service. If your data has entities, relationships, and you want to query across them – Carry might be a good fit.
When to Use Carry for Data
Carry works well for:
- Highly structured data with relationships between entities
- Moderate data volumes (hundreds to thousands of entities)
- Data that benefits from semantic modeling – where the schema itself carries meaning
- Data you want to query from AI tools alongside your code
- Private or sensitive data that shouldn’t leave your machine
Carry is less suited for:
- Large datasets requiring high-throughput analytics (use a proper OLAP database)
- Short bits of declarative data (plain markdown files are simpler)
- Data that needs concurrent multi-user write access (sync is still developing)
Example: Lab Data Management
Research labs often struggle with inconsistent data entry across team members. Carry can provide lightweight schemas that enforce structure:
lab.samples:
Sample:
description: A collected sample
with:
id:
description: Sample identifier
as: Text
collector:
description: Who collected it
as: Text
date:
description: Collection date
as: Text
type:
description: Sample type
as: [":blood", ":tissue", ":soil", ":water"]
maybe:
location:
description: Collection location
as: Text
notes:
description: Collection notes
as: Text
ph:
description: pH measurement
as: Float
Measurement:
description: A measurement taken on a sample
with:
sample:
description: The sample measured
metric:
description: What was measured
as: Text
value:
description: The measurement value
as: Float
unit:
description: Unit of measurement
as: Text
maybe:
instrument:
description: Instrument used
as: Text
carry assert schema.yaml
carry assert sample @sample-001 \
id=S-001 collector="Dr. Chen" date="2026-03-15" \
type=water location="Station A" ph=7.2
carry assert measurement \
sample=sample-001 metric="dissolved oxygen" \
value=8.5 unit="mg/L" instrument="YSI ProDSS"
# Query all water samples
carry query sample id collector date type=water
# Find measurements for a specific sample
carry query measurement sample=sample-001
Example: Small Business Operations
A small business tracking customers, orders, and inventory:
biz:
Customer:
description: A customer
with:
name:
description: Customer name
as: Text
email:
description: Email address
as: Text
maybe:
phone:
description: Phone number
as: Text
since:
description: Customer since
as: Text
Product:
description: A product in inventory
with:
name:
description: Product name
as: Text
price:
description: Unit price
as: Float
stock:
description: Units in stock
as: UnsignedInteger
maybe:
category:
description: Product category
as: Text
Order:
description: A customer order
with:
customer:
description: The customer who placed the order
product:
description: The product ordered
quantity:
description: Number of units
as: UnsignedInteger
date:
description: Order date
as: Text
Cross-entity queries:
# All orders for a customer
carry query order customer=did:key:zCustomer1
# All products in a category
carry query product category="electronics"
Example: Analytics Engineering
For analytics engineers familiar with dbt-style transformations, Carry’s rules can model data transformations declaratively:
analytics:
RawEvent:
description: A raw analytics event
with:
event_type:
description: Type of event
as: Text
user_id:
description: User identifier
as: Text
timestamp:
description: Event timestamp
as: Text
maybe:
properties:
description: Event properties
as: Text
ActiveUser:
description: A user who has been active recently
with:
user_id:
description: User identifier
as: Text
last_event:
description: Most recent event type
as: Text
identify-active-users:
description: Derive active users from raw events
deduce:
ActiveUser:
user_id: ?user
last_event: ?event_type
when:
- analytics/RawEvent:
this: ?this
user_id: ?user
event_type: ?event_type
This approach gives you:
- Documented transformations: The rule description and concept descriptions serve as living documentation.
- Testable logic: Assert test data, run the rule, verify the output.
- Version-controlled models: Everything is YAML on disk.
Tips
-
Start with data, add schema later. Assert raw domain claims first. When you see patterns, formalize them into concepts.
-
Use separate repos for environments. Keep production data in one repository and test data in another by using
--repoto target different directories. -
Export with queries. Need CSV? Pipe JSON output through
jq:carry query sample --format json | jq -r '.[] | [.id, .collector, .date] | @csv' -
Rules are your transformation layer. Instead of writing scripts to join and transform data, express the logic as rules. They’re declarative, documented, and always up to date.
Dialog DB
Dialog is the embeddable database engine that powers Carry. Understanding Dialog helps explain why Carry works the way it does and what makes it different from other tools.
What is Dialog?
Dialog is a local-first, semantic database designed for software that works offline, syncs across devices, and keeps data under user control. It is developed as a separate project (dialog-db) and Carry is one application built on top of it.
Dialog has three layers:
Associative Layer (Memory)
The storage layer. All data is stored as claims indexed in three key orderings within a single prolly tree:
- EAV (Entity -> Attribute -> Value): “What are all the claims about this entity?”
- AVE (Attribute -> Value -> Entity): “Which entities have this attribute with this value?”
- VAE (Value -> Attribute -> Entity): “What references this entity?”
Retractions remove claims from these indexes. A Cause chain links each claim to its predecessor, providing single-link provenance.
Semantic Layer (Interpretation)
The interpretation layer. Operates at query time, reading schema primitives (attributes, concepts, rules) from the associative layer and using them to interpret data. This is where schema-on-read happens:
- Attributes add type and cardinality constraints to relations.
- Concepts compose attributes into named schemas.
- Rules derive new concept instances from existing data.
When you carry query person, the semantic layer reads the person concept definition from storage, matches it against existing claims, and assembles the results.
Reactive Layer (Behavior)
The behavioral layer. Where the semantic layer interprets data, the reactive layer responds to it. Processes would observe concepts and relations, and in response produce new claims. This is where effects, triggers, and inductive rules would live.
Note
The reactive layer is not yet implemented. Foundational data structures exist (Z-sets for database stream processing, an
Operatortrait) but there is no wiring to the query engine and no mechanism to trigger rule re-evaluation on data changes.
Key Properties
Schema-on-Read
Dialog doesn’t require you to define a schema before writing data. You can assert any claim at any time. Schemas (attributes, concepts) are themselves claims in the database – they’re interpreted at query time, not enforced at write time.
This means:
- You can evolve your schema without migrations.
- New kinds of data can be added without redesigning existing structures.
- Queries can interpret the same data through different schemas.
Retraction Semantics
When you retract a claim, it is removed from the indexes. Each claim records a Cause link to its predecessor, providing basic provenance tracking. A full temporal index with time-travel queries is planned but not yet implemented.
Content-Addressed Identity
Attributes and concepts derive their identity from the hash of their content. Two attributes with the same relation, type, and cardinality will have the same DID, regardless of when or where they were created. This gives you convergent identity without coordination.
Note: Dialog DB itself creates regular entities with random keypairs. Carry adds content-addressed entity identity on top – when you carry assert, the entity DID is derived from a BLAKE3 hash of the asserted fields. This is a Carry-level behavior, not a Dialog DB primitive.
Structural Sync
Dialog uses prolly trees for synchronization. When two replicas diverge and later sync, efficient structural diffs identify the changes and merge them automatically. Conflicts are resolved deterministically via hash-based tiebreaking, ensuring all replicas converge to the same state.
Anatomy of a Claim
A claim consists of:
the: <relation> -- domain/name identifying the kind of association
of: <entity> -- the entity this claim is about (a DID)
is: <value> -- the value being associated
In Carry’s YAML output:
- the: com.app.person/name
of: did:key:zAlice
is: Alice
Primitive Domains
Dialog reserves several domains for internal use:
| Domain | Purpose |
|---|---|
dialog.attribute | Attribute identity: /id, /type, /cardinality |
dialog.concept.with | Required concept fields |
dialog.concept.maybe | Optional concept fields |
dialog.meta | Universal metadata: /name, /description |
dialog.rule | Rule definitions: /deduce, /when, /unless, /where, /assert |
All domains starting with dialog. are reserved. User-defined domains must not use this prefix.
Further Reading
- Anatomy of Dialog – the authoritative design document
- Dialog DB on GitHub – the source code and architecture decision records
- Datalog – the query language family that inspired Dialog’s rule system
Architecture
This page describes how Carry is built and how the pieces fit together.
Repository Layout
A Carry repository lives in a .carry/ directory:
project/
.carry/
@active # Plain text: DID of the active space
did:key:zSpace1/
credentials # 32-byte Ed25519 secret key (mode 0600)
claims/ # Dialog DB storage (prolly trees)
did:key:zSpace2/
credentials
claims/
src/
...
Repo
A repo is any directory containing a .carry/ subdirectory. Carry discovers repos by walking up from $PWD toward $HOME. The --repo flag or CARRY_REPO environment variable can override this.
Space
A space is a subdirectory of .carry/ named by its did:key:z... DID. Each space contains:
- credentials: A 32-byte Ed25519 private key. The corresponding public key determines the space’s DID.
- claims/: Dialog DB’s on-disk storage using prolly trees.
The active space is tracked in .carry/@active as a plain text DID.
Cryptographic Identity
Every space has an Ed25519 keypair:
- Private key: Stored in
credentials. Used to sign claims and authenticate during sync. - Public key: Encoded as a
did:key:z...using the multicodec Ed25519 prefix + base58-btc encoding. This is the space’s identity.
Entity DIDs are generated from a BLAKE3 hash of the entity’s content, used as an Ed25519 seed:
content fields -> sort -> BLAKE3 hash -> Ed25519 signing key -> public key -> did:key:z...
This makes entity identity deterministic and content-addressed: the same data always produces the same DID.
Storage
Dialog DB stores data in prolly trees – a probabilistic data structure that enables efficient synchronization. Prolly trees are a variant of B-trees where split points are determined by content hashing rather than fixed sizes, making structural diffs between two trees efficient.
Claims are indexed in three operative indexes:
| Index | Lookup pattern | Use case |
|---|---|---|
| EAV | Entity -> Attribute -> Value | “What are all the claims about Alice?” |
| AVE | Attribute -> Value -> Entity | “Who has name = Alice?” |
| VAE | Value -> Attribute -> Entity | “What references this entity?” |
Each claim records a Cause link to its predecessor for basic provenance tracking. A full temporal index is planned but not yet implemented.
Data Flow
Assert
CLI input -> Parse target/fields -> Resolve concept (if applicable)
-> Generate entity DID -> Create claims -> Write to Dialog DB
- The target is parsed: domain (contains
.) or concept (no.). - For concepts, the concept definition is loaded and fields are validated.
- An entity DID is generated from the content (or
this=is used for an existing entity). - Claims are constructed and written to the prolly tree indexes.
Query
CLI input -> Parse target/fields -> Resolve concept (if applicable)
-> Build selector -> Scan indexes -> Filter -> Format output
- The target is resolved to a domain or concept.
- For concepts, the concept definition determines which attributes to query.
- An
ArtifactSelectoris built with appropriate filters. - The EAV and AVE indexes are scanned.
- Results are filtered by any specified field values.
- Output is formatted as YAML, JSON, or triples.
Retract
CLI input -> Parse target/fields -> Find matching claims -> Retract from operative indexes
Retractions remove claims from the indexes (EAV/AVE/VAE). The original claim data still exists and can be queried for specifically, but won’t show up in standard queries.
Dependencies
| Component | Crate | Purpose |
|---|---|---|
| CLI framework | clap | Command parsing and help generation |
| Dialog DB | dialog-query, dialog-artifacts | Database engine |
| Tonk | tonk-space | Space management and filesystem backend |
| Crypto | ed25519-dalek, blake3, bs58 | Key generation, hashing, encoding |
| Async runtime | tokio | Async I/O |
| Serialization | serde_yaml, serde_json | YAML and JSON parsing/formatting |
Platform Support
Carry compiles for native targets (macOS, Linux). The crate is gated with #[cfg(not(target_arch = "wasm32"))] – it compiles to an empty main() on wasm32 targets since the CLI requires filesystem access.
Future: Sync
Carry’s sync capabilities are being developed. The planned architecture:
- Push/Pull:
carry pushandcarry pullsynchronize with a configured upstream remote. - Invite/Join:
carry invitegenerates invite URLs with UCAN delegations.carry joinconfigures an upstream from an invite URL. - Structural merge: Dialog’s prolly tree storage enables efficient structural diffs and deterministic merging of divergent replicas.
Sync uses UCAN (User Controlled Authorization Networks) for capability-based access control. Each invite delegates specific capabilities from the space owner to the invitee.