Skip to main content
The Zendesk data source surfaces a Zendesk Support account as Forest collections. It exposes tickets, users and organizations (with each ticket carrying its comment thread inline) on top of the Zendesk REST API, so you can browse and edit them from your Forest project alongside your other data sources.
This is the Zendesk data source (Zendesk data inside Forest). If you want to embed Forest data and actions inside Zendesk tickets instead, see the Zendesk app.

Installation

Install the package @forestadmin/datasource-zendesk.
yarn add @forestadmin/datasource-zendesk
import { createAgent } from '@forestadmin/agent';
import {
  createZendeskClient,
  createZendeskDataSource,
} from '@forestadmin/datasource-zendesk';

const zendeskClient = createZendeskClient({
  subdomain: process.env.ZENDESK_SUBDOMAIN,
  email: process.env.ZENDESK_EMAIL,
  apiToken: process.env.ZENDESK_API_TOKEN,
});

const agent = createAgent(options).addDataSource(
  createZendeskDataSource({ client: zendeskClient }),
);
Sharing the same zendeskClient instance with the Zendesk plugins keeps auth, base URL and best-effort logger consistent across calls. It is recommended, but not required — the plugins can also be configured with raw credentials directly.

Configuration

The datasource authenticates against Zendesk using an API token.
createZendeskDataSource accepts a ZendeskClientProvider — either a pre-built client, or the three raw credentials below (which the factory then uses to build one). The two shapes are mutually exclusive. When the credentials are passed directly, the client constructor throws ZendeskConfigurationError (a ValidationError) if any of them is missing or blank.
OptionDescription
clientA ZendeskClient instance built with createZendeskClient. Required when subdomain / email / apiToken are not provided.
subdomainThe subdomain of your Zendesk account (e.g. acme for https://acme.zendesk.com). Required when client is not provided.
emailThe email address associated with the API token (typically a Zendesk admin/agent account). Required alongside subdomain and apiToken.
apiTokenA Zendesk API token generated from Admin Center → Apps and integrations → APIs → Zendesk API. Required alongside subdomain and email.
Sharing one pre-built client with the plugins is the recommended pattern (single auth setup, single logger); letting the factory build it for you is convenient when the plugins use raw credentials too:
// Recommended — share the same client with the plugins
createZendeskDataSource({
  client: createZendeskClient({ subdomain, email, apiToken }),
});

// Or let the factory build the client for you
createZendeskDataSource({ subdomain, email, apiToken });
The forest_admin_datasource_zendesk package also ships two action plugins (CreateTicketWithNotification and CloseTicket) that you can attach to any host collection. See the Zendesk plugins page for details.

Provided collections

Once the data source is registered, three collections are added to your Forest project:
CollectionPrimary endpointNotes
zendesk_ticketSearch API (/api/v2/search.json?query=type:ticket)Full read/write. Embeds requester, assignee, organization and an inline comments thread.
zendesk_userSearch API (/api/v2/search.json?query=type:user)Full read/write.
zendesk_organizationSearch API (/api/v2/search.json?query=type:organization)Full read/write.
The exact collection names are exported as the COLLECTION_NAMES constant — use it instead of hard-coding the strings when you customize the collections:
import { COLLECTION_NAMES } from '@forestadmin/datasource-zendesk';

agent.customizeCollection(COLLECTION_NAMES.ticket, collection => {
  /* ... */
});

Relationships

The following relationships are exposed automatically:
  • zendesk_ticket.requesterzendesk_user (foreign key requester_id)
  • zendesk_ticket.assigneezendesk_user (foreign key assignee_id)
  • zendesk_ticket.organizationzendesk_organization (foreign key organization_id)
  • zendesk_user.organizationzendesk_organization
  • zendesk_user.requested_ticketszendesk_ticket
  • zendesk_organization.userszendesk_user
  • zendesk_organization.ticketszendesk_ticket

Comments

Zendesk has no standalone /comments/{id} endpoint, so comments are not exposed as their own collection. Instead, each ticket carries a structured comments array column that is fetched lazily from /api/v2/tickets/{id}/comments only when the projection asks for it (i.e. when comments is rendered on the detail view or referenced in a custom action). Each entry has the following shape:
FieldTypeSource
idNumberZendesk comment id
bodyStringPlain-text body
html_bodyStringHTML-formatted body
publicBooleantrue for public replies, false for internal notes
author_emailStringResolved through batched users/show_many calls (chunks of 100) across all visible authors
author_nameStringSame
created_atDateComment creation timestamp
The column is read-only — comments are added by writing to the ticket’s description on creation (Zendesk converts the description into the first comment).

Custom fields

Custom fields configured in your Zendesk account are introspected at boot and added to the matching collection’s schema:
  • Ticket custom fields are exposed as custom_<zendesk_id> columns on zendesk_ticket.
  • User and organization custom fields are exposed using their Zendesk key (or custom_<zendesk_id> if no key is set) on zendesk_user / zendesk_organization.
The Forest column type is derived from the Zendesk field type:
Zendesk field typeForest column type
text, textarea, regexp, partialcreditcardString
integer, decimal, lookupNumber
dateDateonly
checkboxBoolean
dropdown, taggerEnum (or String if no options are configured)
multiselectJson
Unrecognized field types are skipped and logged with a warning; non-user-created (i.e. non-removable) ticket fields — which include every system ticket field — and inactive fields on any resource are dropped silently. Column-name collisions with a native column are also skipped with a warning.
The initial introspection calls (GET /ticket_fields.json, /user_fields.json, /organization_fields.json) are not wrapped in a best-effort guard: a transport error during boot will fail createZendeskDataSource loudly. Make sure the API token’s role has read access to the field definitions.

Capabilities

Filters

The condition tree is translated into a Zendesk Search API query.
The following operators are supported per column type:
Column typeSupported operators
Primary key (id)Equal, In
String, EnumEqual, NotEqual, In, NotIn, Present, Blank
NumberEqual, NotEqual, In, NotIn, Present, Blank, GreaterThan, LessThan
Date, DateonlyEqual, Before, After, Present, Blank
BooleanEqual, NotEqual
Translations:
OperatorZendesk Search syntax
Equal, NotEqualfield:value / -field:value
In, NotInrepeated field:value clauses (Zendesk ANDs them — see note below)
GreaterThan, Afterfield>value
LessThan, Beforefield<value
Present, Blankfield:* / -field:*
Notes:
  • Only And is supported. Zendesk Search has no general Or operator, so any Or aggregator raises UnsupportedOperatorError. Each column advertises a narrow filterOperators set so Forest’s operator-equivalence decorator never rewrites In / NotIn into an Or the translator cannot express.
  • In and NotIn behave asymmetrically on non-id fields. Both operators are emitted as space-joined clauses, which Zendesk Search then ANDs together. For NotIn, that’s exactly the right semantics: -status:open -status:pending excludes both values (De Morgan: NOT a AND NOT bNotIn [a, b]). For In, the AND is wrong — status:open status:pending asks for a ticket whose status is both, which is empty. So In is only a true membership test on the primary key (where it short-circuits to per-id GETs). On any other field, filter on a single value or split into separate calls.
  • id lookups bypass Search. Any And branch carrying id Equal N or id In [...] short-circuits to per-id GET /tickets/<id>.json (and friends); sibling conditions in the branch are then re-applied in memory.
  • An empty In / NotIn raises UnsupportedOperatorError rather than matching everything.
  • A handful of columns advertise no filter operators at all (so the UI offers no filter widget on them): description, tags, url, comments on tickets; domain_names, details, notes, shared_tickets on organizations; time_zone, locale on users.
  • A null/undefined value passed with Equal / NotEqual / In raises explicitly — use Present / Blank to filter for absence.
  • Filtering zendesk_ticket.requester_email = "x@y.z" is rewritten to Zendesk’s requester:x@y.z operator. Only Equal is supported on requester_email.
  • String values containing whitespace, double quotes, parentheses, colons or hyphens are wrapped in double quotes (with embedded quotes escaped) before being sent to the Search API.

Sorting

Only fields that the Zendesk Search API can sort on are honored. Other sort directives are silently ignored:
  • zendesk_ticket: created_at, updated_at, priority, status, ticket_type
  • zendesk_user: created_at, updated_at, name
  • zendesk_organization: created_at, updated_at, name

Pagination

Forest’s offset/limit pagination is translated to Zendesk’s page / per_page. The Search API caps per_page at 100 (clamped automatically) and caps the total result window at 1000 records (MAX_TOTAL_RESULTS). A request with skip + limit > 1000 raises UnsupportedOperatorError rather than silently returning a truncated set.A bulk update or delete that matches more than 1000 records will affect only the first 1000 and emit a Warn log — narrow the filter when working on larger sets.

Aggregations

Only Count aggregation without grouping is supported. Any other aggregation raises UnsupportedOperatorError — the Zendesk Search API has no group-by primitive. Count uses Zendesk’s /search/count.json for filtered counts, and verifies record existence for the id-lookup path so it never over-counts.
The Forest search bar is shown by default on every collection (the agent’s search decorator advertises searchable: true), but the default Forest search rewrites the user’s query into an Or of Contains predicates — operators that the Zendesk Search translator does not support, so typing in the bar surfaces an UnsupportedOperatorError.You have two options:
// Hide the bar
agent.customizeCollection('zendesk_ticket', collection =>
  collection.disableSearch(),
);

// Or implement search yourself with a translator-compatible condition tree
agent.customizeCollection('zendesk_ticket', collection =>
  collection.replaceSearch(query => ({
    field: 'subject',
    operator: 'Equal',
    value: query,
  })),
);
Filtering through the column filters keeps working as documented above either way.

Writes

Create, update and delete are supported on all three collections. Custom fields are folded into the appropriate Zendesk payload structure (custom_fields for tickets, user_fields / organization_fields for users and organizations). A few fields are intentionally read-only:
  • id, url, created_at and updated_at are ignored on writes.
  • On zendesk_ticket, description is only written on creation (Zendesk turns it into the first comment) — on update the value is dropped with a Warn log because Zendesk exposes no write endpoint for it.
  • zendesk_ticket.requester_email is computed at read time from the requester’s profile and cannot be written directly.
  • The id-lookup short-circuit re-checks the caller’s scopes/segments in memory, so a scoped update / delete never escapes its perimeter.

Errors

All exceptions raised by the datasource are subclasses of Forest’s BusinessError / ValidationError:
ClassParentRaised by
ZendeskConfigurationErrorValidationErrorThe client constructor when subdomain / email / apiToken is missing or empty.
ZendeskApiErrorBusinessErrorEvery HTTP call. Carries operation, HTTP status and the raw response body.
UnsupportedOperatorErrorBusinessErrorThe condition-tree translator (unsupported operator, Or aggregator, empty In/NotIn, …) and the pagination cap.
All three error classes are exported from @forestadmin/datasource-zendesk for use in your own catch blocks.

Logging

Best-effort enrichment paths (bulk user/organization lookups, comment-author resolution, per-ticket comment fetches) log a Warn via the agent logger and degrade to a safe default (typically an empty Map or null fields) rather than failing the whole page render. Critical paths (search, count, ticket bulk fetch, ticket comment fetch, writes, custom-field introspection) raise ZendeskApiError.No retry / backoff is performed on Zendesk responses — if you need to absorb transient 429s or 502s, wrap your own retry layer around the ZendeskHttpClient.

Source code

This connector is open source. Browse the code or contribute on GitHub: