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,136 @@
import type { INodeProperties } from 'n8n-workflow';
export const eventOperations: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['event'],
},
},
options: [
{
name: 'Track',
value: 'track',
description: 'Record the actions a user perform',
action: 'Track an event',
},
],
default: 'track',
},
];
export const eventFields: INodeProperties[] = [
/* -------------------------------------------------------------------------- */
/* event:track */
/* -------------------------------------------------------------------------- */
{
displayName: 'Name',
name: 'name',
type: 'string',
required: true,
displayOptions: {
show: {
resource: ['event'],
operation: ['track'],
},
},
description: 'The name of the event to track',
default: '',
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: ['event'],
operation: ['track'],
},
},
options: [
{
displayName: 'Campaign ID',
name: 'campaignId',
type: 'string',
default: '',
description: 'Campaign tied to conversion',
},
{
displayName: 'Created At',
name: 'createdAt',
type: 'dateTime',
default: '',
description: 'Time event happened',
},
{
displayName: 'Data Fields',
name: 'dataFieldsUi',
type: 'fixedCollection',
default: {},
placeholder: 'Add Data Field',
typeOptions: {
multipleValues: true,
},
options: [
{
name: 'dataFieldValues',
displayName: 'Data Field',
values: [
{
displayName: 'Key',
name: 'key',
type: 'string',
default: '',
description: 'The end event specified key of the event defined data',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
description: 'The end event specified value of the event defined data',
},
],
},
],
},
{
displayName: 'Email',
name: 'email',
type: 'string',
placeholder: 'name@email.com',
default: '',
description:
'Either email or userId must be passed in to identify the user. If both are passed in, email takes precedence.',
},
{
displayName: 'ID',
name: 'id',
type: 'string',
default: '',
description:
'Optional event ID. If an event exists with that ID, the event will be updated. If none is specified, a new ID will automatically be generated and returned.',
},
{
displayName: 'Template ID',
name: 'templateId',
type: 'string',
default: '',
},
{
displayName: 'User ID',
name: 'userId',
type: 'string',
default: '',
// eslint-disable-next-line n8n-nodes-base/node-param-description-lowercase-first-char
description: 'userId that was passed into the updateUser call',
},
],
},
];
@@ -0,0 +1,66 @@
import type {
IDataObject,
IExecuteFunctions,
IHttpRequestMethods,
IHttpRequestOptions,
ILoadOptionsFunctions,
JsonObject,
} from 'n8n-workflow';
import { NodeApiError } from 'n8n-workflow';
export async function iterableApiRequest(
this: IExecuteFunctions | ILoadOptionsFunctions,
method: IHttpRequestMethods,
resource: string,
body: any = {},
qs: IDataObject = {},
uri?: string,
headers: IDataObject = {},
): Promise<any> {
const credentials = await this.getCredentials('iterableApi');
const options: IHttpRequestOptions = {
headers: {
'Content-Type': 'application/json',
},
method,
body,
qs,
url: uri || `${credentials.region}/api${resource}`,
json: true,
};
try {
if (Object.keys(headers).length !== 0) {
options.headers = Object.assign({}, options.headers, headers);
}
if (Object.keys(body as IDataObject).length === 0) {
delete options.body;
}
return await this.helpers.httpRequestWithAuthentication.call(this, 'iterableApi', options);
} catch (error) {
throw new NodeApiError(this.getNode(), error as JsonObject);
}
}
export async function iterableApiRequestAllItems(
this: IExecuteFunctions | ILoadOptionsFunctions,
propertyName: string,
method: IHttpRequestMethods,
endpoint: string,
body: any = {},
query: IDataObject = {},
): Promise<any> {
const returnData: IDataObject[] = [];
let responseData;
query.maxResults = 100;
do {
responseData = await iterableApiRequest.call(this, method, endpoint, body, query);
query.pageToken = responseData.nextPageToken;
returnData.push.apply(returnData, responseData[propertyName] as IDataObject[]);
} while (responseData.nextPageToken !== undefined && responseData.nextPageToken !== '');
return returnData;
}
@@ -0,0 +1,18 @@
{
"node": "n8n-nodes-base.iterable",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": ["Communication", "Marketing"],
"resources": {
"credentialDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/credentials/iterable/"
}
],
"primaryDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.iterable/"
}
]
}
}
@@ -0,0 +1,322 @@
import moment from 'moment-timezone';
import type {
IExecuteFunctions,
IDataObject,
ILoadOptionsFunctions,
INodeExecutionData,
INodePropertyOptions,
INodeType,
INodeTypeDescription,
JsonObject,
} from 'n8n-workflow';
import { NodeConnectionTypes, NodeApiError, NodeOperationError } from 'n8n-workflow';
import { eventFields, eventOperations } from './EventDescription';
import { iterableApiRequest } from './GenericFunctions';
import { userFields, userOperations } from './UserDescription';
import { userListFields, userListOperations } from './UserListDescription';
export class Iterable implements INodeType {
description: INodeTypeDescription = {
displayName: 'Iterable',
name: 'iterable',
// eslint-disable-next-line n8n-nodes-base/node-class-description-icon-not-svg
icon: 'file:iterable.png',
group: ['input'],
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Consume Iterable API',
defaults: {
name: 'Iterable',
},
usableAsTool: true,
inputs: [NodeConnectionTypes.Main],
outputs: [NodeConnectionTypes.Main],
credentials: [
{
name: 'iterableApi',
required: true,
},
],
properties: [
{
displayName: 'Resource',
name: 'resource',
type: 'options',
noDataExpression: true,
options: [
{
name: 'Event',
value: 'event',
},
{
name: 'User',
value: 'user',
},
{
name: 'User List',
value: 'userList',
},
],
default: 'user',
},
...eventOperations,
...eventFields,
...userOperations,
...userFields,
...userListOperations,
...userListFields,
],
};
methods = {
loadOptions: {
// Get all the lists available channels
async getLists(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const { lists } = await iterableApiRequest.call(this, 'GET', '/lists');
const returnData: INodePropertyOptions[] = [];
for (const list of lists) {
returnData.push({
name: list.name,
value: list.id,
});
}
return returnData;
},
},
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: IDataObject[] = [];
const length = items.length;
const timezone = this.getTimezone();
const qs: IDataObject = {};
let responseData;
const resource = this.getNodeParameter('resource', 0);
const operation = this.getNodeParameter('operation', 0);
if (resource === 'event') {
if (operation === 'track') {
// https://api.iterable.com/api/docs#events_trackBulk
const events = [];
for (let i = 0; i < length; i++) {
const name = this.getNodeParameter('name', i) as string;
const additionalFields = this.getNodeParameter('additionalFields', i);
if (!additionalFields.email && !additionalFields.id) {
throw new NodeOperationError(
this.getNode(),
'Either email or userId must be passed in to identify the user. Please add one of both via "Additional Fields". If both are passed in, email takes precedence.',
{ itemIndex: i },
);
}
const body: IDataObject = {
eventName: name,
};
Object.assign(body, additionalFields);
if (body.dataFieldsUi) {
const dataFields = (body.dataFieldsUi as IDataObject).dataFieldValues as IDataObject[];
const data: IDataObject = {};
for (const dataField of dataFields) {
data[dataField.key as string] = dataField.value;
}
body.dataFields = data;
delete body.dataFieldsUi;
}
if (body.createdAt) {
body.createdAt = moment.tz(body.createdAt, timezone).unix();
}
events.push(body);
}
responseData = await iterableApiRequest.call(this, 'POST', '/events/trackBulk', { events });
returnData.push(responseData as IDataObject);
}
}
if (resource === 'user') {
if (operation === 'upsert') {
// https://api.iterable.com/api/docs#users_updateUser
for (let i = 0; i < length; i++) {
const identifier = this.getNodeParameter('identifier', i) as string;
const value = this.getNodeParameter('value', i) as string;
const additionalFields = this.getNodeParameter('additionalFields', i);
const body: IDataObject = {};
if (identifier === 'email') {
body.email = value;
} else {
body.preferUserId = this.getNodeParameter('preferUserId', i) as boolean;
body.userId = value;
}
Object.assign(body, additionalFields);
if (body.dataFieldsUi) {
const dataFields = (body.dataFieldsUi as IDataObject).dataFieldValues as IDataObject[];
const data: IDataObject = {};
for (const dataField of dataFields) {
data[dataField.key as string] = dataField.value;
}
body.dataFields = data;
delete body.dataFieldsUi;
}
responseData = await iterableApiRequest.call(this, 'POST', '/users/update', body);
if (!this.continueOnFail()) {
if (responseData.code !== 'Success') {
throw new NodeOperationError(
this.getNode(),
`Iterable error response [400]: ${responseData.msg}`,
{ itemIndex: i },
);
}
}
returnData.push(responseData as IDataObject);
}
}
if (operation === 'delete') {
// https://api.iterable.com/api/docs#users_delete
// https://api.iterable.com/api/docs#users_delete_0
for (let i = 0; i < length; i++) {
const by = this.getNodeParameter('by', i) as string;
let endpoint;
if (by === 'email') {
const email = this.getNodeParameter('email', i) as string;
endpoint = `/users/${email}`;
} else {
const userId = this.getNodeParameter('userId', i) as string;
endpoint = `/users/byUserId/${userId}`;
}
responseData = await iterableApiRequest.call(this, 'DELETE', endpoint);
if (!this.continueOnFail()) {
if (responseData.code !== 'Success') {
throw new NodeApiError(this.getNode(), responseData as JsonObject);
}
}
returnData.push(responseData as IDataObject);
}
}
if (operation === 'get') {
// https://api.iterable.com/api/docs#users_getUser
// https://api.iterable.com/api/docs#users_getUserById
for (let i = 0; i < length; i++) {
const by = this.getNodeParameter('by', i) as string;
let endpoint;
if (by === 'email') {
const email = this.getNodeParameter('email', i) as string;
endpoint = '/users/getByEmail';
qs.email = email;
} else {
const userId = this.getNodeParameter('userId', i) as string;
endpoint = `/users/byUserId/${userId}`;
}
responseData = await iterableApiRequest.call(this, 'GET', endpoint, {}, qs);
if (!this.continueOnFail()) {
if (Object.keys(responseData as IDataObject).length === 0) {
throw new NodeApiError(this.getNode(), responseData as JsonObject, {
message: 'User not found',
httpCode: '404',
});
}
}
responseData = responseData.user || {};
returnData.push(responseData as IDataObject);
}
}
}
if (resource === 'userList') {
if (operation === 'add') {
//https://api.iterable.com/api/docs#lists_subscribe
const listId = this.getNodeParameter('listId', 0) as string;
const identifier = this.getNodeParameter('identifier', 0) as string;
const body: IDataObject = {
listId: parseInt(listId, 10),
subscribers: [],
};
const subscribers: IDataObject[] = [];
for (let i = 0; i < length; i++) {
const value = this.getNodeParameter('value', i) as string;
if (identifier === 'email') {
subscribers.push({ email: value });
} else {
subscribers.push({ userId: value });
}
}
body.subscribers = subscribers;
responseData = await iterableApiRequest.call(this, 'POST', '/lists/subscribe', body);
returnData.push(responseData as IDataObject);
}
if (operation === 'remove') {
//https://api.iterable.com/api/docs#lists_unsubscribe
const listId = this.getNodeParameter('listId', 0) as string;
const identifier = this.getNodeParameter('identifier', 0) as string;
const additionalFields = this.getNodeParameter('additionalFields', 0);
const body: IDataObject = {
listId: parseInt(listId, 10),
subscribers: [],
campaignId: additionalFields.campaignId as number,
channelUnsubscribe: additionalFields.channelUnsubscribe as boolean,
};
const subscribers: IDataObject[] = [];
for (let i = 0; i < length; i++) {
const value = this.getNodeParameter('value', i) as string;
if (identifier === 'email') {
subscribers.push({ email: value });
} else {
subscribers.push({ userId: value });
}
}
body.subscribers = subscribers;
responseData = await iterableApiRequest.call(this, 'POST', '/lists/unsubscribe', body);
returnData.push(responseData as IDataObject);
}
}
return [this.helpers.returnJsonArray(returnData)];
}
}
@@ -0,0 +1,267 @@
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 or Update',
value: 'upsert',
description: 'Create a new user, or update the current one if it already exists (upsert)',
action: 'Create or update 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',
},
],
default: 'upsert',
},
];
export const userFields: INodeProperties[] = [
/* -------------------------------------------------------------------------- */
/* user:upsert */
/* -------------------------------------------------------------------------- */
{
displayName: 'Identifier',
name: 'identifier',
type: 'options',
required: true,
options: [
{
name: 'Email',
value: 'email',
},
{
name: 'User ID',
value: 'userId',
},
],
displayOptions: {
show: {
resource: ['user'],
operation: ['upsert'],
},
},
default: '',
description: 'Identifier to be used',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
required: true,
displayOptions: {
show: {
resource: ['user'],
operation: ['upsert'],
},
},
default: '',
},
{
displayName: "Create If Doesn't Exist",
name: 'preferUserId',
type: 'boolean',
required: true,
displayOptions: {
show: {
resource: ['user'],
operation: ['upsert'],
identifier: ['userId'],
},
},
default: true,
description: 'Whether to create a new user if the idetifier does not exist',
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: ['user'],
operation: ['upsert'],
},
},
options: [
{
displayName: 'Data Fields',
name: 'dataFieldsUi',
type: 'fixedCollection',
default: {},
placeholder: 'Add Data Field',
typeOptions: {
multipleValues: true,
},
options: [
{
name: 'dataFieldValues',
displayName: 'Data Field',
values: [
{
displayName: 'Key',
name: 'key',
type: 'string',
default: '',
description: 'The end user specified key of the user defined data',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
description: 'The end user specified value of the user defined data',
},
],
},
],
},
{
displayName: 'Merge Nested Objects',
name: 'mergeNestedObjects',
type: 'boolean',
default: false,
description:
'Whether to merge top level objects instead of overwriting (default: false), e.g. if user profile has data: {mySettings:{mobile:true}} and change contact field has data: {mySettings:{email:true}}, the resulting profile: {mySettings:{mobile:true,email:true}}',
},
],
},
/* -------------------------------------------------------------------------- */
/* user:delete */
/* -------------------------------------------------------------------------- */
{
displayName: 'By',
name: 'by',
type: 'options',
required: true,
options: [
{
name: 'Email',
value: 'email',
},
{
name: 'User ID',
value: 'userId',
},
],
displayOptions: {
show: {
resource: ['user'],
operation: ['delete'],
},
},
default: 'email',
description: 'Identifier to be used',
},
{
displayName: 'User ID',
name: 'userId',
type: 'string',
required: true,
displayOptions: {
show: {
resource: ['user'],
operation: ['delete'],
by: ['userId'],
},
},
default: '',
description: 'Unique identifier for a particular user',
},
{
displayName: 'Email',
name: 'email',
type: 'string',
placeholder: 'name@email.com',
required: true,
displayOptions: {
show: {
resource: ['user'],
operation: ['delete'],
by: ['email'],
},
},
default: '',
description: 'Email for a particular user',
},
/* -------------------------------------------------------------------------- */
/* user:get */
/* -------------------------------------------------------------------------- */
{
displayName: 'By',
name: 'by',
type: 'options',
required: true,
options: [
{
name: 'Email',
value: 'email',
},
{
name: 'User ID',
value: 'userId',
},
],
displayOptions: {
show: {
resource: ['user'],
operation: ['get'],
},
},
default: 'email',
description: 'Identifier to be used',
},
{
displayName: 'User ID',
name: 'userId',
type: 'string',
required: true,
displayOptions: {
show: {
resource: ['user'],
operation: ['get'],
by: ['userId'],
},
},
default: '',
description: 'Unique identifier for a particular user',
},
{
displayName: 'Email',
name: 'email',
type: 'string',
placeholder: 'name@email.com',
required: true,
displayOptions: {
show: {
resource: ['user'],
operation: ['get'],
by: ['email'],
},
},
default: '',
description: 'Email for a particular user',
},
];
@@ -0,0 +1,180 @@
import type { INodeProperties } from 'n8n-workflow';
export const userListOperations: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['userList'],
},
},
options: [
{
name: 'Add',
value: 'add',
description: 'Add user to list',
action: 'Add a user to a list',
},
{
name: 'Remove',
value: 'remove',
description: 'Remove a user from a list',
action: 'Remove a user from a list',
},
],
default: 'add',
},
];
export const userListFields: INodeProperties[] = [
/* -------------------------------------------------------------------------- */
/* userList:add */
/* -------------------------------------------------------------------------- */
{
displayName: 'List Name or ID',
name: 'listId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getLists',
},
required: true,
displayOptions: {
show: {
resource: ['userList'],
operation: ['add'],
},
},
default: '',
description:
'Identifier to be used. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
},
{
displayName: 'Identifier',
name: 'identifier',
type: 'options',
required: true,
options: [
{
name: 'Email',
value: 'email',
},
{
name: 'User ID',
value: 'userId',
},
],
displayOptions: {
show: {
resource: ['userList'],
operation: ['add'],
},
},
default: '',
description: 'Identifier to be used',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
required: true,
displayOptions: {
show: {
resource: ['userList'],
operation: ['add'],
},
},
default: '',
},
/* -------------------------------------------------------------------------- */
/* userList:remove */
/* -------------------------------------------------------------------------- */
{
displayName: 'List Name or ID',
name: 'listId',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getLists',
},
required: true,
displayOptions: {
show: {
resource: ['userList'],
operation: ['remove'],
},
},
default: '',
description:
'Identifier to be used. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
},
{
displayName: 'Identifier',
name: 'identifier',
type: 'options',
required: true,
options: [
{
name: 'Email',
value: 'email',
},
{
name: 'User ID',
value: 'userId',
},
],
displayOptions: {
show: {
resource: ['userList'],
operation: ['remove'],
},
},
default: '',
description: 'Identifier to be used',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
required: true,
displayOptions: {
show: {
resource: ['userList'],
operation: ['remove'],
},
},
default: '',
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: ['userList'],
operation: ['remove'],
},
},
options: [
{
displayName: 'Campaign ID',
name: 'campaignId',
type: 'number',
default: 0,
description: 'Attribute unsubscribe to a campaign',
},
{
displayName: 'Channel Unsubscribe',
name: 'channelUnsubscribe',
type: 'boolean',
default: false,
description:
"Whether to unsubscribe email from list's associated channel - essentially a global unsubscribe",
},
],
},
];
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB