The .deeta file standard — merge files, format major 1

Format version 1.4 · format: "deeta-data-export-v2" · rendered from deeta-ios 7cc6a1df5

Import & Merge, which reads these files, is available in Deeta v1.2.

This is the reference for a .deeta file that Deeta's Import & Merge reads. It is the source for the public page at https://docs.deeta.app/deeta-file-standard. The same file format carries a full backup; this document covers the merge use, where a file adds people and changes only the records it names.

ArtifactWhat it is
deeta-import-v1.mdThis reference.
deeta-import-v1.schema.jsonJSON Schema for a merge file.
deeta-import-v1.report.schema.jsonJSON Schema for the error report Deeta produces.
deeta-import-v1.kit.mdThe generation kit: a prompt for an AI agent or script that writes a merge file.
examples/minimal.deetaThe smallest valid file: one new person.
examples/enrich.deetaEvery row class and mode in one file.
examples/failing.deeta + examples/error-report.jsonA file with five defects and the report Deeta produces for it.
examples/kit-round-trip/An export, and the two files an AI agent wrote from it with the generation kit for one request.

The schemas describe each row's shape. Rules that span rows (for example "every new: key is declared once") are listed under Structural rules; a file must satisfy both.

1. How a merge works

  1. You pick a file in Settings → Import → Import & Merge….
  2. Deeta checks the whole file. One defect anywhere rejects the whole file, and nothing is changed. Deeta lists every defect it found in an error report you can save.
  3. Deeta shows a preview of every change. Nothing is saved until you confirm.
  4. The import applies all at once, or not at all.
  5. The summary offers Undo this import until you tap Done.

A merge can never delete a person, a tag, a group or a deet set, and never changes a person's name.

2. The envelope

{
  "format": "deeta-data-export-v2",
  "formatVersion": "1.4",
  "generator": "your-tool/1.0",
  "generatedAt": "2026-10-13T12:00:00Z",
  "contacts": []
}
FieldRequiredRule
formatyesExactly deeta-data-export-v2. Backups carry the same value.
formatVersionyesmajor.minor. See Versioning. Write 1.4.
generatornoFree text naming the tool that wrote the file, at most 200 characters. Never used to decide anything.
generatedAtnoISO-8601 timestamp.
contactsyesThe people in the file. May be empty when the file only changes your own data.

Sections

SectionShapeRead by a merge
contactsarray of person rowsyes
deetsarray of deet rowsyes
tagsarray of tag rowsyes
contact_tag_idsobject: person id → array of tag row idsyes
groupsarray of group rowsyes
group_membersobject: group row id → array of person idsyes
self_profileone row, or absentyes
self_deetsarray of your own deet rowsyes
deet_setsarray of deet set rowsyes
relationshipsmust be absent or emptya file with relationships is refused
ai_preferences, deletion_log, core_field_writes, core_field_set_members, live_sync, settings, contact_apple_links, contact_sync_state, accountMarkeranythingaccepted and ignored

The ignored sections are what a backup carries. They are accepted so that an export you have edited still imports.

A file in which no person row has a "new" id or an importType is a plain backup. Import & Merge refuses it and points you to Restore Backup….

3. Identity: the four row classes

Every person row has an id. It is one of two things:

  • the literal "new", or "new:<key>" where <key> is a name other rows in the same file use to refer to this person ([A-Za-z0-9_-], 1 to 64 characters); or
  • the id of a person already in Deeta, copied exactly from an export. Such a row must also carry importType.

Deeta creates every new id itself. An id in a file is never used to create a person.

idimportTypeFound in Deeta?Result
"new" / "new:<key>"must be absent—Create: a new person.
an existing id"additive"yesAdditive: adds what is missing.
an existing id"update"yesUpdate: changes what is already there.
an existing id"overwrite"yesOverwrite: the person's deets become exactly the file's.
an existing idanyno, or the person is a different typeThe row is rejected and reported. It never becomes a new person.
missing, malformed, an existing id with no importType, or "new" with an importTypeThe whole file is rejected.

Which mode when

You want toUse
Put everything back from a backupNot this import. Use Restore Backup….
Export, add to it, bring it backadditive
Correct what is already there (a number changed)update
Start one person's deets overoverwrite, on that person only

One person can appear once in a file, with one mode. To both add a new email and correct an existing number for the same person, use two files.

What each mode does

Createadditiveupdateoverwrite
Namesetnever changednever changednever changed
organization, notessetfilled if emptychanged; "" clears itnot touched
Phone, email, URL, addresssetan entry under a label the person does not have is addedthe entry under a matching label is changednot touched
Birthdaysetset if the person has nonechangednot touched
Deetsaddedadded; nothing changed or removedvalues of existing deets changedreplaced by the file's deets
Tags and groupsattachedattached if not alreadynot allowed in the fileattached if not already

No mode removes a phone, email, URL or address entry, detaches a tag, or takes someone out of a group. The only values a file can clear are text fields, with "" under update (see below); the preview lists each one as a removal.

