first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled

This commit is contained in:
2026-03-17 16:22:57 +03:30
commit 3d5eaf9445
15349 changed files with 2847338 additions and 0 deletions
@@ -0,0 +1,293 @@
import type { INodeProperties } from 'n8n-workflow';
export const conditionFields: INodeProperties[] = [
{
displayName: 'Resource',
name: 'resource',
type: 'options',
noDataExpression: true,
options: [
{
name: 'Ticket',
value: 'ticket',
},
],
default: 'ticket',
},
{
displayName: 'Field',
name: 'field',
type: 'options',
displayOptions: {
show: {
resource: ['ticket'],
},
},
options: [
{
name: 'Assignee',
value: 'assignee',
},
{
name: 'Group',
value: 'group',
},
{
name: 'Priority',
value: 'priority',
},
{
name: 'Status',
value: 'status',
},
{
name: 'Type',
value: 'type',
},
],
default: 'status',
},
// eslint-disable-next-line n8n-nodes-base/node-param-operation-without-no-data-expression
{
displayName: 'Operation',
name: 'operation',
type: 'options',
options: [
{
name: 'Changed',
value: 'changed',
},
{
name: 'Changed From',
value: 'value_previous',
},
{
name: 'Changed To',
value: 'value',
},
{
name: 'Greater Than',
value: 'greater_than',
},
{
name: 'Is',
value: 'is',
},
{
name: 'Is Not',
value: 'is_not',
},
{
name: 'Less Than',
value: 'less_than',
},
{
name: 'Not Changed',
value: 'not_changed',
},
{
name: 'Not Changed From',
value: 'not_value_previous',
},
{
name: 'Not Changed To',
value: 'not_value',
},
],
displayOptions: {
hide: {
field: ['assignee'],
},
},
default: 'is',
},
// eslint-disable-next-line n8n-nodes-base/node-param-operation-without-no-data-expression
{
displayName: 'Operation',
name: 'operation',
type: 'options',
options: [
{
name: 'Changed',
value: 'changed',
},
{
name: 'Changed From',
value: 'value_previous',
},
{
name: 'Changed To',
value: 'value',
},
{
name: 'Is',
value: 'is',
},
{
name: 'Is Not',
value: 'is_not',
},
{
name: 'Not Changed',
value: 'not_changed',
},
{
name: 'Not Changed From',
value: 'not_value_previous',
},
{
name: 'Not Changed To',
value: 'not_value',
},
],
displayOptions: {
show: {
field: ['assignee'],
},
},
default: 'is',
},
{
displayName: 'Value',
name: 'value',
type: 'options',
displayOptions: {
show: {
field: ['status'],
},
hide: {
operation: ['changed', 'not_changed'],
field: ['assignee', 'group', 'priority', 'type'],
},
},
options: [
{
name: 'Closed',
value: 'closed',
},
{
name: 'New',
value: 'new',
},
{
name: 'Open',
value: 'open',
},
{
name: 'Pending',
value: 'pending',
},
{
name: 'Solved',
value: 'solved',
},
],
default: 'open',
},
{
displayName: 'Value',
name: 'value',
type: 'options',
displayOptions: {
show: {
field: ['type'],
},
hide: {
operation: ['changed', 'not_changed'],
field: ['assignee', 'group', 'priority', 'status'],
},
},
options: [
{
name: 'Question',
value: 'question',
},
{
name: 'Incident',
value: 'incident',
},
{
name: 'Problem',
value: 'problem',
},
{
name: 'Task',
value: 'task',
},
],
default: 'question',
},
{
displayName: 'Value',
name: 'value',
type: 'options',
displayOptions: {
show: {
field: ['priority'],
},
hide: {
operation: ['changed', 'not_changed'],
field: ['assignee', 'group', 'type', 'status'],
},
},
options: [
{
name: 'Low',
value: 'low',
},
{
name: 'Normal',
value: 'normal',
},
{
name: 'High',
value: 'high',
},
{
name: 'Urgent',
value: 'urgent',
},
],
default: 'low',
},
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
displayName: 'Value',
name: 'value',
type: 'options',
description:
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
typeOptions: {
loadOptionsMethod: 'getGroups',
},
displayOptions: {
show: {
field: ['group'],
},
hide: {
field: ['assignee', 'priority', 'type', 'status'],
},
},
default: '',
},
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
displayName: 'Value',
name: 'value',
type: 'options',
description:
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
typeOptions: {
loadOptionsMethod: 'getUsers',
},
displayOptions: {
show: {
field: ['assignee'],
},
hide: {
field: ['group', 'priority', 'type', 'status'],
},
},
default: '',
},
];
@@ -0,0 +1,99 @@
import type {
IDataObject,
IExecuteFunctions,
IHookFunctions,
ILoadOptionsFunctions,
IHttpRequestMethods,
IRequestOptions,
} from 'n8n-workflow';
function getUri(resource: string, subdomain: string) {
if (resource.includes('webhooks')) {
return `https://${subdomain}.zendesk.com/api/v2${resource}`;
} else {
return `https://${subdomain}.zendesk.com/api/v2${resource}.json`;
}
}
export async function zendeskApiRequest(
this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions,
method: IHttpRequestMethods,
resource: string,
body: any = {},
qs: IDataObject = {},
uri?: string,
option: IDataObject = {},
) {
const authenticationMethod = this.getNodeParameter('authentication', 0);
let credentials;
if (authenticationMethod === 'apiToken') {
credentials = await this.getCredentials<{ subdomain: string }>('zendeskApi');
} else {
credentials = await this.getCredentials<{ subdomain: string }>('zendeskOAuth2Api');
}
let options: IRequestOptions = {
method,
qs,
body,
uri: uri || getUri(resource, credentials.subdomain),
json: true,
qsStringifyOptions: {
arrayFormat: 'brackets',
},
};
options = Object.assign({}, options, option);
if (Object.keys(options.body as IDataObject).length === 0) {
delete options.body;
}
const credentialType = authenticationMethod === 'apiToken' ? 'zendeskApi' : 'zendeskOAuth2Api';
return await this.helpers.requestWithAuthentication.call(this, credentialType, options);
}
/**
* Make an API request to paginated flow endpoint
* and return all results
*/
export async function zendeskApiRequestAllItems(
this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions,
propertyName: string,
method: IHttpRequestMethods,
resource: string,
body: any = {},
query: IDataObject = {},
): Promise<any> {
const returnData: IDataObject[] = [];
let responseData;
let uri: string | undefined;
do {
responseData = await zendeskApiRequest.call(this, method, resource, body, query, uri);
uri = responseData.next_page;
returnData.push.apply(returnData, responseData[propertyName] as IDataObject[]);
const limit = query.limit as number | undefined;
if (limit && limit <= returnData.length) {
return returnData;
}
} while (responseData.next_page !== undefined && responseData.next_page !== null);
return returnData;
}
export function validateJSON(json: string | undefined): any {
let result;
try {
result = JSON.parse(json!);
} catch (exception) {
result = undefined;
}
return result;
}
@@ -0,0 +1,347 @@
import type { INodeProperties } from 'n8n-workflow';
export const organizationOperations: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['organization'],
},
},
options: [
{
name: 'Count',
value: 'count',
description: 'Count organizations',
action: 'Count organizations',
},
{
name: 'Create',
value: 'create',
description: 'Create an organization',
action: 'Create an organization',
},
{
name: 'Delete',
value: 'delete',
description: 'Delete an organization',
action: 'Delete an organization',
},
{
name: 'Get',
value: 'get',
description: 'Get an organization',
action: 'Get an organization',
},
{
name: 'Get Many',
value: 'getAll',
description: 'Get many organizations',
action: 'Get many organizations',
},
{
name: 'Get Related Data',
value: 'getRelatedData',
description: 'Get data related to the organization',
action: 'Get data related to an organization',
},
{
name: 'Update',
value: 'update',
description: 'Update a organization',
action: 'Update an organization',
},
],
default: 'create',
},
];
export const organizationFields: INodeProperties[] = [
/* -------------------------------------------------------------------------- */
/* organization:create */
/* -------------------------------------------------------------------------- */
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
displayOptions: {
show: {
resource: ['organization'],
operation: ['create'],
},
},
required: true,
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: ['organization'],
operation: ['create'],
},
},
options: [
{
displayName: 'Details',
name: 'details',
type: 'string',
default: '',
description: 'Details about the organization, such as the address',
},
{
displayName: 'Domain Names',
name: 'domain_names',
type: 'string',
default: '',
description: 'Comma-separated domain names associated with this organization',
},
{
displayName: 'Notes',
name: 'notes',
type: 'string',
default: '',
},
{
displayName: 'Organization Fields',
name: 'organizationFieldsUi',
placeholder: 'Add Organization Field',
description: "Values of custom fields in the organization's profile",
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
default: {},
options: [
{
name: 'organizationFieldValues',
displayName: 'Field',
values: [
{
displayName: 'Field Name or ID',
name: 'field',
type: 'options',
description:
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
typeOptions: {
loadOptionsMethod: 'getOrganizationFields',
},
default: '',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
},
],
},
],
},
{
displayName: 'Tag Names or IDs',
name: 'tags',
type: 'multiOptions',
typeOptions: {
loadOptionsMethod: 'getTags',
},
default: [],
description:
'IDs of tags applied to this organization. Choose from the list, or specify IDs using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
},
],
},
/* -------------------------------------------------------------------------- */
/* organization:update */
/* -------------------------------------------------------------------------- */
{
displayName: 'Organization ID',
name: 'id',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: ['organization'],
operation: ['update'],
},
},
},
{
displayName: 'Update Fields',
name: 'updateFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: ['organization'],
operation: ['update'],
},
},
options: [
{
displayName: 'Details',
name: 'details',
type: 'string',
default: '',
description: 'Details about the organization, such as the address',
},
{
displayName: 'Domain Names',
name: 'domain_names',
type: 'string',
default: '',
description: 'Comma-separated domain names associated with this organization',
},
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
},
{
displayName: 'Notes',
name: 'notes',
type: 'string',
default: '',
},
{
displayName: 'Organization Fields',
name: 'organizationFieldsUi',
placeholder: 'Add Organization Field',
description: "Values of custom fields in the organization's profile",
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
default: {},
options: [
{
name: 'organizationFieldValues',
displayName: 'Field',
values: [
{
displayName: 'Field Name or ID',
name: 'field',
type: 'options',
description:
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>',
typeOptions: {
loadOptionsMethod: 'getOrganizationFields',
},
default: '',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
},
],
},
],
},
{
displayName: 'Tag Names or IDs',
name: 'tags',
type: 'multiOptions',
typeOptions: {
loadOptionsMethod: 'getTags',
},
default: [],
description:
'IDs of tags applied to this organization. Choose from the list, or specify IDs using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
},
],
},
/* -------------------------------------------------------------------------- */
/* organization:get */
/* -------------------------------------------------------------------------- */
{
displayName: 'Organization ID',
name: 'id',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: ['organization'],
operation: ['get'],
},
},
},
/* -------------------------------------------------------------------------- */
/* organization:getAll */
/* -------------------------------------------------------------------------- */
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
resource: ['organization'],
operation: ['getAll'],
},
},
default: false,
description: 'Whether to return all results or only up to a given limit',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
displayOptions: {
show: {
resource: ['organization'],
operation: ['getAll'],
returnAll: [false],
},
},
typeOptions: {
minValue: 1,
maxValue: 100,
},
default: 100,
description: 'Max number of results to return',
},
/* -------------------------------------------------------------------------- */
/* organization:delete */
/* -------------------------------------------------------------------------- */
{
displayName: 'Organization ID',
name: 'id',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: ['organization'],
operation: ['delete'],
},
},
},
/* -------------------------------------------------------------------------- */
/* organization:getRelatedData */
/* -------------------------------------------------------------------------- */
{
displayName: 'Organization ID',
name: 'id',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: ['organization'],
operation: ['getRelatedData'],
},
},
},
];
@@ -0,0 +1,743 @@
import type { INodeProperties } from 'n8n-workflow';
export const ticketOperations: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['ticket'],
},
},
options: [
{
name: 'Create',
value: 'create',
description: 'Create a ticket',
action: 'Create a ticket',
},
{
name: 'Delete',
value: 'delete',
description: 'Delete a ticket',
action: 'Delete a ticket',
},
{
name: 'Get',
value: 'get',
description: 'Get a ticket',
action: 'Get a ticket',
},
{
name: 'Get Many',
value: 'getAll',
description: 'Get many tickets',
action: 'Get many tickets',
},
{
name: 'Recover',
value: 'recover',
description: 'Recover a suspended ticket',
action: 'Recover a ticket',
},
{
name: 'Update',
value: 'update',
description: 'Update a ticket',
action: 'Update a ticket',
},
],
default: 'create',
},
];
export const ticketFields: INodeProperties[] = [
/* -------------------------------------------------------------------------- */
/* ticket:create */
/* -------------------------------------------------------------------------- */
{
displayName: 'Description',
name: 'description',
type: 'string',
default: '',
displayOptions: {
show: {
resource: ['ticket'],
operation: ['create'],
},
},
required: true,
description: 'The first comment on the ticket',
},
{
displayName: 'JSON Parameters',
name: 'jsonParameters',
type: 'boolean',
default: false,
displayOptions: {
show: {
resource: ['ticket'],
operation: ['create'],
},
},
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: ['ticket'],
operation: ['create'],
jsonParameters: [false],
},
},
options: [
{
displayName: 'Custom Fields',
name: 'customFieldsUi',
placeholder: 'Add Custom Field',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
default: {},
options: [
{
displayName: 'Custom Field',
name: 'customFieldsValues',
values: [
{
displayName: 'Name or ID',
name: 'id',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getCustomFields',
},
default: '',
description:
'Custom field ID. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
description: 'Custom field Value',
},
],
},
],
},
{
displayName: 'External ID',
name: 'externalId',
type: 'string',
default: '',
description: 'An ID you can use to link Zendesk Support tickets to local records',
},
{
displayName: 'Group Name or ID',
name: 'group',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getGroups',
},
default: '',
description:
'The group this ticket is assigned to. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
},
{
displayName: 'Recipient',
name: 'recipient',
type: 'string',
default: '',
description: 'The original recipient e-mail address of the ticket',
},
{
displayName: 'Status',
name: 'status',
type: 'options',
options: [
{
name: 'Closed',
value: 'closed',
},
{
name: 'New',
value: 'new',
},
{
name: 'On-Hold',
value: 'hold',
},
{
name: 'Open',
value: 'open',
},
{
name: 'Pending',
value: 'pending',
},
{
name: 'Solved',
value: 'solved',
},
],
default: '',
description: 'The state of the ticket',
},
{
displayName: 'Subject',
name: 'subject',
type: 'string',
default: '',
description: 'The value of the subject field for this ticket',
},
{
displayName: 'Tag Names or IDs',
name: 'tags',
type: 'multiOptions',
typeOptions: {
loadOptionsMethod: 'getTags',
},
default: [],
description:
'The array of tags applied to this ticket. Choose from the list, or specify IDs using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
},
{
displayName: 'Type',
name: 'type',
type: 'options',
options: [
{
name: 'Question',
value: 'question',
},
{
name: 'Incident',
value: 'incident',
},
{
name: 'Problem',
value: 'problem',
},
{
name: 'Task',
value: 'task',
},
],
default: '',
description: 'The type of this ticket',
},
],
},
{
displayName: 'Additional Fields',
name: 'additionalFieldsJson',
type: 'json',
typeOptions: {
alwaysOpenEditWindow: true,
},
default: '',
displayOptions: {
show: {
resource: ['ticket'],
operation: ['create'],
jsonParameters: [true],
},
},
description:
'Object of values to set as described <a href="https://developer.zendesk.com/rest_api/docs/support/tickets">here</a>',
},
/* -------------------------------------------------------------------------- */
/* ticket:update */
/* -------------------------------------------------------------------------- */
{
displayName: 'Ticket ID',
name: 'id',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: ['ticket'],
operation: ['update'],
},
},
},
{
displayName: 'JSON Parameters',
name: 'jsonParameters',
type: 'boolean',
default: false,
displayOptions: {
show: {
resource: ['ticket'],
operation: ['update'],
},
},
},
{
displayName: 'Update Fields',
name: 'updateFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: ['ticket'],
operation: ['update'],
jsonParameters: [false],
},
},
options: [
{
displayName: 'Assignee Email',
name: 'assigneeEmail',
type: 'string',
default: '',
description: 'The e-mail address of the assignee',
},
{
displayName: 'Custom Fields',
name: 'customFieldsUi',
placeholder: 'Add Custom Field',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
default: {},
options: [
{
displayName: 'Custom Field',
name: 'customFieldsValues',
values: [
{
displayName: 'Name or ID',
name: 'id',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getCustomFields',
},
default: '',
description:
'Custom field ID. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
description: 'Custom field Value',
},
],
},
],
},
{
displayName: 'External ID',
name: 'externalId',
type: 'string',
default: '',
description: 'An ID you can use to link Zendesk Support tickets to local records',
},
{
displayName: 'Group Name or ID',
name: 'group',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getGroups',
},
default: '',
description:
'The group this ticket is assigned to. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
},
{
displayName: 'Internal Note',
name: 'internalNote',
type: 'string',
default: '',
description: 'Internal Ticket Note (Accepts HTML)',
},
{
displayName: 'Public Reply',
name: 'publicReply',
type: 'string',
default: '',
description: 'Public ticket reply',
},
{
displayName: 'Recipient',
name: 'recipient',
type: 'string',
default: '',
description: 'The original recipient e-mail address of the ticket',
},
{
displayName: 'Status',
name: 'status',
type: 'options',
options: [
{
name: 'Closed',
value: 'closed',
},
{
name: 'New',
value: 'new',
},
{
name: 'On-Hold',
value: 'hold',
},
{
name: 'Open',
value: 'open',
},
{
name: 'Pending',
value: 'pending',
},
{
name: 'Solved',
value: 'solved',
},
],
default: '',
description: 'The state of the ticket',
},
{
displayName: 'Subject',
name: 'subject',
type: 'string',
default: '',
description: 'The value of the subject field for this ticket',
},
{
displayName: 'Tag Names or IDs',
name: 'tags',
type: 'multiOptions',
typeOptions: {
loadOptionsMethod: 'getTags',
},
default: [],
description:
'The array of tags applied to this ticket. Choose from the list, or specify IDs using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
},
{
displayName: 'Type',
name: 'type',
type: 'options',
options: [
{
name: 'Question',
value: 'question',
},
{
name: 'Incident',
value: 'incident',
},
{
name: 'Problem',
value: 'problem',
},
{
name: 'Task',
value: 'task',
},
],
default: '',
description: 'The type of this ticket',
},
],
},
{
displayName: 'Update Fields',
name: 'updateFieldsJson',
type: 'json',
typeOptions: {
alwaysOpenEditWindow: true,
},
default: '',
displayOptions: {
show: {
resource: ['ticket'],
operation: ['update'],
jsonParameters: [true],
},
},
description:
'Object of values to update as described <a href="https://developer.zendesk.com/rest_api/docs/support/tickets">here</a>',
},
{
displayName: 'Ticket Type',
name: 'ticketType',
type: 'options',
options: [
{
name: 'Regular',
value: 'regular',
},
{
name: 'Suspended',
value: 'suspended',
},
],
default: 'regular',
required: true,
displayOptions: {
show: {
resource: ['ticket'],
operation: ['get', 'delete', 'getAll'],
},
},
},
/* -------------------------------------------------------------------------- */
/* ticket:get */
/* -------------------------------------------------------------------------- */
{
displayName: 'Ticket ID',
name: 'id',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: ['ticket'],
operation: ['get'],
ticketType: ['regular'],
},
},
},
{
displayName: 'Suspended Ticket ID',
name: 'id',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: ['ticket'],
operation: ['get'],
ticketType: ['suspended'],
},
},
description: 'Ticket ID',
},
/* -------------------------------------------------------------------------- */
/* ticket:getAll */
/* -------------------------------------------------------------------------- */
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
resource: ['ticket'],
operation: ['getAll'],
},
},
default: false,
description: 'Whether to return all results or only up to a given limit',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
displayOptions: {
show: {
resource: ['ticket'],
operation: ['getAll'],
returnAll: [false],
},
},
typeOptions: {
minValue: 1,
maxValue: 100,
},
default: 100,
description: 'Max number of results to return',
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add option',
default: {},
displayOptions: {
show: {
resource: ['ticket'],
operation: ['getAll'],
},
},
options: [
{
displayName: 'Group Name or ID',
name: 'group',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getGroups',
},
displayOptions: {
show: {
'/ticketType': ['regular'],
},
},
default: '',
description:
'The group to search. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
},
{
displayName: 'Query',
name: 'query',
type: 'string',
displayOptions: {
show: {
'/ticketType': ['regular'],
},
},
default: '',
description:
'<a href="https://developer.zendesk.com/api-reference/ticketing/ticket-management/search/#syntax-examples">Query syntax</a> to search tickets',
},
{
displayName: 'Sort By',
name: 'sortBy',
type: 'options',
options: [
{
name: 'Created At',
value: 'created_at',
},
{
name: 'Priority',
value: 'priority',
},
{
name: 'Status',
value: 'status',
},
{
name: 'Ticket Type',
value: 'ticket_type',
},
{
name: 'Updated At',
value: 'updated_at',
},
],
default: 'updated_at',
description: 'Defaults to sorting by relevance',
},
{
displayName: 'Sort Order',
name: 'sortOrder',
type: 'options',
options: [
{
name: 'Ascending',
value: 'asc',
},
{
name: 'Descending',
value: 'desc',
},
],
default: 'asc',
},
{
displayName: 'Status',
name: 'status',
type: 'options',
displayOptions: {
show: {
'/ticketType': ['regular'],
},
},
options: [
{
name: 'Closed',
value: 'closed',
},
{
name: 'New',
value: 'new',
},
{
name: 'On-Hold',
value: 'hold',
},
{
name: 'Open',
value: 'open',
},
{
name: 'Pending',
value: 'pending',
},
{
name: 'Solved',
value: 'solved',
},
],
default: '',
description: 'The state of the ticket',
},
],
},
/* -------------------------------------------------------------------------- */
/* ticket:delete */
/* -------------------------------------------------------------------------- */
{
displayName: 'Ticket ID',
name: 'id',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: ['ticket'],
operation: ['delete'],
ticketType: ['regular'],
},
},
},
{
displayName: 'Suspended Ticket ID',
name: 'id',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: ['ticket'],
operation: ['delete'],
ticketType: ['suspended'],
},
},
description: 'Ticket ID',
},
/* -------------------------------------------------------------------------- */
/* ticket:recover */
/* -------------------------------------------------------------------------- */
{
displayName: 'Suspended Ticket ID',
name: 'id',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: ['ticket'],
operation: ['recover'],
},
},
},
];
@@ -0,0 +1,84 @@
import type { INodeProperties } from 'n8n-workflow';
export const ticketFieldOperations: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['ticketField'],
},
},
options: [
{
name: 'Get',
value: 'get',
description: 'Get a ticket field',
action: 'Get a ticket field',
},
{
name: 'Get Many',
value: 'getAll',
description: 'Get many system and custom ticket fields',
action: 'Get many ticket fields',
},
],
default: 'get',
},
];
export const ticketFieldFields: INodeProperties[] = [
/* -------------------------------------------------------------------------- */
/* ticketField:get */
/* -------------------------------------------------------------------------- */
{
displayName: 'Ticket Field ID',
name: 'ticketFieldId',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: ['ticketField'],
operation: ['get'],
},
},
},
/* -------------------------------------------------------------------------- */
/* ticketField:getAll */
/* -------------------------------------------------------------------------- */
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
resource: ['ticketField'],
operation: ['getAll'],
},
},
default: false,
description: 'Whether to return all results or only up to a given limit',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
displayOptions: {
show: {
resource: ['ticketField'],
operation: ['getAll'],
returnAll: [false],
},
},
typeOptions: {
minValue: 1,
maxValue: 100,
},
default: 100,
description: 'Max number of results to return',
},
];
@@ -0,0 +1,20 @@
import type { IDataObject } from 'n8n-workflow';
export interface IComment {
body?: string;
html_body?: string;
public?: boolean;
}
export interface ITicket {
subject?: string;
comment?: IComment;
type?: string;
group_id?: number;
external_id?: string;
tags?: string[];
status?: string;
recipient?: string;
custom_fields?: IDataObject[];
assignee_email?: string;
}
@@ -0,0 +1,225 @@
import type { INodePropertyOptions } from 'n8n-workflow';
export const triggerPlaceholders = [
{
name: 'Title',
value: 'ticket.title',
description: "Ticket's subject",
},
{
name: 'Description',
value: 'ticket.description',
description: "Ticket's description",
},
{
name: 'URL',
value: 'ticket.url',
description: "Ticket's URL",
},
{
name: 'ID',
value: 'ticket.id',
description: "Ticket's ID",
},
{
name: 'External ID',
value: 'ticket.external_id',
description: "Ticket's external ID",
},
{
name: 'Via',
value: 'ticket.via',
description: "Ticket's source",
},
{
name: 'Status',
value: 'ticket.status',
description: "Ticket's status",
},
{
name: 'Priority',
value: 'ticket.priority',
description: "Ticket's priority",
},
{
name: 'Type',
value: 'ticket.ticket_type',
description: "Ticket's type",
},
{
name: 'Group Name',
value: 'ticket.group.name',
description: "Ticket's assigned group",
},
{
name: 'Brand Name',
value: 'ticket.brand.name',
description: "Ticket's brand",
},
{
name: 'Due Date',
value: 'ticket.due_date',
description: "Ticket's due date (relevant for tickets of type Task)",
},
{
name: 'Account',
value: 'ticket.account',
description: "This Zendesk Support's account name",
},
{
name: 'Assignee Email',
value: 'ticket.assignee.email',
description: 'Ticket assignee email (if any)',
},
{
name: 'Assignee Name',
value: 'ticket.assignee.name',
description: "Assignee's full name",
},
{
name: 'Assignee First Name',
value: 'ticket.assignee.first_name',
description: "Assignee's first name",
},
{
name: 'Assignee Last Name',
value: 'ticket.assignee.last_name',
description: "Assignee's last name",
},
{
name: 'Requester Full Name',
value: 'ticket.requester.name',
description: "Requester's full name",
},
{
name: 'Requester First Name',
value: 'ticket.requester.first_name',
description: "Requester's first name",
},
{
name: 'Requester Last Name',
value: 'ticket.requester.last_name',
description: "Requester's last name",
},
{
name: 'Requester Email',
value: 'ticket.requester.email',
description: "Requester's email",
},
{
name: 'Requester Language',
value: 'ticket.requester.language',
description: "Requester's language",
},
{
name: 'Requester Phone',
value: 'ticket.requester.phone',
description: "Requester's phone number",
},
{
name: 'Requester External ID',
value: 'ticket.requester.external_id',
description: "Requester's external ID",
},
{
name: 'Requester Field',
value: 'ticket.requester.requester_field',
description: 'Name or email',
},
{
name: 'Requester Details',
value: 'ticket.requester.details',
description: "Detailed information about the ticket's requester",
},
{
name: 'Requester Organization',
value: 'ticket.organization.name',
description: "Requester's organization",
},
{
name: "Ticket's Organization External ID",
value: 'ticket.organization.external_id',
},
{
name: 'Organization Details',
value: 'ticket.organization.details',
description: "The details about the organization of the ticket's requester",
},
{
name: 'Organization Note',
value: 'ticket.organization.notes',
description: "The notes about the organization of the ticket's requester",
},
{
name: "Ticket's CCs",
value: 'ticket.ccs',
},
{
name: "Ticket's CCs Names",
value: 'ticket.cc_names',
},
{
name: "Ticket's Tags",
value: 'ticket.tags',
},
{
name: 'Current Holiday Name',
value: 'ticket.current_holiday_name',
description: "Displays the name of the current holiday on the ticket's schedule",
},
{
name: 'Current User Name',
value: 'current_user.name',
description: 'Your full name',
},
{
name: 'Current User First Name',
value: 'current_user.first_name',
description: 'Your first name',
},
{
name: 'Current User Email',
value: 'current_user.email',
description: 'Your primary email',
},
{
name: 'Current User Organization Name',
value: 'current_user.organization.name',
description: 'Your default organization',
},
{
name: 'Current User Organization Details',
value: 'current_user.organization.details',
description: "Your default organization's details",
},
{
name: 'Current User Organization Notes',
value: 'current_user.organization.notes',
description: "Your default organization's note",
},
{
name: 'Current User Language',
value: 'current_user.language',
description: 'Your chosen language',
},
{
name: 'Current User External ID',
value: 'current_user.external_id',
description: 'Your external ID',
},
{
name: 'Current User Notes',
value: 'current_user.notes',
description: 'Your notes, stored in your profile',
},
{
name: 'Satisfaction Current Rating',
value: 'satisfaction.current_rating',
description: 'The text of the current satisfaction rating',
},
{
name: 'Satisfaction Current Comment',
value: 'satisfaction.current_comment',
description: 'The text of the current satisfaction rating comment',
},
] as INodePropertyOptions[];
@@ -0,0 +1,785 @@
import type { INodeProperties } from 'n8n-workflow';
export const userOperations: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['user'],
},
},
options: [
{
name: 'Create',
value: 'create',
description: 'Create a user',
action: 'Create a user',
},
{
name: 'Delete',
value: 'delete',
description: 'Delete a user',
action: 'Delete a user',
},
{
name: 'Get',
value: 'get',
description: 'Get a user',
action: 'Get a user',
},
{
name: 'Get Many',
value: 'getAll',
description: 'Get many users',
action: 'Get many users',
},
{
name: 'Get Organizations',
value: 'getOrganizations',
description: "Get a user's organizations",
action: "Get a user's organizations",
},
{
name: 'Get Related Data',
value: 'getRelatedData',
description: 'Get data related to the user',
action: 'Get data related to a user',
},
{
name: 'Search',
value: 'search',
description: 'Search users',
action: 'Search a user',
},
{
name: 'Update',
value: 'update',
description: 'Update a user',
action: 'Update a user',
},
],
default: 'create',
},
];
export const userFields: INodeProperties[] = [
/* -------------------------------------------------------------------------- */
/* user:create */
/* -------------------------------------------------------------------------- */
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
displayOptions: {
show: {
resource: ['user'],
operation: ['create'],
},
},
required: true,
description: "The user's name",
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: ['user'],
operation: ['create'],
},
},
options: [
{
displayName: 'Alias',
name: 'alias',
type: 'string',
default: '',
description: 'An alias displayed to end users',
},
{
displayName: 'Custom Role ID',
name: 'custom_role_id',
type: 'number',
default: 0,
description: 'A custom role if the user is an agent on the Enterprise plan',
},
{
displayName: 'Details',
name: 'details',
type: 'string',
default: '',
description: 'Any details you want to store about the user, such as an address',
},
{
displayName: 'Email',
name: 'email',
type: 'string',
placeholder: 'name@email.com',
default: '',
description: "The user's primary email address",
},
{
displayName: 'External ID',
name: 'external_id',
type: 'string',
default: '',
description: 'A unique identifier from another system',
},
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
displayName: 'Locale',
name: 'locale',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getLocales',
},
default: '',
description:
'The user\'s locale. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
},
{
displayName: 'Moderator',
name: 'moderator',
type: 'boolean',
default: false,
description: 'Whether the user has forum moderation capabilities',
},
{
displayName: 'Notes',
name: 'notes',
type: 'string',
default: '',
description: 'Any notes you want to store about the user',
},
{
displayName: 'Only Private Comments',
name: 'only_private_comments',
type: 'boolean',
default: false,
description: 'Whether the user can only create private comments',
},
{
displayName: 'Organization Name or ID',
name: 'organization_id',
typeOptions: {
loadOptionsMethod: 'getOrganizations',
},
type: 'options',
default: '',
description:
'The ID of the user\'s organization. If the user has more than one organization memberships, the ID of the user\'s default organization. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
},
{
displayName: 'Phone',
name: 'phone',
type: 'string',
default: '',
description: "The user's primary phone number",
},
{
displayName: 'Report CSV',
name: 'report_csv',
type: 'boolean',
default: false,
description:
'Whether or not the user can access the CSV report on the Search tab of the Reporting page in the Support admin interface',
},
{
displayName: 'Restricted Agent',
name: 'restricted_agent',
type: 'boolean',
default: false,
description:
'Whether the agent has any restrictions; false for admins and unrestricted agents, true for other agents',
},
{
displayName: 'Role',
name: 'role',
type: 'options',
options: [
{
name: 'End User',
value: 'end-user',
},
{
name: 'Agent',
value: 'agent',
},
{
name: 'Admin',
value: 'admin',
},
],
default: '',
description: "The user's role",
},
{
displayName: 'Signature',
name: 'signature',
type: 'string',
default: '',
description: "The user's signature. Only agents and admins can have signatures.",
},
{
displayName: 'Suspended',
name: 'suspended',
type: 'boolean',
default: false,
description:
'Whether the agent is suspended. Tickets from suspended users are also suspended, and these users cannot sign in to the end user portal.',
},
{
displayName: 'Tag Names or IDs',
name: 'tags',
type: 'multiOptions',
typeOptions: {
loadOptionsMethod: 'getTags',
},
default: [],
description:
'The array of tags applied to this user. Choose from the list, or specify IDs using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
},
{
displayName: 'Ticket Restriction',
name: 'ticket_restriction',
type: 'options',
options: [
{
name: 'Organization',
value: 'organization',
},
{
name: 'Groups',
value: 'groups',
},
{
name: 'Assigned',
value: 'assigned',
},
{
name: 'Requested',
value: 'requested',
},
],
default: '',
description: 'Specifies which tickets the user has access to',
},
{
displayName: 'Timezone',
name: 'time_zone',
type: 'string',
default: '',
description: "The user's time zone",
},
{
displayName: 'User Fields',
name: 'userFieldsUi',
placeholder: 'Add User Field',
description: "Values of custom fields in the user's profile",
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
default: {},
options: [
{
name: 'userFieldValues',
displayName: 'Field',
values: [
{
displayName: 'Field Name or ID',
name: 'field',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getUserFields',
},
default: '',
description:
'Name of the field to sort on. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
description: 'Value of the field',
},
],
},
],
},
{
displayName: 'Verified',
name: 'verified',
type: 'boolean',
default: false,
description: "Whether the user's primary identity is verified or not",
},
],
},
/* -------------------------------------------------------------------------- */
/* user:update */
/* -------------------------------------------------------------------------- */
{
displayName: 'User ID',
name: 'id',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: ['user'],
operation: ['update'],
},
},
},
{
displayName: 'Update Fields',
name: 'updateFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: ['user'],
operation: ['update'],
},
},
options: [
{
displayName: 'Alias',
name: 'alias',
type: 'string',
default: '',
description: 'An alias displayed to end users',
},
{
displayName: 'Custom Role ID',
name: 'custom_role_id',
type: 'number',
default: 0,
description: 'A custom role if the user is an agent on the Enterprise plan',
},
{
displayName: 'Details',
name: 'details',
type: 'string',
default: '',
description: 'Any details you want to store about the user, such as an address',
},
{
displayName: 'Email',
name: 'email',
type: 'string',
placeholder: 'name@email.com',
default: '',
description: "The user's primary email address",
},
{
displayName: 'External ID',
name: 'external_id',
type: 'string',
default: '',
description: 'A unique identifier from another system',
},
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
displayName: 'Locale',
name: 'locale',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getLocales',
},
default: '',
description:
'The user\'s locale. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
},
{
displayName: 'Moderator',
name: 'moderator',
type: 'boolean',
default: false,
description: 'Whether the user has forum moderation capabilities',
},
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
description: "The user's name",
},
{
displayName: 'Notes',
name: 'notes',
type: 'string',
default: '',
description: 'Any notes you want to store about the user',
},
{
displayName: 'Only Private Comments',
name: 'only_private_comments',
type: 'boolean',
default: false,
description: 'Whether the user can only create private comments',
},
{
displayName: 'Organization Name or ID',
name: 'organization_id',
typeOptions: {
loadOptionsMethod: 'getOrganizations',
},
type: 'options',
default: '',
description:
'The ID of the user\'s organization. If the user has more than one organization memberships, the ID of the user\'s default organization. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
},
{
displayName: 'Phone',
name: 'phone',
type: 'string',
default: '',
description: "The user's primary phone number",
},
{
displayName: 'Report CSV',
name: 'report_csv',
type: 'boolean',
default: false,
description:
'Whether or not the user can access the CSV report on the Search tab of the Reporting page in the Support admin interface',
},
{
displayName: 'Restricted Agent',
name: 'restricted_agent',
type: 'boolean',
default: false,
description:
'Whether the agent has any restrictions; false for admins and unrestricted agents, true for other agents',
},
{
displayName: 'Role',
name: 'role',
type: 'options',
options: [
{
name: 'End User',
value: 'end-user',
},
{
name: 'Agent',
value: 'agent',
},
{
name: 'Admin',
value: 'admin',
},
],
default: '',
description: "The user's role",
},
{
displayName: 'Signature',
name: 'signature',
type: 'string',
default: '',
description: "The user's signature. Only agents and admins can have signatures.",
},
{
displayName: 'Suspended',
name: 'suspended',
type: 'boolean',
default: false,
description:
'Whether the agent is suspended. Tickets from suspended users are also suspended, and these users cannot sign in to the end user portal.',
},
{
displayName: 'Tag Names or IDs',
name: 'tags',
type: 'multiOptions',
typeOptions: {
loadOptionsMethod: 'getTags',
},
default: [],
description:
'The array of tags applied to this user. Choose from the list, or specify IDs using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
},
{
displayName: 'Ticket Restriction',
name: 'ticket_restriction',
type: 'options',
options: [
{
name: 'Organization',
value: 'organization',
},
{
name: 'Groups',
value: 'groups',
},
{
name: 'Assigned',
value: 'assigned',
},
{
name: 'Requested',
value: 'requested',
},
],
default: '',
description: 'Specifies which tickets the user has access to',
},
{
displayName: 'Timezone',
name: 'time_zone',
type: 'string',
default: '',
description: "The user's time zone",
},
{
displayName: 'User Fields',
name: 'userFieldsUi',
placeholder: 'Add User Field',
description: "Values of custom fields in the user's profile",
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
default: {},
options: [
{
name: 'userFieldValues',
displayName: 'Field',
values: [
{
displayName: 'Field Name or ID',
name: 'field',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getUserFields',
},
default: '',
description:
'Name of the field to sort on. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
description: 'Value of the field',
},
],
},
],
},
{
displayName: 'Verified',
name: 'verified',
type: 'boolean',
default: false,
description: "Whether the user's primary identity is verified or not",
},
],
},
/* -------------------------------------------------------------------------- */
/* user:get */
/* -------------------------------------------------------------------------- */
{
displayName: 'User ID',
name: 'id',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: ['user'],
operation: ['get'],
},
},
},
/* -------------------------------------------------------------------------- */
/* user:getAll */
/* -------------------------------------------------------------------------- */
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
resource: ['user'],
operation: ['getAll'],
},
},
default: false,
description: 'Whether to return all results or only up to a given limit',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
displayOptions: {
show: {
resource: ['user'],
operation: ['getAll'],
returnAll: [false],
},
},
typeOptions: {
minValue: 1,
maxValue: 100,
},
default: 100,
description: 'Max number of results to return',
},
{
displayName: 'Filters',
name: 'filters',
type: 'collection',
placeholder: 'Add Filter',
default: {},
displayOptions: {
show: {
resource: ['user'],
operation: ['getAll'],
},
},
options: [
{
displayName: 'Roles',
name: 'role',
type: 'multiOptions',
options: [
{
name: 'End User',
value: 'end-user',
},
{
name: 'Agent',
value: 'agent',
},
{
name: 'Admin',
value: 'admin',
},
],
default: [],
},
],
},
/* -------------------------------------------------------------------------- */
/* user:search */
/* -------------------------------------------------------------------------- */
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
resource: ['user'],
operation: ['search'],
},
},
default: false,
description: 'Whether to return all results or only up to a given limit',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
displayOptions: {
show: {
resource: ['user'],
operation: ['search'],
returnAll: [false],
},
},
typeOptions: {
minValue: 1,
maxValue: 100,
},
default: 100,
description: 'Max number of results to return',
},
{
displayName: 'Filters',
name: 'filters',
type: 'collection',
placeholder: 'Add Filter',
default: {},
displayOptions: {
show: {
resource: ['user'],
operation: ['search'],
},
},
options: [
{
displayName: 'Query',
name: 'query',
type: 'string',
default: '',
},
{
displayName: 'External ID',
name: 'external_id',
type: 'string',
default: '',
},
],
},
/* -------------------------------------------------------------------------- */
/* user:delete */
/* -------------------------------------------------------------------------- */
{
displayName: 'User ID',
name: 'id',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: ['user'],
operation: ['delete'],
},
},
},
/* -------------------------------------------------------------------------- */
/* user:getRelatedData */
/* -------------------------------------------------------------------------- */
{
displayName: 'User ID',
name: 'id',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: ['user'],
operation: ['getRelatedData'],
},
},
},
/* -------------------------------------------------------------------------- */
/* user:getOrganizations */
/* -------------------------------------------------------------------------- */
{
displayName: 'User ID',
name: 'id',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
resource: ['user'],
operation: ['getOrganizations'],
},
},
},
];
@@ -0,0 +1,25 @@
{
"node": "n8n-nodes-base.zendesk",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": ["Communication"],
"resources": {
"credentialDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/credentials/zendesk/"
}
],
"primaryDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.zendesk/"
}
],
"generic": [
{
"label": "Hey founders! Your business doesn't need you to operate",
"icon": " 🖥️",
"url": "https://n8n.io/blog/your-business-doesnt-need-you-to-operate/"
}
]
}
}
@@ -0,0 +1,800 @@
import type {
IExecuteFunctions,
IDataObject,
ILoadOptionsFunctions,
INodeExecutionData,
INodePropertyOptions,
INodeType,
INodeTypeDescription,
JsonObject,
} from 'n8n-workflow';
import { NodeApiError, NodeOperationError, NodeConnectionTypes } from 'n8n-workflow';
import { validateJSON, zendeskApiRequest, zendeskApiRequestAllItems } from './GenericFunctions';
import { organizationFields, organizationOperations } from './OrganizationDescription';
import { ticketFields, ticketOperations } from './TicketDescription';
import { ticketFieldFields, ticketFieldOperations } from './TicketFieldDescription';
import type { IComment, ITicket } from './TicketInterface';
import { userFields, userOperations } from './UserDescription';
export class Zendesk implements INodeType {
description: INodeTypeDescription = {
displayName: 'Zendesk',
name: 'zendesk',
icon: 'file:zendesk.svg',
group: ['output'],
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Consume Zendesk API',
defaults: {
name: 'Zendesk',
},
usableAsTool: true,
inputs: [NodeConnectionTypes.Main],
outputs: [NodeConnectionTypes.Main],
credentials: [
{
name: 'zendeskApi',
required: true,
displayOptions: {
show: {
authentication: ['apiToken'],
},
},
},
{
name: 'zendeskOAuth2Api',
required: true,
displayOptions: {
show: {
authentication: ['oAuth2'],
},
},
},
],
properties: [
{
displayName: 'Authentication',
name: 'authentication',
type: 'options',
options: [
{
name: 'API Token',
value: 'apiToken',
},
{
name: 'OAuth2',
value: 'oAuth2',
},
],
default: 'apiToken',
},
{
displayName: 'Resource',
name: 'resource',
type: 'options',
noDataExpression: true,
options: [
{
name: 'Ticket',
value: 'ticket',
description:
'Tickets are the means through which your end users (customers) communicate with agents in Zendesk Support',
},
{
name: 'Ticket Field',
value: 'ticketField',
description: 'Manage system and custom ticket fields',
},
{
name: 'User',
value: 'user',
description: 'Manage users',
},
{
name: 'Organization',
value: 'organization',
description: 'Manage organizations',
},
],
default: 'ticket',
},
// TICKET
...ticketOperations,
...ticketFields,
// TICKET FIELD
...ticketFieldOperations,
...ticketFieldFields,
// USER
...userOperations,
...userFields,
// ORGANIZATION
...organizationOperations,
...organizationFields,
],
};
methods = {
loadOptions: {
// Get all the custom fields to display them to user so that they can
// select them easily
async getCustomFields(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const customFields = [
'text',
'textarea',
'date',
'integer',
'decimal',
'regexp',
'multiselect',
'tagger',
];
const fields = await zendeskApiRequestAllItems.call(
this,
'ticket_fields',
'GET',
'/ticket_fields',
);
for (const field of fields) {
if (customFields.includes(field.type as string)) {
const fieldName = field.title;
const fieldId = field.id;
returnData.push({
name: fieldName,
value: fieldId,
});
}
}
return returnData;
},
// Get all the groups to display them to user so that they can
// select them easily
async getGroups(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const groups = await zendeskApiRequestAllItems.call(this, 'groups', 'GET', '/groups');
for (const group of groups) {
const groupName = group.name;
const groupId = group.id;
returnData.push({
name: groupName,
value: groupId,
});
}
return returnData;
},
// Get all the tags to display them to user so that they can
// select them easily
async getTags(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const tags = await zendeskApiRequestAllItems.call(this, 'tags', 'GET', '/tags');
for (const tag of tags) {
const tagName = tag.name;
const tagId = tag.name;
returnData.push({
name: tagName,
value: tagId,
});
}
return returnData;
},
// Get all the locales to display them to user so that they can
// select them easily
async getLocales(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const locales = await zendeskApiRequestAllItems.call(this, 'locales', 'GET', '/locales');
for (const locale of locales) {
const localeName = `${locale.locale} - ${locale.name}`;
const localeId = locale.locale;
returnData.push({
name: localeName,
value: localeId,
});
}
return returnData;
},
// Get all the user fields to display them to user so that they can
// select them easily
async getUserFields(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const fields = await zendeskApiRequestAllItems.call(
this,
'user_fields',
'GET',
'/user_fields',
);
for (const field of fields) {
const fieldName = field.title;
const fieldId = field.key;
returnData.push({
name: fieldName,
value: fieldId,
});
}
return returnData;
},
// Get all the organization fields to display them to the user for easy selection
async getOrganizationFields(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const fields = await zendeskApiRequestAllItems.call(
this,
'organization_fields',
'GET',
'/organization_fields',
);
for (const field of fields) {
const fieldName = field.title;
const fieldId = field.key;
returnData.push({
name: fieldName,
value: fieldId,
});
}
return returnData;
},
async getOrganizations(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const fields = await zendeskApiRequestAllItems.call(
this,
'organizations',
'GET',
'/organizations',
{},
{},
);
for (const field of fields) {
returnData.push({
name: field.name,
value: field.id,
});
}
return returnData;
},
},
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: INodeExecutionData[] = [];
const length = items.length;
const qs: IDataObject = {};
let responseData;
for (let i = 0; i < length; i++) {
try {
const resource = this.getNodeParameter('resource', 0);
const operation = this.getNodeParameter('operation', 0);
//https://developer.zendesk.com/api-reference/ticketing/introduction/
if (resource === 'ticket') {
//https://developer.zendesk.com/rest_api/docs/support/tickets
if (operation === 'create') {
const description = this.getNodeParameter('description', i) as string;
const jsonParameters = this.getNodeParameter('jsonParameters', i);
const comment: IComment = {
body: description,
};
const body: ITicket = {
comment,
};
if (jsonParameters) {
const additionalFieldsJson = this.getNodeParameter(
'additionalFieldsJson',
i,
) as string;
if (additionalFieldsJson !== '') {
if (validateJSON(additionalFieldsJson) !== undefined) {
Object.assign(body, JSON.parse(additionalFieldsJson));
} else {
throw new NodeOperationError(
this.getNode(),
'Additional fields must be a valid JSON',
{ itemIndex: i },
);
}
}
} else {
const additionalFields = this.getNodeParameter('additionalFields', i);
if (additionalFields.type) {
body.type = additionalFields.type as string;
}
if (additionalFields.externalId) {
body.external_id = additionalFields.externalId as string;
}
if (additionalFields.subject) {
body.subject = additionalFields.subject as string;
}
if (additionalFields.status) {
body.status = additionalFields.status as string;
}
if (additionalFields.recipient) {
body.recipient = additionalFields.recipient as string;
}
if (additionalFields.group) {
body.group_id = additionalFields.group as number;
}
if (additionalFields.tags) {
body.tags = additionalFields.tags as string[];
}
if (additionalFields.customFieldsUi) {
body.custom_fields = (additionalFields.customFieldsUi as IDataObject)
.customFieldsValues as IDataObject[];
}
}
responseData = await zendeskApiRequest.call(this, 'POST', '/tickets', { ticket: body });
responseData = responseData.ticket;
}
//https://developer.zendesk.com/rest_api/docs/support/tickets#update-ticket
if (operation === 'update') {
const ticketId = this.getNodeParameter('id', i) as string;
const jsonParameters = this.getNodeParameter('jsonParameters', i);
const body: ITicket = {};
if (jsonParameters) {
const updateFieldsJson = this.getNodeParameter('updateFieldsJson', i) as string;
if (updateFieldsJson !== '') {
if (validateJSON(updateFieldsJson) !== undefined) {
Object.assign(body, JSON.parse(updateFieldsJson));
} else {
throw new NodeOperationError(
this.getNode(),
'Additional fields must be a valid JSON',
{ itemIndex: i },
);
}
}
} else {
const updateFields = this.getNodeParameter('updateFields', i);
if (updateFields.type) {
body.type = updateFields.type as string;
}
if (updateFields.externalId) {
body.external_id = updateFields.externalId as string;
}
if (updateFields.subject) {
body.subject = updateFields.subject as string;
}
if (updateFields.status) {
body.status = updateFields.status as string;
}
if (updateFields.recipient) {
body.recipient = updateFields.recipient as string;
}
if (updateFields.group) {
body.group_id = updateFields.group as number;
}
if (updateFields.tags) {
body.tags = updateFields.tags as string[];
}
if (updateFields.customFieldsUi) {
body.custom_fields = (updateFields.customFieldsUi as IDataObject)
.customFieldsValues as IDataObject[];
}
if (updateFields.assigneeEmail) {
body.assignee_email = updateFields.assigneeEmail as string;
}
if (updateFields.internalNote) {
const comment: IComment = {
html_body: updateFields.internalNote as string,
public: false,
};
body.comment = comment;
}
if (updateFields.publicReply) {
const comment: IComment = {
body: updateFields.publicReply as string,
public: true,
};
body.comment = comment;
}
}
responseData = await zendeskApiRequest.call(this, 'PUT', `/tickets/${ticketId}`, {
ticket: body,
});
responseData = responseData.ticket;
}
//https://developer.zendesk.com/rest_api/docs/support/tickets#show-ticket
//https://developer.zendesk.com/api-reference/ticketing/tickets/suspended_tickets/#show-suspended-ticket
if (operation === 'get') {
const ticketType = this.getNodeParameter('ticketType', i) as string;
const ticketId = this.getNodeParameter('id', i) as string;
const endpoint =
ticketType === 'regular' ? `/tickets/${ticketId}` : `/suspended_tickets/${ticketId}`;
responseData = await zendeskApiRequest.call(this, 'GET', endpoint, {});
responseData = responseData.ticket || responseData.suspended_ticket;
}
//https://developer.zendesk.com/rest_api/docs/support/search#list-search-results
//https://developer.zendesk.com/api-reference/ticketing/tickets/suspended_tickets/#list-suspended-tickets
if (operation === 'getAll') {
const ticketType = this.getNodeParameter('ticketType', i) as string;
const returnAll = this.getNodeParameter('returnAll', i);
const options = this.getNodeParameter('options', i);
qs.query = 'type:ticket';
if (options.query) {
qs.query += ` ${options.query}`;
}
if (options.status) {
qs.query += ` status:${options.status}`;
}
if (options.group) {
qs.query += ` group:${options.group}`;
}
if (options.sortBy) {
qs.sort_by = options.sortBy;
}
if (options.sortOrder) {
qs.sort_order = options.sortOrder;
}
const endpoint = ticketType === 'regular' ? '/search' : '/suspended_tickets';
const property = ticketType === 'regular' ? 'results' : 'suspended_tickets';
if (returnAll) {
responseData = await zendeskApiRequestAllItems.call(
this,
property,
'GET',
endpoint,
{},
qs,
);
} else {
const limit = this.getNodeParameter('limit', i);
qs.per_page = limit;
responseData = await zendeskApiRequest.call(this, 'GET', endpoint, {}, qs);
responseData = responseData.results || responseData.suspended_tickets;
}
}
//https://developer.zendesk.com/rest_api/docs/support/tickets#delete-ticket
//https://developer.zendesk.com/api-reference/ticketing/tickets/suspended_tickets/#delete-suspended-ticket
if (operation === 'delete') {
const ticketType = this.getNodeParameter('ticketType', i) as string;
const ticketId = this.getNodeParameter('id', i) as string;
const endpoint =
ticketType === 'regular' ? `/tickets/${ticketId}` : `/suspended_tickets/${ticketId}`;
responseData = await zendeskApiRequest.call(this, 'DELETE', endpoint, {});
responseData = { success: true };
}
//https://developer.zendesk.com/api-reference/ticketing/tickets/suspended_tickets/#recover-suspended-ticket
if (operation === 'recover') {
const ticketId = this.getNodeParameter('id', i) as string;
try {
responseData = await zendeskApiRequest.call(
this,
'PUT',
`/suspended_tickets/${ticketId}/recover`,
{},
);
responseData = responseData.ticket;
} catch (error) {
throw new NodeApiError(this.getNode(), error as JsonObject);
}
}
}
//https://developer.zendesk.com/api-reference/ticketing/tickets/ticket_fields/
if (resource === 'ticketField') {
//https://developer.zendesk.com/rest_api/docs/support/tickets#show-ticket
if (operation === 'get') {
const ticketFieldId = this.getNodeParameter('ticketFieldId', i) as string;
responseData = await zendeskApiRequest.call(
this,
'GET',
`/ticket_fields/${ticketFieldId}`,
{},
);
responseData = responseData.ticket_field;
}
//https://developer.zendesk.com/rest_api/docs/support/ticket_fields#list-ticket-fields
if (operation === 'getAll') {
const returnAll = this.getNodeParameter('returnAll', i);
if (returnAll) {
responseData = await zendeskApiRequestAllItems.call(
this,
'ticket_fields',
'GET',
'/ticket_fields',
{},
qs,
);
} else {
const limit = this.getNodeParameter('limit', i);
qs.limit = limit;
responseData = await zendeskApiRequestAllItems.call(
this,
'ticket_fields',
'GET',
'/ticket_fields',
{},
qs,
);
responseData = responseData.slice(0, limit);
}
}
}
//https://developer.zendesk.com/api-reference/ticketing/users/users/
if (resource === 'user') {
//https://developer.zendesk.com/api-reference/ticketing/users/users/#create-user
if (operation === 'create') {
const name = this.getNodeParameter('name', i) as string;
const additionalFields = this.getNodeParameter('additionalFields', i);
const body: IDataObject = {
name,
};
Object.assign(body, additionalFields);
if (body.userFieldsUi) {
const userFieldsUI = (body.userFieldsUi as IDataObject)
.userFieldValues as IDataObject[];
if (userFieldsUI) {
body.user_fields = {};
for (const userField of userFieldsUI) {
//@ts-ignore
body.user_fields[userField.field] = userField.value;
}
delete body.userFieldsUi;
}
}
responseData = await zendeskApiRequest.call(this, 'POST', '/users', { user: body });
responseData = responseData.user;
}
//https://developer.zendesk.com/api-reference/ticketing/users/users/#update-user
if (operation === 'update') {
const userId = this.getNodeParameter('id', i) as string;
const updateFields = this.getNodeParameter('updateFields', i);
const body: IDataObject = {};
Object.assign(body, updateFields);
if (body.userFieldsUi) {
const userFieldsUI = (body.userFieldsUi as IDataObject)
.userFieldValues as IDataObject[];
if (userFieldsUI) {
body.user_fields = {};
for (const userField of userFieldsUI) {
//@ts-ignore
body.user_fields[userField.field] = userField.value;
}
delete body.userFieldsUi;
}
}
responseData = await zendeskApiRequest.call(this, 'PUT', `/users/${userId}`, {
user: body,
});
responseData = responseData.user;
}
//https://developer.zendesk.com/api-reference/ticketing/users/users/#show-user
if (operation === 'get') {
const userId = this.getNodeParameter('id', i) as string;
responseData = await zendeskApiRequest.call(this, 'GET', `/users/${userId}`, {});
responseData = responseData.user;
}
//https://developer.zendesk.com/api-reference/ticketing/users/users/#list-users
if (operation === 'getAll') {
const returnAll = this.getNodeParameter('returnAll', i);
const options = this.getNodeParameter('filters', i);
Object.assign(qs, options);
if (returnAll) {
responseData = await zendeskApiRequestAllItems.call(
this,
'users',
'GET',
'/users',
{},
qs,
);
} else {
const limit = this.getNodeParameter('limit', i);
qs.per_page = limit;
responseData = await zendeskApiRequest.call(this, 'GET', '/users', {}, qs);
responseData = responseData.users;
}
}
//https://developer.zendesk.com/api-reference/ticketing/organizations/organizations/#list-organizations
if (operation === 'getOrganizations') {
const userId = this.getNodeParameter('id', i) as string;
responseData = await zendeskApiRequest.call(
this,
'GET',
`/users/${userId}/organizations`,
{},
);
responseData = responseData.organizations;
}
//https://developer.zendesk.com/api-reference/ticketing/users/users/#search-users
if (operation === 'search') {
const returnAll = this.getNodeParameter('returnAll', i);
const options = this.getNodeParameter('filters', i);
Object.assign(qs, options);
if (returnAll) {
responseData = await zendeskApiRequestAllItems.call(
this,
'users',
'GET',
'/users/search',
{},
qs,
);
} else {
const limit = this.getNodeParameter('limit', i);
qs.per_page = limit;
responseData = await zendeskApiRequest.call(this, 'GET', '/users/search', {}, qs);
responseData = responseData.users;
}
}
//https://developer.zendesk.com/api-reference/ticketing/users/users/#delete-user
if (operation === 'delete') {
const userId = this.getNodeParameter('id', i) as string;
responseData = await zendeskApiRequest.call(this, 'DELETE', `/users/${userId}`, {});
responseData = responseData.user;
}
//https://developer.zendesk.com/api-reference/ticketing/users/users/#show-user-related-information
if (operation === 'getRelatedData') {
const userId = this.getNodeParameter('id', i) as string;
responseData = await zendeskApiRequest.call(
this,
'GET',
`/users/${userId}/related`,
{},
);
responseData = responseData.user_related;
}
}
//https://developer.zendesk.com/api-reference/ticketing/organizations/organizations/
if (resource === 'organization') {
//https://developer.zendesk.com/api-reference/ticketing/organizations/organizations/#create-organization
if (operation === 'create') {
const name = this.getNodeParameter('name', i) as string;
const body: IDataObject & {
name: string;
organization_fields?: { [key: string]: object | string };
} = {
name,
};
const { organizationFieldsUi, ...rest } = this.getNodeParameter(
'additionalFields',
i,
) as IDataObject & {
organizationFieldsUi?: {
organizationFieldValues: Array<{ field: string; value: string }>;
};
};
Object.assign(body, rest);
if (organizationFieldsUi?.organizationFieldValues.length) {
const organizationFieldsUI = organizationFieldsUi.organizationFieldValues;
if (organizationFieldsUI.length) {
body.organization_fields = {};
for (const organizationField of organizationFieldsUI) {
body.organization_fields[organizationField.field] = organizationField.value;
}
}
}
responseData = await zendeskApiRequest.call(this, 'POST', '/organizations', {
organization: body,
});
responseData = responseData.organization;
}
//https://developer.zendesk.com/api-reference/ticketing/organizations/organizations/#delete-organization
if (operation === 'delete') {
const organizationId = this.getNodeParameter('id', i) as string;
await zendeskApiRequest.call(this, 'DELETE', `/organizations/${organizationId}`, {});
responseData = { success: true };
}
//https://developer.zendesk.com/api-reference/ticketing/organizations/organizations/#count-organizations
if (operation === 'count') {
responseData = await zendeskApiRequest.call(this, 'GET', '/organizations/count', {});
responseData = responseData.count;
}
//https://developer.zendesk.com/api-reference/ticketing/organizations/organizations/#show-organization
if (operation === 'get') {
const organizationId = this.getNodeParameter('id', i) as string;
responseData = await zendeskApiRequest.call(
this,
'GET',
`/organizations/${organizationId}`,
{},
);
responseData = responseData.organization;
}
//https://developer.zendesk.com/api-reference/ticketing/organizations/organizations/#list-organizations
if (operation === 'getAll') {
const returnAll = this.getNodeParameter('returnAll', i);
if (returnAll) {
responseData = await zendeskApiRequestAllItems.call(
this,
'organizations',
'GET',
'/organizations',
{},
qs,
);
} else {
const limit = this.getNodeParameter('limit', i);
qs.per_page = limit;
responseData = await zendeskApiRequest.call(this, 'GET', '/organizations', {}, qs);
responseData = responseData.organizations;
}
}
//https://developer.zendesk.com/api-reference/ticketing/organizations/organizations/#show-organizations-related-information
if (operation === 'getRelatedData') {
const organizationId = this.getNodeParameter('id', i) as string;
responseData = await zendeskApiRequest.call(
this,
'GET',
`/organizations/${organizationId}/related`,
{},
);
responseData = responseData.organization_related;
}
//https://developer.zendesk.com/api-reference/ticketing/organizations/organizations/#update-organization
if (operation === 'update') {
const organizationId = this.getNodeParameter('id', i) as string;
const body: IDataObject & { organization_fields?: { [key: string]: object | string } } =
{};
const { organizationFieldsUi, ...rest } = this.getNodeParameter(
'updateFields',
i,
) as IDataObject & {
organizationFieldsUi?: {
organizationFieldValues: Array<{ field: string; value: string }>;
};
};
Object.assign(body, rest);
if (organizationFieldsUi?.organizationFieldValues.length) {
const organizationFieldsUI = organizationFieldsUi.organizationFieldValues;
if (organizationFieldsUI.length) {
body.organization_fields = {};
for (const organizationField of organizationFieldsUI) {
body.organization_fields[organizationField.field] = organizationField.value;
}
}
}
responseData = await zendeskApiRequest.call(
this,
'PUT',
`/organizations/${organizationId}`,
{ organization: body },
);
responseData = responseData.organization;
}
}
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(responseData as IDataObject),
{ itemData: { item: i } },
);
returnData.push(...executionData);
} catch (error) {
if (this.continueOnFail()) {
returnData.push({ json: { error: error.message } });
continue;
}
throw error;
}
}
return [returnData];
}
}
@@ -0,0 +1,18 @@
{
"node": "n8n-nodes-base.zendeskTrigger",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": ["Communication"],
"resources": {
"credentialDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/credentials/zendesk/"
}
],
"primaryDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/trigger-nodes/n8n-nodes-base.zendesktrigger/"
}
]
}
}
@@ -0,0 +1,454 @@
import type {
IHookFunctions,
IWebhookFunctions,
IDataObject,
ILoadOptionsFunctions,
INodePropertyOptions,
INodeType,
INodeTypeDescription,
IWebhookResponseData,
} from 'n8n-workflow';
import { NodeConnectionTypes, NodeOperationError } from 'n8n-workflow';
import { conditionFields } from './ConditionDescription';
import { zendeskApiRequest, zendeskApiRequestAllItems } from './GenericFunctions';
import { triggerPlaceholders } from './TriggerPlaceholders';
import { verifySignature } from './ZendeskTriggerHelpers';
export class ZendeskTrigger implements INodeType {
description: INodeTypeDescription = {
displayName: 'Zendesk Trigger',
name: 'zendeskTrigger',
icon: 'file:zendesk.svg',
group: ['trigger'],
version: 1,
description: 'Handle Zendesk events via webhooks',
defaults: {
name: 'Zendesk Trigger',
},
inputs: [],
outputs: [NodeConnectionTypes.Main],
credentials: [
{
name: 'zendeskApi',
required: true,
displayOptions: {
show: {
authentication: ['apiToken'],
},
},
},
{
name: 'zendeskOAuth2Api',
required: true,
displayOptions: {
show: {
authentication: ['oAuth2'],
},
},
},
],
webhooks: [
{
name: 'default',
httpMethod: 'POST',
responseMode: 'onReceived',
path: 'webhook',
},
],
properties: [
{
displayName: 'Authentication',
name: 'authentication',
type: 'options',
options: [
{
name: 'API Token',
value: 'apiToken',
},
{
name: 'OAuth2',
value: 'oAuth2',
},
],
default: 'apiToken',
},
{
displayName: 'Service',
name: 'service',
type: 'options',
required: true,
options: [
{
name: 'Support',
value: 'support',
},
],
default: 'support',
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
displayOptions: {
show: {
service: ['support'],
},
},
default: {},
options: [
{
displayName: 'Field Names or IDs',
name: 'fields',
description:
'The fields to return the values of. Choose from the list, or specify IDs using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
type: 'multiOptions',
default: [],
typeOptions: {
loadOptionsMethod: 'getFields',
},
},
],
placeholder: 'Add option',
},
{
displayName: 'Conditions',
name: 'conditions',
placeholder: 'Add Condition',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
displayOptions: {
show: {
service: ['support'],
},
},
description: 'The condition to set',
default: {},
options: [
{
name: 'all',
displayName: 'All',
values: [...conditionFields],
},
{
name: 'any',
displayName: 'Any',
values: [...conditionFields],
},
],
},
],
};
methods = {
loadOptions: {
// Get all the fields to display them to user so that they can
// select them easily
async getFields(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = triggerPlaceholders;
const customFields = [
'text',
'textarea',
'date',
'integer',
'decimal',
'regexp',
'multiselect',
'tagger',
];
const fields = await zendeskApiRequestAllItems.call(
this,
'ticket_fields',
'GET',
'/ticket_fields',
);
for (const field of fields) {
if (customFields.includes(field.type as string) && field.removable && field.active) {
const fieldName = field.title;
const fieldId = field.id;
returnData.push({
name: fieldName,
value: `ticket.ticket_field_${fieldId}`,
description: `Custom field ${fieldName}`,
});
}
}
return returnData;
},
// Get all the groups to display them to user so that they can
// select them easily
async getGroups(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const groups = await zendeskApiRequestAllItems.call(this, 'groups', 'GET', '/groups');
for (const group of groups) {
const groupName = group.name;
const groupId = group.id;
returnData.push({
name: groupName,
value: groupId,
});
}
return returnData;
},
// Get all the users to display them to user so that they can
// select them easily
async getUsers(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const users = await zendeskApiRequestAllItems.call(this, 'users', 'GET', '/users');
for (const user of users) {
const userName = user.name;
const userId = user.id;
returnData.push({
name: userName,
value: userId,
});
}
returnData.push({
name: 'Current User',
value: 'current_user',
});
returnData.push({
name: 'Requester',
value: 'requester_id',
});
return returnData;
},
},
};
webhookMethods = {
default: {
async checkExists(this: IHookFunctions): Promise<boolean> {
const webhookUrl = this.getNodeWebhookUrl('default') as string;
const webhookData = this.getWorkflowStaticData('node');
const conditions = this.getNodeParameter('conditions') as IDataObject;
let endpoint = '';
const resultAll = [],
resultAny = [];
const conditionsAll = conditions.all as [IDataObject];
if (conditionsAll) {
for (const conditionAll of conditionsAll) {
const aux: IDataObject = {};
aux.field = conditionAll.field;
aux.operator = conditionAll.operation;
if (conditionAll.operation !== 'changed' && conditionAll.operation !== 'not_changed') {
aux.value = conditionAll.value;
} else {
aux.value = null;
}
resultAll.push(aux);
}
}
const conditionsAny = conditions.any as [IDataObject];
if (conditionsAny) {
for (const conditionAny of conditionsAny) {
const aux: IDataObject = {};
aux.field = conditionAny.field;
aux.operator = conditionAny.operation;
if (conditionAny.operation !== 'changed' && conditionAny.operation !== 'not_changed') {
aux.value = conditionAny.value;
} else {
aux.value = null;
}
resultAny.push(aux);
}
}
// get all webhooks
// https://developer.zendesk.com/api-reference/event-connectors/webhooks/webhooks/#list-webhooks
const { webhooks } = await zendeskApiRequest.call(this, 'GET', '/webhooks');
for (const webhook of webhooks) {
if (webhook.endpoint === webhookUrl) {
webhookData.targetId = webhook.id;
break;
}
}
// no target was found
if (webhookData.targetId === undefined) {
return false;
}
endpoint = '/triggers/active';
const triggers = await zendeskApiRequestAllItems.call(this, 'triggers', 'GET', endpoint);
for (const trigger of triggers) {
const toDeleteTriggers = [];
// this trigger belong to the current target
if (trigger.actions[0].value[0].toString() === webhookData.targetId?.toString()) {
toDeleteTriggers.push(trigger.id);
}
// delete all trigger attach to this target;
if (toDeleteTriggers.length !== 0) {
await zendeskApiRequest.call(
this,
'DELETE',
'/triggers/destroy_many',
{},
{ ids: toDeleteTriggers.join(',') },
);
}
}
return false;
},
async create(this: IHookFunctions): Promise<boolean> {
const webhookUrl = this.getNodeWebhookUrl('default') as string;
const webhookData = this.getWorkflowStaticData('node');
const service = this.getNodeParameter('service') as string;
if (service === 'support') {
const message: IDataObject = {};
const resultAll = [],
resultAny = [];
const conditions = this.getNodeParameter('conditions') as IDataObject;
const options = this.getNodeParameter('options') as IDataObject;
if (Object.keys(conditions).length === 0) {
throw new NodeOperationError(this.getNode(), 'You must have at least one condition');
}
if (options.fields) {
for (const field of options.fields as string[]) {
message[field] = `{{${field}}}`;
}
} else {
message['ticket.id'] = '{{ticket.id}}';
}
const conditionsAll = conditions.all as [IDataObject];
if (conditionsAll) {
for (const conditionAll of conditionsAll) {
const aux: IDataObject = {};
aux.field = conditionAll.field;
aux.operator = conditionAll.operation;
if (
conditionAll.operation !== 'changed' &&
conditionAll.operation !== 'not_changed'
) {
aux.value = conditionAll.value;
} else {
aux.value = null;
}
resultAll.push(aux);
}
}
const conditionsAny = conditions.any as [IDataObject];
if (conditionsAny) {
for (const conditionAny of conditionsAny) {
const aux: IDataObject = {};
aux.field = conditionAny.field;
aux.operator = conditionAny.operation;
if (
conditionAny.operation !== 'changed' &&
conditionAny.operation !== 'not_changed'
) {
aux.value = conditionAny.value;
} else {
aux.value = null;
}
resultAny.push(aux);
}
}
const urlParts = new URL(webhookUrl);
const bodyTrigger: IDataObject = {
trigger: {
title: `n8n-webhook:${urlParts.pathname}`,
conditions: {
all: resultAll,
any: resultAny,
},
actions: [
{
field: 'notification_webhook',
value: [],
},
],
},
};
const bodyTarget: IDataObject = {
webhook: {
name: 'n8n webhook',
endpoint: webhookUrl,
http_method: 'POST',
status: 'active',
request_format: 'json',
subscriptions: ['conditional_ticket_events'],
},
};
let target: IDataObject = {};
// if target id exists but trigger does not then reuse the target
// and create the trigger else create both
if (webhookData.targetId !== undefined) {
target.id = webhookData.targetId;
} else {
// create a webhook
// https://developer.zendesk.com/api-reference/event-connectors/webhooks/webhooks/#create-or-clone-webhook
target = (await zendeskApiRequest.call(this, 'POST', '/webhooks', bodyTarget))
.webhook as IDataObject;
}
// Fetch the signing secret for webhook signature verification
// https://developer.zendesk.com/api-reference/event-connectors/webhooks/webhooks/#show-webhook-signing-secret
const signingSecretResponse = (await zendeskApiRequest.call(
this,
'GET',
`/webhooks/${target.id as string}/signing_secret`,
)) as { signing_secret?: { secret?: string } };
if (signingSecretResponse.signing_secret?.secret) {
webhookData.webhookSecret = signingSecretResponse.signing_secret.secret;
}
((bodyTrigger.trigger as IDataObject).actions as IDataObject[])[0].value = [
target.id,
JSON.stringify(message),
];
const { trigger } = await zendeskApiRequest.call(this, 'POST', '/triggers', bodyTrigger);
webhookData.webhookId = trigger.id;
webhookData.targetId = target.id;
}
return true;
},
async delete(this: IHookFunctions): Promise<boolean> {
const webhookData = this.getWorkflowStaticData('node');
try {
await zendeskApiRequest.call(this, 'DELETE', `/triggers/${webhookData.webhookId}`);
await zendeskApiRequest.call(this, 'DELETE', `/webhooks/${webhookData.targetId}`);
} catch (error) {
return false;
}
delete webhookData.webhookId;
delete webhookData.targetId;
delete webhookData.webhookSecret;
return true;
},
},
};
async webhook(this: IWebhookFunctions): Promise<IWebhookResponseData> {
// Verify the webhook signature before processing
if (!verifySignature.call(this)) {
const res = this.getResponseObject();
res.status(401).send('Unauthorized').end();
return {
noWebhookResponse: true,
};
}
const req = this.getRequestObject();
return {
workflowData: [this.helpers.returnJsonArray(req.body as IDataObject)],
};
}
}
@@ -0,0 +1,72 @@
import { createHmac, timingSafeEqual } from 'crypto';
import type { IWebhookFunctions } from 'n8n-workflow';
/**
* Verifies the Zendesk webhook signature using HMAC-SHA256.
*
* Zendesk sends a signature in the `X-Zendesk-Webhook-Signature` header
* which is a base64-encoded HMAC-SHA256 digest of (timestamp + body).
*
* The timestamp is provided in the `X-Zendesk-Webhook-Signature-Timestamp` header.
*
* This function computes the expected signature using the stored webhook secret
* and compares it with the provided signature using a constant-time comparison.
*
* @returns true if signature is valid or no secret is configured, false otherwise
*/
export function verifySignature(this: IWebhookFunctions): boolean {
// Get the secret from workflow static data (set during webhook creation)
const webhookData = this.getWorkflowStaticData('node');
const webhookSecret = webhookData.webhookSecret as string | undefined;
// If no secret is configured, skip verification (backwards compatibility).
// This handles legacy webhooks created before signature verification was implemented.
// To enable verification, deactivate and reactivate the workflow.
if (!webhookSecret) {
return true;
}
const req = this.getRequestObject();
const headerData = this.getHeaderData() as Record<string, string | undefined>;
// Get the signature and timestamp from Zendesk's headers
const signature = headerData['x-zendesk-webhook-signature'];
const timestamp = headerData['x-zendesk-webhook-signature-timestamp'];
if (!signature || !timestamp) {
return false;
}
try {
// Get the raw request body
if (!req.rawBody) {
return false;
}
// Compute: base64(HMACSHA256(TIMESTAMP + BODY))
const hmac = createHmac('sha256', webhookSecret);
let rawBodyString: string;
if (Buffer.isBuffer(req.rawBody)) {
rawBodyString = req.rawBody.toString('utf8');
} else {
rawBodyString = typeof req.rawBody === 'string' ? req.rawBody : JSON.stringify(req.rawBody);
}
hmac.update(timestamp + rawBodyString);
const computedSignature = hmac.digest('base64');
// Constant-time comparison to prevent timing attacks
const computedBuffer = Buffer.from(computedSignature, 'utf8');
const providedBuffer = Buffer.from(signature, 'utf8');
// Buffers must be same length for timingSafeEqual
if (computedBuffer.length !== providedBuffer.length) {
return false;
}
return timingSafeEqual(computedBuffer, providedBuffer);
} catch {
return false;
}
}
@@ -0,0 +1,39 @@
{
"type": "object",
"properties": {
"created_at": {
"type": "string"
},
"domain_names": {
"type": "array",
"items": {
"type": "string"
}
},
"id": {
"type": "integer"
},
"name": {
"type": "string"
},
"shared_comments": {
"type": "boolean"
},
"shared_tickets": {
"type": "boolean"
},
"tags": {
"type": "array",
"items": {
"type": "string"
}
},
"updated_at": {
"type": "string"
},
"url": {
"type": "string"
}
},
"version": 1
}
@@ -0,0 +1,133 @@
{
"type": "object",
"properties": {
"allow_attachments": {
"type": "boolean"
},
"allow_channelback": {
"type": "boolean"
},
"brand_id": {
"type": "integer"
},
"collaborator_ids": {
"type": "array",
"items": {
"type": "integer"
}
},
"created_at": {
"type": "string"
},
"custom_fields": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "integer"
}
}
}
},
"custom_status_id": {
"type": "integer"
},
"description": {
"type": "string"
},
"due_at": {
"type": "null"
},
"encoded_id": {
"type": "string"
},
"fields": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "integer"
}
}
}
},
"follower_ids": {
"type": "array",
"items": {
"type": "integer"
}
},
"forum_topic_id": {
"type": "null"
},
"from_messaging_channel": {
"type": "boolean"
},
"generated_timestamp": {
"type": "integer"
},
"has_incidents": {
"type": "boolean"
},
"id": {
"type": "integer"
},
"is_public": {
"type": "boolean"
},
"problem_id": {
"type": "null"
},
"requester_id": {
"type": "integer"
},
"satisfaction_rating": {
"type": "object",
"properties": {
"score": {
"type": "string"
}
}
},
"status": {
"type": "string"
},
"submitter_id": {
"type": "integer"
},
"tags": {
"type": "array",
"items": {
"type": "string"
}
},
"ticket_form_id": {
"type": "integer"
},
"updated_at": {
"type": "string"
},
"url": {
"type": "string"
},
"via": {
"type": "object",
"properties": {
"channel": {
"type": "string"
},
"source": {
"type": "object",
"properties": {
"rel": {
"type": "null"
}
}
}
}
}
},
"version": 1
}
@@ -0,0 +1,137 @@
{
"type": "object",
"properties": {
"allow_attachments": {
"type": "boolean"
},
"allow_channelback": {
"type": "boolean"
},
"brand_id": {
"type": "integer"
},
"collaborator_ids": {
"type": "array",
"items": {
"type": "integer"
}
},
"created_at": {
"type": "string"
},
"custom_fields": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "integer"
}
}
}
},
"custom_status_id": {
"type": "integer"
},
"description": {
"type": "string"
},
"due_at": {
"type": "null"
},
"email_cc_ids": {
"type": "array",
"items": {
"type": "integer"
}
},
"encoded_id": {
"type": "string"
},
"fields": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "integer"
}
}
}
},
"follower_ids": {
"type": "array",
"items": {
"type": "integer"
}
},
"followup_ids": {
"type": "array",
"items": {
"type": "integer"
}
},
"forum_topic_id": {
"type": "null"
},
"from_messaging_channel": {
"type": "boolean"
},
"generated_timestamp": {
"type": "integer"
},
"has_incidents": {
"type": "boolean"
},
"id": {
"type": "integer"
},
"is_public": {
"type": "boolean"
},
"problem_id": {
"type": "null"
},
"requester_id": {
"type": "integer"
},
"satisfaction_rating": {
"type": "object",
"properties": {
"score": {
"type": "string"
}
}
},
"status": {
"type": "string"
},
"submitter_id": {
"type": "integer"
},
"tags": {
"type": "array",
"items": {
"type": "string"
}
},
"ticket_form_id": {
"type": "integer"
},
"updated_at": {
"type": "string"
},
"url": {
"type": "string"
},
"via": {
"type": "object",
"properties": {
"channel": {
"type": "string"
}
}
}
},
"version": 1
}
@@ -0,0 +1,134 @@
{
"type": "object",
"properties": {
"allow_attachments": {
"type": "boolean"
},
"allow_channelback": {
"type": "boolean"
},
"brand_id": {
"type": "integer"
},
"collaborator_ids": {
"type": "array",
"items": {
"type": "integer"
}
},
"created_at": {
"type": "string"
},
"custom_fields": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "integer"
}
}
}
},
"custom_status_id": {
"type": "integer"
},
"description": {
"type": "string"
},
"email_cc_ids": {
"type": "array",
"items": {
"type": "integer"
}
},
"encoded_id": {
"type": "string"
},
"fields": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "integer"
}
}
}
},
"follower_ids": {
"type": "array",
"items": {
"type": "integer"
}
},
"followup_ids": {
"type": "array",
"items": {
"type": "integer"
}
},
"forum_topic_id": {
"type": "null"
},
"from_messaging_channel": {
"type": "boolean"
},
"generated_timestamp": {
"type": "integer"
},
"has_incidents": {
"type": "boolean"
},
"id": {
"type": "integer"
},
"is_public": {
"type": "boolean"
},
"problem_id": {
"type": "null"
},
"requester_id": {
"type": "integer"
},
"result_type": {
"type": "string"
},
"satisfaction_rating": {
"type": "object",
"properties": {
"score": {
"type": "string"
}
}
},
"status": {
"type": "string"
},
"submitter_id": {
"type": "integer"
},
"tags": {
"type": "array",
"items": {
"type": "string"
}
},
"updated_at": {
"type": "string"
},
"url": {
"type": "string"
},
"via": {
"type": "object",
"properties": {
"channel": {
"type": "string"
}
}
}
},
"version": 1
}
@@ -0,0 +1,131 @@
{
"type": "object",
"properties": {
"allow_attachments": {
"type": "boolean"
},
"allow_channelback": {
"type": "boolean"
},
"brand_id": {
"type": "integer"
},
"collaborator_ids": {
"type": "array",
"items": {
"type": "integer"
}
},
"created_at": {
"type": "string"
},
"custom_fields": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "integer"
}
}
}
},
"custom_status_id": {
"type": "integer"
},
"description": {
"type": "string"
},
"due_at": {
"type": "null"
},
"email_cc_ids": {
"type": "array",
"items": {
"type": "integer"
}
},
"encoded_id": {
"type": "string"
},
"fields": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "integer"
}
}
}
},
"follower_ids": {
"type": "array",
"items": {
"type": "integer"
}
},
"forum_topic_id": {
"type": "null"
},
"from_messaging_channel": {
"type": "boolean"
},
"generated_timestamp": {
"type": "integer"
},
"has_incidents": {
"type": "boolean"
},
"id": {
"type": "integer"
},
"is_public": {
"type": "boolean"
},
"problem_id": {
"type": "null"
},
"requester_id": {
"type": "integer"
},
"satisfaction_rating": {
"type": "object",
"properties": {
"score": {
"type": "string"
}
}
},
"status": {
"type": "string"
},
"submitter_id": {
"type": "integer"
},
"tags": {
"type": "array",
"items": {
"type": "string"
}
},
"ticket_form_id": {
"type": "integer"
},
"updated_at": {
"type": "string"
},
"url": {
"type": "string"
},
"via": {
"type": "object",
"properties": {
"channel": {
"type": "string"
}
}
}
},
"version": 1
}
@@ -0,0 +1,95 @@
{
"type": "object",
"properties": {
"active": {
"type": "boolean"
},
"collapsed_for_agents": {
"type": "boolean"
},
"created_at": {
"type": "string"
},
"custom_field_options": {
"type": "array",
"items": {
"type": "object",
"properties": {
"default": {
"type": "boolean"
},
"id": {
"type": "integer"
},
"name": {
"type": "string"
},
"raw_name": {
"type": "string"
},
"value": {
"type": "string"
}
}
}
},
"description": {
"type": "string"
},
"editable_in_portal": {
"type": "boolean"
},
"id": {
"type": "integer"
},
"key": {
"type": "null"
},
"position": {
"type": "integer"
},
"raw_description": {
"type": "string"
},
"raw_title": {
"type": "string"
},
"raw_title_in_portal": {
"type": "string"
},
"regexp_for_validation": {
"type": "null"
},
"removable": {
"type": "boolean"
},
"required": {
"type": "boolean"
},
"required_in_portal": {
"type": "boolean"
},
"tag": {
"type": "null"
},
"title": {
"type": "string"
},
"title_in_portal": {
"type": "string"
},
"type": {
"type": "string"
},
"updated_at": {
"type": "string"
},
"url": {
"type": "string"
},
"visible_in_portal": {
"type": "boolean"
}
},
"version": 1
}
@@ -0,0 +1,105 @@
{
"type": "object",
"properties": {
"active": {
"type": "boolean"
},
"alias": {
"type": "null"
},
"created_at": {
"type": "string"
},
"custom_role_id": {
"type": "null"
},
"default_group_id": {
"type": "null"
},
"details": {
"type": "null"
},
"iana_time_zone": {
"type": "string"
},
"id": {
"type": "integer"
},
"last_active": {
"type": "string"
},
"last_login_at": {
"type": "null"
},
"locale": {
"type": "string"
},
"locale_id": {
"type": "integer"
},
"moderator": {
"type": "boolean"
},
"name": {
"type": "string"
},
"only_private_comments": {
"type": "boolean"
},
"organization_id": {
"type": "null"
},
"photo": {
"type": "null"
},
"report_csv": {
"type": "boolean"
},
"restricted_agent": {
"type": "boolean"
},
"role": {
"type": "string"
},
"role_type": {
"type": "null"
},
"shared": {
"type": "boolean"
},
"shared_agent": {
"type": "boolean"
},
"signature": {
"type": "null"
},
"suspended": {
"type": "boolean"
},
"tags": {
"type": "array",
"items": {
"type": "string"
}
},
"ticket_restriction": {
"type": "string"
},
"time_zone": {
"type": "string"
},
"two_factor_auth_enabled": {
"type": "null"
},
"updated_at": {
"type": "string"
},
"url": {
"type": "string"
},
"verified": {
"type": "boolean"
}
},
"version": 1
}
@@ -0,0 +1,69 @@
{
"type": "object",
"properties": {
"active": {
"type": "boolean"
},
"created_at": {
"type": "string"
},
"iana_time_zone": {
"type": "string"
},
"id": {
"type": "integer"
},
"locale": {
"type": "string"
},
"locale_id": {
"type": "integer"
},
"moderator": {
"type": "boolean"
},
"name": {
"type": "string"
},
"only_private_comments": {
"type": "boolean"
},
"report_csv": {
"type": "boolean"
},
"restricted_agent": {
"type": "boolean"
},
"role": {
"type": "string"
},
"shared": {
"type": "boolean"
},
"shared_agent": {
"type": "boolean"
},
"suspended": {
"type": "boolean"
},
"tags": {
"type": "array",
"items": {
"type": "string"
}
},
"time_zone": {
"type": "string"
},
"updated_at": {
"type": "string"
},
"url": {
"type": "string"
},
"verified": {
"type": "boolean"
}
},
"version": 1
}
@@ -0,0 +1,72 @@
{
"type": "object",
"properties": {
"active": {
"type": "boolean"
},
"created_at": {
"type": "string"
},
"iana_time_zone": {
"type": "string"
},
"id": {
"type": "integer"
},
"locale": {
"type": "string"
},
"locale_id": {
"type": "integer"
},
"moderator": {
"type": "boolean"
},
"name": {
"type": "string"
},
"only_private_comments": {
"type": "boolean"
},
"report_csv": {
"type": "boolean"
},
"restricted_agent": {
"type": "boolean"
},
"role": {
"type": "string"
},
"shared": {
"type": "boolean"
},
"shared_agent": {
"type": "boolean"
},
"suspended": {
"type": "boolean"
},
"tags": {
"type": "array",
"items": {
"type": "string"
}
},
"time_zone": {
"type": "string"
},
"two_factor_auth_enabled": {
"type": "null"
},
"updated_at": {
"type": "string"
},
"url": {
"type": "string"
},
"verified": {
"type": "boolean"
}
},
"version": 1
}
@@ -0,0 +1,21 @@
{
"type": "object",
"properties": {
"assigned_tickets": {
"type": "integer"
},
"ccd_tickets": {
"type": "integer"
},
"followed_tickets": {
"type": "integer"
},
"organization_subscriptions": {
"type": "integer"
},
"requested_tickets": {
"type": "integer"
}
},
"version": 1
}
@@ -0,0 +1,72 @@
{
"type": "object",
"properties": {
"active": {
"type": "boolean"
},
"created_at": {
"type": "string"
},
"iana_time_zone": {
"type": "string"
},
"id": {
"type": "integer"
},
"locale": {
"type": "string"
},
"locale_id": {
"type": "integer"
},
"moderator": {
"type": "boolean"
},
"name": {
"type": "string"
},
"only_private_comments": {
"type": "boolean"
},
"report_csv": {
"type": "boolean"
},
"restricted_agent": {
"type": "boolean"
},
"role": {
"type": "string"
},
"shared": {
"type": "boolean"
},
"shared_agent": {
"type": "boolean"
},
"suspended": {
"type": "boolean"
},
"tags": {
"type": "array",
"items": {
"type": "string"
}
},
"time_zone": {
"type": "string"
},
"two_factor_auth_enabled": {
"type": "null"
},
"updated_at": {
"type": "string"
},
"url": {
"type": "string"
},
"verified": {
"type": "boolean"
}
},
"version": 1
}
@@ -0,0 +1,166 @@
import { ZendeskTrigger } from '../ZendeskTrigger.node';
import * as GenericFunctions from '../GenericFunctions';
import * as ZendeskTriggerHelpers from '../ZendeskTriggerHelpers';
describe('ZendeskTrigger Node', () => {
describe('create webhook method', () => {
let mockThis: any;
let webhookData: Record<string, any>;
beforeEach(() => {
webhookData = {};
mockThis = {
getNodeWebhookUrl: () => 'https://example.com/webhook',
getNodeParameter: jest.fn().mockImplementation((name: string) => {
if (name === 'service') return 'support';
if (name === 'conditions')
return { all: [{ field: 'status', operation: 'is', value: 'open' }] };
if (name === 'options') return {};
}),
getWorkflowStaticData: () => webhookData,
getNode: () => ({}),
};
});
it('should fetch and store signing secret after creating webhook', async () => {
const createdWebhook = { id: 'webhook-123' };
const signingSecretResponse = { signing_secret: { secret: 'test-signing-secret' } };
const createdTrigger = { id: 'trigger-456' };
jest
.spyOn(GenericFunctions, 'zendeskApiRequest')
.mockResolvedValueOnce({ webhook: createdWebhook }) // POST /webhooks
.mockResolvedValueOnce(signingSecretResponse) // GET /webhooks/{id}/signing_secret
.mockResolvedValueOnce({ trigger: createdTrigger }); // POST /triggers
const trigger = new ZendeskTrigger();
const result = await trigger.webhookMethods.default.create.call(mockThis);
expect(result).toBe(true);
expect(webhookData.webhookSecret).toBe('test-signing-secret');
expect(webhookData.webhookId).toBe('trigger-456');
expect(webhookData.targetId).toBe('webhook-123');
});
it('should call signing_secret API with correct webhook ID', async () => {
const createdWebhook = { id: 'webhook-789' };
const signingSecretResponse = { signing_secret: { secret: 'secret-123' } };
const createdTrigger = { id: 'trigger-101' };
const apiRequestSpy = jest
.spyOn(GenericFunctions, 'zendeskApiRequest')
.mockResolvedValueOnce({ webhook: createdWebhook })
.mockResolvedValueOnce(signingSecretResponse)
.mockResolvedValueOnce({ trigger: createdTrigger });
const trigger = new ZendeskTrigger();
await trigger.webhookMethods.default.create.call(mockThis);
expect(apiRequestSpy).toHaveBeenCalledWith('GET', '/webhooks/webhook-789/signing_secret');
});
});
describe('delete webhook method', () => {
let webhookData: Record<string, any>;
let mockThis: any;
beforeEach(() => {
webhookData = {
webhookId: 'trigger-123',
targetId: 'webhook-456',
webhookSecret: 'test-secret',
};
mockThis = {
getWorkflowStaticData: () => webhookData,
};
});
it('should delete webhook data including secret when deletion succeeds', async () => {
jest
.spyOn(GenericFunctions, 'zendeskApiRequest')
.mockResolvedValueOnce({}) // DELETE /triggers
.mockResolvedValueOnce({}); // DELETE /webhooks
const trigger = new ZendeskTrigger();
const result = await trigger.webhookMethods.default.delete.call(mockThis);
expect(result).toBe(true);
expect(webhookData.webhookId).toBeUndefined();
expect(webhookData.targetId).toBeUndefined();
expect(webhookData.webhookSecret).toBeUndefined();
});
it('should return false when deletion fails', async () => {
jest
.spyOn(GenericFunctions, 'zendeskApiRequest')
.mockRejectedValueOnce(new Error('API error'));
const trigger = new ZendeskTrigger();
const result = await trigger.webhookMethods.default.delete.call(mockThis);
expect(result).toBe(false);
});
});
describe('webhook method', () => {
let mockThis: any;
let webhookData: Record<string, any>;
beforeEach(() => {
webhookData = {
webhookSecret: 'test-secret',
};
mockThis = {
getWorkflowStaticData: () => webhookData,
getResponseObject: jest.fn().mockReturnValue({
status: jest.fn().mockReturnThis(),
send: jest.fn().mockReturnThis(),
end: jest.fn(),
}),
getRequestObject: jest.fn().mockReturnValue({
body: { ticket: { id: '123' } },
rawBody: '{"ticket":{"id":"123"}}',
}),
getHeaderData: jest.fn().mockReturnValue({}),
helpers: {
returnJsonArray: jest.fn().mockImplementation((data) => [data]),
},
};
});
it('should reject with 401 when signature verification fails', async () => {
jest.spyOn(ZendeskTriggerHelpers, 'verifySignature').mockReturnValueOnce(false);
const trigger = new ZendeskTrigger();
const result = await trigger.webhook.call(mockThis);
expect(result).toEqual({ noWebhookResponse: true });
expect(mockThis.getResponseObject).toHaveBeenCalled();
const responseObj = mockThis.getResponseObject();
expect(responseObj.status).toHaveBeenCalledWith(401);
expect(responseObj.send).toHaveBeenCalledWith('Unauthorized');
});
it('should process webhook when signature verification succeeds', async () => {
jest.spyOn(ZendeskTriggerHelpers, 'verifySignature').mockReturnValueOnce(true);
const trigger = new ZendeskTrigger();
const result = await trigger.webhook.call(mockThis);
expect(result).toHaveProperty('workflowData');
expect(mockThis.helpers.returnJsonArray).toHaveBeenCalledWith({ ticket: { id: '123' } });
});
it('should return workflow data with the request body', async () => {
jest.spyOn(ZendeskTriggerHelpers, 'verifySignature').mockReturnValueOnce(true);
const trigger = new ZendeskTrigger();
const result = await trigger.webhook.call(mockThis);
expect(result.workflowData).toBeDefined();
expect(result.workflowData).toHaveLength(1);
});
});
});
@@ -0,0 +1,193 @@
import { createHmac } from 'crypto';
import type { IWebhookFunctions } from 'n8n-workflow';
import { verifySignature } from '../ZendeskTriggerHelpers';
describe('ZendeskTriggerHelpers', () => {
describe('verifySignature', () => {
let mockWebhookFunctions: IWebhookFunctions;
const testWebhookSecret = 'dGhpc19zZWNyZXRfaXNfZm9yX3Rlc3Rpbmdfb25seQ==';
const testBody = '{"ticket":{"id":"123","subject":"Test ticket","status":"open"}}';
const testTimestamp = '2024-01-15T10:30:00Z';
function generateValidSignature(timestamp: string, body: string, secret: string): string {
// Zendesk signature: base64(HMACSHA256(TIMESTAMP + BODY))
return createHmac('sha256', secret)
.update(timestamp + body)
.digest('base64');
}
beforeEach(() => {
mockWebhookFunctions = {
getWorkflowStaticData: jest.fn().mockReturnValue({
webhookSecret: testWebhookSecret,
}),
getRequestObject: jest.fn().mockReturnValue({
rawBody: Buffer.from(testBody),
}),
getHeaderData: jest.fn().mockReturnValue({
'x-zendesk-webhook-signature': generateValidSignature(
testTimestamp,
testBody,
testWebhookSecret,
),
'x-zendesk-webhook-signature-timestamp': testTimestamp,
}),
} as unknown as IWebhookFunctions;
});
it('should return true when no webhook secret is stored (backwards compatibility)', () => {
(mockWebhookFunctions.getWorkflowStaticData as jest.Mock).mockReturnValue({});
const result = verifySignature.call(mockWebhookFunctions);
expect(result).toBe(true);
expect(mockWebhookFunctions.getWorkflowStaticData).toHaveBeenCalledWith('node');
});
it('should return false when signature header is missing', () => {
(mockWebhookFunctions.getHeaderData as jest.Mock).mockReturnValue({
'x-zendesk-webhook-signature-timestamp': testTimestamp,
});
const result = verifySignature.call(mockWebhookFunctions);
expect(result).toBe(false);
});
it('should return false when timestamp header is missing', () => {
(mockWebhookFunctions.getHeaderData as jest.Mock).mockReturnValue({
'x-zendesk-webhook-signature': generateValidSignature(
testTimestamp,
testBody,
testWebhookSecret,
),
});
const result = verifySignature.call(mockWebhookFunctions);
expect(result).toBe(false);
});
it('should return false when rawBody is missing', () => {
(mockWebhookFunctions.getRequestObject as jest.Mock).mockReturnValue({
rawBody: undefined,
});
const result = verifySignature.call(mockWebhookFunctions);
expect(result).toBe(false);
});
it('should return true when signature is valid', () => {
const result = verifySignature.call(mockWebhookFunctions);
expect(result).toBe(true);
});
it('should return false when signature is invalid', () => {
(mockWebhookFunctions.getHeaderData as jest.Mock).mockReturnValue({
'x-zendesk-webhook-signature': 'invalid-signature',
'x-zendesk-webhook-signature-timestamp': testTimestamp,
});
const result = verifySignature.call(mockWebhookFunctions);
expect(result).toBe(false);
});
it('should return false when signature is computed with wrong secret', () => {
const wrongSecret = 'wrong_secret_key';
(mockWebhookFunctions.getHeaderData as jest.Mock).mockReturnValue({
'x-zendesk-webhook-signature': generateValidSignature(testTimestamp, testBody, wrongSecret),
'x-zendesk-webhook-signature-timestamp': testTimestamp,
});
const result = verifySignature.call(mockWebhookFunctions);
expect(result).toBe(false);
});
it('should handle Buffer rawBody correctly', () => {
const bufferBody = Buffer.from(testBody);
(mockWebhookFunctions.getRequestObject as jest.Mock).mockReturnValue({
rawBody: bufferBody,
});
const result = verifySignature.call(mockWebhookFunctions);
expect(result).toBe(true);
});
it('should handle string rawBody correctly', () => {
(mockWebhookFunctions.getRequestObject as jest.Mock).mockReturnValue({
rawBody: testBody, // String instead of Buffer
});
const result = verifySignature.call(mockWebhookFunctions);
expect(result).toBe(true);
});
it('should return false when computed and provided signatures have different lengths', () => {
(mockWebhookFunctions.getHeaderData as jest.Mock).mockReturnValue({
'x-zendesk-webhook-signature': 'short',
'x-zendesk-webhook-signature-timestamp': testTimestamp,
});
const result = verifySignature.call(mockWebhookFunctions);
expect(result).toBe(false);
});
it('should handle different timestamp formats', () => {
const differentTimestamp = '1705315800';
const validSignature = generateValidSignature(
differentTimestamp,
testBody,
testWebhookSecret,
);
(mockWebhookFunctions.getHeaderData as jest.Mock).mockReturnValue({
'x-zendesk-webhook-signature': validSignature,
'x-zendesk-webhook-signature-timestamp': differentTimestamp,
});
const result = verifySignature.call(mockWebhookFunctions);
expect(result).toBe(true);
});
it('should handle empty body', () => {
const emptyBody = '';
const validSignature = generateValidSignature(testTimestamp, emptyBody, testWebhookSecret);
(mockWebhookFunctions.getRequestObject as jest.Mock).mockReturnValue({
rawBody: Buffer.from(emptyBody),
});
(mockWebhookFunctions.getHeaderData as jest.Mock).mockReturnValue({
'x-zendesk-webhook-signature': validSignature,
'x-zendesk-webhook-signature-timestamp': testTimestamp,
});
const result = verifySignature.call(mockWebhookFunctions);
expect(result).toBe(true);
});
it('should handle complex JSON body with special characters', () => {
const complexBody =
'{"ticket":{"id":"123","subject":"Test with émojis 🎉 and spëcial chars"}}';
const validSignature = generateValidSignature(testTimestamp, complexBody, testWebhookSecret);
(mockWebhookFunctions.getRequestObject as jest.Mock).mockReturnValue({
rawBody: Buffer.from(complexBody, 'utf8'),
});
(mockWebhookFunctions.getHeaderData as jest.Mock).mockReturnValue({
'x-zendesk-webhook-signature': validSignature,
'x-zendesk-webhook-signature-timestamp': testTimestamp,
});
const result = verifySignature.call(mockWebhookFunctions);
expect(result).toBe(true);
});
});
});
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="none"><path fill="#fff" d="M0 24C0 10.745 10.745 0 24 0s24 10.745 24 24-10.745 24-24 24S0 37.255 0 24"/><path fill="#03363D" fill-rule="evenodd" d="M15.448 18.653c4.05 0 7.332-3.375 7.332-7.538H8.116c0 4.163 3.283 7.538 7.332 7.538m7.332 17.199V17.648L8.116 35.852zm2.415 0c0-4.164 3.283-7.54 7.332-7.54 4.05 0 7.332 3.376 7.332 7.54zm0-24.737v18.203L39.86 11.115z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 462 B