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.
Raw files. Every schema and example below is published unchanged at a stable URL, for tools and agents to fetch.
deeta-import-v1.mddeeta-import-v1.kit.mddeeta-import-v1.schema.jsondeeta-import-v1.report.schema.jsonexamples/minimal.deetaexamples/enrich.deetaexamples/failing.deetaexamples/error-report.jsonexamples/kit-round-trip/export.jsonexamples/kit-round-trip/1-update-and-add.deetaexamples/kit-round-trip/2-add-maya-email.deeta
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.
| Artifact | What it is |
|---|---|
deeta-import-v1.md | This reference. |
deeta-import-v1.schema.json | JSON Schema for a merge file. |
deeta-import-v1.report.schema.json | JSON Schema for the error report Deeta produces. |
deeta-import-v1.kit.md | The generation kit: a prompt for an AI agent or script that writes a merge file. |
examples/minimal.deeta | The smallest valid file: one new person. |
examples/enrich.deeta | Every row class and mode in one file. |
examples/failing.deeta + examples/error-report.json | A 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
- You pick a file in Settings → Import → Import & Merge….
- 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.
- Deeta shows a preview of every change. Nothing is saved until you confirm.
- The import applies all at once, or not at all.
- 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": []
}
| Field | Required | Rule |
|---|---|---|
format | yes | Exactly deeta-data-export-v2. Backups carry the same value. |
formatVersion | yes | major.minor. See Versioning. Write 1.4. |
generator | no | Free text naming the tool that wrote the file, at most 200 characters. Never used to decide anything. |
generatedAt | no | ISO-8601 timestamp. |
contacts | yes | The people in the file. May be empty when the file only changes your own data. |
Sections
| Section | Shape | Read by a merge |
|---|---|---|
contacts | array of person rows | yes |
deets | array of deet rows | yes |
tags | array of tag rows | yes |
contact_tag_ids | object: person id → array of tag row ids | yes |
groups | array of group rows | yes |
group_members | object: group row id → array of person ids | yes |
self_profile | one row, or absent | yes |
self_deets | array of your own deet rows | yes |
deet_sets | array of deet set rows | yes |
relationships | must be absent or empty | a 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, accountMarker | anything | accepted 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.
id | importType | Found in Deeta? | Result |
|---|---|---|---|
"new" / "new:<key>" | must be absent | — | Create: a new person. |
| an existing id | "additive" | yes | Additive: adds what is missing. |
| an existing id | "update" | yes | Update: changes what is already there. |
| an existing id | "overwrite" | yes | Overwrite: the person's deets become exactly the file's. |
| an existing id | any | no, or the person is a different type | The row is rejected and reported. It never becomes a new person. |
missing, malformed, an existing id with no importType, or "new" with an importType | The whole file is rejected. |
Which mode when
| You want to | Use |
|---|---|
| Put everything back from a backup | Not this import. Use Restore Backup…. |
| Export, add to it, bring it back | additive |
| Correct what is already there (a number changed) | update |
| Start one person's deets over | overwrite, 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
| Create | additive | update | overwrite | |
|---|---|---|---|---|
| Name | set | never changed | never changed | never changed |
organization, notes | set | filled if empty | changed; "" clears it | not touched |
| Phone, email, URL, address | set | an entry under a label the person does not have is added | the entry under a matching label is changed | not touched |
| Birthday | set | set if the person has none | changed | not touched |
| Deets | added | added; nothing changed or removed | values of existing deets changed | replaced by the file's deets |
| Tags and groups | attached | attached if not already | not allowed in the file | attached 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
updateto 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
overwritekeeps 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 writes | Meaning |
|---|---|
| no such field | Leave 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. |
null | Refused (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
| Field | Create row | Existing row | Rule |
|---|---|---|---|
id | required | required | See section 3. |
importType | forbidden | required | additive, update or overwrite. |
contactType | required | optional | person or organization. On an existing row it must match the person in Deeta. |
firstName, lastName | optional | accepted and ignored | Up to 150 characters each. |
organization | optional | optional | Up to 100 characters. |
notes | optional | optional | Up to 10,000 characters. Line breaks allowed. |
phoneNumbers | optional | optional | Array of { "label", "value" }. |
emailAddresses | optional | optional | Array of { "label", "value" }. |
urlAddresses | optional | optional | Array of { "label", "value" }. |
physicalAddresses | optional | optional | Array of address entries. |
birthday | optional | optional | { "month", "day", "year" }; year may be left out. |
isFavorite, createdAt, updatedAt | accepted and ignored | accepted 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 }
| Field | Rule |
|---|---|
label | Required on every entry, 1 to 50 characters. Common labels are mobile, home, work, main, other and homepage; any text is allowed. |
Phone value | Up to 100 characters. The international form (+44 7700 900456) is recommended, not required. |
Email value | Up to 254 characters. |
URL value | Up to 2,048 characters. A full address with https:// is recommended, not required. |
| Address | Each 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. |
| Birthday | month 1–12, day 1–31, year optional; when given, 1900–9999. |
| Entries per field | At 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" }
| Field | Required | Rule |
|---|---|---|
id | yes | A 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. |
contactId | yes | The 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. |
key | yes | 1 to 50 characters. |
value | yes | Up to 280 characters. May be empty. |
parentDeetId | no | The id of another deet row in this file for the same person, or null for a top-level deet. |
sourceLinkId | no | Optional. If Deeta already holds a deet from that source, the row is skipped. It is never saved. |
relationshipId | no | Must be absent or null. |
source, createdAt, updatedAt, archivedAt | no | Accepted 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"] }
| Field | Rule |
|---|---|
Tag id, group id | A label for the row inside the file, as for a deet. |
name | 1 to 50 characters. |
parentGroupId | The id of another group row in this file, or null. |
contact_tag_ids | Each 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_members | Each 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.
| Field | Rule |
|---|---|
importType | Required. |
organization | Up to 100 characters. Filled if empty (additive) or changed (update); "" under update clears it. |
bio | Up to 500 characters. Same rule. |
firstName, lastName, id, contactId, createdAt, updatedAt | Accepted and ignored. Your name is never changed. |
self_deets rows have id, key, value and parentDeetId as in section 5, plus:
| Field | Rule |
|---|---|
visibility | Optional: always, ask or never. Used for a deet the import creates; without it the deet gets Deeta's default. |
version, notifySubscribers, deetSource, createdAt, updatedAt | Accepted 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.
| Field | Rule |
|---|---|
name | 1 to 50 characters. Required on a new set. |
description | Up to 140 characters. "" under update clears it. |
liveSyncDefault | always, ask or never. |
cardVisibilityConfig | { "show_set_name", "show_deet_keys", "show_deet_values" }, each true or false. |
memberSelfDeetIds | Row ids from this file's self_deets. At most 200. |
isDefault, defaultKind, createdAt, updatedAt | Accepted and ignored. |
| Mode on a set you have | Effect |
|---|---|
additive | Adds the listed deets to the set. |
update | Adds the listed deets and changes the name, description, liveSyncDefault and cardVisibilityConfig the row states. |
overwrite | The 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
| Limit | Value |
|---|---|
| File size | 32 MB |
People in contacts | 10,000 |
Rows in deets | 100,000 |
| Deet nesting depth | 7 |
Text
| Field | Limit |
|---|---|
Deet key | 50 |
Deet value | 280 |
Tag, group and deet set name; any label | 50 |
firstName, lastName | 150 each |
organization | 100 |
notes | 10,000 |
bio | 500 |
Deet set description | 140 |
| Phone | 100 |
| 254 | |
| URL | 2,048 |
Address street | 500 |
| Other address parts | 200 each |
Address isoCountryCode | 12 |
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.
| Limit | Value |
|---|---|
| People | 25,000 |
| Deets on your account, people's and your own together | 50,000 |
| Deets on one person | 500 |
| Your own deets | 500 |
| Tags on one person | 30 |
| Tags in total | 500 |
| Top-level groups | 500 |
| Sub-groups under one group | 200 |
| Deet sets | 50 |
| Deets in one set | 200 |
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.
notesand an addressstreetmay 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.
- 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. - An existing person's id appears on at most one row in
contacts. - Every deet row's
contactIdnames a person with a row incontacts. - Row ids are unique within
deets, withintags, withingroups, withinself_deetsand withindeet_sets. - A
parentDeetIdnames a row in the same section, for the same person, and the chain has no loop and is at most 7 deep. - One person has at most one entry per field and label.
- A person whose row says
updatedoes not appear incontact_tag_idsorgroup_members. self_deetsrows need aself_profilerow.- No field appears that this document does not list (see Versioning).
- The file changes something: at least one person row, or a
self_profilerow.
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." }
]
}
| Field | Meaning |
|---|---|
reportId | A random id made for this report, shown on the screen beside the failure. It is not derived from the file. |
totals | How many entries of each severity Deeta found, including any past the cap. |
truncated | true when Deeta found more than the 500 entries a report holds. |
collection, rowIndex | The section and the zero-based position of the row in it. null for a problem with the file as a whole. |
rowId | The row's own id exactly as the file gave it, or null. |
field | The field at fault, or null. |
message | A sentence for a person. |
severity | Meaning |
|---|---|
file_fatal | The file was rejected. Nothing was imported. |
row_rejected | That row could not be matched in Deeta. The rest of the file was still offered. |
row_skipped | That 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
| Code | Severity | Meaning |
|---|---|---|
invalid_json | file | Not valid JSON, or not UTF-8. |
not_a_deeta_file | file | format is missing or is not a Deeta format. |
unsupported_format | file | A Deeta format other than deeta-data-export-v2. |
missing_format_version, invalid_format_version | file | formatVersion is absent or does not parse. |
newer_format_version | file | Written for a newer Deeta. Update the app. |
older_format_version | file | Too old for this Deeta to read. |
file_too_large | file | Over 32 MB. |
is_backup_file | file | A plain backup. Use Restore Backup. |
relationships_not_supported | file | The file has relationships. |
no_rows | file | Nothing in the file to import. |
unknown_field | file | A field this document does not list. |
missing_field | file | A required field is absent. |
wrong_type | file | A value of the wrong JSON type. |
null_value | file | null in a text field. Leave the field out to keep the value, or write "" to clear it. |
invalid_value | file | A value outside the allowed set. |
invalid_id | file | An id or reference that is neither an id nor new / new:<key>. |
missing_import_type | file | An existing record's row has no importType. |
import_type_on_new_row | file | A new row carries importType. |
unknown_import_type | file | importType is not one of the three. |
duplicate_local_key | file | The same new:<key> is declared twice. |
undeclared_local_key | file | A new:<key> is referred to and declared nowhere. |
duplicate_row_id | file | Two rows in one section share an id. |
duplicate_target | file | Two rows target the same existing record. |
orphan_row | file | A row belongs to a person with no row in contacts. |
parent_not_found | file | parentDeetId or parentGroupId names no row. |
parent_cycle | file | A parent chain loops. |
nesting_too_deep | file | Deets nested more than 7 deep. |
duplicate_core_field_label | file | Two entries with the same field and label for one person. |
membership_under_update | file | A tag or group membership for a person in update mode. |
self_rows_without_profile | file | self_deets rows with no self_profile. |
string_empty, string_too_long | file | Text shorter or longer than allowed. |
forbidden_character | file | A character from the not-allowed list. |
invalid_birthday | file | The birthday does not meet its rule in section 4. |
over_file_cap | file | More rows than a file may hold. |
over_account_cap | file | The 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_found | row rejected | No such record in Deeta. Deleted since the export, or mistyped. |
type_mismatch | row rejected | The record in Deeta is a different type. |
ambiguous_match | row rejected | More than one existing deet or entry matches. |
already_present | row skipped | Already there. Use update to change it. |
not_present | row skipped | Not there. Use additive to add it. |
late_constraint | row skipped | The 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:
- the prompt below;
deeta-import-v1.schema.json, the file's JSON Schema;deeta-import-v1.report.schema.json, the error report's JSON Schema;- the examples
examples/minimal.deetaandexamples/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.
overwritelets the generator remove deets it did not know about. That is why the prompt makesadditivethe default. Useoverwriteonly 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" }]
}
]
}