Additive, in detail. The file's deets for the person are matched against the person's existing deets level by level, by key under the same parent. Keys are matched ignoring case, in every mode.

  • A deet with the same key exists: its children are added under it.
  • No deet with that key exists: it is created, with its children.
  • Same key and same value already there: skipped, reported already_present.
  • Same key, different value, no children: added beside it.
  • More than one existing deet with that key at that level: the row is rejected, reported ambiguous_match.
  • A phone, email, URL or address entry whose label the person already has is skipped and reported already_present, even when the value differs.

Update, in detail. Nothing is created.

  • A deet found by its key path has its value changed. The same value is a no-op.
  • A deet not found is skipped, reported not_present.
  • More than one match is rejected, reported ambiguous_match.
  • A phone, email, URL or address entry is matched by its label, ignoring case. No match: skipped, not_present. Two existing entries under that label: rejected, ambiguous_match.
  • An address is replaced whole.

Overwrite, in detail. The deet rows in the file for that person are the person's complete deets afterwards. Deets the file leaves out are removed, and the preview lists every one. An overwrite with no deet rows removes all of the person's deets.

Values someone shares with you live

When a value on a person is kept current by a live share from someone else, an import does not change it:

  • an update to that value is added beside it as a second entry (a second phone number under the same label, or a second deet with the same key);
  • an overwrite keeps that deet even when the file leaves it out; when the file gives it a different value, the deet is kept and the file's value is added beside it.

The preview says so for each one. A generator should not retry such a change.

Leaving a field out, "" and null

Every optional text field follows one rule: organization, notes, a new person's firstName and lastName, the address parts, your bio and organization, and a deet set's description.

The file writesMeaning
no such fieldLeave the value as it is.
""Clear the value. Under update an existing value is removed; under additive there is nothing to fill, so nothing changes; on a new row the field is empty.
"text"Set the value to that text. "null" in quotes is text like any other.
nullRefused (null_value). Leave the field out to keep the value, or write "" to clear it.

null is refused because JSON libraries write it for "no value": a generator filling rows from a table with blank cells would otherwise send it for everyone, and it could not mean both "leave it" and "clear it". A Deeta export never writes null in a text field: a field with no value is left out.

null is still how a row says it has no link: parentDeetId and parentGroupId on a top-level row, self_profile when there is none, a deet's relationshipId, and a birthday's year.

4. People — contacts

FieldCreate rowExisting rowRule
idrequiredrequiredSee section 3.
importTypeforbiddenrequiredadditive, update or overwrite.
contactTyperequiredoptionalperson or organization. On an existing row it must match the person in Deeta.
firstName, lastNameoptionalaccepted and ignoredUp to 150 characters each.
organizationoptionaloptionalUp to 100 characters.
notesoptionaloptionalUp to 10,000 characters. Line breaks allowed.
phoneNumbersoptionaloptionalArray of { "label", "value" }.
emailAddressesoptionaloptionalArray of { "label", "value" }.
urlAddressesoptionaloptionalArray of { "label", "value" }.
physicalAddressesoptionaloptionalArray of address entries.
birthdayoptionaloptional{ "month", "day", "year" }; year may be left out.
isFavorite, createdAt, updatedAtaccepted and ignoredaccepted and ignored

Phone, email, URL, address, birthday

These are written the same way on every row, the way an export writes them.

"phoneNumbers":      [{ "label": "mobile", "value": "+44 7700 900456" }],
"emailAddresses":    [{ "label": "work", "value": "priya.patel@example.org" }],
"urlAddresses":      [{ "label": "homepage", "value": "https://example.org" }],
"physicalAddresses": [{ "label": "home", "street": "1 Example Street", "city": "London",
                        "postalCode": "N1 9GU", "country": "United Kingdom",
                        "isoCountryCode": "GB" }],
"birthday":          { "month": 3, "day": 14, "year": 1990 }
FieldRule
labelRequired on every entry, 1 to 50 characters. Common labels are mobile, home, work, main, other and homepage; any text is allowed.
Phone valueUp to 100 characters. The international form (+44 7700 900456) is recommended, not required.
Email valueUp to 254 characters.
URL valueUp to 2,048 characters. A full address with https:// is recommended, not required.
AddressEach part optional; a part left out or "" is empty, since an address is written whole. street up to 500 characters and may have several lines; city, state, postalCode, country up to 200 each; isoCountryCode up to 12.
Birthdaymonth 1–12, day 1–31, year optional; when given, 1900–9999.
Entries per fieldAt most 50 phone, 50 email, 50 URL and 50 address entries on one person.

Phone, email, URL and address values are checked only for length and for the characters in section 8's not-allowed list. Their content is not checked, so any value Apple Contacts holds can be exported, edited and imported again.

Two entries with the same field and label for one person in one file reject the file.

5. Deets — deets

{ "id": "new:jonas-kid1", "contactId": "9d4e1f70-2b3c-4a5d-9e6f-1b2c3d4e5f60",
  "key": "Mila", "value": "Born 2021", "parentDeetId": "new:jonas-kids" }
