Ember Archive Format

Ember is designed with zero lock-in. When you export your data, you get a clean, human-readable JSON archive inside a standard .zip file. This reference outlines the structure of that export so you can build your own tools, parse your data, or migrate elsewhere.

Archive Layout

The export is a single ZIP archive containing your data split into independently-parseable JSON array files, a schema, a manifest, and all your media attachments.

ember-export-YYYY-MM-DD.zip
├── manifest.json              ← producer, schema version, exportedAt, counts
├── schema.json                ← JSON Schema for every DTO below
├── README.md                  ← human-readable format guide
│
├── worlds.json                ← [World]
├── people.json                ← [Person]
├── how_we_met_tags.json       ← [HowWeMetTag]
├── interaction_media.json     ← [InteractionMedium]
├── interactions.json          ← [Interaction]
├── stories.json               ← [Story]
├── journal_entries.json       ← [JournalEntry]
├── tags.json                  ← [Tag]
├── media_assets.json          ← [MediaAsset]
├── person_attributes.json     ← [PersonAttribute]
├── person_connections.json    ← [PersonConnection]
├── we_good.json               ← [WeGood]
├── world_stints.json          ← [WorldStint]
├── world_layouts.json         ← [WorldLayout]
│
└── media/                     ← Binary attachments referenced by UUID
    ├── Photos/<uuid>.<ext>
    ├── Audio/<uuid>.<ext>
    ├── Video/<uuid>.<ext>
    ├── Songs/<uuid>.<ext>
    ├── People/<uuid>.<ext>
    └── Stories/<uuid>.<ext>

Identity & Relationships

Ember flattens all relationships to ensure parsing is simple and memory-efficient.

  • Primary key everywhere: uuid (8-4-4-4-12 hex with hyphens).
  • Relationships are flat: Never nested. Each side stores the other's UUID. A many-to-many relationship like Story ↔ Person lives as a peopleIDs: [UUID] array on the Story side.
  • Dates: All dates are formatted as ISO 8601 / RFC 3339 in UTC, e.g. 2026-03-12T14:30:00Z. When a moment has fractional seconds they're included (2026-03-12T14:30:00.123456Z) so timestamps round-trip exactly, so accept both forms.
  • Enums: Encoded as human-readable string values (e.g., "Tending", "Rooted"), stable across schema changes.

Derived Fields

The archive includes fields that are pre-computed for fidelity, such as lastContactDate on a Person or previewSnippet on a Journal Entry. If you are importing this data into your own tool, it is recommended to recompute these fields rather than trust the snapshot completely.

Markdown Mentions

Fields like contentMarkdown, noteMarkdown, and narrativeMarkdown are written in CommonMark. Ember adds inline "pill" extensions for mentions: a one-character prefix on a standard Markdown link to https://ember.mov/<kind>/<uuid>, so other parsers see an ordinary link:

Mention Type Syntax Target Row
Person @[Display Name](https://ember.mov/person/<uuid>) Person
Tag #[Tag name](https://ember.mov/tag/<uuid>) Tag
World ~[World name](https://ember.mov/world/<uuid>) World
Story &[Title](https://ember.mov/story/<uuid>) Story
Interaction +[Display](https://ember.mov/interaction/<uuid>) Interaction
Journal Entry %[Title](https://ember.mov/journal/<uuid>) JournalEntry

Story and journal pills also have a "card" form with a doubled prefix (&&[…](…), %%[…](…)) that shows as a block instead of inline. The label is display text only: Ember finds the target by its uuid. Person labels keep the name used at the time; tag and world labels follow renames. A converter should match the URL, not the label.

Dates & Time Zones

Every date is an ISO 8601 instant in UTC. Journal entries, interactions, stories, world stints and person connections also carry timeZoneIdentifier: the IANA zone (like Asia/Tokyo) their dates were entered in. To get the day and time a person remembers, read the instant in that zone.

This matters for days picked from a calendar. A story picked as March 5 in Kolkata is stored as March 5 at 00:00 Kolkata time, which is 2020-03-04T18:30:00Z. Read in UTC or in New York it looks like March 4; read in Asia/Kolkata it is March 5, as intended.

All dates on one record share its one zone. Person.timeZoneIdentifier is different: it's where that person lives. When importing an archive without the field, Ember uses the importing Mac's zone.

All Schemas

World (worlds.json)

{
  "uuid": "UUID",
  "name": "String",
  "colorHex": "String",
  "summary": "String",
  "decayThresholdDays": 90,
  "fadingDurationDays": 30,
  "nudgePolicy": "String? (fading | dormant | never)",
  "defaultIntention": "String",
  "createdAt": "Date (ISO 8601)",
  "updatedAt": "Date (ISO 8601)",
  "deletedAt": "Date (ISO 8601)?",
  "importSource": "String?",
  "importBatchID": "UUID?"
}

Person (people.json)

{
  "uuid": "UUID",
  "name": "String",
  "nicknames": ["String"]?,
  "locationName": "String?",
  "locationLatitude": "Double?",
  "locationLongitude": "Double?",
  "locationCountry": "String?",
  "locationCountryCode": "String? (ISO 3166-1 alpha-2)",
  "locationAdminArea": "String?",
  "locationLocality": "String?",
  "locationContinent": "String?",
  "timeZoneIdentifier": "String?",
  "phone": "String?",
  "email": "String?",
  "birthMonth": 1,
  "birthDay": 28,
  "birthYear": 1990,
  "intention": "String",
  "customDecayThresholdDays": 60,
  "decayResolution": "String",
  "isMemoriesPaused": false,
  "lastContactDate": "Date (ISO 8601)?",
  "createdAt": "Date (ISO 8601)",
  "updatedAt": "Date (ISO 8601)",
  "deletedAt": "Date (ISO 8601)?",
  "importSource": "String?",
  "importBatchID": "UUID?",
  "colorHex": "String?",
  "photoRelativePath": "String?",
  "howWeMetTagID": "UUID?",
  "worldIDs": ["UUID"]
}

How We Met Tag (how_we_met_tags.json)

{
  "uuid": "UUID",
  "value": "String",
  "isDefault": false,
  "createdAt": "Date (ISO 8601)",
  "updatedAt": "Date (ISO 8601)"
}

Interaction Medium (interaction_media.json)

{
  "uuid": "UUID",
  "value": "String",
  "icon": "String",
  "promptDuration": false,
  "isDefault": false,
  "createdAt": "Date (ISO 8601)",
  "updatedAt": "Date (ISO 8601)"
}

Interaction (interactions.json)

{
  "uuid": "UUID",
  "date": "Date (ISO 8601)",
  "timeZoneIdentifier": "String?",
  "initiatedByUser": true,
  "presenceScoreUser": 5,
  "presenceScoreThem": 5,
  "noteMarkdown": "String?",
  "previewSnippet": "String?",
  "createdAt": "Date (ISO 8601)",
  "updatedAt": "Date (ISO 8601)",
  "deletedAt": "Date (ISO 8601)?",
  "conflictParentUUID": "UUID? (set on a conflict copy)",
  "importSource": "String?",
  "importBatchID": "UUID?",
  "mediumID": "UUID?",
  "peopleIDs": ["UUID"],
  "tagIDs": ["UUID"],
  "writeMoreJournalEntryID": "UUID?"
}

Story (stories.json)

{
  "uuid": "UUID",
  "title": "String",
  "startDate": "Date (ISO 8601)",
  "endDate": "Date (ISO 8601)",
  "timeZoneIdentifier": "String?",
  "narrativeMarkdown": "String?",
  "previewSnippet": "String?",
  "createdAt": "Date (ISO 8601)",
  "updatedAt": "Date (ISO 8601)",
  "deletedAt": "Date (ISO 8601)?",
  "importSource": "String?",
  "importBatchID": "UUID?",
  "conflictParentUUID": "UUID? (set on a conflict copy)",
  "coverRelativePath": "String?",
  "places": [{ "id": "UUID", "name": "String", "latitude": "Double?", "longitude": "Double?", "country": "String?", "countryCode": "String?", "adminArea": "String?", "locality": "String?", "continent": "String?" }]?,
  "peopleIDs": ["UUID"],
  "tagIDs": ["UUID"],
  "worldIDs": ["UUID"],
  "assetIDs": ["UUID"],
  "journalEntryIDs": ["UUID"]
}

Journal Entry (journal_entries.json)

{
  "uuid": "UUID",
  "entryDate": "Date (ISO 8601)?",
  "timeZoneIdentifier": "String?",
  "title": "String?",
  "contentMarkdown": "String?",
  "previewSnippet": "String?",
  "createdAt": "Date (ISO 8601)",
  "updatedAt": "Date (ISO 8601)",
  "deletedAt": "Date (ISO 8601)?",
  "isPinned": false,
  "locationName": "String?",
  "locationLatitude": "Double?",
  "locationLongitude": "Double?",
  "locationCountry": "String?",
  "locationCountryCode": "String? (ISO 3166-1 alpha-2)",
  "locationAdminArea": "String?",
  "locationLocality": "String?",
  "locationContinent": "String?",
  "weatherCondition": "String? (SF Symbol name)",
  "weatherTemperatureCelsius": "Double? (always °C)",
  "weatherNotes": "String?",
  "conflictParentUUID": "UUID? (set on a conflict copy)",
  "importSource": "String?",
  "importBatchID": "UUID?",
  "tagIDs": ["UUID"],
  "mentionedPeopleIDs": ["UUID"],
  "taggedStoryIDs": ["UUID"],
  "taggedWorldIDs": ["UUID"],
  "mentionedInteractionIDs": ["UUID"],
  "mentionedJournalEntryIDs": ["UUID"],
  "assetIDs": ["UUID"],
  "belongsToInteractionID": "UUID?",
  "belongsToPersonHowWeMetID": "UUID?",
  "belongsToPersonNoteID": "UUID?"
}

Tag (tags.json)

{
  "uuid": "UUID",
  "name": "String",
  "isStarred": false,
  "createdAt": "Date (ISO 8601)",
  "updatedAt": "Date (ISO 8601)",
  "deletedAt": "Date (ISO 8601)?"
}

Media Asset (media_assets.json)

{
  "uuid": "UUID",
  "originalRelativePath": "String?",
  "displayRelativePath": "String?",
  "mimeType": "String",
  "filename": "String",
  "createdAt": "Date (ISO 8601)",
  "updatedAt": "Date (ISO 8601)",
  "entryID": "UUID?",
  "storyID": "UUID?",
  "songTrackID": "String?",
  "songArtist": "String?",
  "songAlbumTitle": "String?",
  "songPreviewURL": "String?",
  "songDuration": 210.5?
}

Person Attribute (person_attributes.json)

{
  "uuid": "UUID",
  "key": "String",
  "value": "String",
  "type": "String",
  "createdAt": "Date (ISO 8601)",
  "updatedAt": "Date (ISO 8601)",
  "personID": "UUID?"
}

Person Connection (person_connections.json)

{
  "uuid": "UUID",
  "fromPersonID": "UUID?",
  "toPersonID": "UUID?",
  "note": "String?",
  "connectionDate": "Date (ISO 8601)",
  "timeZoneIdentifier": "String?",
  "createdAt": "Date (ISO 8601)",
  "updatedAt": "Date (ISO 8601)",
  "deletedAt": "Date (ISO 8601)?",
  "endedAt": "Date (ISO 8601)?"
}

We Good (we_good.json)

{
  "uuid": "UUID",
  "personID": "UUID?",
  "date": "Date (ISO 8601)",
  "createdAt": "Date (ISO 8601)",
  "updatedAt": "Date (ISO 8601)"
}

World Stint (world_stints.json)

{
  "uuid": "UUID",
  "personID": "UUID?",
  "worldID": "UUID?",
  "startDate": "Date (ISO 8601)",
  "endDate": "Date (ISO 8601)?",
  "timeZoneIdentifier": "String?",
  "createdAt": "Date (ISO 8601)",
  "updatedAt": "Date (ISO 8601)"
}

World Layout (world_layouts.json)

{
  "uuid": "UUID",
  "worldID": "UUID",
  "x": 100.0,
  "y": 100.0,
  "radius": 50.0,
  "updatedAt": "Date (ISO 8601)"
}

Importing and Re-importing

  • Same uuid, same row: importing an archive into a library that already has a row with that uuid keeps whichever copy has the later updatedAt, so an older backup never undoes newer edits, and a newer one brings its changes in (renamed tags and labels included).
  • Undo Import: moves that import's rows to the Bin. Importing the same archive again brings them back.
  • Trash and permanent delete: rows in the Bin are exported with deletedAt set and arrive in the Bin. Permanently deleted rows aren't exported.
  • Conflict copies: when the same note was edited on two devices at once, Ember keeps both. The extra copy has conflictParentUUID pointing at the original.

Media Files

Images, audio, video, and other attachments are stored in the media/ folder. Models reference these files using relative paths.

  • MediaAsset.originalRelativePath and displayRelativePath indicate where the file is inside the media/ folder (e.g., Photos/abc.jpg).
  • Avatars (Person.photoRelativePath) and Story Covers (Story.coverRelativePath) follow the same convention but do not have dedicated MediaAsset rows.