Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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?

SectionWhat you’ll find
Getting StartedInstallation and first steps
PhilosophyWhy Carry exists and what it believes
Core ConceptsClaims, entities, domains, and asserted notation
Domain ModelingDefining attributes, concepts, and rules
CLI ReferenceEvery command, flag, and option
Use CasesConcrete examples of what to build with Carry
Dialog DBThe 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

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

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

  1. You assert claims – like “the name of Alice is Alice” – into a domain like com.app.person.

  2. When patterns emerge, you define attributes that refine relations with types (Text, UnsignedInteger, etc.) and cardinality (one or many).

  3. You compose attributes into concepts – named schemas like “person” that group related attributes together.

  4. 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.”

  5. 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

ComponentWhat it isExample
theThe relation – identifies the kind of association. Composed of domain/name.com.app.person/name
ofThe entity this claim is about. Always a DID.did:key:zAlice
isThe 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:

  1. The field values are sorted and hashed with BLAKE3.
  2. The hash is used as an Ed25519 signing key seed.
  3. 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:

TypeDescriptionExample
TextUTF-8 string"Alice"
UnsignedIntegerNon-negative integer28
SignedIntegerSigned integer-5
FloatFloating-point number3.14
BooleanTrue or falsetrue
SymbolA namespaced symbolcarry.profile/work
EntityReference to another entity (a DID)did:key:zAlice
BytesRaw 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:

DomainPurpose
dialog.attributeStores attribute identity fields (/id, /type, /cardinality)
dialog.concept.withStores required concept membership by field name
dialog.concept.maybeStores optional concept membership by field name
dialog.metaUniversal 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 data
  • me.myname.notes – for personal data
  • diy.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.

FormMeaningExample
Contains :Global identifier (a DID or URI)did:key:zAlice
No :Local bookmark namequantity, person

Level 2: Context

The second key declares how the fields beneath it should be interpreted.

FormMeaningExample
Contains .Domain context – fields expand to domain/field relation identifierscom.app.person
No .Concept context – fields are named attributes of that conceptattribute, 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

FieldRequiredDescription
theYesThe relation identifier – domain/name format
asYesThe value type (see below)
cardinalityNoone (default) or many
descriptionYesHuman-readable description

Value Types

TypeDescriptionExample Values
TextUTF-8 string"Alice", "hello world"
UnsignedIntegerNon-negative integer0, 28, 1000
SignedIntegerSigned integer-5, 0, 42
FloatFloating-point number3.14, -0.5
BooleanTrue or falsetrue, false
SymbolA namespaced constantcarry.profile/work
EntityReference to another entitydid:key:z...
BytesRaw 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 name pointing at attribute A is distinct from one with a field named fullname pointing at the same attribute A.
  • 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 QueryConcept Query