FieldRequiredRule
idyesA label for this row inside the file: an id as an export wrote it, or "new:<key>". It is used only so other rows can name this one. Deeta never looks it up and never stores it.
contactIdyesThe person the deet belongs to: an existing id, or the "new:<key>" of a person created in this file. That person must have a row in contacts.
keyyes1 to 50 characters.
valueyesUp to 280 characters. May be empty.
parentDeetIdnoThe id of another deet row in this file for the same person, or null for a top-level deet.
sourceLinkIdnoOptional. If Deeta already holds a deet from that source, the row is skipped. It is never saved.
relationshipIdnoMust be absent or null.
source, createdAt, updatedAt, archivedAtnoAccepted and ignored.

Deets nest at most 7 levels deep. An existing deet is found by where it sits and its key (for example Jonas → Kids → Mila), never by its id.

Every deet an import adds or changes is marked imported in Deeta.

6. Tags and groups

"tags": [{ "id": "new:climbing", "name": "climbing" }],
"contact_tag_ids": { "9d4e1f70-2b3c-4a5d-9e6f-1b2c3d4e5f60": ["new:climbing"] },
"groups": [{ "id": "new:work", "name": "Work", "parentGroupId": null }],
"group_members": { "new:work": ["c7a1e2d3-4b5c-4d6e-8f70-a1b2c3d4e5f6", "new:lena"] }
FieldRule
Tag id, group idA label for the row inside the file, as for a deet.
name1 to 50 characters.
parentGroupIdThe id of another group row in this file, or null.
contact_tag_idsEach key is a person id in this file's contacts; each value lists tag row ids from this file's tags. At most 30 tags per person.
group_membersEach key is a group row id from this file's groups; each value lists person ids from this file's contacts.

A tag is matched to one you already have by name, ignoring case; a group by name, ignoring case, under the same parent. If none exists it is created. If you have two groups with that name under that parent, the row is rejected (ambiguous_match). A person is attached to a tag or group they are not already in; nothing is ever detached.

A person whose row says update must not appear in contact_tag_ids or group_members.

7. Your own data — self_profile, self_deets, deet_sets

Leave these sections out and none of your own data changes.

"self_profile": { "importType": "additive" },
"self_deets": [{ "id": "new:me-diet", "key": "Diet", "value": "Vegetarian",
                 "parentDeetId": null, "visibility": "ask" }],
"deet_sets": [{ "id": "new:dinner", "name": "Dinner guests",
                "liveSyncDefault": "ask", "memberSelfDeetIds": ["new:me-diet"] }]

self_profile carries importType, and that mode applies to the file's self_deets exactly as it would to a person's deets.

FieldRule
importTypeRequired.
organizationUp to 100 characters. Filled if empty (additive) or changed (update); "" under update clears it.
bioUp to 500 characters. Same rule.
firstName, lastName, id, contactId, createdAt, updatedAtAccepted and ignored. Your name is never changed.

self_deets rows have id, key, value and parentDeetId as in section 5, plus:

FieldRule
visibilityOptional: always, ask or never. Used for a deet the import creates; without it the deet gets Deeta's default.
version, notifySubscribers, deetSource, createdAt, updatedAtAccepted and ignored.

A file with self_deets rows and no self_profile row is rejected.

deet_sets rows follow the same identity rule as people: "new" / "new:<key>" with no importType, or the id of a set you have with an importType.

FieldRule
name1 to 50 characters. Required on a new set.
descriptionUp to 140 characters. "" under update clears it.
liveSyncDefaultalways, ask or never.
cardVisibilityConfig{ "show_set_name", "show_deet_keys", "show_deet_values" }, each true or false.
memberSelfDeetIdsRow ids from this file's self_deets. At most 200.
isDefault, defaultKind, createdAt, updatedAtAccepted and ignored.
Mode on a set you haveEffect
additiveAdds the listed deets to the set.
updateAdds the listed deets and changes the name, description, liveSyncDefault and cardVisibilityConfig the row states.
overwriteThe set's members become exactly the listed deets.

A file cannot delete a set.

Changing your own deets or a set is the same as editing them in the app: people you share with live receive the change. The preview says so.

8. Limits

Lengths count Unicode scalar values. An emoji made of several joined parts counts each part, so a family emoji can use seven or more of a deet value's 280.

Per file

LimitValue
File size32 MB
People in contacts10,000
Rows in deets100,000
Deet nesting depth7

Text

FieldLimit
Deet key50
Deet value280
Tag, group and deet set name; any label50
firstName, lastName150 each
organization100
notes10,000
bio500
Deet set description140
Phone100
Email254
URL2,048
Address street500
Other address parts200 each
Address isoCountryCode12

What your account may hold afterwards

Deeta also works out what your account would hold after the import. If that passes any of these, the whole file is refused; it is never trimmed to fit.

LimitValue
People25,000
Deets on your account, people's and your own together50,000
Deets on one person500
Your own deets500
Tags on one person30
Tags in total500
Top-level groups500
Sub-groups under one group200
Deet sets50
Deets in one set200

