Generate a printable address book from contacts
Export Northwind's Google Contacts to a formatted Doc you can actually print.
Published Apr 21, 2026
Google Contacts is fine on a screen and useless on paper. There’s no “print the whole address book” button, and copying contacts into a document by hand is the kind of job nobody ever gets round to. So when someone wants a physical list — for a reception desk, an event, or just a drawer — it doesn’t exist.
This script builds one for Northwind. It reads every contact through the People API, sorts them alphabetically, and writes a formatted Google Doc grouped under A–Z headings, with each person’s name, company, email, and phone. The result is a clean, printable address book that you can regenerate in seconds whenever the contact list changes.
What you’ll need
- Google Contacts with the people you want in the book. Empty contacts (no name) are skipped.
- The People API enabled in Advanced Google Services: in the editor open
Services (the
+icon), find People API, and add it. - Drive access — the script creates a new Doc in your Drive each run.
The script
// Page size for the People API. 500 is fine; for thousands of
// contacts the script pages through automatically.
const CONTACTS_PAGE_SIZE = 500;
// The fields to request for each contact.
const PERSON_FIELDS = 'names,emailAddresses,phoneNumbers,organizations';
/**
* Builds a printable, alphabetically grouped address book Doc from
* every contact in Google Contacts.
*/
function buildAddressBook() {
// 1. Pull every contact, then sort them by name.
const all = listContacts();
if (all.length === 0) {
Logger.log('No contacts found — nothing to build.');
return;
}
const sorted = all.sort((a, b) =>
(a.name || '').localeCompare(b.name || ''));
// 2. Create the Doc and give it a dated title.
const stamp = new Date().toISOString().slice(0, 10);
const doc = DocumentApp.create(`Northwind Address Book — ${stamp}`);
const body = doc.getBody();
body.appendParagraph('Northwind Studios — Address Book')
.setHeading(DocumentApp.ParagraphHeading.TITLE);
// 3. Walk the sorted list, inserting an A–Z heading whenever the
// first letter changes.
let lastLetter = '';
for (const c of sorted) {
const letter = (c.name || '?')[0].toUpperCase();
if (letter !== lastLetter) {
body.appendParagraph(letter)
.setHeading(DocumentApp.ParagraphHeading.HEADING1);
lastLetter = letter;
}
// 4. Write one block per contact: bold name, optional company,
// then email and phone on their own lines.
const para = body.appendParagraph('');
para.appendText(c.name || '').setBold(true);
if (c.org) para.appendText(` · ${c.org}`);
para.appendText('\n');
if (c.email) para.appendText(c.email + '\n');
if (c.phone) para.appendText(c.phone + '\n');
}
// 5. Save and close so the Doc is ready to open or print.
doc.saveAndClose();
Logger.log('Address book created: ' + doc.getUrl());
}
/**
* Returns every Google contact as a flat list of plain objects,
* paging through the People API until there are no more results.
*/
function listContacts() {
const out = [];
let pageToken;
do {
const res = People.People.Connections.list('people/me', {
personFields: PERSON_FIELDS,
pageSize: CONTACTS_PAGE_SIZE,
pageToken,
});
// Flatten each person down to the four fields we print.
for (const p of res.connections || []) {
out.push({
name: p.names?.[0]?.displayName,
email: p.emailAddresses?.[0]?.value,
phone: p.phoneNumbers?.[0]?.value,
org: p.organizations?.[0]?.name,
});
}
pageToken = res.nextPageToken;
} while (pageToken);
return out;
}
How it works
buildAddressBookcallslistContactsto fetch everyone, then stops early if there are no contacts at all — no point creating an empty Doc.- The list is sorted with
localeCompareso names order naturally, and a missing name sorts as an empty string rather than throwing. - A new Doc is created with a dated title like
Northwind Address Book — 2026-04-21, so each run is its own snapshot you can keep or discard. - The loop tracks
lastLetter. Whenever a contact’s first letter differs from the previous one, it inserts an A–Z heading — that’s what gives the printed book its tabbed structure. - Each contact becomes one paragraph: the name in bold, the company appended after a separator if there is one, then email and phone on their own lines. Every field is guarded, so a contact with only a name still prints cleanly.
listContactsuses the People API’sConnections.listand followsnextPageTokenin ado...whileloop, so it works whether you have 50 contacts or 5,000. The?.chains mean a contact missing a phone number or company simply yieldsundefinedinstead of an error.
Example run
Given three Google Contacts, the generated Doc looks like this:
| Contact | Name | Company | Phone | |
|---|---|---|---|---|
| 1 | Amara Okafor | Riverside Co | [email protected] | 020 7946 0100 |
| 2 | Ben Carter | — | [email protected] | — |
| 3 | Priya Shah | Acme Ltd | [email protected] | 07700 900123 |
In the Doc that becomes:
Northwind Studios — Address Book (title)
A (heading 1)
Amara Okafor · Riverside Co (bold name + company)
[email protected]
020 7946 0100
B (heading 1)
Ben Carter (bold name, no company)
[email protected]
P (heading 1)
Priya Shah · Acme Ltd
[email protected]
07700 900123
Open it, hit print, and you have a tidy A–Z directory.
Run it
This is an on-demand job — you regenerate the book when contacts have changed enough to warrant a fresh print:
- In the Apps Script editor, select
buildAddressBookand click Run. - Approve the authorisation prompt the first time — it covers Contacts and Drive access.
- Open the new Doc from the logged URL, check it, and print.
If you’d rather it refresh on a schedule, add a Time-driven trigger on a Month timer so a fresh book lands in Drive each quarter.
Watch out for
- Each run creates a new Doc — old address books pile up in Drive. Delete
the previous one, or have the script open a fixed Doc and clear its body
instead of calling
DocumentApp.create. - The script reads the first email, phone, and organisation for each
contact. People with several numbers will only show one; if you need them all,
loop over the arrays instead of taking index
0. - Contacts with no name are skipped from the headings logic by defaulting to
'?', but they’ll still appear under a?heading. Clean up nameless contacts in Google Contacts if you don’t want that section. - The People API counts against a per-user quota. One full export is trivial;
don’t call
buildAddressBookin a tight loop. - “Other contacts” — addresses Google auto-saved but you never added — are not
returned by
Connections.list. Only contacts you’ve actually saved appear in the book.
Related
Send meeting follow-ups with the notes attached
After a Calendar event ends, email attendees the linked notes Doc automatically.
Updated May 19, 2026
Embed inline charts in a status email
Render a Sheets chart as an image inside the email body, not as an attachment.
Updated May 12, 2026
Send HTML email from a Google Doc template
Use a styled Doc as the source for branded, on-brand HTML email — no design tool needed.
Updated May 5, 2026
Parse bank-alert emails into an expense ledger
Convert transaction alerts from Northwind's bank into categorised spend rows automatically.
Updated Apr 28, 2026
Email a weekly "what changed" report from a Sheet
Diff the Projects sheet week over week and email the team the rows that changed.
Updated Apr 14, 2026