Targetcom.app.person (contains .)person (no .)
Fields returnedOnly those you requestAll fields the concept defines
Schema validationNoneValidated against concept
Requires schemaNoYes (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 its dialog.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:

PartRequiredDescription
deduceYesThe concept being derived – what the rule produces
whenYesPositive premises – conditions that must hold
unlessNoNegative 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:

VariableMeaning
?thisBinds to the entity being matched or derived
?personUser-defined variable; unifies across premises
?recipeUser-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:

  • .RecipeStep references another concept within the same domain (the leading . means “relative to this domain”).
  • cardinality: many on ingredient and steps means a recipe can have multiple of each.
  • Self-reference: RecipeStep.after references another RecipeStep, 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:

  1. Cross-domain references: The allergy conflict rule joins data from diy.cook (recipes and ingredients) with diy.health (allergies).
  2. Derived concepts: AllergyConflict doesn’t store data directly – it’s derived by a rule from existing data.
  3. Negation: The respect-dietary-restrictions rule uses unless to exclude meals where an allergy conflict exists.
  4. Variable unification: ?substance in the find-allergy-conflicts rule 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

  1. Start with raw domain assertions. Don’t design a schema upfront. Assert data as you have it and let patterns emerge.

  2. Use domains for namespacing, concepts for structure. Domains prevent name collisions. Concepts give you validation and named queries.

  3. Prefer cardinality: many for lists. Instead of comma-separated values in a text field, use a many-valued attribute. This makes each item independently queryable.

  4. Use entity references for relationships. Instead of embedding data, reference other entities. This keeps your model normalized and composable.

  5. 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

CommandAliasDescription
carry initiCreate a new repository
carry assertaAssert claims (add or update data)
carry queryqQuery entities by domain or concept
carry retractrRetract claims (remove data)
carry statusstShow repository info

Global Options

Every command accepts:

FlagDescription
--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

FormatDescriptionBest for
yamlAsserted notation (default)Human reading, file round-trips
jsonArray of objects with id fieldProgrammatic consumption
triplesFlat 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:

PatternInterpretationExample
Contains .Domain targetcom.app.person
No .Concept target (resolved by bookmark name)person
-Read from stdin-
Contains / or ends in .yaml/.yml/.jsonFile pathschema.yaml

Field Syntax

Commands that accept fields use the format FIELD[=VALUE]:

SyntaxMeaning
nameProjection: include this field in output
name="Alice"Filter: only match entities where name is Alice
this=did:key:z...Target a specific entity
@mynameAssert dialog.meta/name on the entity (bookmark)

Value Auto-Detection

When asserting values via the CLI, Carry auto-detects the type:

InputDetected Type
did:key:z...Entity reference
123Unsigned integer
-5Signed integer
3.14Float
true / falseBoolean
Anything elseText (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:

  1. Generates an Ed25519 keypair for the repository.
  2. Creates .carry/<did>/ with a credentials file and claims/ directory.
  3. Bootstraps the builtin concepts (attribute, concept, bookmark) so they can be used immediately.
  4. If LABEL is provided, asserts it as the repository label.

If a .carry/ directory already exists at the target location, the command reports its status.

Arguments

ArgumentDescription
LABELOptional label for the repository (e.g., “my-project”)

Options

FlagDescription
--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 init inside 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>/credentials is stored with mode 0600 (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

ModeSyntaxDescription
Targetcarry assert <domain-or-concept> field=value ...Assert from CLI arguments
Filecarry assert <file.yaml>Assert from a YAML or JSON file
Stdincarry 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

ArgumentDescription
TARGETDomain (e.g., com.app.person) or concept name (e.g., person)
FILEPath to a YAML or JSON file
-Read from stdin

Fields

SyntaxDescription
field=valueAssert this field with this value
this=<DID>Target an existing entity instead of creating a new one
@nameAssert 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

FlagDescription
--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 @name syntax is shorthand for asserting dialog.meta/name on the entity.
  • For concept assertions, if a with/maybe value 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 one for 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

ArgumentDescription
TARGETDomain (e.g., com.app.person) or concept name (e.g., person)

Fields

SyntaxDescription
nameProjection – 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

FlagDescription
--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

ModeSyntaxDescription
Targetcarry retract <domain-or-concept> this=<entity> field ...Retract from CLI arguments
Filecarry retract <file.yaml>Retract from a YAML or JSON file
Stdincarry retract -Retract from standard input

Arguments

ArgumentDescription
TARGETDomain (e.g., com.app.person) or concept name (e.g., person)
FILEPath to a YAML or JSON file
-Read from stdin

Fields

SyntaxDescription
fieldRetract this field regardless of its current value
field=valueRetract 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

FlagDescription
--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-mode retract requires this= 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

FlagDescription
--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:

  1. You explain your project conventions to Claude. Next session, it’s forgotten.
  2. You set up rules in Cursor via .cursorrules. Claude doesn’t know about them.
  3. You build a useful pattern in ChatGPT. There’s no way to share it with your coding tools.
  4. 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/RoamCarry
Data formatProprietary or semi-structured markdownStructured YAML claims
Query languageLimited (Dataview, formulas)Full EAV queries + rules
AI accessPlugin-dependentDirect via CLI
SchemaInformal, drifts over timeExplicit, validated
OfflineVariesAlways (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

  1. Start with data, add schema later. Assert raw domain claims first. When you see patterns, formalize them into concepts.

  2. Use separate repos for environments. Keep production data in one repository and test data in another by using --repo to target different directories.

  3. Export with queries. Need CSV? Pipe JSON output through jq:

    carry query sample --format json | jq -r '.[] | [.id, .collector, .date] | @csv'
    
  4. 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 Operator trait) 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:

DomainPurpose
dialog.attributeAttribute identity: /id, /type, /cardinality
dialog.concept.withRequired concept fields
dialog.concept.maybeOptional concept fields
dialog.metaUniversal metadata: /name, /description
dialog.ruleRule definitions: /deduce, /when, /unless, /where, /assert

All domains starting with dialog. are reserved. User-defined domains must not use this prefix.

Further Reading

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:

  1. Private key: Stored in credentials. Used to sign claims and authenticate during sync.
  2. 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:

IndexLookup patternUse case
EAVEntity -> Attribute -> Value“What are all the claims about Alice?”
AVEAttribute -> Value -> Entity“Who has name = Alice?”
VAEValue -> 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
  1. The target is parsed: domain (contains .) or concept (no .).
  2. For concepts, the concept definition is loaded and fields are validated.
  3. An entity DID is generated from the content (or this= is used for an existing entity).
  4. 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
  1. The target is resolved to a domain or concept.
  2. For concepts, the concept definition determines which attributes to query.
  3. An ArtifactSelector is built with appropriate filters.
  4. The EAV and AVE indexes are scanned.
  5. Results are filtered by any specified field values.
  6. 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

ComponentCratePurpose
CLI frameworkclapCommand parsing and help generation
Dialog DBdialog-query, dialog-artifactsDatabase engine
Tonktonk-spaceSpace management and filesystem backend
Cryptoed25519-dalek, blake3, bs58Key generation, hashing, encoding
Async runtimetokioAsync I/O
Serializationserde_yaml, serde_jsonYAML 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 push and carry pull synchronize with a configured upstream remote.
  • Invite/Join: carry invite generates invite URLs with UCAN delegations. carry join configures 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.