Positive Experience Format
P.E.A. is designed to hold all your positive experiences. That also means it's been designed to bring in any data that you would like to capture inside the archive, such as Swarm/Foursquare, Day One, or anything else you'd like to add via a spreadsheet.
This data is kept absolutely private on your devices and personal 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. This page describes format v3.6 (exportSchemaVersion 3) and was last updated on October 8, 2026. The version history is at the bottom.
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:
exportSchemaVersionsets the rules for how P.E.A. reads your data. It won't import a file it doesn't know how to read, rather than guess.entries,places,mottos,people,conceptsare the building blocks of data inside P.E.A. so they are independent top-level collections. (conceptsholds the Topics. The app called them Concepts when the format was published, and a key in a public format never changes, so your older backups keep working.) Each data point for them carries a unique identifier oruuidso 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-levelplacesarray gives you the list of places as the user organized them. People and topics work the other way around — entries reference them by uuid (peopleUUIDs,conceptUUIDs). The top-levelpeople/conceptslists are simpler and hold all records.Optional fields are not stored when the data isn't there. Some other apps carry
nullor empty fields. 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 | String | This number identifies each entry uniquely / Canonical UUID (8-4-4-4-12 hex). |
R | Integer | This is the number |
R | String | Text you entered or dictated about the experience. This is optional and can be empty. |
R | Integer 1–7 | Strength of the experience. 7 being life changing. |
R | String | Labels that accompany the intensity number: "Minimum", "Light", "Mild", "Medium", "Strong", "High", "Maximum". |
Time
Field | Type | Notes |
|---|---|---|
R | String (ISO 8601) | When the dot was released. |
R | String (ISO 8601) | Last time an experience entry was edited. |
R | String | "Morning" (midnight to 11:59), "Afternoon" (noon to 16:59), "Evening" (17:00 to 20:59), "Night" (21:00 to 23:59), on the local clock. |
R | String | "Monday" through "Sunday". |
R | String | "Spring" (March to May), "Summer" (June to August), "Autumn" (September to November), "Winter" (December to February). |
| String | The time zone the experience happened in, as an IANA name ("America/New_York", "Asia/Tokyo"). It lets the entry keep the clock of the place where it happened. When it's there, P.E.A. reads |
Your physical location at capture
This metadata comes from the device. They are testimony, not opinion. P.E.A. never edits a captured value. It does fill in missing ones: when an imported entry has coordinates and leaves the fields below empty, P.E.A. looks up the city, neighbourhood, county, postal code, region, country and time zone after import, and the day's weather and moon from the historical record where it exists. So bring the coordinates, leave out what you don't know, and never guess.
Field | Type | Notes |
|---|---|---|
| Number | Raw GPS, decimal degrees. |
| Number | Raw GPS, decimal degrees. |
| String | Reverse-geocoded name ("Café Vita"). |
| String | City or town. Omitted? P.E.A. derives it from the coordinates after import. |
| String | Region or state, exactly as the map data gives it ("WA", "Bavaria"). Same completion as the city. |
| String | Neighbourhood or district ("Ballard"). Same completion as the city. |
| String | County, or whatever the country's equivalent is called ("King County"). Same completion as the city. |
| String | Postal or ZIP code ("98117"), exactly as the map data gives it. Same completion as the city. |
| String | Country. Same completion as the city. |
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, later experiences within its radius (100 m unless you change it) get that place automatically. The fields below are optional. They mirror a row in the top-level places collection.
Field | Type | Notes |
|---|---|---|
| String | The label you chose ("home", "Mom's house"). |
| String | Locality context for that place. |
| String |
|
| Number | Place coordinate. |
| Number | Place coordinate. |
| String | Apple Maps identifier, when assigned from POI. |
| String | Apple Maps point-of-interest category, human-readable ("Cafe", "Amusement Park"). Only present for real POIs — plain addresses and custom places have none. |
| Boolean | True if you explicitly assigned this place. |
Weather and sky
Omit these when your source doesn't have them: for entries with coordinates, P.E.A. recovers the hour's condition and temperature and the day's moon phase from the historical weather record after import, where coverage exists. (This lookup honors the in-app weather capture setting — users who turned weather capture off get no lookups.)
Field | Type | Notes |
|---|---|---|
| String | "Clear", "Cloudy", "Rain", … |
| Number | Captured in metric; both units shown in-app. |
| String | "Full Moon", "Waxing Crescent", … |
Activity, motion, environment
Field | Type | Notes |
|---|---|---|
| String | "stationary", "walking", "running", "cycling", "driving". |
| Number | Meters per second. |
| Number | Elevation in meters. |
| Integer | Pedometer count for the day of capture. |
| Number | Ambient noise in dB. |
| String | Comma-separated nearby points of interest. |
Music
Field | Type | Notes |
|---|---|---|
| String | Song title playing at capture (Apple Music). |
| String | Artist name. |
| String | Apple Music catalog ID, for deep linking. |
Capture source device
Field | Type | Notes |
|---|---|---|
| String | "Timothy's iPhone", "Siri", "Watch" etc. |
| String | Semantic device category: |
Source provenance and identity (v2)
Which app or format the entry came from, plus a stable identifier from that source. Together they keep re-imports from creating duplicates: when you bring data in through Add data to P.E.A., a row with the same (source, sourceID) as one already in the archive is skipped. So is a row whose createdAt and text both match an entry in the archive or an earlier row in the same file, whatever its ids. One moment recorded twice lands once, and the preview tells you before you confirm.
P.E.A. keeps these values forever. Once an entry is stamped ("swarm", "5c1b…"), it stays that way through every export, even if you re-import the archive on another device.
Field | Type | Notes |
|---|---|---|
| String |
|
| 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, |
| 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. |
| 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 |
|---|---|---|
| Boolean | Only on rows that came in from another source through Add data to P.E.A. and haven't been rated yet. When it's |
People links (v2)
Field | Type | Notes |
|---|---|---|
| Array of Strings | The UUIDs of the people associated with this experience. Each one matches one row in the top-level |
Topic links (v3)
Field | Type | Notes |
|---|---|---|
| Array of Strings | The UUIDs of the topics associated with this experience (sports, travel, first time, …). Each one matches one row in the top-level |
Soft-deletion (Neutralization)
When you neutralize a positive experience it's removed from the list and available in the Index via the Neutralized filter for 30 days. After that time, or when you delete it yourself, it's gone from P.E.A.
Field | Type | Notes |
|---|---|---|
| Boolean | Present and |
| String (ISO 8601) | When you neutralized it. |
| 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 | String | Identifier (Canonical UUID) for places to match them across exports. |
R | String | The name you gave the place ("Home", "Jan's Haus"). |
R | String (ISO 8601) | When you saved the place. |
| String | Locality context ("Helsinki", "Seattle", "Prague"). |
| Number | Place coordinate, decimal degrees. |
| Number | Place coordinate, decimal degrees. |
| String | Apple Maps identifier, when selected from a point of interest (POI). |
| String | The venue's own web address ("https://www.cafevita.com"). |
| String | The venue's phone number, exactly as the listing gives it. |
| String | Street line — house number and street ("1005 E Pike St"). |
| String | The town the venue stands in ("Seattle"). |
| String | Region or state, as the map data spells it ("WA", "Bavaria"). |
| String | Postal code ("98122"). |
| String | Country, in English ("United States"). |
The last seven are the place's own listing (v3.6): what Apple Maps knows about the venue, so the place card can show an address, a website and a phone number without going online. They are separate from detail, which stays the short "City, Country" line. All of them are optional. A place P.E.A. never looked up has none, and a listing that only had an address has no website or phone. Once recorded, P.E.A. doesn't update them. A listing is what the place said when you linked it, not what it says today.
Experiences carry their place inline, with placeName / placeLatitude / placeLongitude / placeMapItemIdentifier (see the Entry Schema above). The entry's own place fields say what's assigned to that experience. The top-level places array is your list of saved places. (An earlier version of this page said a future version "may add a foreign-key placeUUID". It didn't. Place identity below is what happened instead.)
On Add data to P.E.A. (merge), a place is a duplicate when the uuid matches, or when the name and both coordinates match to five decimals (about a meter). The content match is there because the same place can have different UUIDs on different devices, so a uuid-only check would double every place on a cross-device re-import. Two places with the same name at different coordinates stay two places, and both keep their name. Merge used to rename the incoming one ("Home 2"). It doesn't anymore. Names come through exactly as they are in the file, in both modes.
Place names are not unique. One archive can hold several places called "Home". Three offices of one company are three places with the same label. If you write code against this format, don't key places by name. Key by
uuid, or byidentityKeywhere it's there.
Place identity (v3.3)
A place has a stable key that doesn't depend on its name. These fields are in every build since August 2026. exportSchemaVersion stays 3, because they are all optional.
Field | On | Type | Meaning |
|---|---|---|---|
| entries | String, optional | Which place this experience belongs to. Starts with |
| places | String, optional | The same key on the saved place row. |
| places | String, optional | What Apple Maps called the place when you linked it. P.E.A. doesn't refresh it. |
| places | Number, optional | How far the place reaches, if you changed it. Absent means the app's default (100 m). |
| places | String, optional | The SF Symbol you picked for the place. Absent means the app picks one by category. |
| places | String, optional | Whether the place may associate itself with new experiences automatically: |
| places | String, optional |
|
| places | ISO-8601 string, optional | When the role was last set. Copy it as is; import doesn't re-stamp it. |
If you write archive files:
Treat identity keys as opaque. Don't read a coordinate or an Apple ID out of them, and don't make one up. Leave the field out and P.E.A. works out the identity on import.
Never copy a key onto a different place. Two places with one key are one place.
A file without any of these fields is still a valid v3 file.
Motto Schema
A motto is three words you entered or picked that show before archiving, on every device, to make it your own. Every motto is one object in the top-level mottos array.
Field | Type | Notes |
|---|---|---|
R | String | Identifier (Canonical UUID) for Mottos to match them across exports. |
R | String | First word/line of the motto. |
R | String | Second word/line. |
R | String | Third word/line. |
R | String (ISO 8601) | When the motto was saved. |
| Boolean |
|
Mottos are off until you turn them on in the app. Importing an archive with mottos turns them on, so the words you brought in show up.
On Add data to P.E.A. (merge), a motto is a duplicate when the uuid matches or when all three lines match exactly. Mottos are typed by hand and have no other identity, so the same three lines are the same motto even with different UUIDs (which happens across devices). inactive is a state, not an identity. It's not part of the match, and a merge never flips it on a motto you already have.
People Schema (v2)
People you can associate with an experience: friends, family, the person you ran into at the coffee shop. You add and associate them in the entry editor's People field. A converter can fill the same table: supply a top-level people array and point at its UUIDs from each entry's peopleUUIDs (a spreadsheet with a people column converts cleanly this way). One note: the Swarm converter creates no people, because Foursquare's export leaves out who you were with. The only trace is an @mention in the shout text.
Every person is one object in the top-level people array. Required fields are marked R.
Field | Type | Notes |
|---|---|---|
R | String | Unique ID (Canonical UUID (8-4-4-4-12 hex)). Stable across imports/exports. |
R | String | Display name ("Sawa", "Murat", "Fabian"). |
R | String (ISO 8601) | When this person was first added. |
R | String (ISO 8601) | Last time this person record changed. |
| String |
|
| String | Stable identifier from the originating system, when a converter supplies one. With |
| String | URL or local-asset path for a photo. P.E.A. doesn't download or display these yet — they round-trip only. |
| String | Free-form notes the user might keep on a person. Round-trip only — P.E.A. doesn't surface them yet. |
Entries point at people with a peopleUUIDs array (see Entry Schema → People links). One person can be on many experiences, and one experience can list many people. On import, P.E.A. creates every person first and then connects the entries, so every UUID finds its row.
People are created and associated in the entry editor: tap a suggestion or confirm a typed name. Archiving on the iPhone, the Mac, the watch, the widget or through Siri never creates a person, but when the text names someone already in People, P.E.A. associates them on arrival. People have their own page under Manage, a filter in the Index and on the Map, and a grouping in the Map's list, and they round-trip through every export.
On Add data to P.E.A. (merge), a person is a duplicate when the uuid matches, or when source and sourceID both match. People made in P.E.A. have no sourceID, so for them only the uuid counts.
P.E.A./Topic Schema (v3)
A topic is a word or two that says what an experience was about: a vocabulary you build as you archive ("soccer", "made", "noticed", "first time"). People answer who, places answer where, topics answer what. You create and associate them in the entry editor's Topics field: tap a suggestion, a default seed, or type a new one.
On screen they are Topics. In the file they are concepts. The app called them Concepts until September 2026, the key was published under that name, and a key in a public format doesn't change. Your older backups keep restoring, and a converter writes concepts, conceptUUIDs and conceptCount as before.
Every topic is one object in the top-level concepts array. Required fields are marked R. The shape is the same as People, so a converter or a reader can treat the two the same way.
Field | Type | Notes |
|---|---|---|
R | String | Canonical UUID (8-4-4-4-12 hex). Stable across exports. |
R | String | Display label, in the user's original casing ("soccer", "First Time"). |
R | String (ISO 8601) | When this topic was first added. |
R | String (ISO 8601) | Last time this topic changed. |
| String |
|
| String | Stable identifier from the originating system. With |
Entries point at topics with a conceptUUIDs array (see Entry Schema → Topic links). One topic can be on many experiences, and one experience can list many topics. On import, P.E.A. creates every topic first and then connects the entries.
On Add data to P.E.A. (merge), a topic is a duplicate when the uuid matches, or when source and sourceID both match. Topics made in P.E.A. have no sourceID, so for them only the uuid counts. Two archives that each made a "made" topic end up with two.
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-15T23:32:00Z", "updatedAt": "2026-01-15T23:32:00Z", "timeOfDay": "Evening", "dayOfWeek": "Thursday", "season": "Winter", "timeZoneID": "America/New_York", "latitude": 35.2271, "longitude": -80.8431, "locationName": "Walmart Supercenter", "locationLocality": "Charlotte", "locationAdministrativeArea": "NC", "locationSubLocality": "Wesley Heights", "locationSubAdministrativeArea": "Mecklenburg County", "locationPostalCode": "28208", "locationCountry": "United States", "placeName": "the Walmart by the airport", "placeDetail": "Charlotte, NC", "placeSource": "custom", "placeLatitude": 35.2270, "placeLongitude": -80.8430, "placeMapItemIdentifier": "I3F4A2C8B9D0E1F2A3B4C5D6E7F8A9B0", "placeCategory": "Store", "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", "websiteURL": "https://www.example-store.com/charlotte", "phoneNumber": "(704) 555-0148", "street": "3850 E Independence Blvd", "locality": "Charlotte", "administrativeArea": "NC", "postalCode": "28205", "country": "United States" } ], "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.
placeNameis 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 into a single PEA-Export-….json file and then bring it in via Manage → Import & Export → Add data to P.E.A. (on the Mac, File → Add Data to P.E.A.… opens the same picker). That's a merge. Everything you already have stays 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 rate it yourself by dragging the dot in the app, and that's the moment it gets its permanent number. So the prompts below don't ask the model to invent a strength. They set every row to Unset and leave the rating to you.
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 topic (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 people fields: uuid, name, createdAt, updatedAt.
- Required topic fields (in "concepts"): 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, using the LOCAL
time where the entry happened (not UTC).
- If the source data records the time zone an entry was written in,
emit it as "timeZoneID" using the IANA name (e.g. "Asia/Tokyo").
If the source only has a UTC offset or no zone at all, omit the
field entirely — never guess a zone from an offset.
- Omit any optional field you don't have data for. Do not write null.
Do NOT try to infer city, neighbourhood, county, postal code, region,
country, time zone, weather, or moon from
other fields — for entries that carry latitude/longitude, P.E.A.
completes those from the coordinates and the historical record after
import. Bringing accurate coordinates is worth more than guessed
metadata.
- The same rule for a place's listing fields ("websiteURL",
"phoneNumber", "street", "locality", "administrativeArea",
"postalCode", "country"): write them only where the source actually
records the venue's address or contact details. Never look one up and
never guess one from the name — P.E.A. resolves them itself after
import for any place that carries coordinates or an Apple Maps
identifier.
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 Manage → Import & Export → Add data to P.E.A.
Short prompt for Siri and Apple Intelligence
Siri, Apple Intelligence and other on-device models can't open links and don't have much room. This prompt carries the rules with it, so nothing needs to be fetched. Keep it short: a dozen entries at a time on-device, more through Private Cloud Compute or a chat model. In Shortcuts, add the Use Model action, pick On-Device or Private Cloud Compute, paste the prompt with your entries, and save what comes back as a .json file. It works the same way pasted into any chat.
Turn the entries below into one P.E.A. archive file. Answer with the
JSON only.
The file is one object with exactly these keys:
"exportSchemaVersion": 3, "exportDate" (now, ISO 8601, like
"2026-10-07T09:00:00Z"), "appVersion": "1.0", "readme": "Converted
for P.E.A.", "entryCount", "placeCount": 0, "mottoCount": 0,
"peopleCount", "conceptCount", "entries", "places": [], "mottos": [],
"people", "concepts".
One object in "entries" per entry:
- "uuid": a new random UUID (8-4-4-4-12 hex), different for every entry
- "archiveNumber": 0
- "text": the entry's own words, or "" if there are none
- "intensity": 1, "intensityLabel": "Minimum", "isIntensityUnset": true
- "createdAt" and "updatedAt": the entry's date and time, ISO 8601 with
the time zone (like "2025-04-29T20:05:00-07:00"). The same value in
both. If only the day is known, use 12:00.
- "timeOfDay" from the local clock: "Morning" before 12:00, "Afternoon"
from 12:00, "Evening" from 17:00, "Night" from 21:00
- "dayOfWeek": "Monday" to "Sunday"
- "season": "Spring" for March to May, "Summer" for June to August,
"Autumn" for September to November, "Winter" for December to February
- "source": "notes"
- Only when the entry says so: "locationName" (the place's name),
"locationLocality" (the town), "locationAdministrativeArea" (the
state or region), "locationCountry", "latitude" and "longitude" as
numbers. If an entry names a place, put the place's name in
"locationName" too.
People and topics: for every person named, one object in "people" with
"uuid", "name", "createdAt", "updatedAt" (same moment as "exportDate")
and "source": "notes". Put that person's uuid in the entry's
"peopleUUIDs". Topics go the same way into "concepts" and
"conceptUUIDs". Use one uuid per name, however often it comes up.
Leave out every field you don't have. Never write null. Don't guess a
place, a time zone, the weather or a rating. "entryCount",
"peopleCount" and "conceptCount" are the lengths of their arrays.
Entries:
[paste your notes, or rows from the P.E.A. spreadsheet template]Load the file through Manage → Import & Export → Add data to P.E.A. Every entry arrives Unset. Rate it, and it gets its number.
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.
Don't worry about how thin the venue data looks — Foursquare's export ships only the venue's name and a coordinate, no city or country. That's enough: after import, P.E.A. completes the city, neighbourhood, county, postal code, region, country, and time zone from each check-in's coordinates (and the day's weather and moon from the historical record, where available), so the entries fill out on their own.
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:
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. archive next to the originals (format v2, which P.E.A. still reads):
That writes data-export-NNNN/pea-swarm.json. Then Manage → Import & Export → 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 have no number. Every row arrives with archiveNumber: 0 and shows in the app as P.E. #NaN. The app gives it a permanent P.E. #N the moment you rate it in the editor (drag the dot to any green step). Until then a check-in is a candidate, not a P.E. Setting it back to Unset later keeps the number it got. A row you never rate stays numberless, and if you neutralize it, it's deleted after 30 days without ever using a number.
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:
createdAtis 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 undervenue. Thevenueblock 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 noshoutat all.timeZoneOffsetis minutes from UTC at the time of the check-in (e.g.-420is Pacific Daylight Time).
The corresponding P.E.A. 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. | Note |
|---|---|---|
|
| Half of the |
|
| Constructed as |
|
| Treat the string as UTC, shift by |
|
| Empty shouts are fine; |
|
| Copy verbatim. |
|
| Omitted if the check-in has no |
derived from local time |
| Compute from the local time (after the timezone shift), not from UTC. |
derived per-row |
| Fresh random UUID, one per check-in. |
constant |
| Always |
constant |
| 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 |
| Stamp every row. Lets the app tell native captures from imported ones; sticky forever in round-trips. |
raw check-in (stringified) |
| 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. |
| — | Dropped. No P.E.A. equivalent today. Preserved inside |
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. 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:
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 topics you want to associate, 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 person (Felix) and two topics, captured on an Apple Watch.
Row 2 — Bunched and finished so many annoying tasks today. The most. No-coordinate case: street name + city + state + weather only, with one topic. 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:
Open the CSV in your spreadsheet of choice.
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.
To associate people or topics, type their names in the
peopleandtopicscells. Separate several with semicolons ("Felix; Mara"). Don't worry about UUIDs, the conversion step makes them.Save as CSV.
Convert CSV → P.E.A. JSON. The simplest path is to paste the CSV plus this prompt into a local LLM (the short Siri prompt above takes the rows too):
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 "topics" 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". Topics
go into "concepts"; that is the key's name. 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]Save the resulting JSON file and load it through Manage → Import & Export → 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 leaves out what the conversion fills in for you (the uuid, the timeOfDay / dayOfWeek / season labels) and the strength columns (every imported row arrives Unset, so there's nothing to rate in advance). If you need saved places, moon phase, speed/elevation/steps, the Apple Music store ID or a sourceURL, edit the JSON 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 merge. New rows are added. Rows that already exist (same
uuid, same(source, sourceID)for imported data, or the samecreatedAtandtext) are skipped, so one moment under two ids lands once. This is for converted imports, Swarm, Day One, a spreadsheet, and also for adding an existing P.E.A. backup to what you have.Restore P.E.A. from backup — This replaces your current data. It's 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 P.E.A. exported.
Schema Version History
exportSchemaVersion is 3 and has been since June 2026. The point releases below added optional fields and changed nothing for files that don't have them. The build numbers are TestFlight builds.
Page revision, October 8, 2026. No format change. Concepts are called Topics in the app since September 27, 2026; the keys (
concepts,conceptUUIDs,conceptCount) stay as they are. Every "not shipped yet" note below is gone, because everything shipped. New on the page: the short prompt for Siri and Apple Intelligence, and the hour bands fortimeOfDayand the months forseasonin the Time table. The spreadsheet template'sconceptscolumn is nowtopics, it gainedlocationAdministrativeArea, and its watch row sayswatch.v3.6 — Region, neighbourhood, county and postal code on entries, and the listing on places. (September 2026, in every build since 91.)
Per-entry
locationAdministrativeArea: the state or region, exactly as the map data gives it: "WA" in one country, "Bavaria" in another. P.E.A. shows it next to the town ("Seattle, WA") and falls back to the two-letter country code when there is no region. There is no table that expands or shortens these, so copy your source's spelling. Leave it out when you don't know it, and P.E.A. looks it up from the coordinates after import, like the city and the country.Per-entry
locationSubLocality,locationSubAdministrativeAreaandlocationPostalCode: the neighbourhood ("Ballard"), the county or its local equivalent ("King County") and the postal code ("98117"). Same rules as the region. Nothing in the app shows these three yet. They are kept so an archive written today already has them.Per-place
websiteURL,phoneNumber,street,locality,administrativeArea,postalCodeandcountry: the place's own listing, so the place card can show an address, a website and a phone number without going online.detailis unchanged and still the short "City, Country" line. Write these if your source knows the venue's address. If it only knows a coordinate, leave them out and P.E.A. looks the place up after import. Once recorded, P.E.A. doesn't overwrite them.All eleven are optional.
exportSchemaVersionstays 3, older files import unchanged, older builds ignore the fields.
v3.5 — Place association and role. (August 2026, in every build since 70.)
Per-place
associationState: whether the place may associate itself with new experiences automatically ("notLicensed"/"disabled"/"enabled"). Absent means something: the place was saved before the field existed and behaves as enabled. Keep it absent when you convert a file.Per-place
role("home"/"work"/"other") androleUpdatedAt(ISO 8601): the place's role in your life and when you last set it. There is at most one home and one work. The timestamp is there so a sync conflict can be settled by a person. Imports copy it as is.All three are optional.
exportSchemaVersionstays 3.
v3.4 — Motto active state. (August 2026, in every build since 70.)
Per-motto
inactive:truekeeps a motto in the archive but stops showing it, on every device. Left out when the motto is active. Not part of the motto's identity, so the merge still matches byuuidor by the three lines. Optional.exportSchemaVersionstays 3.
v3.3 — Place identity. (July 2026, in every build since 70. See Place identity above.)
Per-entry
placeIdentityKey: which place an experience belongs to, as a stable key.apple:for a place backed by an Apple Maps Place ID,local:for one P.E.A. created. The key, not the name, says "these two experiences happened at the same place", which is why one archive can hold several places called "Home".Per-place
identityKey,providerName,radiusMeters,symbol: the same key on the saved place, what Apple Maps called the place when you linked it (not refreshed), how far the place reaches in meters, and the SF Symbol you picked. NoradiusMetersmeans the app's default. Nosymbolmeans the app picks one by category.Identity keys are opaque. Don't parse one, don't make one up, don't copy one onto another place. Leave them out and P.E.A. works out identity itself.
All five are optional.
exportSchemaVersionstays 3.
v3.2 — Time zone. (July 2026, in every build since 70.)
Per-entry
timeZoneID: the IANA time zone the experience happened in ("Asia/Tokyo"), set at creation. When it's there,timeOfDayanddayOfWeekare read on that clock, so an experience archived in another zone keeps its wall clock instead of drifting with the device. Converters write it only when the source really records a zone (Day One'stimeZone, for example), never from a bare UTC offset. Optional.exportSchemaVersionstays 3.
v3.1 — Place category. (July 2026, in every build since 70.)
Per-entry
placeCategory: the Apple Maps point-of-interest category of an assigned place, human-readable ("Cafe", "Amusement Park"). Set when a place is picked from Apple Maps search. Plain addresses and custom places have none. Optional.exportSchemaVersionstays 3.
v3 — Concepts, which are Topics now. (June 2026.)
conceptstop-level collection +conceptCount. A word or two that says what an experience was about ("soccer", "made", "noticed"), created and associated in the entry editor. Native rows are stampedsource: "pea"with nosourceID. The app renamed them Topics in September 2026; the keys didn't change.Per-entry
conceptUUIDsarray, linking each entry to its topics. Many-to-many, same shape aspeopleUUIDs.On Add data to P.E.A. (merge), topics match by
uuidor(source, sourceID). Native topics (nosourceID) match byuuidonly.People reached the editor. People came in at v2 as a round trip only. From v3 you create and associate them in the entry editor's People field. The wire format is unchanged.
Both additions are optional, so a v1 or v2 archive imports unchanged on a v3 build. An older build refuses a v3 archive with a "please update" message rather than dropping the topics.
v2 — Add data + Unset + People (schema only). (May 2026.)
peopletop-level collection +peopleCount. People round-trip through export and import. The in-app surfaces came with v3.Per-entry
peopleUUIDsarray linking each entry to people in the top-levelpeoplearray.Per-entry
sourceID: a stable identifier from the originating system. Withsource, it's the merge key for "Add data to P.E.A.". The samecreatedAtandtextalso mark a duplicate.Per-entry
importPayload: the original record as a string. P.E.A. doesn't read it. It's there so a future import can reach back into fields P.E.A. didn't model the first time.Per-entry
sourceURL: a link back to the original record (Swarm check-in, Day One entry, tweet). Shows as a tappable "View external experience" row. Native captures have none.Per-entry
isIntensityUnset: true on rows that came in through merge from another source and haven't been rated. Shows as a solid light-gray dot at the smallest size (the dashed gray dot is Neutralized, a different thing). Never on native entries.All five are optional, so a v1 archive imports unchanged on a v2 build.
v1 — The first public schema. (May 2026.)
One JSON file, no zip.
Top-level
entries,places,mottos.A stable
uuidon every row, for round trips.A permanent
archiveNumberon every entry (the "P.E. #X").A
readmefield, so a person can read the file.Place fields on the entry next to the top-level
placesarray, so an entry is self-describing without cross-references.Per-entry
source:"pea"for native captures, the service name for converted imports. Optional. A file without it is treated as all native.
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 update the app.
P.E.A. is designed to hold all your positive experiences. That also means it's been designed to bring in any data that you would like to capture inside the archive, such as Swarm/Foursquare, Day One, or anything else you'd like to add via a spreadsheet.
This data is kept absolutely private on your devices and personal 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. This page describes format v3.6 (exportSchemaVersion 3) and was last updated on October 8, 2026. The version history is at the bottom.
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:
exportSchemaVersionsets the rules for how P.E.A. reads your data. It won't import a file it doesn't know how to read, rather than guess.entries,places,mottos,people,conceptsare the building blocks of data inside P.E.A. so they are independent top-level collections. (conceptsholds the Topics. The app called them Concepts when the format was published, and a key in a public format never changes, so your older backups keep working.) Each data point for them carries a unique identifier oruuidso 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-levelplacesarray gives you the list of places as the user organized them. People and topics work the other way around — entries reference them by uuid (peopleUUIDs,conceptUUIDs). The top-levelpeople/conceptslists are simpler and hold all records.Optional fields are not stored when the data isn't there. Some other apps carry
nullor empty fields. 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 | String | This number identifies each entry uniquely / Canonical UUID (8-4-4-4-12 hex). |
R | Integer | This is the number |
R | String | Text you entered or dictated about the experience. This is optional and can be empty. |
R | Integer 1–7 | Strength of the experience. 7 being life changing. |
R | String | Labels that accompany the intensity number: "Minimum", "Light", "Mild", "Medium", "Strong", "High", "Maximum". |
Time
Field | Type | Notes |
|---|---|---|
R | String (ISO 8601) | When the dot was released. |
R | String (ISO 8601) | Last time an experience entry was edited. |
R | String | "Morning" (midnight to 11:59), "Afternoon" (noon to 16:59), "Evening" (17:00 to 20:59), "Night" (21:00 to 23:59), on the local clock. |
R | String | "Monday" through "Sunday". |
R | String | "Spring" (March to May), "Summer" (June to August), "Autumn" (September to November), "Winter" (December to February). |
| String | The time zone the experience happened in, as an IANA name ("America/New_York", "Asia/Tokyo"). It lets the entry keep the clock of the place where it happened. When it's there, P.E.A. reads |
Your physical location at capture
This metadata comes from the device. They are testimony, not opinion. P.E.A. never edits a captured value. It does fill in missing ones: when an imported entry has coordinates and leaves the fields below empty, P.E.A. looks up the city, neighbourhood, county, postal code, region, country and time zone after import, and the day's weather and moon from the historical record where it exists. So bring the coordinates, leave out what you don't know, and never guess.
Field | Type | Notes |
|---|---|---|
| Number | Raw GPS, decimal degrees. |
| Number | Raw GPS, decimal degrees. |
| String | Reverse-geocoded name ("Café Vita"). |
| String | City or town. Omitted? P.E.A. derives it from the coordinates after import. |
| String | Region or state, exactly as the map data gives it ("WA", "Bavaria"). Same completion as the city. |
| String | Neighbourhood or district ("Ballard"). Same completion as the city. |
| String | County, or whatever the country's equivalent is called ("King County"). Same completion as the city. |
| String | Postal or ZIP code ("98117"), exactly as the map data gives it. Same completion as the city. |
| String | Country. Same completion as the city. |
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, later experiences within its radius (100 m unless you change it) get that place automatically. The fields below are optional. They mirror a row in the top-level places collection.
Field | Type | Notes |
|---|---|---|
| String | The label you chose ("home", "Mom's house"). |
| String | Locality context for that place. |
| String |
|
| Number | Place coordinate. |
| Number | Place coordinate. |
| String | Apple Maps identifier, when assigned from POI. |
| String | Apple Maps point-of-interest category, human-readable ("Cafe", "Amusement Park"). Only present for real POIs — plain addresses and custom places have none. |
| Boolean | True if you explicitly assigned this place. |
Weather and sky
Omit these when your source doesn't have them: for entries with coordinates, P.E.A. recovers the hour's condition and temperature and the day's moon phase from the historical weather record after import, where coverage exists. (This lookup honors the in-app weather capture setting — users who turned weather capture off get no lookups.)
Field | Type | Notes |
|---|---|---|
| String | "Clear", "Cloudy", "Rain", … |
| Number | Captured in metric; both units shown in-app. |
| String | "Full Moon", "Waxing Crescent", … |
Activity, motion, environment
Field | Type | Notes |
|---|---|---|
| String | "stationary", "walking", "running", "cycling", "driving". |
| Number | Meters per second. |
| Number | Elevation in meters. |
| Integer | Pedometer count for the day of capture. |
| Number | Ambient noise in dB. |
| String | Comma-separated nearby points of interest. |
Music
Field | Type | Notes |
|---|---|---|
| String | Song title playing at capture (Apple Music). |
| String | Artist name. |
| String | Apple Music catalog ID, for deep linking. |
Capture source device
Field | Type | Notes |
|---|---|---|
| String | "Timothy's iPhone", "Siri", "Watch" etc. |
| String | Semantic device category: |
Source provenance and identity (v2)
Which app or format the entry came from, plus a stable identifier from that source. Together they keep re-imports from creating duplicates: when you bring data in through Add data to P.E.A., a row with the same (source, sourceID) as one already in the archive is skipped. So is a row whose createdAt and text both match an entry in the archive or an earlier row in the same file, whatever its ids. One moment recorded twice lands once, and the preview tells you before you confirm.
P.E.A. keeps these values forever. Once an entry is stamped ("swarm", "5c1b…"), it stays that way through every export, even if you re-import the archive on another device.
Field | Type | Notes |
|---|---|---|
| String |
|
| 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, |
| 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. |
| 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 |
|---|---|---|
| Boolean | Only on rows that came in from another source through Add data to P.E.A. and haven't been rated yet. When it's |
People links (v2)
Field | Type | Notes |
|---|---|---|
| Array of Strings | The UUIDs of the people associated with this experience. Each one matches one row in the top-level |
Topic links (v3)
Field | Type | Notes |
|---|---|---|
| Array of Strings | The UUIDs of the topics associated with this experience (sports, travel, first time, …). Each one matches one row in the top-level |
Soft-deletion (Neutralization)
When you neutralize a positive experience it's removed from the list and available in the Index via the Neutralized filter for 30 days. After that time, or when you delete it yourself, it's gone from P.E.A.
Field | Type | Notes |
|---|---|---|
| Boolean | Present and |
| String (ISO 8601) | When you neutralized it. |
| 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 | String | Identifier (Canonical UUID) for places to match them across exports. |
R | String | The name you gave the place ("Home", "Jan's Haus"). |
R | String (ISO 8601) | When you saved the place. |
| String | Locality context ("Helsinki", "Seattle", "Prague"). |
| Number | Place coordinate, decimal degrees. |
| Number | Place coordinate, decimal degrees. |
| String | Apple Maps identifier, when selected from a point of interest (POI). |
| String | The venue's own web address ("https://www.cafevita.com"). |
| String | The venue's phone number, exactly as the listing gives it. |
| String | Street line — house number and street ("1005 E Pike St"). |
| String | The town the venue stands in ("Seattle"). |
| String | Region or state, as the map data spells it ("WA", "Bavaria"). |
| String | Postal code ("98122"). |
| String | Country, in English ("United States"). |
The last seven are the place's own listing (v3.6): what Apple Maps knows about the venue, so the place card can show an address, a website and a phone number without going online. They are separate from detail, which stays the short "City, Country" line. All of them are optional. A place P.E.A. never looked up has none, and a listing that only had an address has no website or phone. Once recorded, P.E.A. doesn't update them. A listing is what the place said when you linked it, not what it says today.
Experiences carry their place inline, with placeName / placeLatitude / placeLongitude / placeMapItemIdentifier (see the Entry Schema above). The entry's own place fields say what's assigned to that experience. The top-level places array is your list of saved places. (An earlier version of this page said a future version "may add a foreign-key placeUUID". It didn't. Place identity below is what happened instead.)
On Add data to P.E.A. (merge), a place is a duplicate when the uuid matches, or when the name and both coordinates match to five decimals (about a meter). The content match is there because the same place can have different UUIDs on different devices, so a uuid-only check would double every place on a cross-device re-import. Two places with the same name at different coordinates stay two places, and both keep their name. Merge used to rename the incoming one ("Home 2"). It doesn't anymore. Names come through exactly as they are in the file, in both modes.
Place names are not unique. One archive can hold several places called "Home". Three offices of one company are three places with the same label. If you write code against this format, don't key places by name. Key by
uuid, or byidentityKeywhere it's there.
Place identity (v3.3)
A place has a stable key that doesn't depend on its name. These fields are in every build since August 2026. exportSchemaVersion stays 3, because they are all optional.
Field | On | Type | Meaning |
|---|---|---|---|
| entries | String, optional | Which place this experience belongs to. Starts with |
| places | String, optional | The same key on the saved place row. |
| places | String, optional | What Apple Maps called the place when you linked it. P.E.A. doesn't refresh it. |
| places | Number, optional | How far the place reaches, if you changed it. Absent means the app's default (100 m). |
| places | String, optional | The SF Symbol you picked for the place. Absent means the app picks one by category. |
| places | String, optional | Whether the place may associate itself with new experiences automatically: |
| places | String, optional |
|
| places | ISO-8601 string, optional | When the role was last set. Copy it as is; import doesn't re-stamp it. |
If you write archive files:
Treat identity keys as opaque. Don't read a coordinate or an Apple ID out of them, and don't make one up. Leave the field out and P.E.A. works out the identity on import.
Never copy a key onto a different place. Two places with one key are one place.
A file without any of these fields is still a valid v3 file.
Motto Schema
A motto is three words you entered or picked that show before archiving, on every device, to make it your own. Every motto is one object in the top-level mottos array.
Field | Type | Notes |
|---|---|---|
R | String | Identifier (Canonical UUID) for Mottos to match them across exports. |
R | String | First word/line of the motto. |
R | String | Second word/line. |
R | String | Third word/line. |
R | String (ISO 8601) | When the motto was saved. |
| Boolean |
|
Mottos are off until you turn them on in the app. Importing an archive with mottos turns them on, so the words you brought in show up.
On Add data to P.E.A. (merge), a motto is a duplicate when the uuid matches or when all three lines match exactly. Mottos are typed by hand and have no other identity, so the same three lines are the same motto even with different UUIDs (which happens across devices). inactive is a state, not an identity. It's not part of the match, and a merge never flips it on a motto you already have.
People Schema (v2)
People you can associate with an experience: friends, family, the person you ran into at the coffee shop. You add and associate them in the entry editor's People field. A converter can fill the same table: supply a top-level people array and point at its UUIDs from each entry's peopleUUIDs (a spreadsheet with a people column converts cleanly this way). One note: the Swarm converter creates no people, because Foursquare's export leaves out who you were with. The only trace is an @mention in the shout text.
Every person is one object in the top-level people array. Required fields are marked R.
Field | Type | Notes |
|---|---|---|
R | String | Unique ID (Canonical UUID (8-4-4-4-12 hex)). Stable across imports/exports. |
R | String | Display name ("Sawa", "Murat", "Fabian"). |
R | String (ISO 8601) | When this person was first added. |
R | String (ISO 8601) | Last time this person record changed. |
| String |
|
| String | Stable identifier from the originating system, when a converter supplies one. With |
| String | URL or local-asset path for a photo. P.E.A. doesn't download or display these yet — they round-trip only. |
| String | Free-form notes the user might keep on a person. Round-trip only — P.E.A. doesn't surface them yet. |
Entries point at people with a peopleUUIDs array (see Entry Schema → People links). One person can be on many experiences, and one experience can list many people. On import, P.E.A. creates every person first and then connects the entries, so every UUID finds its row.
People are created and associated in the entry editor: tap a suggestion or confirm a typed name. Archiving on the iPhone, the Mac, the watch, the widget or through Siri never creates a person, but when the text names someone already in People, P.E.A. associates them on arrival. People have their own page under Manage, a filter in the Index and on the Map, and a grouping in the Map's list, and they round-trip through every export.
On Add data to P.E.A. (merge), a person is a duplicate when the uuid matches, or when source and sourceID both match. People made in P.E.A. have no sourceID, so for them only the uuid counts.
P.E.A./Topic Schema (v3)
A topic is a word or two that says what an experience was about: a vocabulary you build as you archive ("soccer", "made", "noticed", "first time"). People answer who, places answer where, topics answer what. You create and associate them in the entry editor's Topics field: tap a suggestion, a default seed, or type a new one.
On screen they are Topics. In the file they are concepts. The app called them Concepts until September 2026, the key was published under that name, and a key in a public format doesn't change. Your older backups keep restoring, and a converter writes concepts, conceptUUIDs and conceptCount as before.
Every topic is one object in the top-level concepts array. Required fields are marked R. The shape is the same as People, so a converter or a reader can treat the two the same way.
Field | Type | Notes |
|---|---|---|
R | String | Canonical UUID (8-4-4-4-12 hex). Stable across exports. |
R | String | Display label, in the user's original casing ("soccer", "First Time"). |
R | String (ISO 8601) | When this topic was first added. |
R | String (ISO 8601) | Last time this topic changed. |
| String |
|
| String | Stable identifier from the originating system. With |
Entries point at topics with a conceptUUIDs array (see Entry Schema → Topic links). One topic can be on many experiences, and one experience can list many topics. On import, P.E.A. creates every topic first and then connects the entries.
On Add data to P.E.A. (merge), a topic is a duplicate when the uuid matches, or when source and sourceID both match. Topics made in P.E.A. have no sourceID, so for them only the uuid counts. Two archives that each made a "made" topic end up with two.
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-15T23:32:00Z", "updatedAt": "2026-01-15T23:32:00Z", "timeOfDay": "Evening", "dayOfWeek": "Thursday", "season": "Winter", "timeZoneID": "America/New_York", "latitude": 35.2271, "longitude": -80.8431, "locationName": "Walmart Supercenter", "locationLocality": "Charlotte", "locationAdministrativeArea": "NC", "locationSubLocality": "Wesley Heights", "locationSubAdministrativeArea": "Mecklenburg County", "locationPostalCode": "28208", "locationCountry": "United States", "placeName": "the Walmart by the airport", "placeDetail": "Charlotte, NC", "placeSource": "custom", "placeLatitude": 35.2270, "placeLongitude": -80.8430, "placeMapItemIdentifier": "I3F4A2C8B9D0E1F2A3B4C5D6E7F8A9B0", "placeCategory": "Store", "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", "websiteURL": "https://www.example-store.com/charlotte", "phoneNumber": "(704) 555-0148", "street": "3850 E Independence Blvd", "locality": "Charlotte", "administrativeArea": "NC", "postalCode": "28205", "country": "United States" } ], "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.
placeNameis 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 into a single PEA-Export-….json file and then bring it in via Manage → Import & Export → Add data to P.E.A. (on the Mac, File → Add Data to P.E.A.… opens the same picker). That's a merge. Everything you already have stays 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 rate it yourself by dragging the dot in the app, and that's the moment it gets its permanent number. So the prompts below don't ask the model to invent a strength. They set every row to Unset and leave the rating to you.
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 topic (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 people fields: uuid, name, createdAt, updatedAt.
- Required topic fields (in "concepts"): 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, using the LOCAL
time where the entry happened (not UTC).
- If the source data records the time zone an entry was written in,
emit it as "timeZoneID" using the IANA name (e.g. "Asia/Tokyo").
If the source only has a UTC offset or no zone at all, omit the
field entirely — never guess a zone from an offset.
- Omit any optional field you don't have data for. Do not write null.
Do NOT try to infer city, neighbourhood, county, postal code, region,
country, time zone, weather, or moon from
other fields — for entries that carry latitude/longitude, P.E.A.
completes those from the coordinates and the historical record after
import. Bringing accurate coordinates is worth more than guessed
metadata.
- The same rule for a place's listing fields ("websiteURL",
"phoneNumber", "street", "locality", "administrativeArea",
"postalCode", "country"): write them only where the source actually
records the venue's address or contact details. Never look one up and
never guess one from the name — P.E.A. resolves them itself after
import for any place that carries coordinates or an Apple Maps
identifier.
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 Manage → Import & Export → Add data to P.E.A.
Short prompt for Siri and Apple Intelligence
Siri, Apple Intelligence and other on-device models can't open links and don't have much room. This prompt carries the rules with it, so nothing needs to be fetched. Keep it short: a dozen entries at a time on-device, more through Private Cloud Compute or a chat model. In Shortcuts, add the Use Model action, pick On-Device or Private Cloud Compute, paste the prompt with your entries, and save what comes back as a .json file. It works the same way pasted into any chat.
Turn the entries below into one P.E.A. archive file. Answer with the
JSON only.
The file is one object with exactly these keys:
"exportSchemaVersion": 3, "exportDate" (now, ISO 8601, like
"2026-10-07T09:00:00Z"), "appVersion": "1.0", "readme": "Converted
for P.E.A.", "entryCount", "placeCount": 0, "mottoCount": 0,
"peopleCount", "conceptCount", "entries", "places": [], "mottos": [],
"people", "concepts".
One object in "entries" per entry:
- "uuid": a new random UUID (8-4-4-4-12 hex), different for every entry
- "archiveNumber": 0
- "text": the entry's own words, or "" if there are none
- "intensity": 1, "intensityLabel": "Minimum", "isIntensityUnset": true
- "createdAt" and "updatedAt": the entry's date and time, ISO 8601 with
the time zone (like "2025-04-29T20:05:00-07:00"). The same value in
both. If only the day is known, use 12:00.
- "timeOfDay" from the local clock: "Morning" before 12:00, "Afternoon"
from 12:00, "Evening" from 17:00, "Night" from 21:00
- "dayOfWeek": "Monday" to "Sunday"
- "season": "Spring" for March to May, "Summer" for June to August,
"Autumn" for September to November, "Winter" for December to February
- "source": "notes"
- Only when the entry says so: "locationName" (the place's name),
"locationLocality" (the town), "locationAdministrativeArea" (the
state or region), "locationCountry", "latitude" and "longitude" as
numbers. If an entry names a place, put the place's name in
"locationName" too.
People and topics: for every person named, one object in "people" with
"uuid", "name", "createdAt", "updatedAt" (same moment as "exportDate")
and "source": "notes". Put that person's uuid in the entry's
"peopleUUIDs". Topics go the same way into "concepts" and
"conceptUUIDs". Use one uuid per name, however often it comes up.
Leave out every field you don't have. Never write null. Don't guess a
place, a time zone, the weather or a rating. "entryCount",
"peopleCount" and "conceptCount" are the lengths of their arrays.
Entries:
[paste your notes, or rows from the P.E.A. spreadsheet template]Load the file through Manage → Import & Export → Add data to P.E.A. Every entry arrives Unset. Rate it, and it gets its number.
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.
Don't worry about how thin the venue data looks — Foursquare's export ships only the venue's name and a coordinate, no city or country. That's enough: after import, P.E.A. completes the city, neighbourhood, county, postal code, region, country, and time zone from each check-in's coordinates (and the day's weather and moon from the historical record, where available), so the entries fill out on their own.
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:
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. archive next to the originals (format v2, which P.E.A. still reads):
That writes data-export-NNNN/pea-swarm.json. Then Manage → Import & Export → 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 have no number. Every row arrives with archiveNumber: 0 and shows in the app as P.E. #NaN. The app gives it a permanent P.E. #N the moment you rate it in the editor (drag the dot to any green step). Until then a check-in is a candidate, not a P.E. Setting it back to Unset later keeps the number it got. A row you never rate stays numberless, and if you neutralize it, it's deleted after 30 days without ever using a number.
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:
createdAtis 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 undervenue. Thevenueblock 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 noshoutat all.timeZoneOffsetis minutes from UTC at the time of the check-in (e.g.-420is Pacific Daylight Time).
The corresponding P.E.A. 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. | Note |
|---|---|---|
|
| Half of the |
|
| Constructed as |
|
| Treat the string as UTC, shift by |
|
| Empty shouts are fine; |
|
| Copy verbatim. |
|
| Omitted if the check-in has no |
derived from local time |
| Compute from the local time (after the timezone shift), not from UTC. |
derived per-row |
| Fresh random UUID, one per check-in. |
constant |
| Always |
constant |
| 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 |
| Stamp every row. Lets the app tell native captures from imported ones; sticky forever in round-trips. |
raw check-in (stringified) |
| 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. |
| — | Dropped. No P.E.A. equivalent today. Preserved inside |
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. 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:
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 topics you want to associate, 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 person (Felix) and two topics, captured on an Apple Watch.
Row 2 — Bunched and finished so many annoying tasks today. The most. No-coordinate case: street name + city + state + weather only, with one topic. 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:
Open the CSV in your spreadsheet of choice.
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.
To associate people or topics, type their names in the
peopleandtopicscells. Separate several with semicolons ("Felix; Mara"). Don't worry about UUIDs, the conversion step makes them.Save as CSV.
Convert CSV → P.E.A. JSON. The simplest path is to paste the CSV plus this prompt into a local LLM (the short Siri prompt above takes the rows too):
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 "topics" 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". Topics
go into "concepts"; that is the key's name. 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]Save the resulting JSON file and load it through Manage → Import & Export → 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 leaves out what the conversion fills in for you (the uuid, the timeOfDay / dayOfWeek / season labels) and the strength columns (every imported row arrives Unset, so there's nothing to rate in advance). If you need saved places, moon phase, speed/elevation/steps, the Apple Music store ID or a sourceURL, edit the JSON 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 merge. New rows are added. Rows that already exist (same
uuid, same(source, sourceID)for imported data, or the samecreatedAtandtext) are skipped, so one moment under two ids lands once. This is for converted imports, Swarm, Day One, a spreadsheet, and also for adding an existing P.E.A. backup to what you have.Restore P.E.A. from backup — This replaces your current data. It's 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 P.E.A. exported.
Schema Version History
exportSchemaVersion is 3 and has been since June 2026. The point releases below added optional fields and changed nothing for files that don't have them. The build numbers are TestFlight builds.
Page revision, October 8, 2026. No format change. Concepts are called Topics in the app since September 27, 2026; the keys (
concepts,conceptUUIDs,conceptCount) stay as they are. Every "not shipped yet" note below is gone, because everything shipped. New on the page: the short prompt for Siri and Apple Intelligence, and the hour bands fortimeOfDayand the months forseasonin the Time table. The spreadsheet template'sconceptscolumn is nowtopics, it gainedlocationAdministrativeArea, and its watch row sayswatch.v3.6 — Region, neighbourhood, county and postal code on entries, and the listing on places. (September 2026, in every build since 91.)
Per-entry
locationAdministrativeArea: the state or region, exactly as the map data gives it: "WA" in one country, "Bavaria" in another. P.E.A. shows it next to the town ("Seattle, WA") and falls back to the two-letter country code when there is no region. There is no table that expands or shortens these, so copy your source's spelling. Leave it out when you don't know it, and P.E.A. looks it up from the coordinates after import, like the city and the country.Per-entry
locationSubLocality,locationSubAdministrativeAreaandlocationPostalCode: the neighbourhood ("Ballard"), the county or its local equivalent ("King County") and the postal code ("98117"). Same rules as the region. Nothing in the app shows these three yet. They are kept so an archive written today already has them.Per-place
websiteURL,phoneNumber,street,locality,administrativeArea,postalCodeandcountry: the place's own listing, so the place card can show an address, a website and a phone number without going online.detailis unchanged and still the short "City, Country" line. Write these if your source knows the venue's address. If it only knows a coordinate, leave them out and P.E.A. looks the place up after import. Once recorded, P.E.A. doesn't overwrite them.All eleven are optional.
exportSchemaVersionstays 3, older files import unchanged, older builds ignore the fields.
v3.5 — Place association and role. (August 2026, in every build since 70.)
Per-place
associationState: whether the place may associate itself with new experiences automatically ("notLicensed"/"disabled"/"enabled"). Absent means something: the place was saved before the field existed and behaves as enabled. Keep it absent when you convert a file.Per-place
role("home"/"work"/"other") androleUpdatedAt(ISO 8601): the place's role in your life and when you last set it. There is at most one home and one work. The timestamp is there so a sync conflict can be settled by a person. Imports copy it as is.All three are optional.
exportSchemaVersionstays 3.
v3.4 — Motto active state. (August 2026, in every build since 70.)
Per-motto
inactive:truekeeps a motto in the archive but stops showing it, on every device. Left out when the motto is active. Not part of the motto's identity, so the merge still matches byuuidor by the three lines. Optional.exportSchemaVersionstays 3.
v3.3 — Place identity. (July 2026, in every build since 70. See Place identity above.)
Per-entry
placeIdentityKey: which place an experience belongs to, as a stable key.apple:for a place backed by an Apple Maps Place ID,local:for one P.E.A. created. The key, not the name, says "these two experiences happened at the same place", which is why one archive can hold several places called "Home".Per-place
identityKey,providerName,radiusMeters,symbol: the same key on the saved place, what Apple Maps called the place when you linked it (not refreshed), how far the place reaches in meters, and the SF Symbol you picked. NoradiusMetersmeans the app's default. Nosymbolmeans the app picks one by category.Identity keys are opaque. Don't parse one, don't make one up, don't copy one onto another place. Leave them out and P.E.A. works out identity itself.
All five are optional.
exportSchemaVersionstays 3.
v3.2 — Time zone. (July 2026, in every build since 70.)
Per-entry
timeZoneID: the IANA time zone the experience happened in ("Asia/Tokyo"), set at creation. When it's there,timeOfDayanddayOfWeekare read on that clock, so an experience archived in another zone keeps its wall clock instead of drifting with the device. Converters write it only when the source really records a zone (Day One'stimeZone, for example), never from a bare UTC offset. Optional.exportSchemaVersionstays 3.
v3.1 — Place category. (July 2026, in every build since 70.)
Per-entry
placeCategory: the Apple Maps point-of-interest category of an assigned place, human-readable ("Cafe", "Amusement Park"). Set when a place is picked from Apple Maps search. Plain addresses and custom places have none. Optional.exportSchemaVersionstays 3.
v3 — Concepts, which are Topics now. (June 2026.)
conceptstop-level collection +conceptCount. A word or two that says what an experience was about ("soccer", "made", "noticed"), created and associated in the entry editor. Native rows are stampedsource: "pea"with nosourceID. The app renamed them Topics in September 2026; the keys didn't change.Per-entry
conceptUUIDsarray, linking each entry to its topics. Many-to-many, same shape aspeopleUUIDs.On Add data to P.E.A. (merge), topics match by
uuidor(source, sourceID). Native topics (nosourceID) match byuuidonly.People reached the editor. People came in at v2 as a round trip only. From v3 you create and associate them in the entry editor's People field. The wire format is unchanged.
Both additions are optional, so a v1 or v2 archive imports unchanged on a v3 build. An older build refuses a v3 archive with a "please update" message rather than dropping the topics.
v2 — Add data + Unset + People (schema only). (May 2026.)
peopletop-level collection +peopleCount. People round-trip through export and import. The in-app surfaces came with v3.Per-entry
peopleUUIDsarray linking each entry to people in the top-levelpeoplearray.Per-entry
sourceID: a stable identifier from the originating system. Withsource, it's the merge key for "Add data to P.E.A.". The samecreatedAtandtextalso mark a duplicate.Per-entry
importPayload: the original record as a string. P.E.A. doesn't read it. It's there so a future import can reach back into fields P.E.A. didn't model the first time.Per-entry
sourceURL: a link back to the original record (Swarm check-in, Day One entry, tweet). Shows as a tappable "View external experience" row. Native captures have none.Per-entry
isIntensityUnset: true on rows that came in through merge from another source and haven't been rated. Shows as a solid light-gray dot at the smallest size (the dashed gray dot is Neutralized, a different thing). Never on native entries.All five are optional, so a v1 archive imports unchanged on a v2 build.
v1 — The first public schema. (May 2026.)
One JSON file, no zip.
Top-level
entries,places,mottos.A stable
uuidon every row, for round trips.A permanent
archiveNumberon every entry (the "P.E. #X").A
readmefield, so a person can read the file.Place fields on the entry next to the top-level
placesarray, so an entry is self-describing without cross-references.Per-entry
source:"pea"for native captures, the service name for converted imports. Optional. A file without it is treated as all native.
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 update the app.
P.E.A. is designed to hold all your positive experiences. That also means it's been designed to bring in any data that you would like to capture inside the archive, such as Swarm/Foursquare, Day One, or anything else you'd like to add via a spreadsheet.
This data is kept absolutely private on your devices and personal 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. This page describes format v3.6 (exportSchemaVersion 3) and was last updated on October 8, 2026. The version history is at the bottom.
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:
exportSchemaVersionsets the rules for how P.E.A. reads your data. It won't import a file it doesn't know how to read, rather than guess.entries,places,mottos,people,conceptsare the building blocks of data inside P.E.A. so they are independent top-level collections. (conceptsholds the Topics. The app called them Concepts when the format was published, and a key in a public format never changes, so your older backups keep working.) Each data point for them carries a unique identifier oruuidso 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-levelplacesarray gives you the list of places as the user organized them. People and topics work the other way around — entries reference them by uuid (peopleUUIDs,conceptUUIDs). The top-levelpeople/conceptslists are simpler and hold all records.Optional fields are not stored when the data isn't there. Some other apps carry
nullor empty fields. 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 | String | This number identifies each entry uniquely / Canonical UUID (8-4-4-4-12 hex). |
R | Integer | This is the number |
R | String | Text you entered or dictated about the experience. This is optional and can be empty. |
R | Integer 1–7 | Strength of the experience. 7 being life changing. |
R | String | Labels that accompany the intensity number: "Minimum", "Light", "Mild", "Medium", "Strong", "High", "Maximum". |
Time
Field | Type | Notes |
|---|---|---|
R | String (ISO 8601) | When the dot was released. |
R | String (ISO 8601) | Last time an experience entry was edited. |
R | String | "Morning" (midnight to 11:59), "Afternoon" (noon to 16:59), "Evening" (17:00 to 20:59), "Night" (21:00 to 23:59), on the local clock. |
R | String | "Monday" through "Sunday". |
R | String | "Spring" (March to May), "Summer" (June to August), "Autumn" (September to November), "Winter" (December to February). |
| String | The time zone the experience happened in, as an IANA name ("America/New_York", "Asia/Tokyo"). It lets the entry keep the clock of the place where it happened. When it's there, P.E.A. reads |
Your physical location at capture
This metadata comes from the device. They are testimony, not opinion. P.E.A. never edits a captured value. It does fill in missing ones: when an imported entry has coordinates and leaves the fields below empty, P.E.A. looks up the city, neighbourhood, county, postal code, region, country and time zone after import, and the day's weather and moon from the historical record where it exists. So bring the coordinates, leave out what you don't know, and never guess.
Field | Type | Notes |
|---|---|---|
| Number | Raw GPS, decimal degrees. |
| Number | Raw GPS, decimal degrees. |
| String | Reverse-geocoded name ("Café Vita"). |
| String | City or town. Omitted? P.E.A. derives it from the coordinates after import. |
| String | Region or state, exactly as the map data gives it ("WA", "Bavaria"). Same completion as the city. |
| String | Neighbourhood or district ("Ballard"). Same completion as the city. |
| String | County, or whatever the country's equivalent is called ("King County"). Same completion as the city. |
| String | Postal or ZIP code ("98117"), exactly as the map data gives it. Same completion as the city. |
| String | Country. Same completion as the city. |
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, later experiences within its radius (100 m unless you change it) get that place automatically. The fields below are optional. They mirror a row in the top-level places collection.
Field | Type | Notes |
|---|---|---|
| String | The label you chose ("home", "Mom's house"). |
| String | Locality context for that place. |
| String |
|
| Number | Place coordinate. |
| Number | Place coordinate. |
| String | Apple Maps identifier, when assigned from POI. |
| String | Apple Maps point-of-interest category, human-readable ("Cafe", "Amusement Park"). Only present for real POIs — plain addresses and custom places have none. |
| Boolean | True if you explicitly assigned this place. |
Weather and sky
Omit these when your source doesn't have them: for entries with coordinates, P.E.A. recovers the hour's condition and temperature and the day's moon phase from the historical weather record after import, where coverage exists. (This lookup honors the in-app weather capture setting — users who turned weather capture off get no lookups.)
Field | Type | Notes |
|---|---|---|
| String | "Clear", "Cloudy", "Rain", … |
| Number | Captured in metric; both units shown in-app. |
| String | "Full Moon", "Waxing Crescent", … |
Activity, motion, environment
Field | Type | Notes |
|---|---|---|
| String | "stationary", "walking", "running", "cycling", "driving". |
| Number | Meters per second. |
| Number | Elevation in meters. |
| Integer | Pedometer count for the day of capture. |
| Number | Ambient noise in dB. |
| String | Comma-separated nearby points of interest. |
Music
Field | Type | Notes |
|---|---|---|
| String | Song title playing at capture (Apple Music). |
| String | Artist name. |
| String | Apple Music catalog ID, for deep linking. |
Capture source device
Field | Type | Notes |
|---|---|---|
| String | "Timothy's iPhone", "Siri", "Watch" etc. |
| String | Semantic device category: |
Source provenance and identity (v2)
Which app or format the entry came from, plus a stable identifier from that source. Together they keep re-imports from creating duplicates: when you bring data in through Add data to P.E.A., a row with the same (source, sourceID) as one already in the archive is skipped. So is a row whose createdAt and text both match an entry in the archive or an earlier row in the same file, whatever its ids. One moment recorded twice lands once, and the preview tells you before you confirm.
P.E.A. keeps these values forever. Once an entry is stamped ("swarm", "5c1b…"), it stays that way through every export, even if you re-import the archive on another device.
Field | Type | Notes |
|---|---|---|
| String |
|
| 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, |
| 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. |
| 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 |
|---|---|---|
| Boolean | Only on rows that came in from another source through Add data to P.E.A. and haven't been rated yet. When it's |
People links (v2)
Field | Type | Notes |
|---|---|---|
| Array of Strings | The UUIDs of the people associated with this experience. Each one matches one row in the top-level |
Topic links (v3)
Field | Type | Notes |
|---|---|---|
| Array of Strings | The UUIDs of the topics associated with this experience (sports, travel, first time, …). Each one matches one row in the top-level |
Soft-deletion (Neutralization)
When you neutralize a positive experience it's removed from the list and available in the Index via the Neutralized filter for 30 days. After that time, or when you delete it yourself, it's gone from P.E.A.
Field | Type | Notes |
|---|---|---|
| Boolean | Present and |
| String (ISO 8601) | When you neutralized it. |
| 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 | String | Identifier (Canonical UUID) for places to match them across exports. |
R | String | The name you gave the place ("Home", "Jan's Haus"). |
R | String (ISO 8601) | When you saved the place. |
| String | Locality context ("Helsinki", "Seattle", "Prague"). |
| Number | Place coordinate, decimal degrees. |
| Number | Place coordinate, decimal degrees. |
| String | Apple Maps identifier, when selected from a point of interest (POI). |
| String | The venue's own web address ("https://www.cafevita.com"). |
| String | The venue's phone number, exactly as the listing gives it. |
| String | Street line — house number and street ("1005 E Pike St"). |
| String | The town the venue stands in ("Seattle"). |
| String | Region or state, as the map data spells it ("WA", "Bavaria"). |
| String | Postal code ("98122"). |
| String | Country, in English ("United States"). |
The last seven are the place's own listing (v3.6): what Apple Maps knows about the venue, so the place card can show an address, a website and a phone number without going online. They are separate from detail, which stays the short "City, Country" line. All of them are optional. A place P.E.A. never looked up has none, and a listing that only had an address has no website or phone. Once recorded, P.E.A. doesn't update them. A listing is what the place said when you linked it, not what it says today.
Experiences carry their place inline, with placeName / placeLatitude / placeLongitude / placeMapItemIdentifier (see the Entry Schema above). The entry's own place fields say what's assigned to that experience. The top-level places array is your list of saved places. (An earlier version of this page said a future version "may add a foreign-key placeUUID". It didn't. Place identity below is what happened instead.)
On Add data to P.E.A. (merge), a place is a duplicate when the uuid matches, or when the name and both coordinates match to five decimals (about a meter). The content match is there because the same place can have different UUIDs on different devices, so a uuid-only check would double every place on a cross-device re-import. Two places with the same name at different coordinates stay two places, and both keep their name. Merge used to rename the incoming one ("Home 2"). It doesn't anymore. Names come through exactly as they are in the file, in both modes.
Place names are not unique. One archive can hold several places called "Home". Three offices of one company are three places with the same label. If you write code against this format, don't key places by name. Key by
uuid, or byidentityKeywhere it's there.
Place identity (v3.3)
A place has a stable key that doesn't depend on its name. These fields are in every build since August 2026. exportSchemaVersion stays 3, because they are all optional.
Field | On | Type | Meaning |
|---|---|---|---|
| entries | String, optional | Which place this experience belongs to. Starts with |
| places | String, optional | The same key on the saved place row. |
| places | String, optional | What Apple Maps called the place when you linked it. P.E.A. doesn't refresh it. |
| places | Number, optional | How far the place reaches, if you changed it. Absent means the app's default (100 m). |
| places | String, optional | The SF Symbol you picked for the place. Absent means the app picks one by category. |
| places | String, optional | Whether the place may associate itself with new experiences automatically: |
| places | String, optional |
|
| places | ISO-8601 string, optional | When the role was last set. Copy it as is; import doesn't re-stamp it. |
If you write archive files:
Treat identity keys as opaque. Don't read a coordinate or an Apple ID out of them, and don't make one up. Leave the field out and P.E.A. works out the identity on import.
Never copy a key onto a different place. Two places with one key are one place.
A file without any of these fields is still a valid v3 file.
Motto Schema
A motto is three words you entered or picked that show before archiving, on every device, to make it your own. Every motto is one object in the top-level mottos array.
Field | Type | Notes |
|---|---|---|
R | String | Identifier (Canonical UUID) for Mottos to match them across exports. |
R | String | First word/line of the motto. |
R | String | Second word/line. |
R | String | Third word/line. |
R | String (ISO 8601) | When the motto was saved. |
| Boolean |
|
Mottos are off until you turn them on in the app. Importing an archive with mottos turns them on, so the words you brought in show up.
On Add data to P.E.A. (merge), a motto is a duplicate when the uuid matches or when all three lines match exactly. Mottos are typed by hand and have no other identity, so the same three lines are the same motto even with different UUIDs (which happens across devices). inactive is a state, not an identity. It's not part of the match, and a merge never flips it on a motto you already have.
People Schema (v2)
People you can associate with an experience: friends, family, the person you ran into at the coffee shop. You add and associate them in the entry editor's People field. A converter can fill the same table: supply a top-level people array and point at its UUIDs from each entry's peopleUUIDs (a spreadsheet with a people column converts cleanly this way). One note: the Swarm converter creates no people, because Foursquare's export leaves out who you were with. The only trace is an @mention in the shout text.
Every person is one object in the top-level people array. Required fields are marked R.
Field | Type | Notes |
|---|---|---|
R | String | Unique ID (Canonical UUID (8-4-4-4-12 hex)). Stable across imports/exports. |
R | String | Display name ("Sawa", "Murat", "Fabian"). |
R | String (ISO 8601) | When this person was first added. |
R | String (ISO 8601) | Last time this person record changed. |
| String |
|
| String | Stable identifier from the originating system, when a converter supplies one. With |
| String | URL or local-asset path for a photo. P.E.A. doesn't download or display these yet — they round-trip only. |
| String | Free-form notes the user might keep on a person. Round-trip only — P.E.A. doesn't surface them yet. |
Entries point at people with a peopleUUIDs array (see Entry Schema → People links). One person can be on many experiences, and one experience can list many people. On import, P.E.A. creates every person first and then connects the entries, so every UUID finds its row.
People are created and associated in the entry editor: tap a suggestion or confirm a typed name. Archiving on the iPhone, the Mac, the watch, the widget or through Siri never creates a person, but when the text names someone already in People, P.E.A. associates them on arrival. People have their own page under Manage, a filter in the Index and on the Map, and a grouping in the Map's list, and they round-trip through every export.
On Add data to P.E.A. (merge), a person is a duplicate when the uuid matches, or when source and sourceID both match. People made in P.E.A. have no sourceID, so for them only the uuid counts.
P.E.A./Topic Schema (v3)
A topic is a word or two that says what an experience was about: a vocabulary you build as you archive ("soccer", "made", "noticed", "first time"). People answer who, places answer where, topics answer what. You create and associate them in the entry editor's Topics field: tap a suggestion, a default seed, or type a new one.
On screen they are Topics. In the file they are concepts. The app called them Concepts until September 2026, the key was published under that name, and a key in a public format doesn't change. Your older backups keep restoring, and a converter writes concepts, conceptUUIDs and conceptCount as before.
Every topic is one object in the top-level concepts array. Required fields are marked R. The shape is the same as People, so a converter or a reader can treat the two the same way.
Field | Type | Notes |
|---|---|---|
R | String | Canonical UUID (8-4-4-4-12 hex). Stable across exports. |
R | String | Display label, in the user's original casing ("soccer", "First Time"). |
R | String (ISO 8601) | When this topic was first added. |
R | String (ISO 8601) | Last time this topic changed. |
| String |
|
| String | Stable identifier from the originating system. With |
Entries point at topics with a conceptUUIDs array (see Entry Schema → Topic links). One topic can be on many experiences, and one experience can list many topics. On import, P.E.A. creates every topic first and then connects the entries.
On Add data to P.E.A. (merge), a topic is a duplicate when the uuid matches, or when source and sourceID both match. Topics made in P.E.A. have no sourceID, so for them only the uuid counts. Two archives that each made a "made" topic end up with two.
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-15T23:32:00Z", "updatedAt": "2026-01-15T23:32:00Z", "timeOfDay": "Evening", "dayOfWeek": "Thursday", "season": "Winter", "timeZoneID": "America/New_York", "latitude": 35.2271, "longitude": -80.8431, "locationName": "Walmart Supercenter", "locationLocality": "Charlotte", "locationAdministrativeArea": "NC", "locationSubLocality": "Wesley Heights", "locationSubAdministrativeArea": "Mecklenburg County", "locationPostalCode": "28208", "locationCountry": "United States", "placeName": "the Walmart by the airport", "placeDetail": "Charlotte, NC", "placeSource": "custom", "placeLatitude": 35.2270, "placeLongitude": -80.8430, "placeMapItemIdentifier": "I3F4A2C8B9D0E1F2A3B4C5D6E7F8A9B0", "placeCategory": "Store", "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", "websiteURL": "https://www.example-store.com/charlotte", "phoneNumber": "(704) 555-0148", "street": "3850 E Independence Blvd", "locality": "Charlotte", "administrativeArea": "NC", "postalCode": "28205", "country": "United States" } ], "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.
placeNameis 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 into a single PEA-Export-….json file and then bring it in via Manage → Import & Export → Add data to P.E.A. (on the Mac, File → Add Data to P.E.A.… opens the same picker). That's a merge. Everything you already have stays 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 rate it yourself by dragging the dot in the app, and that's the moment it gets its permanent number. So the prompts below don't ask the model to invent a strength. They set every row to Unset and leave the rating to you.
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 topic (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 people fields: uuid, name, createdAt, updatedAt.
- Required topic fields (in "concepts"): 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, using the LOCAL
time where the entry happened (not UTC).
- If the source data records the time zone an entry was written in,
emit it as "timeZoneID" using the IANA name (e.g. "Asia/Tokyo").
If the source only has a UTC offset or no zone at all, omit the
field entirely — never guess a zone from an offset.
- Omit any optional field you don't have data for. Do not write null.
Do NOT try to infer city, neighbourhood, county, postal code, region,
country, time zone, weather, or moon from
other fields — for entries that carry latitude/longitude, P.E.A.
completes those from the coordinates and the historical record after
import. Bringing accurate coordinates is worth more than guessed
metadata.
- The same rule for a place's listing fields ("websiteURL",
"phoneNumber", "street", "locality", "administrativeArea",
"postalCode", "country"): write them only where the source actually
records the venue's address or contact details. Never look one up and
never guess one from the name — P.E.A. resolves them itself after
import for any place that carries coordinates or an Apple Maps
identifier.
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 Manage → Import & Export → Add data to P.E.A.
Short prompt for Siri and Apple Intelligence
Siri, Apple Intelligence and other on-device models can't open links and don't have much room. This prompt carries the rules with it, so nothing needs to be fetched. Keep it short: a dozen entries at a time on-device, more through Private Cloud Compute or a chat model. In Shortcuts, add the Use Model action, pick On-Device or Private Cloud Compute, paste the prompt with your entries, and save what comes back as a .json file. It works the same way pasted into any chat.
Turn the entries below into one P.E.A. archive file. Answer with the
JSON only.
The file is one object with exactly these keys:
"exportSchemaVersion": 3, "exportDate" (now, ISO 8601, like
"2026-10-07T09:00:00Z"), "appVersion": "1.0", "readme": "Converted
for P.E.A.", "entryCount", "placeCount": 0, "mottoCount": 0,
"peopleCount", "conceptCount", "entries", "places": [], "mottos": [],
"people", "concepts".
One object in "entries" per entry:
- "uuid": a new random UUID (8-4-4-4-12 hex), different for every entry
- "archiveNumber": 0
- "text": the entry's own words, or "" if there are none
- "intensity": 1, "intensityLabel": "Minimum", "isIntensityUnset": true
- "createdAt" and "updatedAt": the entry's date and time, ISO 8601 with
the time zone (like "2025-04-29T20:05:00-07:00"). The same value in
both. If only the day is known, use 12:00.
- "timeOfDay" from the local clock: "Morning" before 12:00, "Afternoon"
from 12:00, "Evening" from 17:00, "Night" from 21:00
- "dayOfWeek": "Monday" to "Sunday"
- "season": "Spring" for March to May, "Summer" for June to August,
"Autumn" for September to November, "Winter" for December to February
- "source": "notes"
- Only when the entry says so: "locationName" (the place's name),
"locationLocality" (the town), "locationAdministrativeArea" (the
state or region), "locationCountry", "latitude" and "longitude" as
numbers. If an entry names a place, put the place's name in
"locationName" too.
People and topics: for every person named, one object in "people" with
"uuid", "name", "createdAt", "updatedAt" (same moment as "exportDate")
and "source": "notes". Put that person's uuid in the entry's
"peopleUUIDs". Topics go the same way into "concepts" and
"conceptUUIDs". Use one uuid per name, however often it comes up.
Leave out every field you don't have. Never write null. Don't guess a
place, a time zone, the weather or a rating. "entryCount",
"peopleCount" and "conceptCount" are the lengths of their arrays.
Entries:
[paste your notes, or rows from the P.E.A. spreadsheet template]Load the file through Manage → Import & Export → Add data to P.E.A. Every entry arrives Unset. Rate it, and it gets its number.
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.
Don't worry about how thin the venue data looks — Foursquare's export ships only the venue's name and a coordinate, no city or country. That's enough: after import, P.E.A. completes the city, neighbourhood, county, postal code, region, country, and time zone from each check-in's coordinates (and the day's weather and moon from the historical record, where available), so the entries fill out on their own.
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:
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. archive next to the originals (format v2, which P.E.A. still reads):
That writes data-export-NNNN/pea-swarm.json. Then Manage → Import & Export → 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 have no number. Every row arrives with archiveNumber: 0 and shows in the app as P.E. #NaN. The app gives it a permanent P.E. #N the moment you rate it in the editor (drag the dot to any green step). Until then a check-in is a candidate, not a P.E. Setting it back to Unset later keeps the number it got. A row you never rate stays numberless, and if you neutralize it, it's deleted after 30 days without ever using a number.
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:
createdAtis 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 undervenue. Thevenueblock 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 noshoutat all.timeZoneOffsetis minutes from UTC at the time of the check-in (e.g.-420is Pacific Daylight Time).
The corresponding P.E.A. 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. | Note |
|---|---|---|
|
| Half of the |
|
| Constructed as |
|
| Treat the string as UTC, shift by |
|
| Empty shouts are fine; |
|
| Copy verbatim. |
|
| Omitted if the check-in has no |
derived from local time |
| Compute from the local time (after the timezone shift), not from UTC. |
derived per-row |
| Fresh random UUID, one per check-in. |
constant |
| Always |
constant |
| 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 |
| Stamp every row. Lets the app tell native captures from imported ones; sticky forever in round-trips. |
raw check-in (stringified) |
| 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. |
| — | Dropped. No P.E.A. equivalent today. Preserved inside |
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. 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:
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 topics you want to associate, 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 person (Felix) and two topics, captured on an Apple Watch.
Row 2 — Bunched and finished so many annoying tasks today. The most. No-coordinate case: street name + city + state + weather only, with one topic. 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:
Open the CSV in your spreadsheet of choice.
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.
To associate people or topics, type their names in the
peopleandtopicscells. Separate several with semicolons ("Felix; Mara"). Don't worry about UUIDs, the conversion step makes them.Save as CSV.
Convert CSV → P.E.A. JSON. The simplest path is to paste the CSV plus this prompt into a local LLM (the short Siri prompt above takes the rows too):
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 "topics" 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". Topics
go into "concepts"; that is the key's name. 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]Save the resulting JSON file and load it through Manage → Import & Export → 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 leaves out what the conversion fills in for you (the uuid, the timeOfDay / dayOfWeek / season labels) and the strength columns (every imported row arrives Unset, so there's nothing to rate in advance). If you need saved places, moon phase, speed/elevation/steps, the Apple Music store ID or a sourceURL, edit the JSON 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 merge. New rows are added. Rows that already exist (same
uuid, same(source, sourceID)for imported data, or the samecreatedAtandtext) are skipped, so one moment under two ids lands once. This is for converted imports, Swarm, Day One, a spreadsheet, and also for adding an existing P.E.A. backup to what you have.Restore P.E.A. from backup — This replaces your current data. It's 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 P.E.A. exported.
Schema Version History
exportSchemaVersion is 3 and has been since June 2026. The point releases below added optional fields and changed nothing for files that don't have them. The build numbers are TestFlight builds.
Page revision, October 8, 2026. No format change. Concepts are called Topics in the app since September 27, 2026; the keys (
concepts,conceptUUIDs,conceptCount) stay as they are. Every "not shipped yet" note below is gone, because everything shipped. New on the page: the short prompt for Siri and Apple Intelligence, and the hour bands fortimeOfDayand the months forseasonin the Time table. The spreadsheet template'sconceptscolumn is nowtopics, it gainedlocationAdministrativeArea, and its watch row sayswatch.v3.6 — Region, neighbourhood, county and postal code on entries, and the listing on places. (September 2026, in every build since 91.)
Per-entry
locationAdministrativeArea: the state or region, exactly as the map data gives it: "WA" in one country, "Bavaria" in another. P.E.A. shows it next to the town ("Seattle, WA") and falls back to the two-letter country code when there is no region. There is no table that expands or shortens these, so copy your source's spelling. Leave it out when you don't know it, and P.E.A. looks it up from the coordinates after import, like the city and the country.Per-entry
locationSubLocality,locationSubAdministrativeAreaandlocationPostalCode: the neighbourhood ("Ballard"), the county or its local equivalent ("King County") and the postal code ("98117"). Same rules as the region. Nothing in the app shows these three yet. They are kept so an archive written today already has them.Per-place
websiteURL,phoneNumber,street,locality,administrativeArea,postalCodeandcountry: the place's own listing, so the place card can show an address, a website and a phone number without going online.detailis unchanged and still the short "City, Country" line. Write these if your source knows the venue's address. If it only knows a coordinate, leave them out and P.E.A. looks the place up after import. Once recorded, P.E.A. doesn't overwrite them.All eleven are optional.
exportSchemaVersionstays 3, older files import unchanged, older builds ignore the fields.
v3.5 — Place association and role. (August 2026, in every build since 70.)
Per-place
associationState: whether the place may associate itself with new experiences automatically ("notLicensed"/"disabled"/"enabled"). Absent means something: the place was saved before the field existed and behaves as enabled. Keep it absent when you convert a file.Per-place
role("home"/"work"/"other") androleUpdatedAt(ISO 8601): the place's role in your life and when you last set it. There is at most one home and one work. The timestamp is there so a sync conflict can be settled by a person. Imports copy it as is.All three are optional.
exportSchemaVersionstays 3.
v3.4 — Motto active state. (August 2026, in every build since 70.)
Per-motto
inactive:truekeeps a motto in the archive but stops showing it, on every device. Left out when the motto is active. Not part of the motto's identity, so the merge still matches byuuidor by the three lines. Optional.exportSchemaVersionstays 3.
v3.3 — Place identity. (July 2026, in every build since 70. See Place identity above.)
Per-entry
placeIdentityKey: which place an experience belongs to, as a stable key.apple:for a place backed by an Apple Maps Place ID,local:for one P.E.A. created. The key, not the name, says "these two experiences happened at the same place", which is why one archive can hold several places called "Home".Per-place
identityKey,providerName,radiusMeters,symbol: the same key on the saved place, what Apple Maps called the place when you linked it (not refreshed), how far the place reaches in meters, and the SF Symbol you picked. NoradiusMetersmeans the app's default. Nosymbolmeans the app picks one by category.Identity keys are opaque. Don't parse one, don't make one up, don't copy one onto another place. Leave them out and P.E.A. works out identity itself.
All five are optional.
exportSchemaVersionstays 3.
v3.2 — Time zone. (July 2026, in every build since 70.)
Per-entry
timeZoneID: the IANA time zone the experience happened in ("Asia/Tokyo"), set at creation. When it's there,timeOfDayanddayOfWeekare read on that clock, so an experience archived in another zone keeps its wall clock instead of drifting with the device. Converters write it only when the source really records a zone (Day One'stimeZone, for example), never from a bare UTC offset. Optional.exportSchemaVersionstays 3.
v3.1 — Place category. (July 2026, in every build since 70.)
Per-entry
placeCategory: the Apple Maps point-of-interest category of an assigned place, human-readable ("Cafe", "Amusement Park"). Set when a place is picked from Apple Maps search. Plain addresses and custom places have none. Optional.exportSchemaVersionstays 3.
v3 — Concepts, which are Topics now. (June 2026.)
conceptstop-level collection +conceptCount. A word or two that says what an experience was about ("soccer", "made", "noticed"), created and associated in the entry editor. Native rows are stampedsource: "pea"with nosourceID. The app renamed them Topics in September 2026; the keys didn't change.Per-entry
conceptUUIDsarray, linking each entry to its topics. Many-to-many, same shape aspeopleUUIDs.On Add data to P.E.A. (merge), topics match by
uuidor(source, sourceID). Native topics (nosourceID) match byuuidonly.People reached the editor. People came in at v2 as a round trip only. From v3 you create and associate them in the entry editor's People field. The wire format is unchanged.
Both additions are optional, so a v1 or v2 archive imports unchanged on a v3 build. An older build refuses a v3 archive with a "please update" message rather than dropping the topics.
v2 — Add data + Unset + People (schema only). (May 2026.)
peopletop-level collection +peopleCount. People round-trip through export and import. The in-app surfaces came with v3.Per-entry
peopleUUIDsarray linking each entry to people in the top-levelpeoplearray.Per-entry
sourceID: a stable identifier from the originating system. Withsource, it's the merge key for "Add data to P.E.A.". The samecreatedAtandtextalso mark a duplicate.Per-entry
importPayload: the original record as a string. P.E.A. doesn't read it. It's there so a future import can reach back into fields P.E.A. didn't model the first time.Per-entry
sourceURL: a link back to the original record (Swarm check-in, Day One entry, tweet). Shows as a tappable "View external experience" row. Native captures have none.Per-entry
isIntensityUnset: true on rows that came in through merge from another source and haven't been rated. Shows as a solid light-gray dot at the smallest size (the dashed gray dot is Neutralized, a different thing). Never on native entries.All five are optional, so a v1 archive imports unchanged on a v2 build.
v1 — The first public schema. (May 2026.)
One JSON file, no zip.
Top-level
entries,places,mottos.A stable
uuidon every row, for round trips.A permanent
archiveNumberon every entry (the "P.E. #X").A
readmefield, so a person can read the file.Place fields on the entry next to the top-level
placesarray, so an entry is self-describing without cross-references.Per-entry
source:"pea"for native captures, the service name for converted imports. Optional. A file without it is treated as all native.
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 update the app.
Questions? Found a problem?
Email Florian with questions.
Email data questions →
Colophon
Positive Experience Archive
is made by Florian in Seattle with care and good intentions.
Made with
Ableton / Acorn / Apple Watch / Darkroom / BBEdit / Claude Code / CleanShot X / Darkroom / Framer / FUJIFILM X100T+V / GitHub / HandBrake / iCloud / Icon Designer / iPhone / Keynote / Linear / JavaScript / Mac Mini / MacBook Pro / Paper / Pencil / Pixelmator Pro / Photos app / Python / SF Rounded Regular, Medium, & Bold / Sketch / SwiftUI / Terminal / Xcode
2163 P.E.s archived in the process.
When we lose focus,
we forget who we are.
No animals harmed,
human-tested only.
”Support people, not corporations.”
—David Watanabe
Positive
Experience
Archive
©2026 Fangohr, LLC
Colophon
Positive Experience Archive
is made by Florian in Seattle with care and good intentions.
Made with
Ableton / Acorn / Apple Watch / Darkroom / BBEdit / Claude Code / CleanShot X / Darkroom / Framer / FUJIFILM X100T+V / GitHub / HandBrake / iCloud / Icon Designer / iPhone / Keynote / Linear / JavaScript / Mac Mini / MacBook Pro / Paper / Pencil / Pixelmator Pro / Photos app / Python / SF Rounded Regular, Medium, & Bold / Sketch / SwiftUI / Terminal / Xcode
2163 P.E.s archived in the process.
When we lose focus,
we forget who we are.
No animals harmed,
human-tested only.
”Support people, not corporations.”
—David Watanabe
Positive
Experience
Archive
©2026 Fangohr, LLC