Characters

  • The file is JSON in valid UTF-8.
  • Not allowed in any text: control characters (U+0000–U+001F, U+007F–U+009F), the direction-override characters U+202A–U+202E and U+2066–U+2069, and Unicode noncharacters. notes and an address street may contain line breaks and tabs.
  • Emoji are allowed, including joined sequences and skin tones.
  • Text is trimmed, and runs of spaces inside it become one space.

Structural rules

A file that breaks any of these is rejected whole.

  1. Every "new:<key>" on a person row is unique among people, and every "new:<key>" that a row refers to is declared by a row of the right section.
  2. An existing person's id appears on at most one row in contacts.
  3. Every deet row's contactId names a person with a row in contacts.
  4. Row ids are unique within deets, within tags, within groups, within self_deets and within deet_sets.
  5. A parentDeetId names a row in the same section, for the same person, and the chain has no loop and is at most 7 deep.
  6. One person has at most one entry per field and label.
  7. A person whose row says update does not appear in contact_tag_ids or group_members.
  8. self_deets rows need a self_profile row.
  9. No field appears that this document does not list (see Versioning).
  10. The file changes something: at least one person row, or a self_profile row.

9. The error report

When a file is rejected, and when an import leaves rows out, Deeta offers Save error report: a JSON file (deeta-import-report.json) matching deeta-import-v1.report.schema.json. Give it back to whatever wrote the file.

{
  "reportVersion": "1",
  "reportId": "RPT-7F3K2A9Q",
  "formatVersion": "1.4",
  "totals": { "file_fatal": 5, "row_rejected": 0, "row_skipped": 0 },
  "truncated": false,
  "entries": [
    { "code": "missing_import_type", "severity": "file_fatal", "collection": "contacts",
      "rowIndex": 0, "rowId": "3f2b8c1e-5a7d-4e9b-8c21-0a4d6f7b9e10", "field": "importType",
      "message": "A row for an existing person needs importType." }
  ]
}
FieldMeaning
reportIdA random id made for this report, shown on the screen beside the failure. It is not derived from the file.
totalsHow many entries of each severity Deeta found, including any past the cap.
truncatedtrue when Deeta found more than the 500 entries a report holds.
collection, rowIndexThe section and the zero-based position of the row in it. null for a problem with the file as a whole.
rowIdThe row's own id exactly as the file gave it, or null.
fieldThe field at fault, or null.
messageA sentence for a person.
severityMeaning
file_fatalThe file was rejected. Nothing was imported.
row_rejectedThat row could not be matched in Deeta. The rest of the file was still offered.
row_skippedThat row was left out because it was already there, not there, or could not be saved.

The report holds locations, never values. It never contains a deet value, a phone number, a name or anything from your Deeta data. The only text taken from the file is a row's own id or new: key and the name of a field it wrote, so the report says nothing the file's author did not already write.

Codes

CodeSeverityMeaning
invalid_jsonfileNot valid JSON, or not UTF-8.
not_a_deeta_filefileformat is missing or is not a Deeta format.
unsupported_formatfileA Deeta format other than deeta-data-export-v2.
missing_format_version, invalid_format_versionfileformatVersion is absent or does not parse.
newer_format_versionfileWritten for a newer Deeta. Update the app.
older_format_versionfileToo old for this Deeta to read.
file_too_largefileOver 32 MB.
is_backup_filefileA plain backup. Use Restore Backup.
relationships_not_supportedfileThe file has relationships.
no_rowsfileNothing in the file to import.
unknown_fieldfileA field this document does not list.
missing_fieldfileA required field is absent.
wrong_typefileA value of the wrong JSON type.
null_valuefilenull in a text field. Leave the field out to keep the value, or write "" to clear it.
invalid_valuefileA value outside the allowed set.
invalid_idfileAn id or reference that is neither an id nor new / new:<key>.
missing_import_typefileAn existing record's row has no importType.
import_type_on_new_rowfileA new row carries importType.
unknown_import_typefileimportType is not one of the three.
duplicate_local_keyfileThe same new:<key> is declared twice.
undeclared_local_keyfileA new:<key> is referred to and declared nowhere.
duplicate_row_idfileTwo rows in one section share an id.
duplicate_targetfileTwo rows target the same existing record.
orphan_rowfileA row belongs to a person with no row in contacts.
parent_not_foundfileparentDeetId or parentGroupId names no row.
parent_cyclefileA parent chain loops.
nesting_too_deepfileDeets nested more than 7 deep.
duplicate_core_field_labelfileTwo entries with the same field and label for one person.
membership_under_updatefileA tag or group membership for a person in update mode.
self_rows_without_profilefileself_deets rows with no self_profile.
string_empty, string_too_longfileText shorter or longer than allowed.
forbidden_characterfileA character from the not-allowed list.
invalid_birthdayfileThe birthday does not meet its rule in section 4.
over_file_capfileMore rows than a file may hold.
over_account_capfileThe import would take the account past a limit. field names it: contacts, account_deets, deets_per_contact, self_deets, tags_per_contact, tags, top_level_groups, subgroups_per_group, deet_sets or members_per_set.
uuid_not_foundrow rejectedNo such record in Deeta. Deleted since the export, or mistyped.
type_mismatchrow rejectedThe record in Deeta is a different type.
ambiguous_matchrow rejectedMore than one existing deet or entry matches.
already_presentrow skippedAlready there. Use update to change it.
not_presentrow skippedNot there. Use additive to add it.
late_constraintrow skippedThe row could not be saved.

