Weave your Apple / iCloud Contacts — and the relationships between them — into Obsidian as clean, queryable Markdown.
The name. textum (Latin) — a woven fabric; also: a text. The plugin weaves your contacts and the relationships already in your address book into one connected, textual graph inside Obsidian.
Sync macOS / iCloud Contacts into Obsidian as clean, well-structured Markdown notes — and turn the relationships that already live in your address book (family, project teams, company membership) into data you can query, chart and visualise inside Obsidian.
The frontmatter is deliberately aligned with Charted Roots, the genealogy/relationship plugin textum targets first (see Orientation toward Charted Roots).
- textum
Apple Contacts.app is an excellent place to store contact data but a poor place to
analyse it. Years — sometimes a decade — of carefully maintained names, addresses,
dates, photos, group memberships and related names sit in a flat store with no query
language, no joins, no graph.
This extension extracts that data into one Markdown note per contact with a clean YAML frontmatter, so it can be used and cross-analysed in Obsidian with extensions like Charted Roots, Relations, Breadcrumbs, Bases, Dataview and others.
Typical uses:
- Family / genealogy — render family trees and pedigrees from the related names
you already maintain. Apple's standard kinship labels (Mother, Father, Brother, Sister,
Son, Daughter, Spouse) become typed, gendered frontmatter keys
(
father,mother,brother,sister,children,spouse) that map straight onto Charted Roots' tree fields. - Project teams — link every contact assigned to a building project, see all participants of "Project renovation city hall", filter by trade (carpenter, bricklayer, etc.): who is on which project, which trades are covered, who rated "recommended".
- Company / org membership — who belongs to which firm or department.
- Map — plot contact addresses on a map (requires a geocoding step, see Usage).
The original syncs contacts faithfully but flattens several things that this fork now makes structured and analysable. Status per item (all implemented):
| # | Improvement | Why it matters ("leverage") | Status |
|---|---|---|---|
| 1 | Write group membership as a groups: property per contact |
The original already fetches iCloud group cards (CardDAV X-ADDRESSBOOKSERVER-KIND:group) but only uses them to filter which contacts to sync. Writing membership exposes your entire group taxonomy as a queryable property. |
done |
| 2 | Write addresses as a flat, readable list under addresses: |
The original joins ADR into one raw string and could render it as a JSON-looking array. This fork writes one human-readable line per address (optional label + street, postcode + city, state, country), e.g. Privat: Vogelsangstr. 27, 70176 Stuttgart, Deutschland. |
done |
| 3 | Un-escape notes (\n, \, → real newline/comma) |
The original leaks vCard escaping into the note text. | done |
| 4 | Typed, gendered relationship keys (father, mother, spouse, children, brother, sister, cousin / cousine, …) |
The original puts all related names into one related names list. Splitting into typed keys is what lets genealogy/relationship plugins draw family trees. The keys match Charted Roots' tree fields directly (father / mother / spouse / children); side relatives map onto Charted Roots custom relationship types. Charted Roots reconstructs the reverse direction of family edges itself, so each relationship only needs to be recorded once. |
done |
| 5 | Localisation layer (relationship_locales.json) |
English canonical relationship keys + per-language label maps, so the tool works for non-German/English address books. | done |
| 6 | Filesystem-safe filenames + name-based linking | Sanitises only the characters that are unsafe on file systems / WebDAV (slash, colon, asterisk, quote, angle brackets, pipe, hash, leading/trailing whitespace, trailing dots), keeps spaces and umlauts, and normalises to Unicode NFC (avoids macOS ↔ Nextcloud mismatches). Relationships resolve against the name frontmatter / alias, not the raw filename — so sanitising a name never breaks a link. |
done |
"Leverage" = biggest benefit per unit of effort. #1 and #4 unlock the most (groups + genealogy visualisation); #2 makes addresses both readable and map-ready; #3 fixes readability.
Mapping from Apple Contacts field → recommended use → Obsidian frontmatter key.
Rule of thumb: Person ↔ person → Related name. Categories → Groups. Role → Job title.
Free text → Notes (with the --- convention below).
| Apple field | vCard | Recommended use | Frontmatter key | Multi |
|---|---|---|---|---|
| First/Last/ Prefix/Suffix | N / FN | Name, note title | name / file name |
– |
| Nickname | NICKNAME | Nickname | nickname |
– |
| Company | ORG | Organisation | organization |
– |
| Department | – | Sub-grouping | department |
– |
| Job title | TITLE | Role / trade (Gewerk) of a person | role |
– |
| Phone | TEL | Contact | telephone |
yes |
| Contact | email |
yes | ||
| Homepage | URL | Web | url |
yes |
| Address | ADR | Location → map (after geocoding) | addresses (flat list) |
yes |
| Birthday | BDAY | Life dates | birthday |
– |
| Date (custom) | X-ABDATE + label | Death date, anniversary, events | death / date |
yes |
| Related name | X-ABRELATEDNAMES + label | Genealogy + person relations | father / mother / spouse / children / brother / sister / … |
yes |
| Social profile | X-SOCIALPROFILE | Social | social profile |
yes |
| Instant message | IMPP | Messaging | instant message |
yes |
| Notes | NOTE | Tag block + prose (see below) | tags + body |
– |
| Groups | group card | Project / trade / rating / district / selection | groups |
yes |
- macOS (tested on macOS 15.7.5)
- macOS Contacts (tested on v14.0)
- Obsidian (tested on v1.12.7)
- An iCloud app-specific password (or a Nextcloud address-book share URL)
Not yet in the community store. Two ways to install:
- Install BRAT (Settings → Community plugins → Browse → "BRAT").
- BRAT → Add beta plugin → paste:
netzbub/textum - Confirm. BRAT installs textum from the latest release and keeps it updated.
- Enable textum under Settings → Community plugins.
- Build:
npm installthennpm run build→ producesmain.js. - Copy
main.js,manifest.jsonandstyles.cssinto<your-vault>/.obsidian/plugins/textum/. - Enable the plugin under Settings → Community plugins.
(For development: npm run dev for a watching build; npm test runs the Jest suite.)
- iCloud username — your iCloud email.
- iCloud app-specific password — generate at support.apple.com/102654.
- iCloud server URL — default
https://contacts.icloud.com(China:https://contacts.icloud.com.cn; Nextcloud: the address-book share URL). - Contacts folder — vault folder for the notes (default
Contacts). - Labels toggles — add labels to phone / email / url / related names / addresses.
- Write Charted Roots keys — write
cr_type(person, ororganizationfor Apple "Company" cards) and a stablecr_id(the Apple UID) to every contact, so the notes are recognised by the Charted Roots plugin. On by default; turn off if you don't use Charted Roots. - Excluded keys — space-delimited keys not written to frontmatter (raw data stays in
iCloudVCard). - Groups — select which iCloud groups to sync, with a Select all groups master toggle.
.
├── main.ts # plugin entry, commands, Obsidian API wrapper
├── manifest.json # Obsidian plugin manifest
├── src/
│ ├── ICloudContactsApi.ts # core sync orchestration
│ ├── iCloudClient.ts # CardDAV client (adapted from tsdav)
│ ├── parser.ts # vCard → jCard parsing, full-name logic
│ ├── frontMatter.ts # jCard → YAML frontmatter (the mapping core)
│ ├── relationshipMapping.ts # related-name labels → canonical keys
│ ├── relationship_locales.json # per-language label maps + display names
│ ├── SettingTab.ts # settings UI incl. group discovery
│ ├── sanitize.ts # filesystem-safe filename sanitisation
│ ├── VCards.d.ts # types
│ └── *.test.ts # Jest tests
├── esbuild.config.mjs # build
├── package.json / tsconfig.json / jest.config.js / .eslintrc
├── version-bump.mjs / versions.json
└── LICENSE.TXT # AGPL-3.0
This is where the value is created.
Conventions:
-
Person ↔ person relationships (genealogy): use Related name with labels (Mother, Father, Brother, Sister, Son, Daughter, Spouse, and custom labels such as Uncle, Grandfather, Cousin). The value (=first name & family name) must exactly match the other contact's display name — resolution is by name. textum turns these labels into typed, gendered frontmatter keys: Mother →
mother, Father →father, Brother →brother, Sister →sister, Son/Daughter →children, Spouse →spouse; side relatives keep their own keys (cousin,cousine,uncle,aunt,nephew,niece,grandfather,grandmother).
As soon as the localisation is finished, it will be possible to use the localized expressions in Contacts.app for the following languages:
Ready: English · German
Planned: French · Spanish · Italian · Portuguese · Dutch · Danish · Norwegian · Swedish · Finnish · Russian · Ukrainian · Polish · Turkish · Japanese · Chinese (Mandarin) · Korean · Hindi · Arabic -
Categories (project, trade/Gewerk, rating, district, selection): use Groups.
-
Role / trade of a person: use the Job title field.
-
Free text + structured tags in Notes — use a delimiter convention so the note stays parseable:
Doctors, ENT --- Opening hours Mon–Thu 8–17:00, Fri 8–12:00Everything above the
---line is treated as comma- or line-separated tags (here →Doctors,ENT); everything below stays as free note body. Pick one style (comma or one entry per line) and keep it consistent.
Apple Contacts cannot create arbitrary new field types — only custom labels on existing types. That is why categorical data goes into Groups, not invented fields.
Run the command "Update contacts" (only changed contacts) or "Update all contacts" (rewrites everything — use after changing settings/excluded keys).
The plugin only manages a fixed set of keys plus the title and the top H1. Anything
else you write in a note — and any extra frontmatter keys you add yourself (e.g.
rating:, lat:, lng:) — is preserved across
syncs. Sync is one-way (iCloud → Obsidian) by design: edits or corruption in Obsidian
can never flow back to your address book.
The primary target is Charted Roots (see the next section for the field mapping and the one-time setup). Other plugins consume the same frontmatter:
| Purpose | Extension | Output |
|---|---|---|
| Family trees, pedigrees, organisation hierarchies, GEDCOM round-trip | Charted Roots | family/pedigree charts, canvas trees, maps, reports |
| Family trees, pedigrees, relationship graphs | Relations | family graph |
| Hierarchy navigation, project/org trees | Breadcrumbs (typed links) | tree / matrix / Mermaid / Markmap / Canvas |
| Tables, cards, map of contacts | Bases (core) | table / card / map view |
| Ad-hoc queries (e.g. birthdays today, phone list of linked contacts) | Dataview | tables / lists |
| Address pins (after geocoding) | Map View / Leaflet | interactive map |
Example — Dataview list of contacts linked in the current note:
```dataview
TABLE email, telephone
FROM outgoing([[#]])
WHERE iCloudVCard
```Map: addresses need geocoding (address string → lat/lng) before plotting — a
one-time batch step (Nominatim/OSM with rate limits, or a paid geocoder) that writes
coordinates into the notes. Then Bases map view or Map View renders them.
textum's frontmatter was not designed in isolation. Before settling the field schema, the
existing Obsidian genealogy/relationship ecosystem was surveyed to see what a mature target
looks like. Among the available tools,
Charted Roots stood out as the
most advanced and the most actively developed: beyond family trees it models places,
sources, events and organisational hierarchies as first-class entity types (with its own
organization notes and membership arrays), and the repository is under active development.
On that basis textum's output keys were aligned with the properties Charted Roots reads, so
that contacts maintained in Apple Contacts feed a forward-looking target without manual
reshaping.
This is an alignment, not an affiliation: textum is independent and not endorsed by the Charted Roots project.
Field mapping — textum → Charted Roots:
| textum frontmatter | Charted Roots property | Role in Charted Roots |
|---|---|---|
note title / name |
name |
person node |
cr_type (person / organization) |
cr_type |
note-type detection — written automatically (see below) |
cr_id (Apple UID) |
cr_id |
stable unique id — written automatically |
father |
father |
parent edge (drawn in the tree) |
mother |
mother |
parent edge (drawn in the tree) |
spouse |
spouse |
spouse edge (drawn in the tree) |
children |
children |
child edge (drawn in the tree) |
brother / sister |
custom relationship type | shown in Entity Profile; siblings are otherwise derived from shared parents |
cousin / cousine, uncle, aunt, nephew, niece, grandfather, grandmother |
custom relationship types | shown in Entity Profile (not drawn in the tree) |
birthday |
born (via property alias) |
birth date |
death |
died (via property alias) |
death date |
organization (plain text from Apple ORG) |
— | mapping to Charted Roots' organization notes / membership_* arrays is manual for now (see Organisation hierarchies are Phase 2, below) |
Charted Roots stores family links as a wikilink plus an optional <field>_id; the bare
wikilink textum writes is sufficient, and Charted Roots reconstructs the reverse direction
of each family edge itself.
One-time Charted Roots configuration (in the Charted Roots vault, not in textum):
- Property aliases — Settings → Charted Roots → Property & value aliases: map
birthday→bornanddeath→died. - Custom relationship types — define
brother,sister,cousin,cousine,uncle,aunt,nephew,niece,grandfather,grandmotherso they render with a label/line style in the Entity Profile.
Note-type detection. Charted Roots only treats a note as a person when it carries
cr_type: person (companies use cr_type: organization; the legacy type: or a
#person tag also work). textum writes cr_type and a stable cr_id (the Apple UID)
automatically — controlled by the Write Charted Roots keys setting, on by default. Apple
"Company" cards (X-ABShowAs:COMPANY) are written as organization, everyone else as
person. Disable the setting if you don't use Charted Roots. Toggling this setting counts
as a settings change, so the next Update contacts run rewrites every note to add (or
remove) the keys.
Organisation hierarchies are Phase 2. Company cards are marked cr_type: organization, but Apple only provides the firm as a plain text string (ORG, written to
organization). Charted Roots' richer organisation model — parent_org,
membership_orgs / membership_roles, seats, roles — is not populated; linking people to
firms and building org hierarchies from the flat ORG text is left for a later iteration.
Relationship type names are English canonical (father, mother, brother,
sister, children, spouse, cousin, grandfather, …). Apple's standard relationship
labels are already stored as English tokens internally (_$!<Mother>!$_), so only
custom labels (in the user's language) need a per-language map in
relationship_locales.json (label_map_source_to_canonical).
Adding a language = adding ~20–40 kinship terms → canonical keys. Mechanically cheap.
Linguistically, languages with finer kinship systems (Chinese, Japanese, Arabic, Hindi)
distinguish e.g. paternal vs. maternal uncle; map several source terms onto one canonical
key (lossy) or extend the canonical keys (uncle_paternal, uncle_maternal).
Widely spoken languages worth supporting: English, Mandarin Chinese, Hindi, Spanish, French, Arabic, Portuguese, Russian, Japanese, plus Italian, Korean, Turkish, Dutch, Polish.
Several of the planned languages — Chinese, Japanese, Korean, Arabic and Hindi — encode kinship distinctions that English and German simply collapse. Where English has a single word uncle, these languages mark the father's vs. the mother's side, and often the relative age of the linked relative: Mandarin separates 伯 (father's elder brother), 叔 (father's younger brother) and 舅 (mother's brother); Arabic distinguishes عم (paternal uncle) from خال (maternal uncle); Hindi has चाचा / ताऊ versus मामा. Siblings are split by seniority in Chinese, Japanese and Korean (e.g. 哥 elder vs. 弟 younger brother; 兄 vs. 弟), and Korean terms additionally depend on the speaker's own gender (a male says 형 for an older brother, a female says 오빠).
Because these distinctions have no one-to-one equivalent among the English canonical keys, the localisation resolves them in one of two ways, configurable per language:
- Collapse (default). Every source term is mapped to the nearest canonical key — 伯, 叔
and 舅 all become
uncle. This keeps relationship graphs consistent and comparable across languages. The nuance is not lost silently: the original label is preserved on the link (as its display alias /relation_label), so it stays visible in the note even though the structural key is generic. - Extended keys (opt-in). Finer canonical keys such as
uncle_paternal/uncle_maternalorbrother_elder/brother_youngercan be enabled, preserving the distinction as structured data. Note that the family-tree visualisers build their layout only from the structural core (father,mother,spouse,children); extended kinship keys therefore act as descriptive metadata and do not change the shape of the tree.
The default is collapse, because it yields clean, comparable graphs and never discards information (the original term is retained as a label). Speakers of these languages who want full fidelity can switch on the extended keys for their locale.
GNU Affero General Public License v3.0 (AGPL-3.0). See LICENSE.TXT.
(The upstream package.json declared GPL-3.0 while its manifest/LICENSE stated
AGPL-3.0; this fork uses AGPL-3.0 consistently.)
- Built on Truls Aagaard's obsidian-icloud-contacts. Not affiliated with or endorsed by Truls.
- Frontmatter aligned with Charted Roots by banisterious. Independent project; not affiliated with or endorsed by Charted Roots.
- The CardDAV client (
iCloudClient) is adapted from tsdav. - Not affiliated with Apple in any way.
- The idea and concept were mine; coordination, structuring and realisation grew out of close teamwork with Claude Opus 4.8; the code — where not still Truls' original work — was mostly written by Claude Code.

