
Automating Contacts
- 19 installs
- 39 repo stars
- Updated January 14, 2026
- spillwavesolutions/automating-mac-apps-plugin
Helps with ai & agent building tasks during AI-assisted development.
About
automating-contacts is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- automating-contacts
- AI & Agent Building
- AI-coding skill
Automating Contacts by the numbers
- 19 all-time installs (skills.sh)
- Ranked #10,571 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/spillwavesolutions/automating-mac-apps-plugin --skill automating-contactsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 19 |
|---|---|
| repo stars | ★ 39 |
| Last updated | January 14, 2026 |
| Repository | spillwavesolutions/automating-mac-apps-plugin ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
Files
Automating Contacts (JXA-first, AppleScript discovery)
Relationship to Other Skills
- Standalone for Contacts: Use this skill for Contacts-specific operations (querying, CRUD, groups).
- Reuse `automating-mac-apps` for: TCC permissions setup, shell command helpers, UI scripting fallbacks, and ObjC bridge patterns.
- Integration: Load both skills when combining Contacts automation with broader macOS scripting.
- PyXA Installation: To use PyXA examples in this skill, see the installation instructions in
automating-mac-appsskill (PyXA Installation section).
Core Framing
- Contacts dictionary is AppleScript-first; discover there, implement in JXA
- Object specifiers: read with methods (
name(),emails()), write with assignments - Multi-value fields (emails, phones, addresses) are elements; use constructor +
.push() - Group membership:
add ... tocommand or.people.push; handle duplicates defensively - TCC permissions required: running host must have Contacts access
Workflow (default)
1) Inspect the Contacts dictionary in Script Editor (JavaScript view). 2) Prototype minimal AppleScript to validate verbs; port to JXA with specifier reads/writes. 3) Use .whose for coarse filtering; fall back to hybrid (coarse filter + JS refine) when needed. 4) Create records with proxy + make, then assign primitives and push multi-values; Contacts.save() to persist. 5) Verify persistence: check person.id() exists after save; handle TCC permission errors. 6) Manage groups after person creation; guard against duplicate membership with existence checks. 7) For photos or broken bridges, use ObjC/clipboard fallback; for heavy queries, batch read or pre-filter. 8) Test operations: run→check results→fix errors in iterative loop.
Validation Checklist
- [ ] Contacts permissions granted (System Settings > Privacy & Security > Contacts)
- [ ] Dictionary inspected and verbs validated in Script Editor
- [ ] AppleScript prototype runs without errors
- [ ] JXA port handles specifiers correctly
- [ ] Multi-value fields pushed to arrays properly
- [ ] Groups existence checked before creation
- [ ] Operations saved and verified with
.id()checks - [ ] Error handling wraps all operations
Quickstart (upsert + group)
const Contacts = Application("Contacts");
const email = "ada@example.com";
try {
const existing = Contacts.people.whose({ emails: { value: { _equals: email } } })();
const person = existing.length ? existing[0] : Contacts.Person().make();
person.firstName = "Ada";
person.lastName = "Lovelace";
// Handle multi-value email
const work = Contacts.Email({ label: "Work", value: email });
person.emails.push(work);
Contacts.save();
// Handle groups with error checking
let grp;
try {
grp = Contacts.groups.byName("VIP");
grp.name(); // Verify exists
} catch (e) {
grp = Contacts.Group().make();
grp.name = "VIP";
}
Contacts.add(person, { to: grp });
Contacts.save();
console.log("Contact upserted successfully");
} catch (error) {
console.error("Contacts operation failed:", error);
}Pitfalls
- TCC Permissions: Photos/attachments require TCC + Accessibility; use clipboard fallback if blocked
- Yearless birthdays: Not cleanly scriptable; use full dates
- Advanced triggers: Delegate geofencing to Shortcuts app
- Heavy queries: Batch read or pre-filter to avoid timeouts
When Not to Use
- Non-macOS platforms (use platform-specific APIs)
- Simple AppleScript-only solutions (skip JXA complexity)
- iCloud sync operations (use native Contacts framework)
- User-facing apps (use native Contacts framework)
- Cross-platform contact management (use CardDAV or vCard APIs)
What to load
- JXA basics & specifiers:
automating-contacts/references/contacts-basics.md - Recipes (query, create, multi-values, groups):
automating-contacts/references/contacts-recipes.md - Advanced (hybrid filters, clipboard image, TCC, date pitfalls):
automating-contacts/references/contacts-advanced.md - Dictionary & type map:
automating-contacts/references/contacts-dictionary.md - PyXA API Reference (complete class/method docs):
automating-contacts/references/contacts-pyxa-api-reference.md
Contacts JXA Advanced Patterns
Performance & hybrid filtering
.whoseis best for coarse filters; complex OR/NOT can throw -1700.- Pattern: coarse
.whose→ resolve → JS.filterfor regex/complex logic. - Batch-read when possible:
.name(),.emails.value()on a filtered specifier.
TCC / permissions
- The executing host (Terminal/Script Editor/app bundle) must be granted Contacts access.
- For headless/CI, pre-approve via MDM profile; cannot auto-click the prompt.
Multi-value write discipline
- Treat emails/phones/addresses/dates as elements; create objects and
.push. - Use
Contacts.save()after meaningful mutations (or after a batch).
Images via clipboard (ObjC bridge)
Direct person.image = Path("...") often fails. Use NSPasteboard to bridge the file:
ObjC.import('AppKit');
const pb = $.NSPasteboard.generalPasteboard;
pb.clearContents;
pb.writeObjects($.NSArray.arrayWithObject("/path/to/photo.jpg"));
// Now try setting or use UI scripting to paste
const person = Contacts.people.byName("Ada Lovelace");
// Depending on OS version you may still need a UI paste; clipboard is the safest staging.Yearless birthdays & date pitfalls
- JXA coerces dates; yearless birthdays become full dates (current year/1604). No clean way to set "yearless" via JXA—accept year or manipulate vCard externally.
- Set custom dates at noon (
T12:00:00) to avoid timezone rollovers.
Group membership errors
- Duplicate adds can throw; guard with an existence check (
group.people.whose({ id: {_equals: person.id()} })). - Prefer
Contacts.add(person, { to: group })for clarity;.people.pushis sugar.
When the dictionary is insufficient
- ObjC
Contacts.frameworkis Swift-heavy and awkward to bridge; fallback is usually: - Deprecated
AddressBookbridge (if still present), or - A small external Swift/Python helper invoked via
doShellScriptfor rare edge cases.
Contacts JXA Basics (specifiers, reads, writes)
Initialize
const app = Application.currentApplication();
app.includeStandardAdditions = true;
const Contacts = Application('Contacts');
if (!Contacts.running()) Contacts.activate();Specifier vs data
Contacts.people→ specifier (zero IPC).Contacts.people()→ fetches every person (high cost).Contacts.people.name()→ one IPC returning array of names.- Always call methods to read:
person.firstName()notperson.firstName.
Read collections efficiently
- Prefer server-side filter:
Contacts.people.whose({ emails: { value: { _contains: 'acme.com' }}}). - Access properties on the filtered specifier:
.name()or.emails.value()to batch read. - If
.whoseis too complex, coarse-filter server-side then refine in JS.
Create & save
- Create root object with proxy + make:
const p = Contacts.Person().make();
p.firstName = "Ada";
p.lastName = "Lovelace";
Contacts.save();- Multi-value fields are elements; create and push:
const workEmail = Contacts.Email({ label: "Work", value: "ada@engine.co.uk" });
p.emails.push(workEmail);
Contacts.save();Groups
- Groups are containers; membership is many-to-many.
- Robust add:
Contacts.add(person, { to: group }); - Alternative sugar:
group.people.push(person);(watch for duplicates).
Common operators for .whose
_equals,_contains,_beginsWith,_endsWith,_greaterThan,_lessThan,_not.- Nested collections:
{ emails: { value: { _endsWith: 'university.edu' }}}.
Contacts Dictionary & Type Map (JXA view)
Core classes (specifiers)
Application('Contacts')people(element):Personobjectsgroups(element):GroupobjectsPersonproperties:firstName,lastName,middleName,name,organization,jobTitle,note,birthday,idPersonelements (multi-value):emails→Email { label, value }phones→Phone { label, value }addresses→Address { label, street, city, state, zip, country, countryCode }dates→CustomDate { label, value (Date) }socialProfiles→SocialProfile { service, userName, url }instantMessages→InstantMessage { service, userName, handle }Groupproperties:name,idGroupelements:people(members)
Commands
make/.make(): createPersonorGroup.add <person> to <group>→ JXA:Contacts.add(person, { to: group })(preferred) orgroup.people.push(person).save→Contacts.save()to persist mutations.
Access patterns
- By name:
Contacts.people.byName("Ada Lovelace") - By ID (stable):
Contacts.people.byId("<UUID>")(preferred for long-lived refs) - Batch read:
Contacts.people.whose({ organization: {_contains: "Acme"} }).name()
Operator map for .whose
_equals,_contains,_beginsWith,_endsWith,_greaterThan,_lessThan,_not- Nested multi-value:
{ emails: { value: { _endsWith: "edu" }}}
Localization & labels
- Standard labels ("Home", "Work", "Mobile") are accepted as plain strings; Contacts maps to localized internal constants.
- Custom labels are created automatically when you pass a new string (e.g.,
"Secure Line").
PyXA Contacts Module API Reference
New in PyXA version 0.0.2 - Control macOS Contacts using JXA-like syntax from Python.
This reference documents all classes, methods, properties, and enums in the PyXA Contacts module. For practical examples and usage patterns, see contacts-recipes.md.
Contents
- Class Hierarchy
- XAContactsApplication
- XAContactsPerson
- XAContactsGroup
- XAContactsEntry
- Contact Information Classes
- XAContactsAddress
- XAContactsEmail
- XAContactsPhone
- XAContactsURL
- XAContactsInstantMessage
- XAContactsSocialProfile
- XAContactsCustomDate
- XAContactsRelatedName
- XAContactsDocument
- List Classes
- Enumerations
- Quick Reference Tables
---
Class Hierarchy
XAObject
├── XAContactsApplication (XASBApplication)
│ ├── XAContactsEntry
│ │ ├── XAContactsPerson
│ │ └── XAContactsGroup
│ ├── XAContactsContactInfo
│ │ ├── XAContactsEmail
│ │ ├── XAContactsPhone
│ │ ├── XAContactsURL
│ │ ├── XAContactsCustomDate
│ │ └── XAContactsRelatedName
│ ├── XAContactsAddress
│ ├── XAContactsInstantMessage
│ ├── XAContactsSocialProfile
│ └── XAContactsDocument---
XAContactsApplication
Bases: XASBApplication
Main entry point for interacting with Contacts.app.
Properties
| Property | Type | Description |
|---|---|---|
name | str | Application name ("Contacts") |
version | str | Application version |
frontmost | bool | Whether Contacts is the frontmost application |
my_card | XAContactsPerson | The user's own contact card |
selection | XAContactsPersonList | Currently selected contacts |
unsaved | bool | Whether there are unsaved changes |
default_country_code | str | Default country code for phone formatting |
Methods
people(filter=None) -> XAContactsPersonList
Returns a list of contacts matching the filter.
Parameters:
filter(dict | None) - Property-value pairs to filter by
Example:
import PyXA
contacts = PyXA.Application("Contacts")
all_people = contacts.people()
for person in all_people:
print(person.first_name, person.last_name)groups(filter=None) -> XAContactsGroupList
Returns a list of groups matching the filter.
Parameters:
filter(dict | None) - Property-value pairs to filter by
Example:
groups = contacts.groups()
print(groups.name())
# ['Family', 'Work', 'Friends', ...]documents(filter=None) -> XAContactsDocumentList
Returns a list of address book documents matching the filter.
Parameters:
filter(dict | None) - Property-value pairs to filter by
make(specifier, properties=None, data=None) -> XAObject
Creates a new element without adding to any list. Use XAList.push() to add.
Parameters:
specifier(str | ObjectType) - Class name to create (e.g., "person", "group")properties(dict) - Properties for the objectdata(Any) - Initialization data
Example:
# Create a new person
new_person = contacts.make("person", {
"firstName": "Ada",
"lastName": "Lovelace"
})
contacts.people().push(new_person)
contacts.save()open(file_path)
Opens and imports contacts from a file (vCard, etc.).
Parameters:
file_path(str | XAPath) - Path to the contact file
Example:
contacts.open("/path/to/contacts.vcf")save()
Persists all changes to the address book.
Example:
person = contacts.people()[0]
person.first_name = "Updated"
contacts.save()---
XAContactsPerson
Bases: XAContactsEntry
Represents an individual contact record in the address book.
Properties
Name Properties
| Property | Type | Description |
|---|---|---|
first_name | str | First/given name |
last_name | str | Last/family name |
middle_name | str | Middle name |
nickname | str | Nickname |
suffix | str | Name suffix (Jr., Sr., III) |
title | str | Name prefix/title (Mr., Dr., etc.) |
maiden_name | str | Maiden name |
Phonetic Properties
| Property | Type | Description |
|---|---|---|
phonetic_first_name | str | Phonetic spelling of first name |
phonetic_last_name | str | Phonetic spelling of last name |
phonetic_middle_name | str | Phonetic spelling of middle name |
Organization Properties
| Property | Type | Description |
|---|---|---|
company | str | Company/organization name |
organization | str | Organization name (alias) |
department | str | Department name |
job_title | str | Job title/position |
Other Properties
| Property | Type | Description |
|---|---|---|
birth_date | datetime | Birth date |
home_page | str | Personal website URL |
image | XAImage | Contact photo |
note | str | Notes/comments |
vcard | str | vCard representation of the contact |
Methods
addresses(filter=None) -> XAContactsAddressList
Returns physical addresses for this contact.
Example:
person = contacts.people()[0]
for addr in person.addresses():
print(f"{addr.street}, {addr.city}, {addr.state} {addr.zip}")emails(filter=None) -> XAContactsEmailList
Returns email addresses for this contact.
Example:
emails = person.emails()
print(emails.value())
# ['john@example.com', 'john.doe@work.com']phones(filter=None) -> XAContactsPhoneList
Returns phone numbers for this contact.
Example:
phones = person.phones()
for phone in phones:
print(f"{phone.label}: {phone.value}")urls(filter=None) -> XAContactsURLList
Returns URLs associated with this contact.
instant_messages(filter=None) -> XAContactsInstantMessageList
Returns instant messaging accounts for this contact.
social_profiles(filter=None) -> XAContactsSocialProfileList
Returns social media profiles for this contact.
custom_dates(filter=None) -> XAContactsCustomDateList
Returns custom date fields for this contact.
related_names(filter=None) -> XAContactsRelatedNameList
Returns related names (spouse, assistant, etc.) for this contact.
groups(filter=None) -> XAContactsGroupList
Returns groups this contact belongs to.
Example:
person_groups = person.groups()
print(person_groups.name())
# ['Family', 'VIP']show() -> XAContactsPerson
Opens Contacts.app and displays this contact.
Example:
person.show() # Opens contact card in Contacts.app---
XAContactsGroup
Bases: XAContactsEntry
Represents a contact group in the address book.
Properties
| Property | Type | Description |
|---|---|---|
name | str | Group name |
Methods
people(filter=None) -> XAContactsPersonList
Returns contacts that belong to this group.
Example:
family = contacts.groups().by_name("Family")
members = family.people()
print(members.first_name())
# ['John', 'Jane', 'Bob']groups(filter=None) -> XAContactsGroupList
Returns subgroups within this group.
---
XAContactsEntry
Bases: XAObject
Base class for all contact entries (persons and groups).
Properties
| Property | Type | Description |
|---|---|---|
id | str | Unique identifier |
creation_date | datetime | Date the entry was created |
modification_date | datetime | Date the entry was last modified |
selected | bool | Whether the entry is currently selected |
Methods
add_to(parent) -> XAContactsPerson
Adds this entry to a group.
Parameters:
parent(XAContactsGroup) - The group to add to
Example:
person = contacts.people()[0]
vip_group = contacts.groups().by_name("VIP")
person.add_to(vip_group)
contacts.save()remove_from(elem) -> XAContactsPerson
Removes this entry from a group.
Parameters:
elem(XAContactsGroup) - The group to remove from
delete()
Permanently deletes this entry from the address book.
Example:
person = contacts.people().by_first_name("Test")
person.delete()
contacts.save()---
Contact Information Classes
XAContactsContactInfo
Bases: XAObject
Base class for contact information items (email, phone, URL, etc.).
Properties
| Property | Type | Description |
|---|---|---|
id | str | Unique identifier |
label | str | Label (e.g., "Home", "Work", "Mobile") |
value | str | The actual value |
---
XAContactsAddress
Bases: XAObject
Represents a physical/mailing address.
Properties
| Property | Type | Description |
|---|---|---|
id | str | Unique identifier |
label | str | Address label ("Home", "Work", etc.) |
street | str | Street address |
city | str | City name |
state | str | State/province |
zip | str | Postal/ZIP code |
country | str | Country name |
country_code | str | ISO country code |
formatted_address | str | Full formatted address string |
Example:
person = contacts.people()[0]
home_addr = person.addresses().by_label("Home")
print(home_addr.formatted_address)
# "123 Main St\nSan Francisco, CA 94102\nUnited States"---
XAContactsEmail
Bases: XAContactsContactInfo
Represents an email address entry.
Properties
| Property | Type | Description |
|---|---|---|
id | str | Unique identifier |
label | str | Email label ("Home", "Work", etc.) |
value | str | Email address |
Example:
# Get work email
work_email = person.emails().by_label("Work")
print(work_email.value)
# "john.doe@company.com"---
XAContactsPhone
Bases: XAContactsContactInfo
Represents a phone number entry.
Properties
| Property | Type | Description |
|---|---|---|
id | str | Unique identifier |
label | str | Phone label ("Home", "Work", "Mobile", "iPhone", etc.) |
value | str | Phone number |
Example:
# Get mobile phone
mobile = person.phones().by_label("Mobile")
print(mobile.value)
# "+1 (555) 123-4567"---
XAContactsURL
Bases: XAContactsContactInfo
Represents a URL entry (website, social profile, etc.).
Properties
| Property | Type | Description |
|---|---|---|
id | str | Unique identifier |
label | str | URL label ("Home Page", "Work", etc.) |
value | str | URL string |
---
XAContactsInstantMessage
Bases: XAObject
Represents an instant messaging account.
Properties
| Property | Type | Description |
|---|---|---|
id | str | Unique identifier |
label | str | IM label |
service_type | ServiceType | Service type (AIM, Jabber, etc.) |
service_name | str | Service name |
user_name | str | Username/handle |
Example:
im_accounts = person.instant_messages()
for im in im_accounts:
print(f"{im.service_name}: {im.user_name}")---
XAContactsSocialProfile
Bases: XAObject
Represents a social media profile.
Properties
| Property | Type | Description |
|---|---|---|
id | str | Unique identifier |
service_name | str | Service name (Twitter, LinkedIn, etc.) |
user_name | str | Username/handle |
user_identifier | str | Unique user ID on the service |
url | str | Profile URL |
Example:
profiles = person.social_profiles()
for profile in profiles:
print(f"{profile.service_name}: @{profile.user_name}")---
XAContactsCustomDate
Bases: XAContactsContactInfo
Represents a custom date field (anniversary, etc.).
Properties
| Property | Type | Description |
|---|---|---|
id | str | Unique identifier |
label | str | Date label ("Anniversary", custom labels) |
value | datetime | The date value |
---
XAContactsRelatedName
Bases: XAContactsContactInfo
Represents a related person (spouse, assistant, etc.).
Properties
| Property | Type | Description |
|---|---|---|
id | str | Unique identifier |
label | str | Relationship type ("Spouse", "Assistant", "Manager", etc.) |
value | str | Related person's name |
---
XAContactsDocument
Bases: XAObject
Represents an address book document.
Properties
| Property | Type | Description |
|---|---|---|
name | str | Document name |
file | str | File location on disk |
modified | bool | Whether document has unsaved changes |
---
List Classes
PyXA provides list wrapper classes with fast enumeration and bulk property access.
XAContactsPersonList
Bulk methods for person lists:
people = contacts.people()
people.first_name() # -> list[str]
people.last_name() # -> list[str]
people.company() # -> list[str]
people.department() # -> list[str]
people.job_title() # -> list[str]
people.birth_date() # -> list[datetime]
people.note() # -> list[str]
people.home_page() # -> list[str]
people.image() # -> list[XAImage]
people.vcard() # -> list[str]Filter methods:
people.by_first_name("John")
people.by_last_name("Doe")
people.by_company("Acme Corp")
people.by_department("Engineering")
people.by_job_title("Developer")
people.by_birth_date(date)
people.by_nickname("Johnny")XAContactsGroupList
Bulk methods for group lists:
groups = contacts.groups()
groups.name() # -> list[str]
groups.id() # -> list[str]
groups.creation_date() # -> list[datetime]
groups.modification_date() # -> list[datetime]Filter methods:
groups.by_name("Family")
groups.by_id("group-uuid")XAContactsAddressList
Bulk methods for address lists:
addresses = person.addresses()
addresses.street() # -> list[str]
addresses.city() # -> list[str]
addresses.state() # -> list[str]
addresses.zip() # -> list[str]
addresses.country() # -> list[str]
addresses.country_code() # -> list[str]
addresses.formatted_address() # -> list[str]
addresses.label() # -> list[str]Filter methods:
addresses.by_label("Home")
addresses.by_city("San Francisco")
addresses.by_state("CA")
addresses.by_zip("94102")
addresses.by_country("United States")
addresses.by_country_code("US")XAContactsEmailList
emails = person.emails()
emails.label() # -> list[str]
emails.value() # -> list[str]
emails.by_label("Work")
emails.by_value("john@example.com")XAContactsPhoneList
phones = person.phones()
phones.label() # -> list[str]
phones.value() # -> list[str]
phones.by_label("Mobile")
phones.by_value("+1555123456")XAContactsURLList
urls = person.urls()
urls.label() # -> list[str]
urls.value() # -> list[str]
urls.by_label("Home Page")
urls.by_value("https://example.com")XAContactsInstantMessageList
ims = person.instant_messages()
ims.service_type() # -> list[ServiceType]
ims.service_name() # -> list[str]
ims.user_name() # -> list[str]
ims.by_service_type(ServiceType.JABBER)
ims.by_user_name("johndoe")XAContactsSocialProfileList
profiles = person.social_profiles()
profiles.service_name() # -> list[str]
profiles.user_name() # -> list[str]
profiles.user_identifier() # -> list[str]
profiles.url() # -> list[str]
profiles.by_service_name("Twitter")
profiles.by_user_name("johndoe")
profiles.by_url("https://twitter.com/johndoe")XAContactsCustomDateList
dates = person.custom_dates()
dates.label() # -> list[str]
dates.value() # -> list[datetime]
dates.by_label("Anniversary")XAContactsRelatedNameList
related = person.related_names()
related.label() # -> list[str]
related.value() # -> list[str]
related.by_label("Spouse")
related.by_value("Jane Doe")XAContactsDocumentList
docs = contacts.documents()
docs.name() # -> list[str]
docs.file() # -> list[str]
docs.modified() # -> list[bool]
docs.by_name("Address Book")
docs.by_modified(True)---
Enumerations
Format
Archive format options.
| Value | Description |
|---|---|
ARCHIVE | Native Address Book archive format |
ObjectType
Creatable object types for make().
| Value | Description |
|---|---|
DOCUMENT | Address book document |
GROUP | Contact group |
PERSON | Contact person |
URL | URL entry |
ServiceType
Instant messaging service types.
| Value | Service |
|---|---|
AIM | AOL Instant Messenger |
FACEBOOK | Facebook Messenger |
GADU_GADU | Gadu-Gadu |
GOOGLE_TALK | Google Talk/Hangouts |
ICQ | ICQ |
JABBER | Jabber/XMPP |
MSN | MSN Messenger |
QQ | |
SKYPE | Skype |
YAHOO | Yahoo Messenger |
---
Quick Reference Tables
Common Operations
| Task | Code |
|---|---|
| Get Contacts app | contacts = PyXA.Application("Contacts") |
| Get all contacts | people = contacts.people() |
| Get contact by name | person = contacts.people().by_first_name("John") |
| Get user's card | me = contacts.my_card |
| Get all groups | groups = contacts.groups() |
| Get group by name | group = contacts.groups().by_name("Family") |
| Get group members | members = group.people() |
| Create new person | person = contacts.make("person", {"firstName": "Ada"}) |
| Create new group | group = contacts.make("group", {"name": "VIP"}) |
| Add to group | person.add_to(group) |
| Remove from group | person.remove_from(group) |
| Delete contact | person.delete() |
| Save changes | contacts.save() |
| Show contact | person.show() |
| Import vCard | contacts.open("/path/to/contact.vcf") |
Contact Multi-Value Fields
| Field | Access Method | Example |
|---|---|---|
| Emails | person.emails() | person.emails().by_label("Work").value |
| Phones | person.phones() | person.phones().by_label("Mobile").value |
| Addresses | person.addresses() | person.addresses().by_label("Home").city |
| URLs | person.urls() | person.urls().by_label("Home Page").value |
| IMs | person.instant_messages() | person.instant_messages().user_name() |
| Social | person.social_profiles() | person.social_profiles().by_service_name("Twitter") |
| Dates | person.custom_dates() | person.custom_dates().by_label("Anniversary").value |
| Related | person.related_names() | person.related_names().by_label("Spouse").value |
Property Access Patterns
# Single object
person = contacts.people()[0]
print(person.first_name)
print(person.company)
# Bulk access on lists
people = contacts.people()
print(people.first_name()) # Returns list[str]
print(people.company()) # Returns list[str]
# Filtering
work_contacts = people.by_company("Acme Corp")
engineers = people.by_department("Engineering")
# Chained filtering (get first work email)
work_email = person.emails().by_label("Work")[0].valueCreating Contacts with Multi-Value Fields
import PyXA
contacts = PyXA.Application("Contacts")
# Create person
person = contacts.make("person", {
"firstName": "Ada",
"lastName": "Lovelace",
"company": "Analytical Engine Inc.",
"jobTitle": "Chief Mathematician"
})
contacts.people().push(person)
# Add email
email = contacts.make("email", {
"label": "Work",
"value": "ada@analyticalengine.com"
})
person.emails().push(email)
# Add phone
phone = contacts.make("phone", {
"label": "Mobile",
"value": "+1 (555) 123-4567"
})
person.phones().push(phone)
# Add address
address = contacts.make("address", {
"label": "Work",
"street": "123 Innovation Way",
"city": "London",
"country": "United Kingdom"
})
person.addresses().push(address)
# Save all changes
contacts.save()---
See Also
- PyXA Contacts Documentation - Official PyXA documentation
- contacts-basics.md - JXA fundamentals for Contacts
- contacts-recipes.md - Common automation patterns
- contacts-advanced.md - Advanced techniques and troubleshooting
- contacts-dictionary.md - AppleScript dictionary reference
Contacts JXA Recipes
Query contacts by email domain (coarse filter + refine)
const matches = Contacts.people.whose({
emails: { value: { _endsWith: "acme.com" } }
})();
// refine in JS if needed
const tagged = matches.filter(p => (p.note() || "").includes("VIP"));Create a person with multi-value fields
const p = Contacts.Person().make();
p.firstName = "Grace";
p.lastName = "Hopper";
p.organization = "US Navy";
p.emails.push(Contacts.Email({ label: "Work", value: "grace@navy.mil" }));
p.phones.push(Contacts.Phone({ label: "Mobile", value: "+1-555-123-4567" }));
p.addresses.push(Contacts.Address({
label: "Office",
street: "1 Programming Way",
city: "Arlington",
state: "VA",
zip: "22202",
country: "USA",
countryCode: "us"
}));
// Dates: set to noon to avoid TZ shifts
p.dates.push(Contacts.CustomDate({ label: "Anniversary", value: new Date("2015-08-14T12:00:00") }));
Contacts.save();Add to group (defensive)
function ensureGroup(name) {
const g = Contacts.groups.whose({ name: { _equals: name } })();
if (g.length) return g[0];
const created = Contacts.Group().make();
created.name = name;
Contacts.save();
return created;
}
const group = ensureGroup("System Architects");
const person = Contacts.people.byName("Grace Hopper");
// Avoid duplicate membership
const already = group.people.whose({ id: { _equals: person.id() } })().length > 0;
if (!already) {
Contacts.add(person, { to: group }); // or group.people.push(person)
Contacts.save();
}Hybrid filter when .whose is fragile
const bayArea = Contacts.people.whose({
addresses: { city: { _equals: "San Francisco" } }
})();
const projectAlpha = bayArea.filter(p => /Project Alpha/i.test(p.note() || ""));#!/usr/bin/env python3
"""
Export Contacts to CSV Script - PyXA Implementation
Exports contacts from macOS Contacts to a CSV file
Usage: python export_contacts_to_csv.py [output.csv]
"""
import sys
import csv
import PyXA
def export_contacts_to_csv(output_file="contacts_export.csv"):
"""Export all contacts to CSV file"""
try:
contacts_app = PyXA.Application("Contacts")
contacts = contacts_app.contacts()
exported_count = 0
with open(output_file, 'w', newline='', encoding='utf-8') as f:
fieldnames = ['first_name', 'last_name', 'emails', 'phones', 'company', 'job_title']
writer = csv.DictWriter(f, fieldnames=fieldnames)
writer.writeheader()
for contact in contacts:
try:
# Extract contact information
first_name = contact.first_name() or ""
last_name = contact.last_name() or ""
company = contact.organization() or ""
job_title = contact.job_title() or ""
# Extract emails
emails = []
try:
for email in contact.emails():
emails.append(f"{email.label()}: {email.value()}")
except:
pass
# Extract phones
phones = []
try:
for phone in contact.phones():
phones.append(f"{phone.label()}: {phone.value()}")
except:
pass
# Write to CSV
writer.writerow({
'first_name': first_name,
'last_name': last_name,
'emails': '; '.join(emails),
'phones': '; '.join(phones),
'company': company,
'job_title': job_title
})
exported_count += 1
if exported_count % 50 == 0:
print(f"Exported {exported_count} contacts...")
except Exception as e:
print(f"Error exporting contact {contact.id()}: {e}")
continue
print(f"\nExport complete: {exported_count} contacts exported to {output_file}")
return exported_count
except Exception as e:
print(f"Error in export process: {e}")
return 0
if __name__ == "__main__":
output_file = sys.argv[1] if len(sys.argv) > 1 else "contacts_export.csv"
exported = export_contacts_to_csv(output_file)
sys.exit(0 if exported > 0 else 1)#!/usr/bin/env python3
"""
Import Contacts from CSV Script - PyXA Implementation
Imports contacts from a CSV file into macOS Contacts
Usage: python import_contacts_from_csv.py contacts.csv
"""
import sys
import csv
import PyXA
def import_contacts_from_csv(csv_file):
"""Import contacts from CSV file"""
try:
contacts_app = PyXA.Application("Contacts")
imported_count = 0
skipped_count = 0
with open(csv_file, 'r', encoding='utf-8') as f:
reader = csv.DictReader(f)
for row in reader:
try:
# Extract contact information
first_name = row.get('first_name', row.get('First Name', ''))
last_name = row.get('last_name', row.get('Last Name', ''))
email = row.get('email', row.get('Email', ''))
phone = row.get('phone', row.get('Phone', ''))
# Skip if no basic information
if not (first_name or last_name or email):
print(f"Skipping contact: insufficient information in row")
skipped_count += 1
continue
# Check if contact already exists
existing_contacts = contacts_app.contacts()
contact_exists = False
for contact in existing_contacts:
if (contact.first_name() == first_name and
contact.last_name() == last_name):
contact_exists = True
break
if contact_exists:
print(f"Contact {first_name} {last_name} already exists")
skipped_count += 1
continue
# Create new contact
new_contact = contacts_app.contacts().push({
"first_name": first_name,
"last_name": last_name
})
# Add email if provided
if email:
new_contact.emails().push({
"label": "work",
"value": email
})
# Add phone if provided
if phone:
new_contact.phones().push({
"label": "mobile",
"value": phone
})
imported_count += 1
print(f"Imported: {first_name} {last_name}")
except Exception as e:
print(f"Error importing contact: {e}")
skipped_count += 1
continue
print(f"\nImport complete:")
print(f"Imported: {imported_count} contacts")
print(f"Skipped: {skipped_count} contacts")
return imported_count
except Exception as e:
print(f"Error in import process: {e}")
return 0
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: python import_contacts_from_csv.py contacts.csv")
print("CSV should have columns: first_name, last_name, email, phone")
sys.exit(1)
csv_file = sys.argv[1]
imported = import_contacts_from_csv(csv_file)
sys.exit(0 if imported > 0 else 1)#!/usr/bin/env python3
"""
Search Contacts Script - PyXA Implementation
Searches for contacts by name, email, or phone
Usage: python search_contacts.py "search term" [--email] [--phone]
"""
import sys
import PyXA
def search_contacts(search_term, search_emails=False, search_phones=False):
"""Search contacts by various criteria"""
try:
contacts_app = PyXA.Application("Contacts")
contacts = contacts_app.contacts()
found_contacts = []
search_lower = search_term.lower()
for contact in contacts:
try:
# Search in name
first_name = (contact.first_name() or "").lower()
last_name = (contact.last_name() or "").lower()
full_name = f"{first_name} {last_name}".strip()
name_match = (search_lower in first_name or
search_lower in last_name or
search_lower in full_name)
# Search in emails if requested
email_match = False
if search_emails:
try:
for email in contact.emails():
if search_lower in (email.value() or "").lower():
email_match = True
break
except:
pass
# Search in phones if requested
phone_match = False
if search_phones:
try:
for phone in contact.phones():
if search_lower in (phone.value() or "").lower():
phone_match = True
break
except:
pass
# If any match found, collect contact info
if name_match or email_match or phone_match:
contact_info = {
'first_name': contact.first_name() or "",
'last_name': contact.last_name() or "",
'company': contact.organization() or "",
'job_title': contact.job_title() or "",
'emails': [],
'phones': []
}
# Collect emails
try:
for email in contact.emails():
contact_info['emails'].append({
'label': email.label() or "work",
'value': email.value() or ""
})
except:
pass
# Collect phones
try:
for phone in contact.phones():
contact_info['phones'].append({
'label': phone.label() or "mobile",
'value': phone.value() or ""
})
except:
pass
found_contacts.append(contact_info)
except Exception as e:
print(f"Error processing contact: {e}")
continue
# Display results
if found_contacts:
print(f"Found {len(found_contacts)} contacts matching '{search_term}':")
print("=" * 60)
for i, contact in enumerate(found_contacts, 1):
print(f"{i}. {contact['first_name']} {contact['last_name']}")
if contact['company']:
print(f" Company: {contact['company']}")
if contact['job_title']:
print(f" Title: {contact['job_title']}")
if contact['emails']:
print(" Emails:")
for email in contact['emails']:
print(f" {email['label']}: {email['value']}")
if contact['phones']:
print(" Phones:")
for phone in contact['phones']:
print(f" {phone['label']}: {phone['value']}")
print()
else:
search_types = []
if not (search_emails or search_phones):
search_types.append("name")
if search_emails:
search_types.append("email")
if search_phones:
search_types.append("phone")
print(f"No contacts found matching '{search_term}' in {', '.join(search_types)}")
return found_contacts
except Exception as e:
print(f"Error in search process: {e}")
return []
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: python search_contacts.py 'search term' [--email] [--phone]")
sys.exit(1)
search_term = sys.argv[1]
search_emails = "--email" in sys.argv
search_phones = "--phone" in sys.argv
results = search_contacts(search_term, search_emails, search_phones)
sys.exit(0 if results else 1)