Positive Experience Format

P.E.A. is designed to hold all your positive eperiences. That also means it's been designed to bring in any data that you would like to capture inside the archive, such as Swarm/Fourrsquare/Day 1/or anything else you'd like to add via a spreadsheet.

This data is kept absolutely private on your devices and presonal iCloud account.

What this page does

It documents the data stored in P.E.A. backup files (.json), and it provides tools and information so you can:

  • Read your experience data in any text editor

  • Move your data between iCloud accounts

  • Merge two archives

  • Bring data in from another app — by hand, script, or by using AI

  • Move your data into another app

The P.E.A. format is versioned and publicly accessible.

Format structure

Every P.E.A. backup/export is a single JSON file. Usually called something like:

The file contains plain text (a JSON object) that looks like this:

{
  "exportSchemaVersion": 3,
  "exportDate": "2026-05-23T17:30:42Z",
  "appVersion": "1.0",
  "entryCount": 247,
  "placeCount": 12,
  "mottoCount": 1,
  "peopleCount": 18,
  "conceptCount": 34,
  "readme": "P.E.A. — Positive Experience Archive\n…",
  "entries":  [  ],
  "places":   [  ],
  "mottos":   [  ],
  "people":   [  ],
  "concepts": [  ]
}

Notes on the format:

  • **exportSchemaVersion** set the rules how P.E.A. understands your data. It won't import a file it doesn't know how to read, rather than guess.

  • **entries**, **places**, **mottos**, **people**, **concepts** are the building blocks of data inside P.E.A. so they are independent top-level collections. Each data point for them carries a unique identifier or uuid so that a future import can recognize them again to avoid creating duplicate entries.

  • Every entry that's associated with a saved place ALSO carries that place's data inline via placeName, placeDetail, placeSource, etc. So you don't have to cross-reference: an entry on its own is self-describing. The top-level places array gives you the list of places as the user organized them. People and concepts work the other way around — entries reference them by uuid (peopleUUIDs, conceptUUIDs). The top-level people / concepts loops are more simple and hold all records.

  • Optional fields are not stored when the data isn't there. Some other apps carrynull, or empty fileds. P.E.A. doesn't do this so the data is easier to read and edit.

Positive Experience Archive Schema

Every Positive Experience is one object in the entries array. Required fields are marked R.

Identity and content

Field

Type

Notes

R uuid

String

This number identifies each entry uniquely / Canonical UUID (8-4-4-4-12 hex).

R archiveNumber

Integer

This is the number P.E. #X you see in the header of an experience in the app. This number gets set when the green marker assigns positivity. Imports from other datasets are defaulting this number to 0, show a title of P.E. #NaN, and render in gray on the map, until you mark them as good experiences. Once they are set (> 0) the P.E.A. number does't change (or in database speak: they become immutable).

R text

String

Text you entered or dictated about the experience. This is optional and can be empty.

R intensity

Integer 1–7

Strength of the experience. 7 being life changing.

R intensityLabel

String

Labels that accompany the intensity number: "Minimum", "Light", "Mild", "Medium", "Strong", "High", "Maximum".

Time

Field

Type

Notes

R createdAt

String (ISO 8601)

When the dot was released.

R updatedAt

String (ISO 8601)

Last time an experience entry was edited.

R timeOfDay

String

"Morning", "Afternoon", "Evening", "Night".

R dayOfWeek

String

"Monday" through "Sunday".

R season

String

"Spring", "Summer", "Autumn", "Winter".

Your physical location at capture

This metadata comes from the device. They are testimony, not opinion. P.E.A. doesn't edit these after capture.

Field

Type

Notes

latitude

Number

Raw GPS, decimal degrees.

longitude

Number

Raw GPS, decimal degrees.

locationName

String

Reverse-geocoded name ("Café Vita").

locationLocality

String

City or town.

locationCountry

String

Country.

A place you want to reference (semantic place)

You can assign a place. This can be because it was a place you were thinking about later, or because you wanted to correct the map pin. These saved places annotate entries without overwriting the location where the experience was captured. When you save a place, subsquent experiences close by (a hundred meters) automatcally enter the location you saved. Saved places are optional set, they mirror a row in the top-level places collection.

Field

Type

Notes

placeName

String

The label you chose ("home", "Mom's house").

placeDetail

String

Locality context for that place.

placeSource

String

"appleMaps", "custom", or "inferred".

placeLatitude

Number

Place coordinate.

placeLongitude

Number

Place coordinate.

placeMapItemIdentifier

String

Apple Maps identifier, when assigned from POI.

isUserConfirmedPlace

Boolean

True if you explicitly assigned this place.

Weather and sky

Field

Type

Notes

weatherCondition

String

"Clear", "Cloudy", "Rain", …

temperatureCelsius

Number

Captured in metric; both units shown in-app.

moonPhase

String

"Full Moon", "Waxing Crescent", …

Activity, motion, environment

Field

Type

Notes

activityType

String

"stationary", "walking", "running", "cycling", "driving".

speedMPS

Number

Meters per second.

altitudeMeters

Number

Elevation in meters.

stepCountToday

Integer

Pedometer count for the day of capture.

noiseDecibelLevel

Number

Ambient noise in dB.

nearbyPOI

String

Comma-separated nearby points of interest.

Music

Field

Type

Notes

musicTrack

String

Song title playing at capture (Apple Music).

musicArtist

String

Artist name.

musicStoreID

String

Apple Music catalog ID, for deep linking.

Capture source device

Field

Type

Notes

deviceName

String

"Timothy's iPhone", "Siri", "Watch" etc.

deviceType

String

Semantic device category: "iphone", "ipad", "mac", "watch", "vision", "tv", or "siri". Older archives may contain other device tokens; they import safely.

Source provenance and identity (v2)

Which app/format originally produced the entry, plus a stable identifier from that source. The two fields together form the merge-deduplication key when you bring data in through Add data to P.E.A.(source, sourceID) keeps re-imports from creating duplicates.

P.E.A. preserves these values forever: once an entry is stamped ("swarm", "5c1b…"), it stays that way through every future export, even if you re-import the archive on another device.

Field

Type

Notes

source

String

"pea" for entries captured by P.E.A. directly (iOS, macOS, watch, widget, Siri). Foreign-format imports stamp with the originating service — "swarm", "foursquare", "dayone", "csv", etc. Lowercase, no spaces.

sourceID

String

Stable identifier from the originating system. Swarm check-in id, Day One entry id, CSV row id. Omit on native PEA rows. When present, (source, sourceID) makes the entry deduplicable across re-imports.

importPayload

String

