Bring Intercom companies, contacts, conversations, and tickets into one connected workspace in Notion. Use the resulting databases to review customer context, triage support work, and spot service trends without sending every collaborator into Intercom.
The worker creates and maintains all four databases for you. Companies and contacts refresh every hour. Conversation and ticket changes arrive every five minutes, with an automatic daily refresh to repair drift and remove records that are no longer visible.
You need Node.js 22+, an Intercom workspace, and a private-app access token. Give the app these permissions:
- Read and list users and companies
- Read conversations
- Read admins
- Read tickets, if your Intercom plan includes Tickets
From the repository root:
npm install --global ntn@latest
cd workers/templates/intercom-sync
npm install
ntn login
ntn workers deploy --name intercom-sync
ntn workers env set INTERCOM_REGION=us
ntn workers env set INTERCOM_ACCESS_TOKEN=your-private-app-tokenUse eu or au instead of us when that is where Intercom hosts your data.
The schedules start automatically after deployment; no recurring CLI action is
required.
The synced records can include contact details and selected support content. Review the managed databases' Notion sharing settings before giving them a broader audience.
| Managed database | Questions it helps answer |
|---|---|
| Companies | Which high-spend or high-usage accounts have gone quiet? Which segments are generating the most conversations, and which contacts belong to each account? |
| Contacts | Which users or leads need follow-up based on when they were last seen, contacted, or replied? Who owns them, and who is unsubscribed, marked as spam, or hard bounced? |
| Conversations | Which open, unread, or priority conversations have waited longest, missed SLA, or lack an assignee? Which teams and channels have slow replies, low ratings, repeated reopens, or AI escalations? |
| Tickets | Which open tickets are unassigned, waiting on a customer, or ready for follow-up after snoozing? How is the queue distributed by team, ticket type, and category? |
| Database | Intercom resource | Schedule |
|---|---|---|
| Companies | Companies | Full refresh every hour |
| Contacts | Contacts | Full refresh every hour |
| Conversations | Conversations | Changes every 5 min + daily full |
| Tickets | Tickets | Changes every 5 min + daily full |
| Notion property | Intercom field or meaning | Type |
|---|---|---|
| Name | name |
title |
| Plan | plan.name |
select |
| Industry | industry |
select |
| Website | website |
url |
| Employees | size |
number |
| Users | user_count |
number |
| Sessions | session_count |
number |
| Monthly Spend | monthly_spend |
number |
| Last Active | last_request_at |
date |
| Tags | tags.tags[].name |
multiSelect |
| Segments | segments.segments[].name |
multiSelect |
| Updated | updated_at |
date |
| Created | created_at |
date |
| Created at Source | remote_created_at |
date |
| External Company ID | company_id |
richText |
| Contacts | Related Intercom contact IDs | relation |
| Conversations | Related Intercom conversation IDs | relation |
| Company ID | Immutable Intercom id |
richText |
Intercom's Company Scroll omits companies with no associated users. Those companies do not appear until Intercom includes them in that result.
| Notion property | Intercom field or meaning | Type |
|---|---|---|
| Name | name, then an available identity field |
title |
| Role | role |
select |
| Owner | Admin name resolved from owner_id |
richText |
| Updated | updated_at |
date |
email |
||
| Phone | phone |
phoneNumber |
| Companies | companies.data[].id |
relation |
| Country | location.country |
select |
| Tags | tags.data[].name |
multiSelect |
| Incomplete Associations | Companies or Tags when the list is partial | multiSelect |
| Last Seen | last_seen_at |
date |
| Signed Up | signed_up_at |
date |
| Last Contacted | last_contacted_at |
date |
| Last Replied | last_replied_at |
date |
| Email Restrictions | Unsubscribe, spam, and bounce flags | multiSelect |
| Created | created_at |
date |
| External ID | external_id |
richText |
| Conversations | Related Intercom conversation IDs | relation |
| Tickets | Related Intercom ticket IDs | relation |
| Contact ID | Immutable Intercom id |
richText |
Intercom embeds at most ten companies and ten tags in a Contact result. When more exist, Incomplete Associations identifies the list that may be partial.
| Notion property | Intercom field or meaning | Type |
|---|---|---|
| Title | title, subject, or opening-message fallback |
title |
| State | state |
select |
| Priority | priority |
checkbox |
| Unread | Inverse of read |
checkbox |
| Contacts | contacts.contacts[].id |
relation |
| Assignee | Admin name resolved from admin_assignee_id |
richText |
| Team | Team name resolved from team_assignee_id |
richText |
| Updated | updated_at |
date |
| Waiting Since | waiting_since |
date |
| Channel | source.type |
select |
| Tags | tags.tags[].name |
multiSelect |
| SLA Status | sla_applied.sla_status |
select |
| Rating | conversation_rating.rating |
number |
| Company | company.id |
relation |
| First Reply (min) | statistics.time_to_admin_reply, converted to min |
number |
| Median Reply (min) | statistics.median_time_to_reply, converted to min |
number |
| Handling Time (min) | Adjusted or standard handling time, converted to min | number |
| Last Contact Reply | statistics.last_contact_reply_at |
date |
| Reopens | statistics.count_reopens |
number |
| AI Resolution | ai_agent.resolution_state |
select |
| Snoozed Until | snoozed_until |
date |
| Created | created_at |
date |
| Conversation ID | Immutable Intercom id |
richText |
Each page body contains the sanitized opening message and customer rating comment when available. It does not copy the full transcript, attachments, or internal notes.
| Notion property | Intercom field or meaning | Type |
|---|---|---|
| Title | Default title, then ticket type and number | title |
| State | Internal or external ticket-state label | select |
| State Category | ticket_state.category |
select |
| Ticket Type | ticket_type.name |
select |
| Category | category |
select |
| Contacts | contacts.contacts[].id |
relation |
| Assignee | Admin name resolved from admin_assignee_id |
richText |
| Team | Team name resolved from team_assignee_id |
richText |
| Updated | updated_at |
date |
| Open | open |
checkbox |
| Snoozed Until | snoozed_until |
date |
| Shared with Customer | is_shared |
checkbox |
| Created | created_at |
date |
| Inbox Ticket ID | Human-facing ticket_id |
richText |
| Ticket ID | Immutable Intercom id |
richText |
Each page body contains only the sanitized default Ticket description. Arbitrary custom attributes and Ticket parts are not copied.
src/
├── index.ts — registers the databases and schedules
├── intercom.ts — regional API client and Intercom types
├── pagination.ts — shared cursor and ordering safeguards
├── companies.ts — Company schema, transform, and sync
├── contacts.ts — Contact schema, transform, and sync
├── conversations.ts — Conversation schema, transform, and syncs
├── tickets.ts — Ticket schema, transform, and syncs
└── helpers.ts — timestamps, safe text, and formatting
- The worker creates four managed databases and connects them with relations based on immutable Intercom IDs.
- Companies and contacts receive a complete refresh every hour. Records that Intercom no longer returns are removed after a successful refresh.
- Conversation and ticket changes are applied every five minutes. Intercom's search index can lag briefly, so a very recent change may arrive on the next cycle.
- An automatic daily full refresh catches missed changes and removes deleted or newly hidden conversations and tickets. Incomplete refreshes do not remove records from a partial result.
- Open Intercom's Developer Hub, select Your Apps, and click New App.
- Name the app and select the workspace whose data you want to sync.
- Under Configure > Authentication, grant the read permissions listed in the Quickstart.
- Copy the app's access token from the same Authentication page.
Private apps do not require OAuth or App Store review. Store the token with
ntn workers env set, not in source control.
| Variable | Required | Default | Description |
|---|---|---|---|
INTERCOM_ACCESS_TOKEN |
Yes | — | Token from one workspace's private Intercom app |
INTERCOM_REGION |
No | us |
Data-hosting region: us, eu, or au |
No NOTION_API_TOKEN is needed—the Workers platform supplies Notion access.
Tickets run automatically when the plan and private app include access. If the
workspace does not use Tickets, pause those two schedules; the other databases
continue independently:
ntn workers sync pause ticketsSync
ntn workers sync pause ticketsReconciliationTo enable Tickets later, add Read tickets and resume the same two schedule names.
Each resource keeps its schema and transform in one file:
| Resource | File |
|---|---|
| Companies | src/companies.ts |
| Contacts | src/contacts.ts |
| Conversations | src/conversations.ts |
| Tickets | src/tickets.ts |
To add an Intercom field:
- Add the provider field to its narrow type in
src/intercom.ts. - Add the Notion property to the resource schema and transform.
- Update this README and add tests for present and missing values. Add custom attributes through an explicit allowlist.
Run the offline checks; they make no Intercom or Notion calls:
npm run check
npm test
npm run buildFor an optional live preview, copy .env.example to .env, add credentials
for a test workspace, and preview one sync without writing to Notion:
ntn workers sync trigger contactsSync --local --previewYou can substitute conversationsSync or ticketsSync; preview Tickets only
when the plan supports them.