Bring your support operation into Notion so teams and agents can investigate queue health, customer feedback, response performance, and SLA targets without leaving the workspace. One deploy creates six managed databases and keeps them updated from Zendesk automatically.
You do not need to build the Notion databases or supply a Notion API token. The worker declares their schemas, and Notion creates and manages them for you.
The default worker needs Zendesk Support Professional or Suite Growth (or higher), the updated CSAT experience, and an admin API user. If your account does not include CSAT or SLA APIs, see Zendesk plan compatibility.
Create a Zendesk API token, then, from the repository root, install the worker, connect your workspace, and deploy:
npm install --global ntn
cd workers/templates/zendesk-sync
npm install
ntn login
ntn workers deploy --name zendesk-syncConfigure the deployed worker:
ntn workers env set ZENDESK_SUBDOMAIN=acme
ntn workers env set ZENDESK_API_TOKEN=your-api-token
ntn workers env set ZENDESK_API_USER_EMAIL=agent@example.comPreview the ticket sync without changing Notion. Then populate Organizations and Users before Tickets so the first ticket pass can resolve its relation targets:
ntn workers sync trigger ticketsSync --preview
ntn workers sync trigger organizationsSync
ntn workers sync trigger usersSync
ntn workers sync trigger ticketsSyncThe other three databases populate on their schedules, or you can trigger them manually.
These databases can contain ticket descriptions, organization notes, user emails and phone numbers, and CSAT feedback. Review their Notion sharing settings before giving a broader audience access.
| Managed database | Example questions |
|---|---|
| Support Tickets | Which urgent tickets are still open? Who owns them? Which channels or organizations drive volume? |
| Zendesk Organizations | How are customer accounts tagged? Which organization records changed recently? |
| Zendesk Users | Who are the agents and admins? Which users are suspended or have not logged in recently? |
| Zendesk CSAT Survey Responses | Which responses have bad ratings? What feedback are customers sharing? |
| Zendesk Ticket Metrics | Which tickets have slow first replies or resolutions? Which are reopened or require many handoffs? |
| Zendesk SLA Policies | What are the first-reply and resolution targets for each priority? How do policies differ? |
To populate all six databases as written, use Zendesk Support Professional or higher, or Zendesk Suite Growth or higher, with the updated CSAT experience enabled and an admin API user with API token access. Accounts that do not meet this configuration can still use the core ticket, organization, user, and ticket-metric syncs after removing the CSAT and SLA sync registrations.
| Database | Zendesk resource | Schedule | Plan |
|---|---|---|---|
| Support Tickets | Tickets | Every 5 min + manual repair | All |
| Zendesk Organizations | Organizations | Every 5 min | All |
| Zendesk Users | Users (agents + end-users) | Every 5 min | All |
| Zendesk CSAT Survey Responses | CSAT Survey Responses | Daily | Support Professional / Suite Growth+ |
| Zendesk Ticket Metrics | Ticket Metrics | Every 5 min + manual repair | All |
| Zendesk SLA Policies | SLA Policies | Daily | Support Professional / Suite Growth+ |
| Notion property | Zendesk field | Type |
|---|---|---|
| Subject | subject |
title |
| Status | status |
select |
| Priority | priority |
select |
| Assignee | agent name (resolved) | richText |
| Assignee Record | assignee_id |
relation |
| Group | group name (resolved) | richText |
| Ticket link | clickable link to ticket | url |
| Updated at | updated_at |
date |
| Requester | requester name (resolved) | richText |
| Requester Record | requester_id |
relation |
| Organization | org name (resolved) | richText |
| Organization Record | organization_id |
relation |
| Type | type |
select |
| Channel | via.channel |
select |
| Tags | tags |
multiSelect |
| Created at | created_at |
date |
| Ticket ID | id |
richText |
Each page body contains the ticket description. Assignee, requester, group,
and organization show real names (resolved via Zendesk's
sideloading,
with no extra API calls). The incremental export includes archived tickets and
uses support_type_scope=all, so both human-agent and AI-agent tickets are
included.
| Notion property | Zendesk field | Type |
|---|---|---|
| Name | name |
title |
| Domains | domain_names |
richText |
| Tags | tags |
multiSelect |
| Details | details |
richText |
| Updated at | updated_at |
date |
| Org ID | id |
richText |
| Created at | created_at |
date |
Page body contains the organization's notes field.
| Notion property | Zendesk field | Type |
|---|---|---|
| Name | name |
title |
| Role | role |
select |
email |
||
| Last login | last_login_at |
date |
| Tags | tags |
multiSelect |
| Updated at | updated_at |
date |
| Organization ID | organization_id |
richText |
| Organization Record | organization_id |
relation |
| Phone | phone |
richText |
| Suspended | suspended |
checkbox |
| User ID | id |
richText |
| Created at | created_at |
date |
| Notion property | Zendesk field | Type |
|---|---|---|
| Response | ticket ID or response ID (derived) | title |
| Rating | customer-satisfaction rating_scale answer |
number |
| Rating category | rating_category |
select |
| Feedback | non-empty open_ended answers |
richText |
| Subject | subjects[0].zrn |
richText |
| Ticket ID | ticket subject id |
richText |
| Ticket Record | ticket subject id |
relation |
| Responder ID | responder_id |
richText |
| Responder Record | responder_id |
relation |
| Survey ID | survey.id |
richText |
| Survey version | survey.version |
number |
| Survey state | survey.state |
select |
| Updated at | latest answer updated_at |
date |
| Expires at | expires_at |
date |
| Response ID | id |
richText |
This database uses Zendesk's current CSAT Survey Responses API. Open-ended feedback is also copied into the page body. It uses a daily replace sweep so answers edited after the response was first offered are refreshed correctly.
| Notion property | Zendesk field | Type |
|---|---|---|
| Ticket ID | ticket_id |
title |
| Ticket Record | ticket_id |
relation |
| First Reply (min) | reply_time_in_minutes.calendar |
number |
| Full Resolution (min) | full_resolution_time_in_minutes |
number |
| Reopens | reopens |
number |
| Agents Touched | assignee_stations |
number |
| Groups Touched | group_stations |
number |
| Solved at | solved_at |
date |
| First Resolution (min) | first_resolution_time_in_minutes |
number |
| Replies | replies |
number |
| On Hold (min) | on_hold_time_in_minutes.calendar |
number |
| Agent Wait (min) | agent_wait_time_in_minutes.calendar |
number |
| Requester Wait (min) | requester_wait_time_in_minutes |
number |
| Updated at | updated_at |
date |
| Created at | created_at |
date |
Times use calendar minutes by default. To switch to business hours, change
.calendar to .business in src/ticket-metrics.ts.
| Notion property | Zendesk field | Type |
|---|---|---|
| Title | title |
title |
| Urgent First Reply (min) | policy_metrics (flattened) |
number |
| High First Reply (min) | policy_metrics (flattened) |
number |
| Normal First Reply (min) | policy_metrics (flattened) |
number |
| Low First Reply (min) | policy_metrics (flattened) |
number |
| Position | position |
number |
| Urgent Resolution (min) | policy_metrics (flattened) |
number |
| High Resolution (min) | policy_metrics (flattened) |
number |
| Normal Resolution (min) | policy_metrics (flattened) |
number |
| Low Resolution (min) | policy_metrics (flattened) |
number |
| Policy ID | id |
richText |
| Updated at | updated_at |
date |
| Created at | created_at |
date |
SLA targets are flattened from the policy_metrics array into individual
columns by priority level, so managers can compare targets at a glance.
Page body contains the policy description.
Relations use immutable Zendesk IDs while keeping readable name and ID columns. Tickets link to users and organizations; users link to organizations; CSAT responses link to tickets and responders; and metrics link to tickets. Relations are two-way; SLA policies remain standalone.
Every transform emits every declared property. Missing nullable values are sent as explicit empty values so stale Notion properties and CSAT page content clear.
src/
├── index.ts — registers all databases and syncs
├── zendesk.ts — API client (auth, pagination, types for all resources)
├── tickets.ts — ticket schema + transform
├── formatters.ts — shared date and label formatting helpers
├── organizations.ts — organization schema + transform
├── users.ts — user schema + transform
├── survey-responses.ts — current CSAT response schema + transform
├── ticket-metrics.ts — ticket metric schema + transform
└── sla-policies.ts — SLA policy schema + transform
- Tickets and ticket metrics sync incrementally every five minutes, including archived records and explicit ticket deletions.
- Manual repairs combine Search Export with an incremental tail and complete
replacement only after the tail reaches
end_of_stream. - Organizations and users use cursor-paginated list endpoints with 100 records
per page and
mode: "replace". - CSAT survey responses use a cursor-paginated daily replace sweep. Zendesk exposes creation-time filters but no update-time cursor, so a full sweep safely catches submitted or edited answers.
- SLA policies use Zendesk's offset pagination and refresh daily.
- Every record uses its stable Zendesk ID as the Notion sync key, preventing duplicates across pages and scheduled runs.
API families are paced separately, and 429 responses honor Retry-After.
- In Zendesk, go to Admin Center > Apps and integrations > Zendesk API
- Enable Token Access if it isn't already
- Click Add API token, give it a name, and copy the token
- Note the email address of the admin account — you'll need it for
ZENDESK_API_USER_EMAIL
| Variable | Description |
|---|---|
ZENDESK_SUBDOMAIN |
Your Zendesk subdomain (e.g. acme for acme.zendesk.com) |
ZENDESK_API_TOKEN |
Zendesk API token (from Admin Center) |
ZENDESK_API_USER_EMAIL |
Email of the Zendesk user associated with the API token |
| Variable | Default | Description |
|---|---|---|
ZENDESK_SYNC_DB_TITLE |
"Support Tickets" |
Title of the tickets Notion database |
Alternatively, you can set ZENDESK_BASIC_AUTH_TOKEN (a base64-encoded
email:password string) instead of ZENDESK_API_TOKEN + ZENDESK_API_USER_EMAIL.
No NOTION_API_TOKEN is needed — the platform handles Notion credentials
automatically.
ntn workers sync trigger organizationsSync
ntn workers sync trigger usersSync
ntn workers sync trigger ticketsSync
ntn workers sync trigger surveyResponsesSync
ntn workers sync trigger ticketMetricsSync
ntn workers sync trigger slaPoliciesSyncSLA Policies refreshes daily, but it can also be triggered manually after a policy change.
Repairs are manual because replace mode deletes unseen keys and must not overlap
the incremental syncs. After upgrading, run this once to backfill relations and
clear stale nullable values: pause both incrementals and wait for in-flight runs
to finish, then reset and trigger both repairs. Resume only after sync status
reports completion:
ntn workers sync pause ticketsSync
ntn workers sync pause ticketMetricsSync
ntn workers sync status
ntn workers sync state reset ticketsReconciliationSync
ntn workers sync state reset ticketMetricsReconciliationSync
ntn workers sync trigger ticketsReconciliationSync
ntn workers sync trigger ticketMetricsReconciliationSync
ntn workers sync status
ntn workers sync resume ticketsSync
ntn workers sync resume ticketMetricsSyncEach synced resource has a file containing its schema and transform function. To change which fields are synced for a resource, edit that resource's file:
| Resource | File |
|---|---|
| Tickets | src/tickets.ts |
| Organizations | src/organizations.ts |
| Users | src/users.ts |
| CSAT Survey Responses | src/survey-responses.ts |
| Ticket Metrics | src/ticket-metrics.ts |
| SLA Policies | src/sla-policies.ts |
To add a new Zendesk field to any resource:
- Add the field to the resource's type in
src/zendesk.ts - Add a property to the schema with the appropriate
Schema.*type - Add a
Builder.*call in the transform function
Keep every schema property in its transform, using [] for missing nullable
values. Do not remove PRIMARY_KEY or the stable-ID relations.
Tickets and ticket metrics already use Zendesk's cursor-based incremental
export. The initial request sends start_time=1 as Unix epoch seconds. Every
later page and scheduled run sends the prior after_cursor; end_of_stream
ends the current cycle without discarding that checkpoint. Do not replace this
with an ISO timestamp or restart from start_time on every schedule—both can
skip or repeatedly export data.
Deleted tickets remain in Zendesk's export for a limited retention window. The
worker converts status: "deleted" records to explicit Notion deletes. Resetting
the sync state starts a new retained-history backfill:
ntn workers sync state reset ticketsSync
ntn workers sync state reset ticketMetricsSyncSearch Export cursors expire after one hour. Incomplete repairs do not commit replacement deletions; reset and trigger the repair again.
Run deterministic checks without contacting Zendesk or Notion:
npm run check
npm test
npm run buildTest a specific sync locally against a real Zendesk instance:
ntn workers exec ticketsSync --local
ntn workers exec organizationsSync --local