The original record verbatim (stringified JSON of the source's raw data). With this P.E.A. can reach back into fields it didn't model.

sourceURL

String

Permalink to the original website of foreign datasets (like the Swarm check-in page, a Day One entry URL, an original tweet, etc). When this field contains a link, the app shows "View external experience" on the detail screen.

When you're hand-converting data into P.E.A. format, set source to a short lowercase identifier, like csv. Recommended values for common sources are above. Set sourceID to the original row's stable identifier when that exists — it's how P.E.A. knows "this Swarm check-in is the same from the import last week".

Intensity state (v2)

Field

Type

Notes

isIntensityUnset

Boolean

Only shows on rows that were brought in from a foreign source via add to P.E.A. and haven't been rated yet by the user. If this value is 1 P.E.A. renders it as a solid light-gray dot at the smallest size. (Dashed-gray is reserved for Neutralized rows; Unset is a separate state with its own solid-fill visual so the two stay distinguishable at a glance.) Native P.E.A. entries should never show this field.

People links (v2)

Field

Type

Notes

peopleUUIDs

Array of Strings

Unique identifier for each person so data for them can come from more than one source (Canonical UUIDs of people associated with this entry.) Each idenftifier (UUID) matches one row in the top-level people collection. Omit when no people are associated.

Concept links (v3)

Field

Type

Notes

conceptUUIDs

Array of Strings

Unique identifier (Canonical UUIDs) for each concept, like sports, travel, etc associated with an entry. Each UUID matches one row in the top-level concepts collection. Omit when no concepts are associated. The relationship is many experiences to many concepts, just like peopleUUIDs.

Soft-deletion (Neutralization)

When you neutralize a positive experience it's removed from the list and avaible in the Index via the Neutralized filter for 30 days. After that time, or when manually triggered, neutral experiences are deleted from P.E.A.

Field

Type

Notes

isNeutralized

Boolean

Present and true only for soft-deleted entries.

neutralizedAt

String (ISO 8601)

When you neutralized it.

neutralizedBy

String

Device that performed the neutralization.

Place Schema

Places you save and reuse, eg: "Home", "Jan's Haus", "Papa's".

Every place is one object in the top-level places array. Required fields are marked R.

Field

Type

Notes

R uuid

String

Identifier (Canonical UUID) for places to match them across exports.

R name

String

The name you gave the place ("Home", "Jan's Haus").

R createdAt

String (ISO 8601)

When you saved the place.

detail

String

Locality context ("Helsinki", "Seattle", "Prague").

latitude

Number

Place coordinate, decimal degrees.

longitude

Number

Place coordinate, decimal degrees.

mapItemIdentifier

String

Apple Maps identifier, when selected from a point of interest (POI).

Experiences reference a place with placeName / placeLatitude / placeLongitude / placeMapItemIdentifier fields inline (see Entry Schema above). A future schema version may add a foreign-key placeUUID field on entries; today the entry-level place fields are the source of truth for what's been assigned to an entry, and the top-level places array is the source of truth for the user's curated list.

On Add data to P.E.A. (merge), the system makes sure that places don't duplicate through the uuid or by content — it uses the name plus both coordinates matching at five-decimal (~1 m) precision to identify duplicates. The content match exists because the same place can carry different UUIDs on different devices (UUIDs were backfilled onto pre-existing rows independently per device), so a uuid-only check would duplicate every patch on a cross-device re-import. Places with the same name at different coordinates remain distinct — the newly imported one is renamed by adding a number like "Home 2" because place names are unique within a library (entries reference places by name).

Motto Schema

A motto is three words to you entered or picked that show first on every device to make it your own. Every motto is one object in the top-level mottos array.

Field

Type

Notes

R uuid

String

Identifier (Canonical UUID) for Mottos to match them across exports.

R line1

String

First word/line of the motto.

R line2

String

Second word/line.

R line3

String

Third word/line.

R createdAt

String (ISO 8601)

When the motto was saved.

Mottos are gated behind a setting in the app. Importing an archive that contains mottos will enable that surface automatically so the words you brought in are visible.

On Add data to P.E.A. (merge), mottos dedupe by uuid or by exact (line1, line2, line3) content match. Mottos are typed by hand and carry no other identity, so an identical three-line match is treated as the same motto even when the UUIDs differ (which happens when UUIDs were backfilled independently on different devices).

People Schema (v2)

People you can associate with an experience: friends, family, the person you ran into at the coffee shop. You add and link them in the entry editor's People field. A foreign-source converter can also populate the same table — supply a top-level people array and reference its UUIDs from each entry's peopleUUIDs (a CSV with a people column converts cleanly this way). Note: the bundled Swarm converter creates no people, because Foursquare's downloadable export strips the companions/with list (it lives only in the live Swarm API); the only trace of who you were with is an @mention left in the shout text.

Every person is one object in the top-level people array. Required fields are marked R.

Field

Type

Notes

R uuid

String

Unique ID (Canonical UUID (8-4-4-4-12 hex)). Stable across imports/exports.

R name

String

Display name ("Sawa", "Murat", "Fabian").

R createdAt

String (ISO 8601)

When this person was first added.

R updatedAt

String (ISO 8601)

Last time this person record changed.

source

String

"pea" for people added inside P.E.A., or the originating service name when imported from foreign data.

sourceID

String

Stable identifier from the originating system, when a converter supplies one. With source, forms the dedupe key. Native (editor-created) people leave it nil.

photoURL

String

URL or local-asset path for a photo. P.E.A. doesn't download or display these yet — they round-trip only.

notes

String

Free-form notes the user might keep on a person. Round-trip only — P.E.A. doesn't surface them yet.

Entries reference people by carrying a peopleUUIDs array (see Entry Schema → People links). The relationship is many-to-many: one person can appear on many entries, and one entry can list many people. On import, the link pass runs after every PeaPerson row is inserted so the entries can resolve each UUID to a live row.

People are created and linked in the entry editor — tap a suggestion or confirm a typed name. Native captures (iPhone, Mac, watch, widget, Siri) never create people; they take no people-aware input. Lists, search, and map don't yet surface people, but the editor reads and writes them, and they round-trip through every export.

On Add data to P.E.A. (merge), people dedupe by uuid or by (source, sourceID) when both are present. Native rows carry no sourceID, so they dedupe by uuid only.

P.E.A./Concept Schema (v3)

A concept is a lightweight keyword tag you attach to an experience — a vocabulary you build as you archive ("soccer", "made", "noticed", "first time"). Concepts are created and linked in the entry editor's Concepts field: tap a suggestion, a default seed, or type a new one.

Every concept is one object in the top-level concepts array. Required fields are marked R. The shape mirrors People exactly so a converter (or a reader) can treat the two the same way.

Field

Type

Notes

R uuid

String

Canonical UUID (8-4-4-4-12 hex). Stable across exports.

R name

String

Display label, in the user's original casing ("soccer", "First Time").

R createdAt

String (ISO 8601)

When this concept was first added.

R updatedAt

String (ISO 8601)

Last time this concept record changed.

source

String

"pea" for concepts created in the editor, or the originating service name when imported from foreign data.

sourceID

String

Stable identifier from the originating system. With source, forms the dedupe key. Native rows leave it nil.

Entries reference concepts by carrying a conceptUUIDs array (see Entry Schema → Concept links). The relationship is many-to-many: one concept can appear on many entries, and one entry can list many concepts. On import, the concept link pass runs after every PeaConcept row is inserted so the entries can resolve each UUID to a live row.

On Add data to P.E.A. (merge), concepts dedupe by uuid or by (source, sourceID) when both are present. Native concepts carry no sourceID, so they dedupe by uuid only — two libraries' independently-created "made" concepts can both land.

P.E.F./One Experience example in detail

This is a native P.E.A. capture (source: "pea"), so it carries a real archiveNumber and a user-chosen intensity — exactly what an entry you made in the app looks like on the way out.

{
  "exportSchemaVersion": 3,
  "exportDate": "2026-05-12T17:30:42Z",
  "appVersion": "1.0",
  "entryCount": 1,
  "placeCount": 1,
  "mottoCount": 0,
  "peopleCount": 0,
  "conceptCount": 0,
  "readme": "P.E.A. — Positive Experience Archive\n=====================================\n…",
  "entries": [
    {
      "uuid": "1A2B3C4D-5E6F-7081-9202-A1B2C3D4E5F6",
      "archiveNumber": 412,
      "text": "Falco on Walmart radio.",
      "intensity": 2,
      "intensityLabel": "Light",
      "createdAt": "2026-01-15T18:32:00Z",
      "updatedAt": "2026-01-15T18:32:00Z",
      "timeOfDay": "Evening",
      "dayOfWeek": "Thursday",
      "season": "Winter",
      "latitude": 35.2271,
      "longitude": -80.8431,
      "locationName": "Walmart Supercenter",
      "locationLocality": "Charlotte",
      "locationCountry": "United States",
      "placeName": "the Walmart by the airport",
      "placeDetail": "Charlotte, NC",
      "placeSource": "custom",
      "placeLatitude": 35.2270,
      "placeLongitude": -80.8430,
      "placeMapItemIdentifier": "I3F4A2C8B9D0E1F2A3B4C5D6E7F8A9B0",
      "isUserConfirmedPlace": true,
      "weatherCondition": "Clear",
      "temperatureCelsius": 7,
      "moonPhase": "Waxing Crescent",
      "activityType": "walking",
      "speedMPS": 0.6,
      "altitudeMeters": 226,
      "stepCountToday": 6428,
      "noiseDecibelLevel": 68,
      "nearbyPOI": "Walmart Supercenter, Bojangles, ExxonMobil",
      "musicTrack": "REAL ONES NEVER DIE",
      "musicArtist": "Kid Cudi",
      "musicStoreID": "1886092613",
      "deviceName": "iPhone 15 Pro",
      "deviceType": "iphone",
      "source": "pea"
    }
  ],
  "places": [
    {
      "uuid": "9F8E7D6C-5B4A-3928-1706-D5C4B3A29180",
      "name": "Airport Walmart",
      "detail": "Charlotte, NC",
      "latitude": 35.2270,
      "longitude": -80.8430,
      "mapItemIdentifier": "I3F4A2C8B9D0E1F2A3B4C5D6E7F8A9B0",
      "createdAt": "2025-12-30T14:10:00Z"
    }
  ],
  "mottos": [],
  "people": [],
  "concepts": []
}

What happened at this Walmart when a person archived their Experience:

  • Music. Even though the person was listening to Kid Cudi, Walmart radio was playing Falco. It must have piqued the person's curiosity enough to pop out the headphones and make a note. Then P.E.A. captured the music that was actually being listened to (Cudi) as well as the note the person left mentioning Falco.

  • Metadata comes from the device or the person. Coordinates, weather, moon phase, steps, and the song are captured from the iPhone. placeName is a place they saved manually. P.E.A. never edits these pieces of information after they are captured, and when a piece of data isn't available (no music playing, no place assigned), the Archive leaves it out. When you create or edit archive files, follow the same rule.

  • Experience Intensity 2. The person dragged the marker to the light setting. Falco at Walmart is not earth shattering but it is weird and fun and something that P.E.A. was built to save.

P.E.F./Import + export

P.E.A. only imports P.E.F. format (json). But you can use any tool you like to convert your data into P.E.A. format — including a chat with an LLM.

The schema above is everything an LLM needs to bring data into the Archive. So, whether you want to move or copy your existing journal or tracking app, you can ask an LLM to rewrite the data to a single PEA-Export-….json file and then bring it in via Settings → Add data to P.E.A. — a non-destructive merge that leaves everything you already have untouched.

Imported entries arrive Unset. P.E.A. never guesses how strong a moment was. Every converted row comes in as a light-gray "Unset" dot with no P.E. # number; you assign the real strength yourself by dragging the dot in the app, and that's the moment the row earns its permanent number. So the prompts below deliberately do not ask the model to invent an intensity — they pin every row to the Unset state and let you do the rating.

Data import prompt

A note on privacy: Make sure you trust the large language model you use and review the company data policy. If the service is free, it's likely they keep your info. If you are running a local or on-device model that can't fetch URLs (Ollama, LM Studio, Apple Intelligence on-device), paste the schema sections from the top of this page into the prompt instead of leaving the link.

You are converting my journal entries into the P.E.A. archive format.

The full schema is here: https://archive.green/format

Please produce a single JSON file matching the structure exactly:

- Top-level "exportSchemaVersion" must be 3.
- Top-level keys: "exportSchemaVersion", "exportDate", "appVersion",
  "entryCount", "placeCount", "mottoCount", "peopleCount",
  "conceptCount", "readme", "entries", "places", "mottos", "people",
  "concepts". No other top-level keys.
- "entries" gets one object per experience.
- "places" gets one object per saved place (may be []).
- "mottos" gets one object per motto (may be []).
- "people" gets one object per person (may be []).
- "concepts" gets one object per keyword tag (may be []).
- Required entry fields: uuid, archiveNumber, text, intensity,
  intensityLabel, createdAt, updatedAt, timeOfDay, dayOfWeek, season.
- Required place fields: uuid, name, createdAt.
- Required motto fields: uuid, line1, line2, line3, createdAt.
- Required concept fields: uuid, name, createdAt, updatedAt.
- "uuid" must be a canonical 8-4-4-4-12 UUID string. Generate fresh
  random UUIDs unless you are reusing IDs from a P.E.A. archive.
- "archiveNumber" must be 0 for every row. P.E.A. assigns the permanent
  `P.E. #N` identifier the moment a user first rates a row in the
  editor (Unset → any green step). Until then the app renders the row
  as `P.E. #NaN`. Do NOT pre-number imported rows; doing so makes them
  indistinguishable from user-rated native captures.
- Imported rows arrive UNRATED. Do NOT guess a strength from the text.
  For EVERY entry set "intensity": 1, "intensityLabel": "Minimum", and
  "isIntensityUnset": true. (P.E.A. forces this state on any non-"pea"
  row at import time anyway, so a guessed number would just be thrown
  away — and the whole point is that I rate each moment myself.)
- "source": a short lowercase tag for where the data came from
  ("dayone", "notes", "import", …). Never "pea" — that's reserved for
  moments captured inside the app. This tag is what marks the row as
  Unset on import and keeps re-imports from duplicating it.
- Use ISO 8601 for all dates with timezone (e.g. 2026-05-12T08:14:03Z).
- Derive timeOfDay/dayOfWeek/season from createdAt.
- Omit any optional field you don't have data for. Do not write null.

Here is my data:
[paste your existing journal export here]

The output should be a single JSON file matching the schema above. Save it, then load it through Settings → Add data to P.E.A.

Keep your data private

With P.E.A., your data stays yours. If you are converting or importing data, that should be no different.

Recommended (on-device only):

  • Apple Intelligence on iOS 26 / macOS 26 — Foundation Models, Writing Tools, or the Shortcuts "Use Model" action. Runs on-device when it can; Private Cloud Compute fallback for longer inputs. Strongest non-local privacy guarantee available today (cryptographic attestation, no persistence, no operator access).

  • Local LLMs via Ollama, LM Studio, or any MLX-based runner. A 7B–8B model (Llama 3.1, Qwen 2.5, Gemma 3, Mistral) on a Mac with 16 GB RAM converts a year of journal entries in a few minutes — no account, no API key, no network traffic.

Hosted APIs (OpenAI, Anthropic) offer no-training and no-retention modes that work for this task, but they require trust in the vendor's policy. Make sure you set your settings for the provider not to train on your data and that you trust them.

What to avoid: Free consumer chat UIs (ChatGPT/Gemini/Claude on the web) where data-handling settings are easy to misconfigure. P.E.A. entries can include exact locations and emotional context — they are not the right thing to paste into a chat you haven't audited.

Sources we expect people to bring

  • Day One JSON — exports include rich timestamp + location data; LLMs convert this cleanly.

  • Swarm / Foursquare check-ins — coordinates and timestamps map well; intensity is your call. Worked example below.

  • Apple Notes / Markdown journals — text + filename dates is enough to start; everything else is optional.

  • Plain CSVs — works. See import template below.

If you have a method for a conversion of a popular data source, send it to Florian. He'll add it here.

Worked example — Swarm / Foursquare

First, get your data. Go to foursquare.com/settings/privacy and hit Initiate Data Download Request. Foursquare emails you a download link when the export is ready — this can take up to 72 hours. The email links a data-export-NNNN.zip; download and unzip it, and that folder of checkinsN.json files is what you feed to the converter below.

Every Swarm check-in maps to a P.E.A. entry. Imported check-ins arrive in the Unset intensity state — solid light-gray dots at the smallest size — and you assign a strength later by dragging the dot in the editor or swiping the row in the timeline.

There are two paths into P.E.A. from a Swarm/Foursquare export:

1. Run the converter script (recommended; works for any size export)

A small Python script does this conversion deterministically — no LLM, no chat-window pasting, no per-row prompt cost:

swarm_to_pea.py →

It's Python 3 stdlib only (no pip install). Open the link above, copy the whole script into a file named swarm_to_pea.py, point it at your unzipped Swarm export, and it writes a P.E.A. v2 archive next to the originals:

That writes data-export-NNNN/pea-swarm.json. Then Settings → Add data to P.E.A. in the app, pick that file, confirm Merge.

Useful flags:

  • --since YYYY-MM-DD — convert only check-ins on/after this local date

  • --seed N — deterministic UUIDs for testing (not needed in normal use)

Run python3 swarm_to_pea.py --help for the full reference, including a built-in FAQ.

Imported rows are numberless. Every row arrives at archiveNumber: 0 and renders in the app as P.E. #NaN. The app assigns a permanent P.E. #N the moment you rate a row in the editor (drag the dot to any green step). Until then a check-in is a candidate, not a P.E. — clearing back to Unset later keeps the earned number; never rating a row leaves it numberless forever (and the freezer can hard-delete it after 30 days without ever burning an archive slot).

2. Convert by hand or with an LLM (good for understanding the mapping, or for tiny exports)

The rest of this section explains the mapping the script implements. If you want to roll your own converter, or you have ten check-ins and don't want to download anything, the JSON example and starter prompt below produce equivalent output.

What a real Swarm check-in looks like

Swarm exports ship as a folder of checkinsN.json files. Each one looks like this (top-level wrapper omitted; one row from the items array shown):

{
  "id": "abcdef0123456789abcdef01",
  "createdAt": "2024-07-21 23:22:37.000000",
  "type": "checkin",
  "timeZoneOffset": -420,
  "lat": 47.700571,
  "lng": -122.378029,
  "venue": {
    "id": "...",
    "name": "Botanica Bar",
    "url": "https://app.foursquare.com/v/..."
  },
  "shout": "Saw a friend.",
  "comments": { "count": 0 }
}

Notes on the shape:

  • createdAt is a quoted string with UTC wall-clock time and microsecond resolution (YYYY-MM-DD HH:MM:SS.ffffff), not a unix timestamp.

  • Coordinates are top-level lat / lng, not nested under venue. The venue block carries only {id, name, url} — no city or country.

  • shout (the user's prose) is present on a small minority of rows; most check-ins have no shout at all.

  • timeZoneOffset is minutes from UTC at the time of the check-in (e.g. -420 is Pacific Daylight Time).

The corresponding P.E.A. v2 entry

{
  "uuid": "550e8400-e29b-41d4-a716-446655440000",
  "archiveNumber": 0,
  "text": "Saw a friend.",
  "intensity": 1,
  "intensityLabel": "Minimum",
  "isIntensityUnset": true,
  "createdAt": "2024-07-21T16:22:37-07:00",
  "updatedAt": "2024-07-21T16:22:37-07:00",
  "timeOfDay": "Afternoon",
  "dayOfWeek": "Sunday",
  "season": "Summer",
  "latitude": 47.700571,
  "longitude": -122.378029,
  "locationName": "Botanica Bar",
  "source": "swarm",
  "sourceID": "abcdef0123456789abcdef01",
  "sourceURL": "https://swarmapp.com/checkin/abcdef0123456789abcdef01",
  "importPayload": "{\"id\":\"abcdef0123456789abcdef01\", ... }"
}

Mapping table

Swarm

P.E.A. v2

Note

id

sourceID

Half of the (source, sourceID) dedupe key on merge import. Stable forever — exports preserve it.

id

sourceURL

Constructed as https://swarmapp.com/checkin/<id>. The app surfaces this as a tappable "View external experience" row that deep-links into Swarm or Safari.

createdAt + timeZoneOffset

createdAt, updatedAt

Treat the string as UTC, shift by timeZoneOffset minutes, emit as ISO 8601 with the local offset (e.g. 2024-07-21T16:22:37-07:00). Same value into both.

shout (or "")

text

Empty shouts are fine; text: "" is valid.

lat, lng (top level)

latitude, longitude

Copy verbatim.

venue.name

locationName

Omitted if the check-in has no venue (off-grid check-ins exist; <1 % of typical exports).

derived from local time

timeOfDay, dayOfWeek, season

Compute from the local time (after the timezone shift), not from UTC.

derived per-row

uuid

Fresh random UUID, one per check-in.

constant

archiveNumber: 0

Always 0. P.E.A. assigns the real P.E. #N when the user first rates a row in the app — imported rows are deliberately numberless (P.E. #NaN) until then.

constant

intensity: 1, intensityLabel: "Minimum", isIntensityUnset: true

Imported rows arrive in the Unset state. The user assigns a real strength later. The merge importer enforces this regardless of what the JSON file says.

constant

source: "swarm"

Stamp every row. Lets the app tell native captures from imported ones; sticky forever in round-trips.

raw check-in (stringified)

importPayload

Compact JSON of the original Swarm row, embedded as a string. The app doesn't read it today; future schema versions can backfill from it.

venue.url, entities, event, hacc, visibility, private, comments

Dropped. No P.E.A. equivalent today. Preserved inside importPayload for future revivals.

Starter prompt for an LLM (small exports only)

For larger exports use the script. For ≲100 check-ins, you can paste your data into an LLM with this prompt and skip the script entirely:

Convert this Swarm/Foursquare export into a P.E.A. v2 archive JSON file.
Follow the schema at https://archive.green/format.

For each Swarm check-in:
- uuid: generate a fresh random UUID (8-4-4-4-12 hex)
- archiveNumber: 0 for every row. P.E.A. assigns the permanent P.E. #N
  when the user first rates a row in the app; imported rows are
  deliberately numberless until then. Do NOT pre-number them.
- text: copy from the Swarm "shout"; use "" if blank
- intensity: 1
- intensityLabel: "Minimum"
- isIntensityUnset: true
- createdAt, updatedAt: parse the Swarm "createdAt" as UTC, shift by
  "timeZoneOffset" minutes, emit ISO 8601 with the local offset
  (e.g. "2024-07-21T16:22:37-07:00"). Use the same value for both.
- timeOfDay, dayOfWeek, season: derive from the LOCAL time (after the shift)
- latitude, longitude: copy from the top-level "lat" / "lng"
- locationName: copy from "venue.name" (omit if no venue)
- source: "swarm" on every entry
- sourceID: copy the Swarm "id" verbatim
- sourceURL: build "https://swarmapp.com/checkin/<id>"  (omit if no venue)
- importPayload: the raw Swarm row, JSON-stringified (compact, no indent)
- Omit every other optional field

Top-level wrapper:
{
  "exportSchemaVersion": 3,
  "readme": "Converted from Swarm.",
  "exportDate": "{today, ISO 8601 with Z}",
  "appVersion": "1.0",
  "entryCount": {number of entries},
  "placeCount": 0,
  "mottoCount": 0,
  "peopleCount": 0,
  "conceptCount": 0,
  "entries": [ ... ]

Using the CSV template

If you have data in a spreadsheet (or you'd rather sketch your archive by hand in Numbers / Excel / Google Sheets than write JSON), grab the template:

PEA-Import-Template.csv →

Open the link, copy the rows into a file named PEA-Import-Template.csv, and open it in your spreadsheet of choice.

The columns are ordered by how likely you are to have the data. The first ones — the date and what you wrote — are all most entries need; then where you were, the people and concepts you want to tag, and finally the device-captured context (weather, activity, music, …) that usually only exists if you're converting from another app's export. Leave any column blank.

The bare minimum is two columns:

createdAt,text
2025-01-16T18:32:00Z,"Falco on Walmart radio."

The template ships with three example rows lifted from the app's bundled sample archive so you can see the spread:

  • Row 1 — Frankies 457 olive oil on Pasta e Piselli. Rich case: GPS, weather, activity, music (Pavarotti), nearby POI, ambient noise, a tagged person (Felix) and concepts, captured on an Apple Watch.

  • Row 2 — Bunched and finished so many annoying tasks today. The most. No-coordinate case: street name + city + weather only, tagged with one concept. Shows you don't need GPS to log an experience.

  • Row 3 — Falco on Walmart radio. The signal case: the user wrote about Falco, but the device captured September by Earth, Wind & Fire — both are preserved. This is what P.E.A. is built for.

To use it:

  1. Open the CSV in your spreadsheet of choice.

  2. Replace the example rows with your own. Leave any column blank if you don't have data for it — really only the date and text matter.

  3. To tag people or concepts, type their names in those cells; separate several with semicolons ("Felix; Mara"). Don't worry about UUIDs — the conversion step mints them.

  4. Save as CSV.

  5. Convert CSV → P.E.A. JSON. The simplest path is to paste the CSV plus this prompt into a local LLM:

     Convert this CSV into a P.E.A. archive JSON file matching the v3
     schema at https://archive.green/format.
    
     - One CSV row = one entry in the "entries" array.
     - Generate a fresh random UUID (8-4-4-4-12 hex) for every entry.
     - Empty cells should be omitted from the JSON, not written as null
       or empty strings (except "text", which is required and may be empty).
     - Every row arrives unrated: set "archiveNumber": 0, "intensity": 1,
       "intensityLabel": "Minimum", and "isIntensityUnset": true on EVERY
       entry. Do NOT guess a strength — I assign it later in the app.
     - Use the "createdAt" cell for both "createdAt" and "updatedAt", and
       derive "timeOfDay", "dayOfWeek", and "season" from it.
     - If the "source" cell is blank, use "csv". Never "pea".
     - The "people" and "concepts" cells hold semicolon-separated names.
       For each distinct name, create one object in the top-level
       "people" / "concepts" array (fresh UUID, "source": "csv"), and put
       those UUIDs in the entry's "peopleUUIDs" / "conceptUUIDs". Reuse
       the same UUID when a name repeats across rows.
     - Wrap the entries in the top-level object with exportSchemaVersion 3,
       exportDate set to today (ISO 8601 with Z), appVersion "1.0", empty
       "places" and "mottos" arrays, and "people" / "concepts" arrays
       built from the columns above. Every *Count field must match the
       length of its array.
    
     Here is the CSV:
     [paste]
  6. Save the resulting JSON file and load it through Settings → Add data to P.E.A. (the non-destructive merge — "Restore P.E.A. from backup" is for your own P.E.A. exports and would replace your whole archive).

The template deliberately omits fields the converter fills for you (the uuid, the timeOfDay / dayOfWeek / season labels) and the forced intensity columns (every imported row arrives Unset, so there's nothing to pre-rate). If you need semantic places, moon phase, speed/elevation/steps, the Apple Music store ID, or an external sourceURL, edit the JSON directly after conversion — the full field reference is in the Entry Schema section above.

Restoring Safely

P.E.A. has two ways to bring data in:

  • Add data to P.E.A. — a non-destructive merge. New database rows are added and data that exists (matched by uuid, or by (source, sourceID) for foreign data) are skipped. This is the case for converted imports — Swarm, Day One, CSV, or you can also add an existing P.E.A. backup to merge it with the current database set.

  • Restore P.E.A. from backup — This replaces your current data. This is for your own P.E.A. exports (moving to a new iCloud account, recovering after a wipe). It is destructive. It only accepts archives that were exported by P.E.A..

Schema Version History

  • v3 — Concepts. (June 2026.)

    • **concepts top-level collection** + conceptCount field. Concepts are lightweight keyword tags ("soccer", "made", "noticed") created and linked in the entry editor's Concepts field. Native rows are stamped source: "pea" with no sourceID.

    • Per-entry **conceptUUIDs** array linking each entry to one or more concepts in the top-level concepts array. Many-to-many, the same shape as peopleUUIDs.

    • On Add data to P.E.A. (merge), concepts dedupe by uuid or (source, sourceID); native concepts (no sourceID) dedupe by uuid only.

    • People graduated to the editor. People shipped at v2 as a schema-only round-trip; as of v3 they're created and linked in the entry editor's People field too — not just brought in from foreign imports. The wire format is unchanged.

    • Both additions are optional, so a v1 or v2 archive imports unchanged on a v3 build. A v3 archive is refused by an older build (with a "please update" message) rather than silently dropping the concept data.

  • v2 — Add data + Unset + People (schema-only). (May 2026.)

    • **people top-level collection** + peopleCount field. People round-trip through export and import; the in-app UI surfaces will follow in a later release.

    • Per-entry **peopleUUIDs** array linking each entry to one or more people in the top-level people array.

    • Per-entry **sourceID** — stable identifier from the originating system. With source, forms the merge-dedupe key for "Add data to P.E.A.".

    • Per-entry **importPayload** — the original record verbatim (stringified JSON of the source's raw row). P.E.A. doesn't read this field; it exists so a future import can reach back into fields P.E.A. didn't model the first time.

    • Per-entry **sourceURL** — canonical permalink back to the original record (Swarm check-in, Day One entry, tweet). Surfaces as a tappable "View external experience" row on the detail and editor screens. Native captures emit nil.

    • Per-entry **isIntensityUnset** flag — true on rows that arrived through merge mode from a foreign source and haven't been rated by the user yet. Renders the row's dot as solid light-gray at the smallest size everywhere it appears (distinct from the dashed-gray treatment reserved for Neutralized rows). Never present on native P.E.A. entries.

    • All five entry-level additions are optional, so a v1 archive imports unchanged on a v2 build.

  • v1 — initial public schema. (May 2026.)

    • Single JSON file (no zip container).

    • Top-level entries, places, mottos collections.

    • Per-row stable uuid for round-trip imports.

    • Permanent per-entry archiveNumber (the "P.E. #X" identity).

    • Embedded readme field for human readability.

    • Entry-level place fields preserved alongside the top-level places array, so an individual entry is still self-describing without cross-references.

    • Per-entry source provenance — "pea" for native captures, originating-service name for converted imports. Additive optional in v1; an archive without the field is treated as fully native ("pea" for every row).

The format version is independent of the P.E.A. app version — it's a plain integer that only changes when the archive's shape changes. P.E.A. always reads older versions. Newer files are refused with a clear message until you upgrade the app.

P.E.A. is designed to hold all your positive eperiences. That also means it's been designed to bring in any data that you would like to capture inside the archive, such as Swarm/Fourrsquare/Day 1/or anything else you'd like to add via a spreadsheet.

This data is kept absolutely private on your devices and presonal iCloud account.

What this page does

It documents the data stored in P.E.A. backup files (.json), and it provides tools and information so you can:

  • Read your experience data in any text editor

  • Move your data between iCloud accounts

  • Merge two archives

  • Bring data in from another app — by hand, script, or by using AI

  • Move your data into another app

The P.E.A. format is versioned and publicly accessible.

Format structure

Every P.E.A. backup/export is a single JSON file. Usually called something like:

The file contains plain text (a JSON object) that looks like this:

{
  "exportSchemaVersion": 3,
  "exportDate": "2026-05-23T17:30:42Z",
  "appVersion": "1.0",
  "entryCount": 247,
  "placeCount": 12,
  "mottoCount": 1,
  "peopleCount": 18,
  "conceptCount": 34,
  "readme": "P.E.A. — Positive Experience Archive\n…",
  "entries":  [  ],
  "places":   [  ],
  "mottos":   [  ],
  "people":   [  ],
  "concepts": [  ]
}

Notes on the format:

  • **exportSchemaVersion** set the rules how P.E.A. understands your data. It won't import a file it doesn't know how to read, rather than guess.

  • **entries**, **places**, **mottos**, **people**, **concepts** are the building blocks of data inside P.E.A. so they are independent top-level collections. Each data point for them carries a unique identifier or uuid so that a future import can recognize them again to avoid creating duplicate entries.

  • Every entry that's associated with a saved place ALSO carries that place's data inline via placeName, placeDetail, placeSource, etc. So you don't have to cross-reference: an entry on its own is self-describing. The top-level places array gives you the list of places as the user organized them. People and concepts work the other way around — entries reference them by uuid (peopleUUIDs, conceptUUIDs). The top-level people / concepts loops are more simple and hold all records.

  • Optional fields are not stored when the data isn't there. Some other apps carrynull, or empty fileds. P.E.A. doesn't do this so the data is easier to read and edit.

Positive Experience Archive Schema

Every Positive Experience is one object in the entries array. Required fields are marked R.

Identity and content

Field

Type

Notes

R uuid

String

This number identifies each entry uniquely / Canonical UUID (8-4-4-4-12 hex).

R archiveNumber

Integer

This is the number P.E. #X you see in the header of an experience in the app. This number gets set when the green marker assigns positivity. Imports from other datasets are defaulting this number to 0, show a title of P.E. #NaN, and render in gray on the map, until you mark them as good experiences. Once they are set (> 0) the P.E.A. number does't change (or in database speak: they become immutable).

R text

String

Text you entered or dictated about the experience. This is optional and can be empty.

R intensity

Integer 1–7

Strength of the experience. 7 being life changing.

R intensityLabel

String

Labels that accompany the intensity number: "Minimum", "Light", "Mild", "Medium", "Strong", "High", "Maximum".

Time

Field

Type

Notes

R createdAt

String (ISO 8601)

When the dot was released.

R updatedAt

String (ISO 8601)

Last time an experience entry was edited.

R timeOfDay

String

"Morning", "Afternoon", "Evening", "Night".

R dayOfWeek

String

"Monday" through "Sunday".

R season

String

"Spring", "Summer", "Autumn", "Winter".

Your physical location at capture

This metadata comes from the device. They are testimony, not opinion. P.E.A. doesn't edit these after capture.

Field

Type

Notes

latitude

Number

Raw GPS, decimal degrees.

longitude

Number

Raw GPS, decimal degrees.

locationName

String

Reverse-geocoded name ("Café Vita").

locationLocality

String

City or town.

locationCountry

String

Country.

A place you want to reference (semantic place)

You can assign a place. This can be because it was a place you were thinking about later, or because you wanted to correct the map pin. These saved places annotate entries without overwriting the location where the experience was captured. When you save a place, subsquent experiences close by (a hundred meters) automatcally enter the location you saved. Saved places are optional set, they mirror a row in the top-level places collection.

Field

Type

Notes

placeName

String

The label you chose ("home", "Mom's house").

placeDetail

String

Locality context for that place.

placeSource

String

"appleMaps", "custom", or "inferred".

placeLatitude

Number

Place coordinate.

placeLongitude

Number

Place coordinate.

placeMapItemIdentifier

String

Apple Maps identifier, when assigned from POI.

isUserConfirmedPlace

Boolean

True if you explicitly assigned this place.

Weather and sky

Field

Type

Notes

weatherCondition

String

"Clear", "Cloudy", "Rain", …

temperatureCelsius

Number

Captured in metric; both units shown in-app.

moonPhase

String

"Full Moon", "Waxing Crescent", …

Activity, motion, environment

Field

Type

Notes

activityType

String

"stationary", "walking", "running", "cycling", "driving".

speedMPS

Number

Meters per second.

altitudeMeters

Number

Elevation in meters.

stepCountToday

Integer

Pedometer count for the day of capture.

noiseDecibelLevel

Number

Ambient noise in dB.

nearbyPOI

String

Comma-separated nearby points of interest.

Music

Field

Type

Notes

musicTrack

String

Song title playing at capture (Apple Music).

musicArtist

String

Artist name.

musicStoreID

String

Apple Music catalog ID, for deep linking.

Capture source device

Field

Type

Notes

deviceName

String

"Timothy's iPhone", "Siri", "Watch" etc.

deviceType

String

Semantic device category: "iphone", "ipad", "mac", "watch", "vision", "tv", or "siri". Older archives may contain other device tokens; they import safely.

Source provenance and identity (v2)

Which app/format originally produced the entry, plus a stable identifier from that source. The two fields together form the merge-deduplication key when you bring data in through Add data to P.E.A.(source, sourceID) keeps re-imports from creating duplicates.

P.E.A. preserves these values forever: once an entry is stamped ("swarm", "5c1b…"), it stays that way through every future export, even if you re-import the archive on another device.

Field

Type

Notes

source

String

"pea" for entries captured by P.E.A. directly (iOS, macOS, watch, widget, Siri). Foreign-format imports stamp with the originating service — "swarm", "foursquare", "dayone", "csv", etc. Lowercase, no spaces.

sourceID

String

Stable identifier from the originating system. Swarm check-in id, Day One entry id, CSV row id. Omit on native PEA rows. When present, (source, sourceID) makes the entry deduplicable across re-imports.

importPayload

String

The original record verbatim (stringified JSON of the source's raw data). With this P.E.A. can reach back into fields it didn't model.

sourceURL

String

Permalink to the original website of foreign datasets (like the Swarm check-in page, a Day One entry URL, an original tweet, etc). When this field contains a link, the app shows "View external experience" on the detail screen.

When you're hand-converting data into P.E.A. format, set source to a short lowercase identifier, like csv. Recommended values for common sources are above. Set sourceID to the original row's stable identifier when that exists — it's how P.E.A. knows "this Swarm check-in is the same from the import last week".

Intensity state (v2)

Field

Type

Notes

isIntensityUnset

Boolean

Only shows on rows that were brought in from a foreign source via add to P.E.A. and haven't been rated yet by the user. If this value is 1 P.E.A. renders it as a solid light-gray dot at the smallest size. (Dashed-gray is reserved for Neutralized rows; Unset is a separate state with its own solid-fill visual so the two stay distinguishable at a glance.) Native P.E.A. entries should never show this field.

People links (v2)

Field

Type

Notes

peopleUUIDs

Array of Strings

Unique identifier for each person so data for them can come from more than one source (Canonical UUIDs of people associated with this entry.) Each idenftifier (UUID) matches one row in the top-level people collection. Omit when no people are associated.

Concept links (v3)

Field

Type

Notes

conceptUUIDs

Array of Strings

Unique identifier (Canonical UUIDs) for each concept, like sports, travel, etc associated with an entry. Each UUID matches one row in the top-level concepts collection. Omit when no concepts are associated. The relationship is many experiences to many concepts, just like peopleUUIDs.

Soft-deletion (Neutralization)

When you neutralize a positive experience it's removed from the list and avaible in the Index via the Neutralized filter for 30 days. After that time, or when manually triggered, neutral experiences are deleted from P.E.A.

Field

Type

Notes

isNeutralized

Boolean

Present and true only for soft-deleted entries.

neutralizedAt

String (ISO 8601)

When you neutralized it.

neutralizedBy

String

Device that performed the neutralization.

Place Schema

Places you save and reuse, eg: "Home", "Jan's Haus", "Papa's".

Every place is one object in the top-level places array. Required fields are marked R.

Field

Type

Notes

R uuid

String

Identifier (Canonical UUID) for places to match them across exports.

R name

String

The name you gave the place ("Home", "Jan's Haus").

R createdAt

String (ISO 8601)

When you saved the place.

detail

String

Locality context ("Helsinki", "Seattle", "Prague").

latitude

Number

Place coordinate, decimal degrees.

longitude

Number

Place coordinate, decimal degrees.

mapItemIdentifier

String

Apple Maps identifier, when selected from a point of interest (POI).

Experiences reference a place with placeName / placeLatitude / placeLongitude / placeMapItemIdentifier fields inline (see Entry Schema above). A future schema version may add a foreign-key placeUUID field on entries; today the entry-level place fields are the source of truth for what's been assigned to an entry, and the top-level places array is the source of truth for the user's curated list.

On Add data to P.E.A. (merge), the system makes sure that places don't duplicate through the uuid or by content — it uses the name plus both coordinates matching at five-decimal (~1 m) precision to identify duplicates. The content match exists because the same place can carry different UUIDs on different devices (UUIDs were backfilled onto pre-existing rows independently per device), so a uuid-only check would duplicate every patch on a cross-device re-import. Places with the same name at different coordinates remain distinct — the newly imported one is renamed by adding a number like "Home 2" because place names are unique within a library (entries reference places by name).

Motto Schema

A motto is three words to you entered or picked that show first on every device to make it your own. Every motto is one object in the top-level mottos array.

Field

Type

Notes

R uuid

String

Identifier (Canonical UUID) for Mottos to match them across exports.

R line1

String

First word/line of the motto.

R line2

String

Second word/line.

R line3

String

Third word/line.

R createdAt

String (ISO 8601)

When the motto was saved.

Mottos are gated behind a setting in the app. Importing an archive that contains mottos will enable that surface automatically so the words you brought in are visible.

On Add data to P.E.A. (merge), mottos dedupe by uuid or by exact (line1, line2, line3) content match. Mottos are typed by hand and carry no other identity, so an identical three-line match is treated as the same motto even when the UUIDs differ (which happens when UUIDs were backfilled independently on different devices).

People Schema (v2)

People you can associate with an experience: friends, family, the person you ran into at the coffee shop. You add and link them in the entry editor's People field. A foreign-source converter can also populate the same table — supply a top-level people array and reference its UUIDs from each entry's peopleUUIDs (a CSV with a people column converts cleanly this way). Note: the bundled Swarm converter creates no people, because Foursquare's downloadable export strips the companions/with list (it lives only in the live Swarm API); the only trace of who you were with is an @mention left in the shout text.

Every person is one object in the top-level people array. Required fields are marked R.

Field

Type

Notes

R uuid

String

Unique ID (Canonical UUID (8-4-4-4-12 hex)). Stable across imports/exports.

R name

String

Display name ("Sawa", "Murat", "Fabian").

R createdAt

String (ISO 8601)

When this person was first added.

R updatedAt

String (ISO 8601)

Last time this person record changed.

source

String

"pea" for people added inside P.E.A., or the originating service name when imported from foreign data.

sourceID

String

Stable identifier from the originating system, when a converter supplies one. With source, forms the dedupe key. Native (editor-created) people leave it nil.

photoURL

String

URL or local-asset path for a photo. P.E.A. doesn't download or display these yet — they round-trip only.

notes

String

Free-form notes the user might keep on a person. Round-trip only — P.E.A. doesn't surface them yet.

Entries reference people by carrying a peopleUUIDs array (see Entry Schema → People links). The relationship is many-to-many: one person can appear on many entries, and one entry can list many people. On import, the link pass runs after every PeaPerson row is inserted so the entries can resolve each UUID to a live row.

People are created and linked in the entry editor — tap a suggestion or confirm a typed name. Native captures (iPhone, Mac, watch, widget, Siri) never create people; they take no people-aware input. Lists, search, and map don't yet surface people, but the editor reads and writes them, and they round-trip through every export.

On Add data to P.E.A. (merge), people dedupe by uuid or by (source, sourceID) when both are present. Native rows carry no sourceID, so they dedupe by uuid only.

P.E.A./Concept Schema (v3)

A concept is a lightweight keyword tag you attach to an experience — a vocabulary you build as you archive ("soccer", "made", "noticed", "first time"). Concepts are created and linked in the entry editor's Concepts field: tap a suggestion, a default seed, or type a new one.

Every concept is one object in the top-level concepts array. Required fields are marked R. The shape mirrors People exactly so a converter (or a reader) can treat the two the same way.

Field

Type

Notes

R uuid

String

Canonical UUID (8-4-4-4-12 hex). Stable across exports.

R name

String

Display label, in the user's original casing ("soccer", "First Time").

R createdAt

String (ISO 8601)

When this concept was first added.

R updatedAt

String (ISO 8601)

Last time this concept record changed.

source

String

"pea" for concepts created in the editor, or the originating service name when imported from foreign data.

sourceID

String

Stable identifier from the originating system. With source, forms the dedupe key. Native rows leave it nil.

Entries reference concepts by carrying a conceptUUIDs array (see Entry Schema → Concept links). The relationship is many-to-many: one concept can appear on many entries, and one entry can list many concepts. On import, the concept link pass runs after every PeaConcept row is inserted so the entries can resolve each UUID to a live row.

On Add data to P.E.A. (merge), concepts dedupe by uuid or by (source, sourceID) when both are present. Native concepts carry no sourceID, so they dedupe by uuid only — two libraries' independently-created "made" concepts can both land.

P.E.F./One Experience example in detail

This is a native P.E.A. capture (source: "pea"), so it carries a real archiveNumber and a user-chosen intensity — exactly what an entry you made in the app looks like on the way out.

{
  "exportSchemaVersion": 3,
  "exportDate": "2026-05-12T17:30:42Z",
  "appVersion": "1.0",
  "entryCount": 1,
  "placeCount": 1,
  "mottoCount": 0,
  "peopleCount": 0,
  "conceptCount": 0,
  "readme": "P.E.A. — Positive Experience Archive\n=====================================\n…",
  "entries": [
    {
      "uuid": "1A2B3C4D-5E6F-7081-9202-A1B2C3D4E5F6",
      "archiveNumber": 412,
      "text": "Falco on Walmart radio.",
      "intensity": 2,
      "intensityLabel": "Light",
      "createdAt": "2026-01-15T18:32:00Z",
      "updatedAt": "2026-01-15T18:32:00Z",
      "timeOfDay": "Evening",
      "dayOfWeek": "Thursday",
      "season": "Winter",
      "latitude": 35.2271,
      "longitude": -80.8431,
      "locationName": "Walmart Supercenter",
      "locationLocality": "Charlotte",
      "locationCountry": "United States",
      "placeName": "the Walmart by the airport",
      "placeDetail": "Charlotte, NC",
      "placeSource": "custom",
      "placeLatitude": 35.2270,
      "placeLongitude": -80.8430,
      "placeMapItemIdentifier": "I3F4A2C8B9D0E1F2A3B4C5D6E7F8A9B0",
      "isUserConfirmedPlace": true,
      "weatherCondition": "Clear",
      "temperatureCelsius": 7,
      "moonPhase": "Waxing Crescent",
      "activityType": "walking",
      "speedMPS": 0.6,
      "altitudeMeters": 226,
      "stepCountToday": 6428,
      "noiseDecibelLevel": 68,
      "nearbyPOI": "Walmart Supercenter, Bojangles, ExxonMobil",
      "musicTrack": "REAL ONES NEVER DIE",
      "musicArtist": "Kid Cudi",
      "musicStoreID": "1886092613",
      "deviceName": "iPhone 15 Pro",
      "deviceType": "iphone",
      "source": "pea"
    }
  ],
  "places": [
    {
      "uuid": "9F8E7D6C-5B4A-3928-1706-D5C4B3A29180",
      "name": "Airport Walmart",
      "detail": "Charlotte, NC",
      "latitude": 35.2270,
      "longitude": -80.8430,
      "mapItemIdentifier": "I3F4A2C8B9D0E1F2A3B4C5D6E7F8A9B0",
      "createdAt": "2025-12-30T14:10:00Z"
    }
  ],
  "mottos": [],
  "people": [],
  "concepts": []
}

What happened at this Walmart when a person archived their Experience:

  • Music. Even though the person was listening to Kid Cudi, Walmart radio was playing Falco. It must have piqued the person's curiosity enough to pop out the headphones and make a note. Then P.E.A. captured the music that was actually being listened to (Cudi) as well as the note the person left mentioning Falco.

  • Metadata comes from the device or the person. Coordinates, weather, moon phase, steps, and the song are captured from the iPhone. placeName is a place they saved manually. P.E.A. never edits these pieces of information after they are captured, and when a piece of data isn't available (no music playing, no place assigned), the Archive leaves it out. When you create or edit archive files, follow the same rule.

  • Experience Intensity 2. The person dragged the marker to the light setting. Falco at Walmart is not earth shattering but it is weird and fun and something that P.E.A. was built to save.

P.E.F./Import + export

P.E.A. only imports P.E.F. format (json). But you can use any tool you like to convert your data into P.E.A. format — including a chat with an LLM.

The schema above is everything an LLM needs to bring data into the Archive. So, whether you want to move or copy your existing journal or tracking app, you can ask an LLM to rewrite the data to a single PEA-Export-….json file and then bring it in via Settings → Add data to P.E.A. — a non-destructive merge that leaves everything you already have untouched.

Imported entries arrive Unset. P.E.A. never guesses how strong a moment was. Every converted row comes in as a light-gray "Unset" dot with no P.E. # number; you assign the real strength yourself by dragging the dot in the app, and that's the moment the row earns its permanent number. So the prompts below deliberately do not ask the model to invent an intensity — they pin every row to the Unset state and let you do the rating.

Data import prompt

A note on privacy: Make sure you trust the large language model you use and review the company data policy. If the service is free, it's likely they keep your info. If you are running a local or on-device model that can't fetch URLs (Ollama, LM Studio, Apple Intelligence on-device), paste the schema sections from the top of this page into the prompt instead of leaving the link.

You are converting my journal entries into the P.E.A. archive format.

The full schema is here: https://archive.green/format

Please produce a single JSON file matching the structure exactly:

- Top-level "exportSchemaVersion" must be 3.
- Top-level keys: "exportSchemaVersion", "exportDate", "appVersion",
  "entryCount", "placeCount", "mottoCount", "peopleCount",
  "conceptCount", "readme", "entries", "places", "mottos", "people",
  "concepts". No other top-level keys.
- "entries" gets one object per experience.
- "places" gets one object per saved place (may be []).
- "mottos" gets one object per motto (may be []).
- "people" gets one object per person (may be []).
- "concepts" gets one object per keyword tag (may be []).
- Required entry fields: uuid, archiveNumber, text, intensity,
  intensityLabel, createdAt, updatedAt, timeOfDay, dayOfWeek, season.
- Required place fields: uuid, name, createdAt.
- Required motto fields: uuid, line1, line2, line3, createdAt.
- Required concept fields: uuid, name, createdAt, updatedAt.
- "uuid" must be a canonical 8-4-4-4-12 UUID string. Generate fresh
  random UUIDs unless you are reusing IDs from a P.E.A. archive.
- "archiveNumber" must be 0 for every row. P.E.A. assigns the permanent
  `P.E. #N` identifier the moment a user first rates a row in the
  editor (Unset → any green step). Until then the app renders the row
  as `P.E. #NaN`. Do NOT pre-number imported rows; doing so makes them
  indistinguishable from user-rated native captures.
- Imported rows arrive UNRATED. Do NOT guess a strength from the text.
  For EVERY entry set "intensity": 1, "intensityLabel": "Minimum", and
  "isIntensityUnset": true. (P.E.A. forces this state on any non-"pea"
  row at import time anyway, so a guessed number would just be thrown
  away — and the whole point is that I rate each moment myself.)
- "source": a short lowercase tag for where the data came from
  ("dayone", "notes", "import", …). Never "pea" — that's reserved for
  moments captured inside the app. This tag is what marks the row as
  Unset on import and keeps re-imports from duplicating it.
- Use ISO 8601 for all dates with timezone (e.g. 2026-05-12T08:14:03Z).
- Derive timeOfDay/dayOfWeek/season from createdAt.
- Omit any optional field you don't have data for. Do not write null.

Here is my data:
[paste your existing journal export here]

The output should be a single JSON file matching the schema above. Save it, then load it through Settings → Add data to P.E.A.

Keep your data private

With P.E.A., your data stays yours. If you are converting or importing data, that should be no different.

Recommended (on-device only):

  • Apple Intelligence on iOS 26 / macOS 26 — Foundation Models, Writing Tools, or the Shortcuts "Use Model" action. Runs on-device when it can; Private Cloud Compute fallback for longer inputs. Strongest non-local privacy guarantee available today (cryptographic attestation, no persistence, no operator access).

  • Local LLMs via Ollama, LM Studio, or any MLX-based runner. A 7B–8B model (Llama 3.1, Qwen 2.5, Gemma 3, Mistral) on a Mac with 16 GB RAM converts a year of journal entries in a few minutes — no account, no API key, no network traffic.

Hosted APIs (OpenAI, Anthropic) offer no-training and no-retention modes that work for this task, but they require trust in the vendor's policy. Make sure you set your settings for the provider not to train on your data and that you trust them.

What to avoid: Free consumer chat UIs (ChatGPT/Gemini/Claude on the web) where data-handling settings are easy to misconfigure. P.E.A. entries can include exact locations and emotional context — they are not the right thing to paste into a chat you haven't audited.

Sources we expect people to bring

  • Day One JSON — exports include rich timestamp + location data; LLMs convert this cleanly.

  • Swarm / Foursquare check-ins — coordinates and timestamps map well; intensity is your call. Worked example below.

  • Apple Notes / Markdown journals — text + filename dates is enough to start; everything else is optional.

  • Plain CSVs — works. See import template below.

If you have a method for a conversion of a popular data source, send it to Florian. He'll add it here.

Worked example — Swarm / Foursquare

First, get your data. Go to foursquare.com/settings/privacy and hit Initiate Data Download Request. Foursquare emails you a download link when the export is ready — this can take up to 72 hours. The email links a data-export-NNNN.zip; download and unzip it, and that folder of checkinsN.json files is what you feed to the converter below.

Every Swarm check-in maps to a P.E.A. entry. Imported check-ins arrive in the Unset intensity state — solid light-gray dots at the smallest size — and you assign a strength later by dragging the dot in the editor or swiping the row in the timeline.

There are two paths into P.E.A. from a Swarm/Foursquare export:

1. Run the converter script (recommended; works for any size export)

A small Python script does this conversion deterministically — no LLM, no chat-window pasting, no per-row prompt cost:

swarm_to_pea.py →

It's Python 3 stdlib only (no pip install). Open the link above, copy the whole script into a file named swarm_to_pea.py, point it at your unzipped Swarm export, and it writes a P.E.A. v2 archive next to the originals:

That writes data-export-NNNN/pea-swarm.json. Then Settings → Add data to P.E.A. in the app, pick that file, confirm Merge.

Useful flags:

  • --since YYYY-MM-DD — convert only check-ins on/after this local date

  • --seed N — deterministic UUIDs for testing (not needed in normal use)

Run python3 swarm_to_pea.py --help for the full reference, including a built-in FAQ.

Imported rows are numberless. Every row arrives at archiveNumber: 0 and renders in the app as P.E. #NaN. The app assigns a permanent P.E. #N the moment you rate a row in the editor (drag the dot to any green step). Until then a check-in is a candidate, not a P.E. — clearing back to Unset later keeps the earned number; never rating a row leaves it numberless forever (and the freezer can hard-delete it after 30 days without ever burning an archive slot).

2. Convert by hand or with an LLM (good for understanding the mapping, or for tiny exports)

The rest of this section explains the mapping the script implements. If you want to roll your own converter, or you have ten check-ins and don't want to download anything, the JSON example and starter prompt below produce equivalent output.

What a real Swarm check-in looks like

Swarm exports ship as a folder of checkinsN.json files. Each one looks like this (top-level wrapper omitted; one row from the items array shown):

{
  "id": "abcdef0123456789abcdef01",
  "createdAt": "2024-07-21 23:22:37.000000",
  "type": "checkin",
  "timeZoneOffset": -420,
  "lat": 47.700571,
  "lng": -122.378029,
  "venue": {
    "id": "...",
    "name": "Botanica Bar",
    "url": "https://app.foursquare.com/v/..."
  },
  "shout": "Saw a friend.",
  "comments": { "count": 0 }
}

Notes on the shape:

  • createdAt is a quoted string with UTC wall-clock time and microsecond resolution (YYYY-MM-DD HH:MM:SS.ffffff), not a unix timestamp.

  • Coordinates are top-level lat / lng, not nested under venue. The venue block carries only {id, name, url} — no city or country.

  • shout (the user's prose) is present on a small minority of rows; most check-ins have no shout at all.

  • timeZoneOffset is minutes from UTC at the time of the check-in (e.g. -420 is Pacific Daylight Time).

The corresponding P.E.A. v2 entry

{
  "uuid": "550e8400-e29b-41d4-a716-446655440000",
  "archiveNumber": 0,
  "text": "Saw a friend.",
  "intensity": 1,
  "intensityLabel": "Minimum",
  "isIntensityUnset": true,
  "createdAt": "2024-07-21T16:22:37-07:00",
  "updatedAt": "2024-07-21T16:22:37-07:00",
  "timeOfDay": "Afternoon",
  "dayOfWeek": "Sunday",
  "season": "Summer",
  "latitude": 47.700571,
  "longitude": -122.378029,
  "locationName": "Botanica Bar",
  "source": "swarm",
  "sourceID": "abcdef0123456789abcdef01",
  "sourceURL": "https://swarmapp.com/checkin/abcdef0123456789abcdef01",
  "importPayload": "{\"id\":\"abcdef0123456789abcdef01\", ... }"
}

Mapping table

Swarm

P.E.A. v2

Note

id

sourceID

Half of the (source, sourceID) dedupe key on merge import. Stable forever — exports preserve it.

id

sourceURL

Constructed as https://swarmapp.com/checkin/<id>. The app surfaces this as a tappable "View external experience" row that deep-links into Swarm or Safari.

createdAt + timeZoneOffset

createdAt, updatedAt

Treat the string as UTC, shift by timeZoneOffset minutes, emit as ISO 8601 with the local offset (e.g. 2024-07-21T16:22:37-07:00). Same value into both.

shout (or "")

text

Empty shouts are fine; text: "" is valid.

lat, lng (top level)

latitude, longitude

Copy verbatim.

venue.name

locationName

Omitted if the check-in has no venue (off-grid check-ins exist; <1 % of typical exports).

derived from local time

timeOfDay, dayOfWeek, season

Compute from the local time (after the timezone shift), not from UTC.

derived per-row

uuid

Fresh random UUID, one per check-in.

constant

archiveNumber: 0

Always 0. P.E.A. assigns the real P.E. #N when the user first rates a row in the app — imported rows are deliberately numberless (P.E. #NaN) until then.

constant

intensity: 1, intensityLabel: "Minimum", isIntensityUnset: true

Imported rows arrive in the Unset state. The user assigns a real strength later. The merge importer enforces this regardless of what the JSON file says.

constant

source: "swarm"

Stamp every row. Lets the app tell native captures from imported ones; sticky forever in round-trips.

raw check-in (stringified)

importPayload

Compact JSON of the original Swarm row, embedded as a string. The app doesn't read it today; future schema versions can backfill from it.

venue.url, entities, event, hacc, visibility, private, comments

Dropped. No P.E.A. equivalent today. Preserved inside importPayload for future revivals.

Starter prompt for an LLM (small exports only)

For larger exports use the script. For ≲100 check-ins, you can paste your data into an LLM with this prompt and skip the script entirely:

Convert this Swarm/Foursquare export into a P.E.A. v2 archive JSON file.
Follow the schema at https://archive.green/format.

For each Swarm check-in:
- uuid: generate a fresh random UUID (8-4-4-4-12 hex)
- archiveNumber: 0 for every row. P.E.A. assigns the permanent P.E. #N
  when the user first rates a row in the app; imported rows are
  deliberately numberless until then. Do NOT pre-number them.
- text: copy from the Swarm "shout"; use "" if blank
- intensity: 1
- intensityLabel: "Minimum"
- isIntensityUnset: true
- createdAt, updatedAt: parse the Swarm "createdAt" as UTC, shift by
  "timeZoneOffset" minutes, emit ISO 8601 with the local offset
  (e.g. "2024-07-21T16:22:37-07:00"). Use the same value for both.
- timeOfDay, dayOfWeek, season: derive from the LOCAL time (after the shift)
- latitude, longitude: copy from the top-level "lat" / "lng"
- locationName: copy from "venue.name" (omit if no venue)
- source: "swarm" on every entry
- sourceID: copy the Swarm "id" verbatim
- sourceURL: build "https://swarmapp.com/checkin/<id>"  (omit if no venue)
- importPayload: the raw Swarm row, JSON-stringified (compact, no indent)
- Omit every other optional field

Top-level wrapper:
{
  "exportSchemaVersion": 3,
  "readme": "Converted from Swarm.",
  "exportDate": "{today, ISO 8601 with Z}",
  "appVersion": "1.0",
  "entryCount": {number of entries},
  "placeCount": 0,
  "mottoCount": 0,
  "peopleCount": 0,
  "conceptCount": 0,
  "entries": [ ... ]

Using the CSV template

If you have data in a spreadsheet (or you'd rather sketch your archive by hand in Numbers / Excel / Google Sheets than write JSON), grab the template:

PEA-Import-Template.csv →

Open the link, copy the rows into a file named PEA-Import-Template.csv, and open it in your spreadsheet of choice.

The columns are ordered by how likely you are to have the data. The first ones — the date and what you wrote — are all most entries need; then where you were, the people and concepts you want to tag, and finally the device-captured context (weather, activity, music, …) that usually only exists if you're converting from another app's export. Leave any column blank.

The bare minimum is two columns:

createdAt,text
2025-01-16T18:32:00Z,"Falco on Walmart radio."

The template ships with three example rows lifted from the app's bundled sample archive so you can see the spread:

  • Row 1 — Frankies 457 olive oil on Pasta e Piselli. Rich case: GPS, weather, activity, music (Pavarotti), nearby POI, ambient noise, a tagged person (Felix) and concepts, captured on an Apple Watch.

  • Row 2 — Bunched and finished so many annoying tasks today. The most. No-coordinate case: street name + city + weather only, tagged with one concept. Shows you don't need GPS to log an experience.

  • Row 3 — Falco on Walmart radio. The signal case: the user wrote about Falco, but the device captured September by Earth, Wind & Fire — both are preserved. This is what P.E.A. is built for.

To use it:

  1. Open the CSV in your spreadsheet of choice.

  2. Replace the example rows with your own. Leave any column blank if you don't have data for it — really only the date and text matter.

  3. To tag people or concepts, type their names in those cells; separate several with semicolons ("Felix; Mara"). Don't worry about UUIDs — the conversion step mints them.

  4. Save as CSV.

  5. Convert CSV → P.E.A. JSON. The simplest path is to paste the CSV plus this prompt into a local LLM:

     Convert this CSV into a P.E.A. archive JSON file matching the v3
     schema at https://archive.green/format.
    
     - One CSV row = one entry in the "entries" array.
     - Generate a fresh random UUID (8-4-4-4-12 hex) for every entry.
     - Empty cells should be omitted from the JSON, not written as null
       or empty strings (except "text", which is required and may be empty).
     - Every row arrives unrated: set "archiveNumber": 0, "intensity": 1,
       "intensityLabel": "Minimum", and "isIntensityUnset": true on EVERY
       entry. Do NOT guess a strength — I assign it later in the app.
     - Use the "createdAt" cell for both "createdAt" and "updatedAt", and
       derive "timeOfDay", "dayOfWeek", and "season" from it.
     - If the "source" cell is blank, use "csv". Never "pea".
     - The "people" and "concepts" cells hold semicolon-separated names.
       For each distinct name, create one object in the top-level
       "people" / "concepts" array (fresh UUID, "source": "csv"), and put
       those UUIDs in the entry's "peopleUUIDs" / "conceptUUIDs". Reuse
       the same UUID when a name repeats across rows.
     - Wrap the entries in the top-level object with exportSchemaVersion 3,
       exportDate set to today (ISO 8601 with Z), appVersion "1.0", empty
       "places" and "mottos" arrays, and "people" / "concepts" arrays
       built from the columns above. Every *Count field must match the
       length of its array.
    
     Here is the CSV:
     [paste]
  6. Save the resulting JSON file and load it through Settings → Add data to P.E.A. (the non-destructive merge — "Restore P.E.A. from backup" is for your own P.E.A. exports and would replace your whole archive).

The template deliberately omits fields the converter fills for you (the uuid, the timeOfDay / dayOfWeek / season labels) and the forced intensity columns (every imported row arrives Unset, so there's nothing to pre-rate). If you need semantic places, moon phase, speed/elevation/steps, the Apple Music store ID, or an external sourceURL, edit the JSON directly after conversion — the full field reference is in the Entry Schema section above.

Restoring Safely

P.E.A. has two ways to bring data in:

  • Add data to P.E.A. — a non-destructive merge. New database rows are added and data that exists (matched by uuid, or by (source, sourceID) for foreign data) are skipped. This is the case for converted imports — Swarm, Day One, CSV, or you can also add an existing P.E.A. backup to merge it with the current database set.

  • Restore P.E.A. from backup — This replaces your current data. This is for your own P.E.A. exports (moving to a new iCloud account, recovering after a wipe). It is destructive. It only accepts archives that were exported by P.E.A..

Schema Version History

  • v3 — Concepts. (June 2026.)

    • **concepts top-level collection** + conceptCount field. Concepts are lightweight keyword tags ("soccer", "made", "noticed") created and linked in the entry editor's Concepts field. Native rows are stamped source: "pea" with no sourceID.

    • Per-entry **conceptUUIDs** array linking each entry to one or more concepts in the top-level concepts array. Many-to-many, the same shape as peopleUUIDs.

    • On Add data to P.E.A. (merge), concepts dedupe by uuid or (source, sourceID); native concepts (no sourceID) dedupe by uuid only.

    • People graduated to the editor. People shipped at v2 as a schema-only round-trip; as of v3 they're created and linked in the entry editor's People field too — not just brought in from foreign imports. The wire format is unchanged.

    • Both additions are optional, so a v1 or v2 archive imports unchanged on a v3 build. A v3 archive is refused by an older build (with a "please update" message) rather than silently dropping the concept data.

  • v2 — Add data + Unset + People (schema-only). (May 2026.)

    • **people top-level collection** + peopleCount field. People round-trip through export and import; the in-app UI surfaces will follow in a later release.

    • Per-entry **peopleUUIDs** array linking each entry to one or more people in the top-level people array.

    • Per-entry **sourceID** — stable identifier from the originating system. With source, forms the merge-dedupe key for "Add data to P.E.A.".

    • Per-entry **importPayload** — the original record verbatim (stringified JSON of the source's raw row). P.E.A. doesn't read this field; it exists so a future import can reach back into fields P.E.A. didn't model the first time.

    • Per-entry **sourceURL** — canonical permalink back to the original record (Swarm check-in, Day One entry, tweet). Surfaces as a tappable "View external experience" row on the detail and editor screens. Native captures emit nil.

    • Per-entry **isIntensityUnset** flag — true on rows that arrived through merge mode from a foreign source and haven't been rated by the user yet. Renders the row's dot as solid light-gray at the smallest size everywhere it appears (distinct from the dashed-gray treatment reserved for Neutralized rows). Never present on native P.E.A. entries.

    • All five entry-level additions are optional, so a v1 archive imports unchanged on a v2 build.

  • v1 — initial public schema. (May 2026.)

    • Single JSON file (no zip container).

    • Top-level entries, places, mottos collections.

    • Per-row stable uuid for round-trip imports.

    • Permanent per-entry archiveNumber (the "P.E. #X" identity).

    • Embedded readme field for human readability.

    • Entry-level place fields preserved alongside the top-level places array, so an individual entry is still self-describing without cross-references.

    • Per-entry source provenance — "pea" for native captures, originating-service name for converted imports. Additive optional in v1; an archive without the field is treated as fully native ("pea" for every row).

The format version is independent of the P.E.A. app version — it's a plain integer that only changes when the archive's shape changes. P.E.A. always reads older versions. Newer files are refused with a clear message until you upgrade the app.

P.E.A. is designed to hold all your positive eperiences. That also means it's been designed to bring in any data that you would like to capture inside the archive, such as Swarm/Fourrsquare/Day 1/or anything else you'd like to add via a spreadsheet.

This data is kept absolutely private on your devices and presonal iCloud account.

What this page does

It documents the data stored in P.E.A. backup files (.json), and it provides tools and information so you can:

  • Read your experience data in any text editor

  • Move your data between iCloud accounts

  • Merge two archives

  • Bring data in from another app — by hand, script, or by using AI

  • Move your data into another app

The P.E.A. format is versioned and publicly accessible.

Format structure

Every P.E.A. backup/export is a single JSON file. Usually called something like:

The file contains plain text (a JSON object) that looks like this:

{
  "exportSchemaVersion": 3,
  "exportDate": "2026-05-23T17:30:42Z",
  "appVersion": "1.0",
  "entryCount": 247,
  "placeCount": 12,
  "mottoCount": 1,
  "peopleCount": 18,
  "conceptCount": 34,
  "readme": "P.E.A. — Positive Experience Archive\n…",
  "entries":  [  ],
  "places":   [  ],
  "mottos":   [  ],
  "people":   [  ],
  "concepts": [  ]
}

Notes on the format:

  • **exportSchemaVersion** set the rules how P.E.A. understands your data. It won't import a file it doesn't know how to read, rather than guess.

  • **entries**, **places**, **mottos**, **people**, **concepts** are the building blocks of data inside P.E.A. so they are independent top-level collections. Each data point for them carries a unique identifier or uuid so that a future import can recognize them again to avoid creating duplicate entries.

  • Every entry that's associated with a saved place ALSO carries that place's data inline via placeName, placeDetail, placeSource, etc. So you don't have to cross-reference: an entry on its own is self-describing. The top-level places array gives you the list of places as the user organized them. People and concepts work the other way around — entries reference them by uuid (peopleUUIDs, conceptUUIDs). The top-level people / concepts loops are more simple and hold all records.

  • Optional fields are not stored when the data isn't there. Some other apps carrynull, or empty fileds. P.E.A. doesn't do this so the data is easier to read and edit.

Positive Experience Archive Schema

Every Positive Experience is one object in the entries array. Required fields are marked R.

Identity and content

Field

Type

Notes

R uuid

String

This number identifies each entry uniquely / Canonical UUID (8-4-4-4-12 hex).

R archiveNumber

Integer

This is the number P.E. #X you see in the header of an experience in the app. This number gets set when the green marker assigns positivity. Imports from other datasets are defaulting this number to 0, show a title of P.E. #NaN, and render in gray on the map, until you mark them as good experiences. Once they are set (> 0) the P.E.A. number does't change (or in database speak: they become immutable).

R text

String

Text you entered or dictated about the experience. This is optional and can be empty.

R intensity

Integer 1–7

Strength of the experience. 7 being life changing.

R intensityLabel

String

Labels that accompany the intensity number: "Minimum", "Light", "Mild", "Medium", "Strong", "High", "Maximum".

Time

Field

Type

Notes

R createdAt

String (ISO 8601)

When the dot was released.

R updatedAt

String (ISO 8601)

Last time an experience entry was edited.

R timeOfDay

String

"Morning", "Afternoon", "Evening", "Night".

R dayOfWeek

String

"Monday" through "Sunday".

R season

String

"Spring", "Summer", "Autumn", "Winter".

Your physical location at capture

This metadata comes from the device. They are testimony, not opinion. P.E.A. doesn't edit these after capture.

Field

Type

Notes

latitude

Number

Raw GPS, decimal degrees.

longitude

Number

Raw GPS, decimal degrees.

locationName

String

Reverse-geocoded name ("Café Vita").

locationLocality

String

City or town.

locationCountry

String

Country.

A place you want to reference (semantic place)

You can assign a place. This can be because it was a place you were thinking about later, or because you wanted to correct the map pin. These saved places annotate entries without overwriting the location where the experience was captured. When you save a place, subsquent experiences close by (a hundred meters) automatcally enter the location you saved. Saved places are optional set, they mirror a row in the top-level places collection.

Field

Type

Notes

placeName

String

The label you chose ("home", "Mom's house").

placeDetail

String

Locality context for that place.

placeSource

String

"appleMaps", "custom", or "inferred".

placeLatitude

Number

Place coordinate.

placeLongitude

Number

Place coordinate.

placeMapItemIdentifier

String

Apple Maps identifier, when assigned from POI.

isUserConfirmedPlace

Boolean

True if you explicitly assigned this place.

Weather and sky

Field

Type

Notes

weatherCondition

String

"Clear", "Cloudy", "Rain", …

temperatureCelsius

Number

Captured in metric; both units shown in-app.

moonPhase

String

"Full Moon", "Waxing Crescent", …

Activity, motion, environment

Field

Type

Notes

activityType

String

"stationary", "walking", "running", "cycling", "driving".

speedMPS

Number

Meters per second.

altitudeMeters

Number

Elevation in meters.

stepCountToday

Integer

Pedometer count for the day of capture.

noiseDecibelLevel

Number

Ambient noise in dB.

nearbyPOI

String

Comma-separated nearby points of interest.

Music

Field

Type

Notes

musicTrack

String

Song title playing at capture (Apple Music).

musicArtist

String

Artist name.

musicStoreID

String

Apple Music catalog ID, for deep linking.

Capture source device

Field

Type

Notes

deviceName

String

"Timothy's iPhone", "Siri", "Watch" etc.

deviceType

String

Semantic device category: "iphone", "ipad", "mac", "watch", "vision", "tv", or "siri". Older archives may contain other device tokens; they import safely.

Source provenance and identity (v2)

Which app/format originally produced the entry, plus a stable identifier from that source. The two fields together form the merge-deduplication key when you bring data in through Add data to P.E.A.(source, sourceID) keeps re-imports from creating duplicates.

P.E.A. preserves these values forever: once an entry is stamped ("swarm", "5c1b…"), it stays that way through every future export, even if you re-import the archive on another device.

Field

Type

Notes

source

String

"pea" for entries captured by P.E.A. directly (iOS, macOS, watch, widget, Siri). Foreign-format imports stamp with the originating service — "swarm", "foursquare", "dayone", "csv", etc. Lowercase, no spaces.

sourceID

String

Stable identifier from the originating system. Swarm check-in id, Day One entry id, CSV row id. Omit on native PEA rows. When present, (source, sourceID) makes the entry deduplicable across re-imports.

importPayload

String

The original record verbatim (stringified JSON of the source's raw data). With this P.E.A. can reach back into fields it didn't model.

sourceURL

String

Permalink to the original website of foreign datasets (like the Swarm check-in page, a Day One entry URL, an original tweet, etc). When this field contains a link, the app shows "View external experience" on the detail screen.

When you're hand-converting data into P.E.A. format, set source to a short lowercase identifier, like csv. Recommended values for common sources are above. Set sourceID to the original row's stable identifier when that exists — it's how P.E.A. knows "this Swarm check-in is the same from the import last week".

Intensity state (v2)

Field

Type

Notes

isIntensityUnset

Boolean

Only shows on rows that were brought in from a foreign source via add to P.E.A. and haven't been rated yet by the user. If this value is 1 P.E.A. renders it as a solid light-gray dot at the smallest size. (Dashed-gray is reserved for Neutralized rows; Unset is a separate state with its own solid-fill visual so the two stay distinguishable at a glance.) Native P.E.A. entries should never show this field.

People links (v2)

Field

Type

Notes

peopleUUIDs

Array of Strings

Unique identifier for each person so data for them can come from more than one source (Canonical UUIDs of people associated with this entry.) Each idenftifier (UUID) matches one row in the top-level people collection. Omit when no people are associated.

Concept links (v3)

Field

Type

Notes

conceptUUIDs

Array of Strings

Unique identifier (Canonical UUIDs) for each concept, like sports, travel, etc associated with an entry. Each UUID matches one row in the top-level concepts collection. Omit when no concepts are associated. The relationship is many experiences to many concepts, just like peopleUUIDs.

Soft-deletion (Neutralization)

When you neutralize a positive experience it's removed from the list and avaible in the Index via the Neutralized filter for 30 days. After that time, or when manually triggered, neutral experiences are deleted from P.E.A.

Field

Type

Notes

isNeutralized

Boolean

Present and true only for soft-deleted entries.

neutralizedAt

String (ISO 8601)

When you neutralized it.

neutralizedBy

String

Device that performed the neutralization.

Place Schema

Places you save and reuse, eg: "Home", "Jan's Haus", "Papa's".

Every place is one object in the top-level places array. Required fields are marked R.

Field

Type

Notes

R uuid

String

Identifier (Canonical UUID) for places to match them across exports.

R name

String

The name you gave the place ("Home", "Jan's Haus").

R createdAt

String (ISO 8601)

When you saved the place.

detail

String

Locality context ("Helsinki", "Seattle", "Prague").

latitude

Number

Place coordinate, decimal degrees.

longitude

Number

Place coordinate, decimal degrees.

mapItemIdentifier

String

Apple Maps identifier, when selected from a point of interest (POI).

Experiences reference a place with placeName / placeLatitude / placeLongitude / placeMapItemIdentifier fields inline (see Entry Schema above). A future schema version may add a foreign-key placeUUID field on entries; today the entry-level place fields are the source of truth for what's been assigned to an entry, and the top-level places array is the source of truth for the user's curated list.

On Add data to P.E.A. (merge), the system makes sure that places don't duplicate through the uuid or by content — it uses the name plus both coordinates matching at five-decimal (~1 m) precision to identify duplicates. The content match exists because the same place can carry different UUIDs on different devices (UUIDs were backfilled onto pre-existing rows independently per device), so a uuid-only check would duplicate every patch on a cross-device re-import. Places with the same name at different coordinates remain distinct — the newly imported one is renamed by adding a number like "Home 2" because place names are unique within a library (entries reference places by name).

Motto Schema

A motto is three words to you entered or picked that show first on every device to make it your own. Every motto is one object in the top-level mottos array.

Field

Type

Notes

R uuid

String

Identifier (Canonical UUID) for Mottos to match them across exports.

R line1

String

First word/line of the motto.

R line2

String

Second word/line.

R line3

String

Third word/line.

R createdAt

String (ISO 8601)

When the motto was saved.

Mottos are gated behind a setting in the app. Importing an archive that contains mottos will enable that surface automatically so the words you brought in are visible.

On Add data to P.E.A. (merge), mottos dedupe by uuid or by exact (line1, line2, line3) content match. Mottos are typed by hand and carry no other identity, so an identical three-line match is treated as the same motto even when the UUIDs differ (which happens when UUIDs were backfilled independently on different devices).

People Schema (v2)

People you can associate with an experience: friends, family, the person you ran into at the coffee shop. You add and link them in the entry editor's People field. A foreign-source converter can also populate the same table — supply a top-level people array and reference its UUIDs from each entry's peopleUUIDs (a CSV with a people column converts cleanly this way). Note: the bundled Swarm converter creates no people, because Foursquare's downloadable export strips the companions/with list (it lives only in the live Swarm API); the only trace of who you were with is an @mention left in the shout text.

Every person is one object in the top-level people array. Required fields are marked R.

Field

Type

Notes

R uuid

String

Unique ID (Canonical UUID (8-4-4-4-12 hex)). Stable across imports/exports.

R name

String

Display name ("Sawa", "Murat", "Fabian").

R createdAt

String (ISO 8601)

When this person was first added.

R updatedAt

String (ISO 8601)

Last time this person record changed.

source

String

"pea" for people added inside P.E.A., or the originating service name when imported from foreign data.

sourceID

String

Stable identifier from the originating system, when a converter supplies one. With source, forms the dedupe key. Native (editor-created) people leave it nil.

photoURL

String

URL or local-asset path for a photo. P.E.A. doesn't download or display these yet — they round-trip only.

notes

String

Free-form notes the user might keep on a person. Round-trip only — P.E.A. doesn't surface them yet.

Entries reference people by carrying a peopleUUIDs array (see Entry Schema → People links). The relationship is many-to-many: one person can appear on many entries, and one entry can list many people. On import, the link pass runs after every PeaPerson row is inserted so the entries can resolve each UUID to a live row.

People are created and linked in the entry editor — tap a suggestion or confirm a typed name. Native captures (iPhone, Mac, watch, widget, Siri) never create people; they take no people-aware input. Lists, search, and map don't yet surface people, but the editor reads and writes them, and they round-trip through every export.

On Add data to P.E.A. (merge), people dedupe by uuid or by (source, sourceID) when both are present. Native rows carry no sourceID, so they dedupe by uuid only.

P.E.A./Concept Schema (v3)

A concept is a lightweight keyword tag you attach to an experience — a vocabulary you build as you archive ("soccer", "made", "noticed", "first time"). Concepts are created and linked in the entry editor's Concepts field: tap a suggestion, a default seed, or type a new one.

Every concept is one object in the top-level concepts array. Required fields are marked R. The shape mirrors People exactly so a converter (or a reader) can treat the two the same way.

Field

Type

Notes

R uuid

String

Canonical UUID (8-4-4-4-12 hex). Stable across exports.

R name

String

Display label, in the user's original casing ("soccer", "First Time").

R createdAt

String (ISO 8601)

When this concept was first added.

R updatedAt

String (ISO 8601)

Last time this concept record changed.

source

String

"pea" for concepts created in the editor, or the originating service name when imported from foreign data.

sourceID

String

Stable identifier from the originating system. With source, forms the dedupe key. Native rows leave it nil.

Entries reference concepts by carrying a conceptUUIDs array (see Entry Schema → Concept links). The relationship is many-to-many: one concept can appear on many entries, and one entry can list many concepts. On import, the concept link pass runs after every PeaConcept row is inserted so the entries can resolve each UUID to a live row.

On Add data to P.E.A. (merge), concepts dedupe by uuid or by (source, sourceID) when both are present. Native concepts carry no sourceID, so they dedupe by uuid only — two libraries' independently-created "made" concepts can both land.

P.E.F./One Experience example in detail

This is a native P.E.A. capture (source: "pea"), so it carries a real archiveNumber and a user-chosen intensity — exactly what an entry you made in the app looks like on the way out.

{
  "exportSchemaVersion": 3,
  "exportDate": "2026-05-12T17:30:42Z",
  "appVersion": "1.0",
  "entryCount": 1,
  "placeCount": 1,
  "mottoCount": 0,
  "peopleCount": 0,
  "conceptCount": 0,
  "readme": "P.E.A. — Positive Experience Archive\n=====================================\n…",
  "entries": [
    {
      "uuid": "1A2B3C4D-5E6F-7081-9202-A1B2C3D4E5F6",
      "archiveNumber": 412,
      "text": "Falco on Walmart radio.",
      "intensity": 2,
      "intensityLabel": "Light",
      "createdAt": "2026-01-15T18:32:00Z",
      "updatedAt": "2026-01-15T18:32:00Z",
      "timeOfDay": "Evening",
      "dayOfWeek": "Thursday",
      "season": "Winter",
      "latitude": 35.2271,
      "longitude": -80.8431,
      "locationName": "Walmart Supercenter",
      "locationLocality": "Charlotte",
      "locationCountry": "United States",
      "placeName": "the Walmart by the airport",
      "placeDetail": "Charlotte, NC",
      "placeSource": "custom",
      "placeLatitude": 35.2270,
      "placeLongitude": -80.8430,
      "placeMapItemIdentifier": "I3F4A2C8B9D0E1F2A3B4C5D6E7F8A9B0",
      "isUserConfirmedPlace": true,
      "weatherCondition": "Clear",
      "temperatureCelsius": 7,
      "moonPhase": "Waxing Crescent",
      "activityType": "walking",
      "speedMPS": 0.6,
      "altitudeMeters": 226,
      "stepCountToday": 6428,
      "noiseDecibelLevel": 68,
      "nearbyPOI": "Walmart Supercenter, Bojangles, ExxonMobil",
      "musicTrack": "REAL ONES NEVER DIE",
      "musicArtist": "Kid Cudi",
      "musicStoreID": "1886092613",
      "deviceName": "iPhone 15 Pro",
      "deviceType": "iphone",
      "source": "pea"
    }
  ],
  "places": [
    {
      "uuid": "9F8E7D6C-5B4A-3928-1706-D5C4B3A29180",
      "name": "Airport Walmart",
      "detail": "Charlotte, NC",
      "latitude": 35.2270,
      "longitude": -80.8430,
      "mapItemIdentifier": "I3F4A2C8B9D0E1F2A3B4C5D6E7F8A9B0",
      "createdAt": "2025-12-30T14:10:00Z"
    }
  ],
  "mottos": [],
  "people": [],
  "concepts": []
}

What happened at this Walmart when a person archived their Experience:

  • Music. Even though the person was listening to Kid Cudi, Walmart radio was playing Falco. It must have piqued the person's curiosity enough to pop out the headphones and make a note. Then P.E.A. captured the music that was actually being listened to (Cudi) as well as the note the person left mentioning Falco.

  • Metadata comes from the device or the person. Coordinates, weather, moon phase, steps, and the song are captured from the iPhone. placeName is a place they saved manually. P.E.A. never edits these pieces of information after they are captured, and when a piece of data isn't available (no music playing, no place assigned), the Archive leaves it out. When you create or edit archive files, follow the same rule.

  • Experience Intensity 2. The person dragged the marker to the light setting. Falco at Walmart is not earth shattering but it is weird and fun and something that P.E.A. was built to save.

P.E.F./Import + export

P.E.A. only imports P.E.F. format (json). But you can use any tool you like to convert your data into P.E.A. format — including a chat with an LLM.

The schema above is everything an LLM needs to bring data into the Archive. So, whether you want to move or copy your existing journal or tracking app, you can ask an LLM to rewrite the data to a single PEA-Export-….json file and then bring it in via Settings → Add data to P.E.A. — a non-destructive merge that leaves everything you already have untouched.

Imported entries arrive Unset. P.E.A. never guesses how strong a moment was. Every converted row comes in as a light-gray "Unset" dot with no P.E. # number; you assign the real strength yourself by dragging the dot in the app, and that's the moment the row earns its permanent number. So the prompts below deliberately do not ask the model to invent an intensity — they pin every row to the Unset state and let you do the rating.

Data import prompt

A note on privacy: Make sure you trust the large language model you use and review the company data policy. If the service is free, it's likely they keep your info. If you are running a local or on-device model that can't fetch URLs (Ollama, LM Studio, Apple Intelligence on-device), paste the schema sections from the top of this page into the prompt instead of leaving the link.

You are converting my journal entries into the P.E.A. archive format.

The full schema is here: https://archive.green/format

Please produce a single JSON file matching the structure exactly:

- Top-level "exportSchemaVersion" must be 3.
- Top-level keys: "exportSchemaVersion", "exportDate", "appVersion",
  "entryCount", "placeCount", "mottoCount", "peopleCount",
  "conceptCount", "readme", "entries", "places", "mottos", "people",
  "concepts". No other top-level keys.
- "entries" gets one object per experience.
- "places" gets one object per saved place (may be []).
- "mottos" gets one object per motto (may be []).
- "people" gets one object per person (may be []).
- "concepts" gets one object per keyword tag (may be []).
- Required entry fields: uuid, archiveNumber, text, intensity,
  intensityLabel, createdAt, updatedAt, timeOfDay, dayOfWeek, season.
- Required place fields: uuid, name, createdAt.
- Required motto fields: uuid, line1, line2, line3, createdAt.
- Required concept fields: uuid, name, createdAt, updatedAt.
- "uuid" must be a canonical 8-4-4-4-12 UUID string. Generate fresh
  random UUIDs unless you are reusing IDs from a P.E.A. archive.
- "archiveNumber" must be 0 for every row. P.E.A. assigns the permanent
  `P.E. #N` identifier the moment a user first rates a row in the
  editor (Unset → any green step). Until then the app renders the row
  as `P.E. #NaN`. Do NOT pre-number imported rows; doing so makes them
  indistinguishable from user-rated native captures.
- Imported rows arrive UNRATED. Do NOT guess a strength from the text.
  For EVERY entry set "intensity": 1, "intensityLabel": "Minimum", and
  "isIntensityUnset": true. (P.E.A. forces this state on any non-"pea"
  row at import time anyway, so a guessed number would just be thrown
  away — and the whole point is that I rate each moment myself.)
- "source": a short lowercase tag for where the data came from
  ("dayone", "notes", "import", …). Never "pea" — that's reserved for
  moments captured inside the app. This tag is what marks the row as
  Unset on import and keeps re-imports from duplicating it.
- Use ISO 8601 for all dates with timezone (e.g. 2026-05-12T08:14:03Z).
- Derive timeOfDay/dayOfWeek/season from createdAt.
- Omit any optional field you don't have data for. Do not write null.

Here is my data:
[paste your existing journal export here]

The output should be a single JSON file matching the schema above. Save it, then load it through Settings → Add data to P.E.A.

Keep your data private

With P.E.A., your data stays yours. If you are converting or importing data, that should be no different.

Recommended (on-device only):

  • Apple Intelligence on iOS 26 / macOS 26 — Foundation Models, Writing Tools, or the Shortcuts "Use Model" action. Runs on-device when it can; Private Cloud Compute fallback for longer inputs. Strongest non-local privacy guarantee available today (cryptographic attestation, no persistence, no operator access).

  • Local LLMs via Ollama, LM Studio, or any MLX-based runner. A 7B–8B model (Llama 3.1, Qwen 2.5, Gemma 3, Mistral) on a Mac with 16 GB RAM converts a year of journal entries in a few minutes — no account, no API key, no network traffic.

Hosted APIs (OpenAI, Anthropic) offer no-training and no-retention modes that work for this task, but they require trust in the vendor's policy. Make sure you set your settings for the provider not to train on your data and that you trust them.

What to avoid: Free consumer chat UIs (ChatGPT/Gemini/Claude on the web) where data-handling settings are easy to misconfigure. P.E.A. entries can include exact locations and emotional context — they are not the right thing to paste into a chat you haven't audited.

Sources we expect people to bring

  • Day One JSON — exports include rich timestamp + location data; LLMs convert this cleanly.

  • Swarm / Foursquare check-ins — coordinates and timestamps map well; intensity is your call. Worked example below.

  • Apple Notes / Markdown journals — text + filename dates is enough to start; everything else is optional.

  • Plain CSVs — works. See import template below.

If you have a method for a conversion of a popular data source, send it to Florian. He'll add it here.

Worked example — Swarm / Foursquare

First, get your data. Go to foursquare.com/settings/privacy and hit Initiate Data Download Request. Foursquare emails you a download link when the export is ready — this can take up to 72 hours. The email links a data-export-NNNN.zip; download and unzip it, and that folder of checkinsN.json files is what you feed to the converter below.

Every Swarm check-in maps to a P.E.A. entry. Imported check-ins arrive in the Unset intensity state — solid light-gray dots at the smallest size — and you assign a strength later by dragging the dot in the editor or swiping the row in the timeline.

There are two paths into P.E.A. from a Swarm/Foursquare export:

1. Run the converter script (recommended; works for any size export)

A small Python script does this conversion deterministically — no LLM, no chat-window pasting, no per-row prompt cost:

swarm_to_pea.py →

It's Python 3 stdlib only (no pip install). Open the link above, copy the whole script into a file named swarm_to_pea.py, point it at your unzipped Swarm export, and it writes a P.E.A. v2 archive next to the originals:

That writes data-export-NNNN/pea-swarm.json. Then Settings → Add data to P.E.A. in the app, pick that file, confirm Merge.

Useful flags:

  • --since YYYY-MM-DD — convert only check-ins on/after this local date

  • --seed N — deterministic UUIDs for testing (not needed in normal use)

Run python3 swarm_to_pea.py --help for the full reference, including a built-in FAQ.

Imported rows are numberless. Every row arrives at archiveNumber: 0 and renders in the app as P.E. #NaN. The app assigns a permanent P.E. #N the moment you rate a row in the editor (drag the dot to any green step). Until then a check-in is a candidate, not a P.E. — clearing back to Unset later keeps the earned number; never rating a row leaves it numberless forever (and the freezer can hard-delete it after 30 days without ever burning an archive slot).

2. Convert by hand or with an LLM (good for understanding the mapping, or for tiny exports)

The rest of this section explains the mapping the script implements. If you want to roll your own converter, or you have ten check-ins and don't want to download anything, the JSON example and starter prompt below produce equivalent output.

What a real Swarm check-in looks like

Swarm exports ship as a folder of checkinsN.json files. Each one looks like this (top-level wrapper omitted; one row from the items array shown):

{
  "id": "abcdef0123456789abcdef01",
  "createdAt": "2024-07-21 23:22:37.000000",
  "type": "checkin",
  "timeZoneOffset": -420,
  "lat": 47.700571,
  "lng": -122.378029,
  "venue": {
    "id": "...",
    "name": "Botanica Bar",
    "url": "https://app.foursquare.com/v/..."
  },
  "shout": "Saw a friend.",
  "comments": { "count": 0 }
}

Notes on the shape:

  • createdAt is a quoted string with UTC wall-clock time and microsecond resolution (YYYY-MM-DD HH:MM:SS.ffffff), not a unix timestamp.

  • Coordinates are top-level lat / lng, not nested under venue. The venue block carries only {id, name, url} — no city or country.

  • shout (the user's prose) is present on a small minority of rows; most check-ins have no shout at all.

  • timeZoneOffset is minutes from UTC at the time of the check-in (e.g. -420 is Pacific Daylight Time).

The corresponding P.E.A. v2 entry

{
  "uuid": "550e8400-e29b-41d4-a716-446655440000",
  "archiveNumber": 0,
  "text": "Saw a friend.",
  "intensity": 1,
  "intensityLabel": "Minimum",
  "isIntensityUnset": true,
  "createdAt": "2024-07-21T16:22:37-07:00",
  "updatedAt": "2024-07-21T16:22:37-07:00",
  "timeOfDay": "Afternoon",
  "dayOfWeek": "Sunday",
  "season": "Summer",
  "latitude": 47.700571,
  "longitude": -122.378029,
  "locationName": "Botanica Bar",
  "source": "swarm",
  "sourceID": "abcdef0123456789abcdef01",
  "sourceURL": "https://swarmapp.com/checkin/abcdef0123456789abcdef01",
  "importPayload": "{\"id\":\"abcdef0123456789abcdef01\", ... }"
}

Mapping table

Swarm

P.E.A. v2

Note

id

sourceID

Half of the (source, sourceID) dedupe key on merge import. Stable forever — exports preserve it.

id

sourceURL

Constructed as https://swarmapp.com/checkin/<id>. The app surfaces this as a tappable "View external experience" row that deep-links into Swarm or Safari.

createdAt + timeZoneOffset

createdAt, updatedAt

Treat the string as UTC, shift by timeZoneOffset minutes, emit as ISO 8601 with the local offset (e.g. 2024-07-21T16:22:37-07:00). Same value into both.

shout (or "")

text

Empty shouts are fine; text: "" is valid.

lat, lng (top level)

latitude, longitude

Copy verbatim.

venue.name

locationName

Omitted if the check-in has no venue (off-grid check-ins exist; <1 % of typical exports).

derived from local time

timeOfDay, dayOfWeek, season

Compute from the local time (after the timezone shift), not from UTC.

derived per-row

uuid

Fresh random UUID, one per check-in.

constant

archiveNumber: 0

Always 0. P.E.A. assigns the real P.E. #N when the user first rates a row in the app — imported rows are deliberately numberless (P.E. #NaN) until then.

constant

intensity: 1, intensityLabel: "Minimum", isIntensityUnset: true

Imported rows arrive in the Unset state. The user assigns a real strength later. The merge importer enforces this regardless of what the JSON file says.

constant

source: "swarm"

Stamp every row. Lets the app tell native captures from imported ones; sticky forever in round-trips.

raw check-in (stringified)

importPayload

Compact JSON of the original Swarm row, embedded as a string. The app doesn't read it today; future schema versions can backfill from it.

venue.url, entities, event, hacc, visibility, private, comments

Dropped. No P.E.A. equivalent today. Preserved inside importPayload for future revivals.

Starter prompt for an LLM (small exports only)

For larger exports use the script. For ≲100 check-ins, you can paste your data into an LLM with this prompt and skip the script entirely:

Convert this Swarm/Foursquare export into a P.E.A. v2 archive JSON file.
Follow the schema at https://archive.green/format.

For each Swarm check-in:
- uuid: generate a fresh random UUID (8-4-4-4-12 hex)
- archiveNumber: 0 for every row. P.E.A. assigns the permanent P.E. #N
  when the user first rates a row in the app; imported rows are
  deliberately numberless until then. Do NOT pre-number them.
- text: copy from the Swarm "shout"; use "" if blank
- intensity: 1
- intensityLabel: "Minimum"
- isIntensityUnset: true
- createdAt, updatedAt: parse the Swarm "createdAt" as UTC, shift by
  "timeZoneOffset" minutes, emit ISO 8601 with the local offset
  (e.g. "2024-07-21T16:22:37-07:00"). Use the same value for both.
- timeOfDay, dayOfWeek, season: derive from the LOCAL time (after the shift)
- latitude, longitude: copy from the top-level "lat" / "lng"
- locationName: copy from "venue.name" (omit if no venue)
- source: "swarm" on every entry
- sourceID: copy the Swarm "id" verbatim
- sourceURL: build "https://swarmapp.com/checkin/<id>"  (omit if no venue)
- importPayload: the raw Swarm row, JSON-stringified (compact, no indent)
- Omit every other optional field

Top-level wrapper:
{
  "exportSchemaVersion": 3,
  "readme": "Converted from Swarm.",
  "exportDate": "{today, ISO 8601 with Z}",
  "appVersion": "1.0",
  "entryCount": {number of entries},
  "placeCount": 0,
  "mottoCount": 0,
  "peopleCount": 0,
  "conceptCount": 0,
  "entries": [ ... ]

Using the CSV template

If you have data in a spreadsheet (or you'd rather sketch your archive by hand in Numbers / Excel / Google Sheets than write JSON), grab the template:

PEA-Import-Template.csv →

Open the link, copy the rows into a file named PEA-Import-Template.csv, and open it in your spreadsheet of choice.

The columns are ordered by how likely you are to have the data. The first ones — the date and what you wrote — are all most entries need; then where you were, the people and concepts you want to tag, and finally the device-captured context (weather, activity, music, …) that usually only exists if you're converting from another app's export. Leave any column blank.

The bare minimum is two columns:

createdAt,text
2025-01-16T18:32:00Z,"Falco on Walmart radio."

The template ships with three example rows lifted from the app's bundled sample archive so you can see the spread:

  • Row 1 — Frankies 457 olive oil on Pasta e Piselli. Rich case: GPS, weather, activity, music (Pavarotti), nearby POI, ambient noise, a tagged person (Felix) and concepts, captured on an Apple Watch.

  • Row 2 — Bunched and finished so many annoying tasks today. The most. No-coordinate case: street name + city + weather only, tagged with one concept. Shows you don't need GPS to log an experience.

  • Row 3 — Falco on Walmart radio. The signal case: the user wrote about Falco, but the device captured September by Earth, Wind & Fire — both are preserved. This is what P.E.A. is built for.

To use it:

  1. Open the CSV in your spreadsheet of choice.

  2. Replace the example rows with your own. Leave any column blank if you don't have data for it — really only the date and text matter.

  3. To tag people or concepts, type their names in those cells; separate several with semicolons ("Felix; Mara"). Don't worry about UUIDs — the conversion step mints them.

  4. Save as CSV.

  5. Convert CSV → P.E.A. JSON. The simplest path is to paste the CSV plus this prompt into a local LLM:

     Convert this CSV into a P.E.A. archive JSON file matching the v3
     schema at https://archive.green/format.
    
     - One CSV row = one entry in the "entries" array.
     - Generate a fresh random UUID (8-4-4-4-12 hex) for every entry.
     - Empty cells should be omitted from the JSON, not written as null
       or empty strings (except "text", which is required and may be empty).
     - Every row arrives unrated: set "archiveNumber": 0, "intensity": 1,
       "intensityLabel": "Minimum", and "isIntensityUnset": true on EVERY
       entry. Do NOT guess a strength — I assign it later in the app.
     - Use the "createdAt" cell for both "createdAt" and "updatedAt", and
       derive "timeOfDay", "dayOfWeek", and "season" from it.
     - If the "source" cell is blank, use "csv". Never "pea".
     - The "people" and "concepts" cells hold semicolon-separated names.
       For each distinct name, create one object in the top-level
       "people" / "concepts" array (fresh UUID, "source": "csv"), and put
       those UUIDs in the entry's "peopleUUIDs" / "conceptUUIDs". Reuse
       the same UUID when a name repeats across rows.
     - Wrap the entries in the top-level object with exportSchemaVersion 3,
       exportDate set to today (ISO 8601 with Z), appVersion "1.0", empty
       "places" and "mottos" arrays, and "people" / "concepts" arrays
       built from the columns above. Every *Count field must match the
       length of its array.
    
     Here is the CSV:
     [paste]
  6. Save the resulting JSON file and load it through Settings → Add data to P.E.A. (the non-destructive merge — "Restore P.E.A. from backup" is for your own P.E.A. exports and would replace your whole archive).

The template deliberately omits fields the converter fills for you (the uuid, the timeOfDay / dayOfWeek / season labels) and the forced intensity columns (every imported row arrives Unset, so there's nothing to pre-rate). If you need semantic places, moon phase, speed/elevation/steps, the Apple Music store ID, or an external sourceURL, edit the JSON directly after conversion — the full field reference is in the Entry Schema section above.

Restoring Safely

P.E.A. has two ways to bring data in:

  • Add data to P.E.A. — a non-destructive merge. New database rows are added and data that exists (matched by uuid, or by (source, sourceID) for foreign data) are skipped. This is the case for converted imports — Swarm, Day One, CSV, or you can also add an existing P.E.A. backup to merge it with the current database set.

  • Restore P.E.A. from backup — This replaces your current data. This is for your own P.E.A. exports (moving to a new iCloud account, recovering after a wipe). It is destructive. It only accepts archives that were exported by P.E.A..

Schema Version History

  • v3 — Concepts. (June 2026.)

    • **concepts top-level collection** + conceptCount field. Concepts are lightweight keyword tags ("soccer", "made", "noticed") created and linked in the entry editor's Concepts field. Native rows are stamped source: "pea" with no sourceID.

    • Per-entry **conceptUUIDs** array linking each entry to one or more concepts in the top-level concepts array. Many-to-many, the same shape as peopleUUIDs.

    • On Add data to P.E.A. (merge), concepts dedupe by uuid or (source, sourceID); native concepts (no sourceID) dedupe by uuid only.

    • People graduated to the editor. People shipped at v2 as a schema-only round-trip; as of v3 they're created and linked in the entry editor's People field too — not just brought in from foreign imports. The wire format is unchanged.

    • Both additions are optional, so a v1 or v2 archive imports unchanged on a v3 build. A v3 archive is refused by an older build (with a "please update" message) rather than silently dropping the concept data.

  • v2 — Add data + Unset + People (schema-only). (May 2026.)

    • **people top-level collection** + peopleCount field. People round-trip through export and import; the in-app UI surfaces will follow in a later release.

    • Per-entry **peopleUUIDs** array linking each entry to one or more people in the top-level people array.

    • Per-entry **sourceID** — stable identifier from the originating system. With source, forms the merge-dedupe key for "Add data to P.E.A.".

    • Per-entry **importPayload** — the original record verbatim (stringified JSON of the source's raw row). P.E.A. doesn't read this field; it exists so a future import can reach back into fields P.E.A. didn't model the first time.

    • Per-entry **sourceURL** — canonical permalink back to the original record (Swarm check-in, Day One entry, tweet). Surfaces as a tappable "View external experience" row on the detail and editor screens. Native captures emit nil.

    • Per-entry **isIntensityUnset** flag — true on rows that arrived through merge mode from a foreign source and haven't been rated by the user yet. Renders the row's dot as solid light-gray at the smallest size everywhere it appears (distinct from the dashed-gray treatment reserved for Neutralized rows). Never present on native P.E.A. entries.

    • All five entry-level additions are optional, so a v1 archive imports unchanged on a v2 build.

  • v1 — initial public schema. (May 2026.)

    • Single JSON file (no zip container).

    • Top-level entries, places, mottos collections.

    • Per-row stable uuid for round-trip imports.

    • Permanent per-entry archiveNumber (the "P.E. #X" identity).

    • Embedded readme field for human readability.

    • Entry-level place fields preserved alongside the top-level places array, so an individual entry is still self-describing without cross-references.

    • Per-entry source provenance — "pea" for native captures, originating-service name for converted imports. Additive optional in v1; an archive without the field is treated as fully native ("pea" for every row).

The format version is independent of the P.E.A. app version — it's a plain integer that only changes when the archive's shape changes. P.E.A. always reads older versions. Newer files are refused with a clear message until you upgrade the app.

Questions? Found a problem?

Email Florian with questions.

Email Florian with data questions →

Support people, not corporations.
—David Watanabe

Support people, not corporations.
—David Watanabe

Create a free website with Framer, the website builder loved by startups, designers and agencies.