Correcting a file from a report

Correct exactly the rows the report lists, found by collection, rowIndex and rowId, and change nothing else.

After an import succeeds, do not send its "new" rows again. A new person is created every time a "new" row is imported, so sending one twice makes a duplicate.

10. Versioning

formatVersion is major.minor.

  • Deeta reads the current major and the one before it. A newer major is refused with a message to update the app.
  • A field Deeta does not know is an error when the file's version is one Deeta fully knows.
  • When the file declares a newer minor of a major Deeta reads, unknown fields are skipped.
  • A change that removes or redefines a field raises the major, and is announced on the public page before it ships.

11. Platforms

There is one format for every Deeta app. The schemas in this folder are the contract; each app implements them. Where an app cannot store a kind of field, it leaves those entries out and reports them as row_skipped. Today the iOS app is the only one that imports.

Examples

The smallest valid file

{
  "format": "deeta-data-export-v2",
  "formatVersion": "1.4",
  "generator": "example-agent/1.0",
  "generatedAt": "2026-10-13T12:00:00Z",
  "contacts": [
    {
      "id": "new",
      "contactType": "person",
      "firstName": "Sam",
      "lastName": "Okafor",
      "phoneNumbers": [
        {
          "label": "mobile",
          "value": "+44 7700 900111"
        }
      ]
    }
  ]
}

Every row class and mode

{
  "format": "deeta-data-export-v2",
  "formatVersion": "1.4",
  "generator": "example-agent/1.0",
  "generatedAt": "2026-10-13T12:00:00Z",
  "contacts": [
    {
      "id": "3f2b8c1e-5a7d-4e9b-8c21-0a4d6f7b9e10",
      "importType": "update",
      "phoneNumbers": [
        {
          "label": "mobile",
          "value": "+44 7700 900456"
        }
      ]
    },
    {
      "id": "9d4e1f70-2b3c-4a5d-9e6f-1b2c3d4e5f60",
      "importType": "overwrite"
    },
    {
      "id": "c7a1e2d3-4b5c-4d6e-8f70-a1b2c3d4e5f6",
      "importType": "additive",
      "emailAddresses": [
        {
          "label": "work",
          "value": "priya.patel@example.org"
        }
      ]
    },
    {
      "id": "new:lena",
      "contactType": "person",
      "firstName": "Lena",
      "lastName": "Fischer",
      "organization": "Example Studio",
      "emailAddresses": [
        {
          "label": "work",
          "value": "lena@example.org"
        }
      ],
      "birthday": {
        "month": 3,
        "day": 14
      }
    }
  ],
  "deets": [
    {
      "id": "new:maya-coffee",
      "contactId": "3f2b8c1e-5a7d-4e9b-8c21-0a4d6f7b9e10",
      "key": "Coffee order",
      "value": "Oat flat white",
      "parentDeetId": null
    },
    {
      "id": "new:jonas-gym",
      "contactId": "9d4e1f70-2b3c-4a5d-9e6f-1b2c3d4e5f60",
      "key": "Gym",
      "value": "Riverside Climbing",
      "parentDeetId": null
    },
    {
      "id": "new:jonas-kids",
      "contactId": "9d4e1f70-2b3c-4a5d-9e6f-1b2c3d4e5f60",
      "key": "Kids",
      "value": "Two",
      "parentDeetId": null
    },
    {
      "id": "new:jonas-kid1",
      "contactId": "9d4e1f70-2b3c-4a5d-9e6f-1b2c3d4e5f60",
      "key": "Mila",
      "value": "Born 2021",
      "parentDeetId": "new:jonas-kids"
    },
    {
      "id": "new:priya-wine",
      "contactId": "c7a1e2d3-4b5c-4d6e-8f70-a1b2c3d4e5f6",
      "key": "Favorite wine",
      "value": "Malbec",
      "parentDeetId": null
    },
    {
      "id": "new:priya-uni",
      "contactId": "c7a1e2d3-4b5c-4d6e-8f70-a1b2c3d4e5f6",
      "key": "Alma mater",
      "value": "UCL",
      "parentDeetId": null
    },
    {
      "id": "new:lena-met",
      "contactId": "new:lena",
      "key": "Met at",
      "value": "Design conference, Berlin",
      "parentDeetId": null
    }
  ],
  "tags": [
    {
      "id": "new:climbing",
      "name": "climbing"
    }
  ],
  "contact_tag_ids": {
    "9d4e1f70-2b3c-4a5d-9e6f-1b2c3d4e5f60": [
      "new:climbing"
    ],
    "new:lena": [
      "new:climbing"
    ]
  },
  "groups": [
    {
      "id": "new:work",
      "name": "Work",
      "parentGroupId": null
    }
  ],
  "group_members": {
    "new:work": [
      "c7a1e2d3-4b5c-4d6e-8f70-a1b2c3d4e5f6",
      "new:lena"
    ]
  },
  "self_profile": {
    "importType": "additive"
  },
  "self_deets": [
    {
      "id": "new:me-diet",
      "key": "Diet",
      "value": "Vegetarian",
      "parentDeetId": null,
      "visibility": "ask"
    }
  ],
  "deet_sets": [
    {
      "id": "new:dinner",
      "name": "Dinner guests",
      "description": "What a host should know",
      "liveSyncDefault": "ask",
      "memberSelfDeetIds": [
        "new:me-diet"
      ]
    },
    {
      "id": "5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9",
      "importType": "additive",
      "memberSelfDeetIds": [
        "new:me-diet"
      ]
    }
  ]
}

