Skip to content
Open Breno

Export format

The contract for the archive Breno writes and reads: layout, file naming, the bundle.json manifest, member order, checksums and the rules an importer must follow.

This is the contract for the archive Breno produces and accepts, breno.export/v2. The writer is tested against it.

Two rules#

  1. An export is complete or it is not an export. Everything the caller can read is in the archive, in an open format, without a Breno account needed to read it. Markdown and JSONL, no proprietary container.
  2. The read rule applies. The archive contains exactly what the requesting caller may read, no more. The rule is the one retrieval uses; export does not re-implement it.

Archive layout#

breno-export-<workspace-slug>-<yyyymmdd-hhmm>.zip
├── bundle.json                      # manifest + all structured item data
├── markdown/<slug>--<id8>.md        # one file per item; front matter + body
├── files/<id>/<original_filename>   # original uploads, byte-exact
├── memory/records.jsonl             # one JSON object per line
├── decisions/decisions.jsonl
├── graph/entities.jsonl
├── graph/relationships.jsonl
├── account.json                     # only on a "download my data" export
└── checksums.sha256                 # sha256 of every file above

memory/, decisions/ and graph/ are always present, even when empty. An empty JSONL file means there were none, which is different from an export that does not do that.

account.json is the one member that is absent when it does not apply. It appears only on a personal "download my data" export, holds only the requester's own profile, sign-in events, sessions and notifications, and is never present on a workspace export. bundle.json records it as account_path; when that key is absent, no account data is in the archive.

Member order#

ZIP members are addressed by name, so a reader must not depend on their order. The writer's order is fixed, and it is what lets an export stream in one pass without holding the archive in memory:

  1. markdown/* and files/*;
  2. the four JSONL members;
  3. account.json, when present;
  4. bundle.json;
  5. checksums.sha256.

bundle.json carries the SHA-256 of every member, so it cannot come first. checksums.sha256 then covers bundle.json itself. A file cannot contain its own hash, so files[] in the manifest lists every member except checksums.sha256.

File naming#

  • markdown/<slug>--<id8>.md: <slug> is the title lower-cased, with runs of non-alphanumeric characters collapsed to -, trimmed, and truncated to 60 characters (untitled if nothing remains). <id8> is the first eight characters of the item's UUID, which makes the name collision-free and lets a reader recover the item id from the filename.
  • files/<id>/<original_filename> keeps the id as the directory so two items with the same original filename cannot collide. The filename is the uploader's, sanitized.
  • Earlier versions of an item are markdown/<slug>--<id8>.v<version>.md.

bundle.json#

The manifest. schema is the version discriminator.

bundle.json
{
  "schema": "breno.export/v2",
  "exported_at": "2026-10-09T07:49:00Z",
  "exported_by": { "id": "…", "name": "…", "email": "…" },
  "scope": "personal",              // "personal" | "workspace"
  "workspace": { "id": "…", "name": "…" },

  "items": [
    {
      "id": "…",
      "title": "…",
      "source_type": "upload",       // upload | url | connector | note | …
      "original_filename": "…",      // "" when not derived from a file
      "mime_type": "…",
      "size_bytes": 12345,
      "created_at": "…",
      "updated_at": "…",
      "created_by": { "id": "…", "name": "…" },
      "visibility": "private",
      "sensitivity": "internal",     // public | internal | confidential | restricted
      "space": { "id": "…", "name": "…" },
      "tags": ["…"],
      "collections": ["…"],
      "provenance": {
        "provider": "github",
        "connection_name": "…",
        "url": "…",
        "author": "…",
        "external_id": "…",
        "source_created_at": "…",
        "source_updated_at": "…"
      },
      "text_path": "markdown/…--…md",
      "file_path": "files/…/…",      // "" when there was no original file
      "sha256_text": "…",
      "sha256_file": "…",            // "" when there was no original file
      "versions": [
        { "version": 2, "created_at": "…", "edited_by": { "id": "…", "name": "…" },
          "text_path": "markdown/…--…v2.md" }
      ]
    }
  ],

  "memory_count": 0,
  "decision_count": 0,
  "entity_count": 0,
  "relationship_count": 0,

  "files": [ { "path": "…", "sha256": "…", "bytes": 1234 } ],

  "account_path": "account.json"     // present only when the export carries account data
}

Notes the schema alone does not carry:

  • text_path is the item's current full text, and the Markdown body is that text. It is read from the same stored text the indexing pipeline reads, not reassembled from passages, which is lossy.
  • versions are older texts. Each points at an additional Markdown file named <slug>--<id8>.v<version>.md.
  • The four *_count fields tell a reader how many records to expect in each JSONL file.

The JSONL record shapes (memory, decisions, entities, relationships) and the Markdown front matter are specified in docs/api/export-format.md in the repository.

Checksums#

checksums.sha256 is in the format sha256sum writes:

text
<64-hex>  <relative/path>

Paths are relative to the archive root and use /. The file lists every entry except itself, sorted by path, so two exports of the same content produce the same checksum file.

Verify
sha256sum -c checksums.sha256

What an importer must do#

  • Verify checksums.sha256 before ingesting, and reject the archive with a clear error naming the first mismatched path.
  • Treat filenames as untrusted input (they are sanitized on write, and must be again on read).
  • Restore metadata (tags, collections, space, sensitivity, timestamps, owner, provenance) rather than re-deriving it, and skip duplicates.

Breno's own import reads this archive, and also an Obsidian or plain Markdown vault and a Notion export. See Import knowledge.