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
- Node.js
- Ruby
Install the package Sharing the same
@forestadmin/datasource-zendesk.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.- Node.js
- Ruby
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.| Option | Description |
|---|---|
client | A ZendeskClient instance built with createZendeskClient. Required when subdomain / email / apiToken are not provided. |
subdomain | The subdomain of your Zendesk account (e.g. acme for https://acme.zendesk.com). Required when client is not provided. |
email | The email address associated with the API token (typically a Zendesk admin/agent account). Required alongside subdomain and apiToken. |
apiToken | A Zendesk API token generated from Admin Center → Apps and integrations → APIs → Zendesk API. Required alongside subdomain and email. |
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:- Node.js
- Ruby
| Collection | Primary endpoint | Notes |
|---|---|---|
zendesk_ticket | Search API (/api/v2/search.json?query=type:ticket) | Full read/write. Embeds requester, assignee, organization and an inline comments thread. |
zendesk_user | Search API (/api/v2/search.json?query=type:user) | Full read/write. |
zendesk_organization | Search API (/api/v2/search.json?query=type:organization) | Full read/write. |
COLLECTION_NAMES constant — use it instead of hard-coding the strings when you customize the collections:Relationships
The following relationships are exposed automatically:- Node.js
- Ruby
zendesk_ticket.requester→zendesk_user(foreign keyrequester_id)zendesk_ticket.assignee→zendesk_user(foreign keyassignee_id)zendesk_ticket.organization→zendesk_organization(foreign keyorganization_id)zendesk_user.organization→zendesk_organizationzendesk_user.requested_tickets→zendesk_ticketzendesk_organization.users→zendesk_userzendesk_organization.tickets→zendesk_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:
- Node.js
- Ruby
| Field | Type | Source |
|---|---|---|
id | Number | Zendesk comment id |
body | String | Plain-text body |
html_body | String | HTML-formatted body |
public | Boolean | true for public replies, false for internal notes |
author_email | String | Resolved through batched users/show_many calls (chunks of 100) across all visible authors |
author_name | String | Same |
created_at | Date | Comment creation timestamp |
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:- Node.js
- Ruby
- Ticket custom fields are exposed as
custom_<zendesk_id>columns onzendesk_ticket. - User and organization custom fields are exposed using their Zendesk
key(orcustom_<zendesk_id>if no key is set) onzendesk_user/zendesk_organization.
| Zendesk field type | Forest column type |
|---|---|
text, textarea, regexp, partialcreditcard | String |
integer, decimal, lookup | Number |
date | Dateonly |
checkbox | Boolean |
dropdown, tagger | Enum (or String if no options are configured) |
multiselect | Json |
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.- Node.js
- Ruby
The following operators are supported per column type:
Translations:
Notes:
| Column type | Supported operators |
|---|---|
Primary key (id) | Equal, In |
String, Enum | Equal, NotEqual, In, NotIn, Present, Blank |
Number | Equal, NotEqual, In, NotIn, Present, Blank, GreaterThan, LessThan |
Date, Dateonly | Equal, Before, After, Present, Blank |
Boolean | Equal, NotEqual |
| Operator | Zendesk Search syntax |
|---|---|
Equal, NotEqual | field:value / -field:value |
In, NotIn | repeated field:value clauses (Zendesk ANDs them — see note below) |
GreaterThan, After | field>value |
LessThan, Before | field<value |
Present, Blank | field:* / -field:* |
- Only
Andis supported. Zendesk Search has no generalOroperator, so anyOraggregator raisesUnsupportedOperatorError. Each column advertises a narrowfilterOperatorsset so Forest’s operator-equivalence decorator never rewritesIn/NotIninto anOrthe translator cannot express. InandNotInbehave asymmetrically on non-id fields. Both operators are emitted as space-joined clauses, which Zendesk Search then ANDs together. ForNotIn, that’s exactly the right semantics:-status:open -status:pendingexcludes both values (De Morgan:NOT a AND NOT b≡NotIn [a, b]). ForIn, the AND is wrong —status:open status:pendingasks for a ticket whose status is both, which is empty. SoInis 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.idlookups bypass Search. AnyAndbranch carryingid Equal Norid In [...]short-circuits to per-idGET /tickets/<id>.json(and friends); sibling conditions in the branch are then re-applied in memory.- An empty
In/NotInraisesUnsupportedOperatorErrorrather 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,commentson tickets;domain_names,details,notes,shared_ticketson organizations;time_zone,localeon users. - A
null/undefinedvalue passed withEqual/NotEqual/Inraises explicitly — usePresent/Blankto filter for absence. - Filtering
zendesk_ticket.requester_email = "x@y.z"is rewritten to Zendesk’srequester:x@y.zoperator. OnlyEqualis supported onrequester_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:- Node.js
- Ruby
zendesk_ticket:created_at,updated_at,priority,status,ticket_typezendesk_user:created_at,updated_at,namezendesk_organization:created_at,updated_at,name
Pagination
- Node.js
- Ruby
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
- Node.js
- Ruby
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.Search
- Node.js
- Ruby
The Forest search bar is shown by default on every collection (the agent’s search decorator advertises Filtering through the column filters keeps working as documented above either way.
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: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:
- Node.js
- Ruby
id,url,created_atandupdated_atare ignored on writes.- On
zendesk_ticket,descriptionis only written on creation (Zendesk turns it into the first comment) — on update the value is dropped with aWarnlog because Zendesk exposes no write endpoint for it. zendesk_ticket.requester_emailis 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/deletenever escapes its perimeter.
Errors
- Node.js
- Ruby
All exceptions raised by the datasource are subclasses of Forest’s
All three error classes are exported from
BusinessError / ValidationError:| Class | Parent | Raised by |
|---|---|---|
ZendeskConfigurationError | ValidationError | The client constructor when subdomain / email / apiToken is missing or empty. |
ZendeskApiError | BusinessError | Every HTTP call. Carries operation, HTTP status and the raw response body. |
UnsupportedOperatorError | BusinessError | The condition-tree translator (unsupported operator, Or aggregator, empty In/NotIn, …) and the pagination cap. |
@forestadmin/datasource-zendesk for use in your own catch blocks.Logging
- Node.js
- Ruby
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:- Node.js
- Ruby