A failing file and its report

failing.deeta

{
  "format": "deeta-data-export-v2",
  "formatVersion": "1.4",
  "generator": "example-agent/1.0",
  "generatedAt": "2026-10-13T12:00:00Z",
  "contacts": [
    {
      "id": "3f2b8c1e-5a7d-4e9b-8c21-0a4d6f7b9e10",
      "phoneNumbers": [
        {
          "label": "mobile",
          "value": "+44 7700 900456"
        }
      ]
    },
    {
      "id": "new:lena",
      "contactType": "person",
      "firstName": "Lena"
    },
    {
      "id": "new:lena",
      "contactType": "person",
      "firstName": "Lena again"
    },
    {
      "id": "c7a1e2d3-4b5c-4d6e-8f70-a1b2c3d4e5f6",
      "importType": "additive"
    }
  ],
  "deets": [
    {
      "id": "new:a",
      "contactId": "c7a1e2d3-4b5c-4d6e-8f70-a1b2c3d4e5f6",
      "key": "Favorite wine",
      "value": "Malbec",
      "parentDeetId": null,
      "importtype": "additive"
    },
    {
      "id": "new:b",
      "contactId": "c7a1e2d3-4b5c-4d6e-8f70-a1b2c3d4e5f6",
      "key": "KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKK",
      "value": "too long a key",
      "parentDeetId": null
    },
    {
      "id": "new:c",
      "contactId": "new:nobody",
      "key": "Met at",
      "value": "Berlin",
      "parentDeetId": null
    }
  ]
}

error-report.json

{
  "reportVersion": "1",
  "reportId": "RPT-7F3K2A9Q",
  "formatVersion": "1.4",
  "totals": {
    "file_fatal": 5,
    "row_rejected": 0,
    "row_skipped": 0
  },
  "truncated": false,
  "entries": [
    {
      "code": "missing_import_type",
      "severity": "file_fatal",
      "collection": "contacts",
      "rowIndex": 0,
      "rowId": "3f2b8c1e-5a7d-4e9b-8c21-0a4d6f7b9e10",
      "field": "importType",
      "message": "A row for an existing person needs importType."
    },
    {
      "code": "duplicate_local_key",
      "severity": "file_fatal",
      "collection": "contacts",
      "rowIndex": 2,
      "rowId": "new:lena",
      "field": "id",
      "message": "This file uses the same new: key on more than one row."
    },
    {
      "code": "unknown_field",
      "severity": "file_fatal",
      "collection": "deets",
      "rowIndex": 0,
      "rowId": "new:a",
      "field": "importtype",
      "message": "Unknown field."
    },
    {
      "code": "string_too_long",
      "severity": "file_fatal",
      "collection": "deets",
      "rowIndex": 1,
      "rowId": "new:b",
      "field": "key",
      "message": "A deet key can be at most 50 characters."
    },
    {
      "code": "undeclared_local_key",
      "severity": "file_fatal",
      "collection": "deets",
      "rowIndex": 2,
      "rowId": "new:c",
      "field": "contactId",
      "message": "No row in this file declares this new: key."
    }
  ]
}

The .deeta generation kit — for AI agents and scripts

This kit lets an AI agent or a script write a .deeta merge file for Import & Merge. Give the agent four things:

  1. the prompt below;
  2. deeta-import-v1.schema.json, the file's JSON Schema;
  3. deeta-import-v1.report.schema.json, the error report's JSON Schema;
  4. the examples examples/minimal.deeta and examples/enrich.deeta.

Then give it what you want changed: usually your own Deeta export (Settings → Privacy → Export My Data) and what to add or correct. The full rules are in deeta-import-v1.md; the prompt carries the ones a generator most often gets wrong.

Before you use it

  • Generated content is unverified. An agent can be wrong about a real person. Everything a file adds is marked imported in Deeta, and the preview shows every change before anything is saved. Read the preview.
  • overwrite lets the generator remove deets it did not know about. That is why the prompt makes additive the default. Use overwrite only when you mean to start one person's deets over.
  • Use it on your own records. The kit is for bringing your own data in: migrating from another tool, or adding what you already know about the people in your life. It is not for building profiles of strangers.

The prompt

Copy everything inside the block.

You write a .deeta merge file for Deeta's Import & Merge. The file is JSON and must
validate against the JSON Schema you were given (deeta-import-v1.schema.json). The two
example files show the shape.

ENVELOPE
- "format" is exactly "deeta-data-export-v2". "formatVersion" is "1.4".
- "generator" names your tool (up to 200 characters). "generatedAt" (an ISO-8601 time) is
  optional. "contacts" is required, and may be
  empty when the file changes only the user's own data.
- Never include "relationships". A file with relationships is refused.

IDENTITY
- Every person row's "id" is either the exact id copied from the user's export, or the
  literal "new". Never invent an id. Write "new:<key>" instead of "new" when another row in
  the file needs to refer to this person; <key> is 1-64 characters of A-Z a-z 0-9 _ -.
- A row with an existing id must carry "importType". A "new" row must not.
- One person appears at most once in a file, with one mode. If one person needs both a
  correction ("update") and an addition ("additive"), write two files. The first holds
  everything else and that person's "update" row; the second holds only their "additive"
  row. The user imports the first file, then the second. Importing the addition first can
  leave two deets with the same key, and the correction is then refused.
- Deet, tag and group row ids are labels inside the file, used only so other rows can name
  them. Use "new:<key>" for every row you write.
- Every reference names a row in this same file: a deet's "contactId" and "parentDeetId",
  every key and value in "contact_tag_ids" and "group_members", and "memberSelfDeetIds".
  Never point a reference at an id that is only in the export.
- To add a deet under one the person already has, also write a row for that existing deet
  (same key, same parent) and use it as the parent. Deeta finds existing deets by their
  place and key, never by id, so under "additive" the existing deet is matched, not
  duplicated. Copy its value from the export; children are added under it either way.
- To put someone in a tag or group the user already has, write a "tags" or "groups" row
  with that name. Deeta matches an existing tag by name, ignoring case, and an existing
  group by name under the same parent, so the user's tag is reused, not duplicated.

MODES (importType)
- "additive" only adds. It adds deets. It adds a phone, email, URL or address entry under
  a label the person does not have yet, and a birthday if the person has none. An existing
  entry is skipped and never changed. It fills organization and notes only when empty.
- "update" only changes values of things that already exist: deets by key, phone, email,
  URL and address entries by label, and the birthday. It is the ONLY way to change an
  existing phone, email, URL, address or birthday. It creates nothing.
- "overwrite" makes the person's deets exactly the deet rows you write for them. Every deet
  you leave out is removed. It does not touch phone, email, URL, address, birthday,
  organization or notes.
- When in doubt, use "additive". Use "update" for corrections. Use "overwrite" only when the
  user asks to start a person's deets over.
- No mode changes a name, deletes a person, removes a phone, email, URL or address entry,
  or detaches a tag or group.
- A person whose row says "update" must not appear in "contact_tag_ids" or "group_members".
  To add tags or groups for that person, use a separate file with "additive".
- A value another person shares with the user live cannot be changed. An "update" to it is
  added beside it, and an "overwrite" keeps it. Do not retry such a change.

FIELDS
- Phone, email and URL entries are { "label", "value" }. Addresses are
  { "label", "street", "city", "state", "postalCode", "country", "isoCountryCode" }.
  Birthday is { "month", "day", "year" } with "year" optional. Every entry needs a "label"
  (1-50 characters); one person has at most one entry per field and label.
- To keep a value as it is, leave the field out. To clear a text value under "update",
  write "". Never write null in a text field: the file is refused.
- Deets: "contactId" is the person's id from "contacts", "key" is 1-50 characters, "value"
  is up to 280, "parentDeetId" is another deet row's id for the same person or null.
  Deets nest at most 7 levels.
- Stay within the limits in the schema. Do not invent fields the schema does not list.

THE USER'S OWN DATA (only when the user asks)
- "self_profile" carries "importType" (same three modes) and may set "bio" and
  "organization". The user's name is never changed.
- "self_deets" rows are like deet rows without "contactId", plus optional "visibility"
  ("always", "ask" or "never"). They need a "self_profile" row.
- "deet_sets" rows follow the person identity rule. "memberSelfDeetIds" lists
  "self_deets" row ids. A file cannot delete a set.
- Changing the user's own deets reaches the people they share with live, as an edit in
  the app does.

ERROR REPORTS
- If the user gives you a Deeta error report, correct exactly the rows it lists, matched
  by "collection" + "rowIndex" + "rowId", and change nothing else. Then return the whole
  corrected file.

AFTER A SUCCESSFUL IMPORT
- Never send a file's "new" rows again. Every imported "new" row creates a person, so
  sending it twice makes a duplicate. To change someone you created, ask the user for a
  fresh export and use that person's id.

OUTPUT
- Return only the JSON file, nothing else. When the request needs two files, return
  both, labelled "1" and "2" in the order to import them.

When the import fails

Deeta shows the first problem and offers Save error report. Give the report file (deeta-import-report.json) back to the agent with the same prompt and the file it wrote. The prompt tells it to correct exactly the rows the report lists.

What the agent does not get

The kit contains no Deeta data. The agent sees only what you give it, and the error report holds locations, never values (deeta-import-v1.md, section 9).

A round trip with the kit

An export, and the two files an AI agent wrote from it with the kit for one request. The first is imported first.

{
  "format": "deeta-data-export-v2",
  "formatVersion": "1.4",
  "generator": "deeta-ios/1.1",
  "generatedAt": "2026-10-15T09:00:00Z",
  "contacts": [
    {
      "id": "3f2b8c1e-5a7d-4e9b-8c21-0a4d6f7b9e10",
      "contactType": "person",
      "firstName": "Maya",
      "lastName": "Chen",
      "organization": "Harbour & Co",
      "phoneNumbers": [{ "label": "mobile", "value": "+44 7700 900123" }],
      "emailAddresses": [{ "label": "home", "value": "maya.chen@example.org" }],
      "isFavorite": false,
      "createdAt": "2026-01-04T10:00:00Z",
      "updatedAt": "2026-09-01T10:00:00Z"
    },
    {
      "id": "9d4e1f70-2b3c-4a5d-9e6f-1b2c3d4e5f60",
      "contactType": "person",
      "firstName": "Jonas",
      "lastName": "Weber",
      "phoneNumbers": [{ "label": "mobile", "value": "+49 30 901820" }],
      "isFavorite": true,
      "createdAt": "2026-02-10T10:00:00Z",
      "updatedAt": "2026-09-01T10:00:00Z"
    }
  ],
  "deets": [
    { "id": "a1b2c3d4-0000-4000-8000-000000000001", "contactId": "3f2b8c1e-5a7d-4e9b-8c21-0a4d6f7b9e10",
      "key": "Coffee order", "value": "Flat white", "parentDeetId": null, "source": "manual",
      "createdAt": "2026-03-01T10:00:00Z", "updatedAt": "2026-03-01T10:00:00Z" },
    { "id": "a1b2c3d4-0000-4000-8000-000000000002", "contactId": "9d4e1f70-2b3c-4a5d-9e6f-1b2c3d4e5f60",
      "key": "Kids", "value": "", "parentDeetId": null, "source": "manual",
      "createdAt": "2026-03-01T10:00:00Z", "updatedAt": "2026-03-01T10:00:00Z" },
    { "id": "a1b2c3d4-0000-4000-8000-000000000003", "contactId": "9d4e1f70-2b3c-4a5d-9e6f-1b2c3d4e5f60",
      "key": "Mila", "value": "Born 2020", "parentDeetId": "a1b2c3d4-0000-4000-8000-000000000002", "source": "manual",
      "createdAt": "2026-03-01T10:00:00Z", "updatedAt": "2026-03-01T10:00:00Z" }
  ],
  "tags": [{ "id": "b1b2c3d4-0000-4000-8000-000000000001", "name": "Book club" }],
  "contact_tag_ids": { "9d4e1f70-2b3c-4a5d-9e6f-1b2c3d4e5f60": ["b1b2c3d4-0000-4000-8000-000000000001"] },
  "groups": [],
  "group_members": {},
  "self_profile": null,
  "self_deets": [],
  "deet_sets": [],
  "relationships": []
}
{
  "format": "deeta-data-export-v2",
  "formatVersion": "1.4",
  "generator": "kit-agent/1.0",
  "generatedAt": "2026-10-15T12:00:00Z",
  "contacts": [
    {
      "id": "3f2b8c1e-5a7d-4e9b-8c21-0a4d6f7b9e10",
      "importType": "update",
      "phoneNumbers": [{ "label": "mobile", "value": "+44 7700 900456" }]
    },
    {
      "id": "9d4e1f70-2b3c-4a5d-9e6f-1b2c3d4e5f60",
      "importType": "additive"
    },
    {
      "id": "new:ana",
      "contactType": "person",
      "firstName": "Ana",
      "lastName": "Lima",
      "phoneNumbers": [{ "label": "mobile", "value": "+351 912 345 678" }]
    }
  ],
  "deets": [
    {
      "id": "new:jonas-kids",
      "contactId": "9d4e1f70-2b3c-4a5d-9e6f-1b2c3d4e5f60",
      "key": "Kids",
      "value": "",
      "parentDeetId": null
    },
    {
      "id": "new:jonas-leo",
      "contactId": "9d4e1f70-2b3c-4a5d-9e6f-1b2c3d4e5f60",
      "key": "Leo",
      "value": "Born 2023",
      "parentDeetId": "new:jonas-kids"
    },
    {
      "id": "new:ana-coffee",
      "contactId": "new:ana",
      "key": "Coffee order",
      "value": "Cortado",
      "parentDeetId": null
    }
  ],
  "tags": [{ "id": "new:book-club", "name": "Book club" }],
  "contact_tag_ids": { "new:ana": ["new:book-club"] }
}
{
  "format": "deeta-data-export-v2",
  "formatVersion": "1.4",
  "generator": "kit-agent/1.0",
  "generatedAt": "2026-10-15T12:00:00Z",
  "contacts": [
    {
      "id": "3f2b8c1e-5a7d-4e9b-8c21-0a4d6f7b9e10",
      "importType": "additive",
      "emailAddresses": [{ "label": "work", "value": "maya@harbourco.example.org" }]
    }
  ]
}