diff --git a/packages/nodes-base/credentials/LoneScaleApi.credentials.ts b/packages/nodes-base/credentials/LoneScaleApi.credentials.ts
new file mode 100644
index 0000000000..24787e347f
--- /dev/null
+++ b/packages/nodes-base/credentials/LoneScaleApi.credentials.ts
@@ -0,0 +1,38 @@
+import type {
+ IAuthenticateGeneric,
+ ICredentialTestRequest,
+ ICredentialType,
+ INodeProperties,
+} from 'n8n-workflow';
+
+export class LoneScaleApi implements ICredentialType {
+ name = 'loneScaleApi';
+
+ displayName = 'LoneScale API';
+
+ properties: INodeProperties[] = [
+ {
+ displayName: 'API Key',
+ name: 'apiKey',
+ type: 'string',
+ typeOptions: { password: true },
+ default: '',
+ },
+ ];
+
+ authenticate: IAuthenticateGeneric = {
+ type: 'generic',
+ properties: {
+ headers: {
+ Authorization: '={{$credentials.apiKey}}',
+ },
+ },
+ };
+
+ test: ICredentialTestRequest = {
+ request: {
+ baseURL: 'https://public-api.lonescale.com',
+ url: '/users',
+ },
+ };
+}
diff --git a/packages/nodes-base/nodes/LoneScale/GenericFunctions.ts b/packages/nodes-base/nodes/LoneScale/GenericFunctions.ts
new file mode 100644
index 0000000000..872e749287
--- /dev/null
+++ b/packages/nodes-base/nodes/LoneScale/GenericFunctions.ts
@@ -0,0 +1,46 @@
+import type { OptionsWithUri } from 'request';
+
+import type { IExecuteFunctions, ILoadOptionsFunctions } from 'n8n-core';
+
+import type { IDataObject, IHookFunctions, IWebhookFunctions } from 'n8n-workflow';
+import { BASE_URL } from './constants';
+
+export async function lonescaleApiRequest(
+ this: IExecuteFunctions | IWebhookFunctions | IHookFunctions | ILoadOptionsFunctions,
+ method: string,
+ resource: string,
+ body: IDataObject = {},
+ query: IDataObject = {},
+ uri?: string,
+) {
+ const endpoint = `${BASE_URL}`;
+ const credentials = await this.getCredentials('loneScaleApi');
+ const options: OptionsWithUri = {
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-API-KEY': credentials?.apiKey,
+ },
+ method,
+ body,
+ qs: query,
+ uri: uri || `${endpoint}${resource}`,
+ json: true,
+ };
+ if (!Object.keys(body).length) {
+ delete options.body;
+ }
+ if (!Object.keys(query).length) {
+ delete options.qs;
+ }
+
+ try {
+ return await this.helpers.requestWithAuthentication.call(this, 'loneScaleApi', options);
+ } catch (error) {
+ if (error.response) {
+ const errorMessage =
+ error.response.body.message || error.response.body.description || error.message;
+ throw new Error(`Autopilot error response [${error.statusCode}]: ${errorMessage}`);
+ }
+ throw error;
+ }
+}
diff --git a/packages/nodes-base/nodes/LoneScale/LoneScale.node.json b/packages/nodes-base/nodes/LoneScale/LoneScale.node.json
new file mode 100644
index 0000000000..06cc5f1531
--- /dev/null
+++ b/packages/nodes-base/nodes/LoneScale/LoneScale.node.json
@@ -0,0 +1,18 @@
+{
+ "node": "n8n-nodes-base.lonescale",
+ "nodeVersion": "1.0",
+ "codexVersion": "1.0",
+ "categories": ["Sales"],
+ "resources": {
+ "credentialDocumentation": [
+ {
+ "url": "https://docs.n8n.io/credentials/lonescale"
+ }
+ ],
+ "primaryDocumentation": [
+ {
+ "url": "https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.lonescale/"
+ }
+ ]
+ }
+}
diff --git a/packages/nodes-base/nodes/LoneScale/LoneScaleList.node.ts b/packages/nodes-base/nodes/LoneScale/LoneScaleList.node.ts
new file mode 100644
index 0000000000..daca4616a5
--- /dev/null
+++ b/packages/nodes-base/nodes/LoneScale/LoneScaleList.node.ts
@@ -0,0 +1,482 @@
+import type {
+ IDataObject,
+ IExecuteFunctions,
+ ILoadOptionsFunctions,
+ INodeExecutionData,
+ INodePropertyOptions,
+ INodeType,
+ INodeTypeDescription,
+} from 'n8n-workflow';
+
+import { lonescaleApiRequest } from './GenericFunctions';
+
+export class LoneScaleList implements INodeType {
+ description: INodeTypeDescription = {
+ displayName: 'LoneScale List',
+ name: 'loneScaleList',
+ group: ['transform'],
+ icon: 'file:lonescale-logo.svg',
+ version: 1,
+ description: 'Create List, add / delete items',
+ subtitle: '={{$parameter["resource"] + ": " + $parameter["operation"]}}',
+ defaults: {
+ name: 'LoneScale List',
+ },
+ inputs: ['main'],
+ outputs: ['main'],
+ credentials: [
+ {
+ name: 'loneScaleApi',
+ required: true,
+ },
+ ],
+ properties: [
+ {
+ displayName: 'Resource',
+ name: 'resource',
+ type: 'options',
+ options: [
+ {
+ name: 'List',
+ value: 'list',
+ description: 'Manipulate list',
+ },
+ {
+ name: 'Item',
+ value: 'item',
+ description: 'Manipulate item',
+ },
+ ],
+ default: 'list',
+ noDataExpression: true,
+ required: true,
+ description: 'Create a new list',
+ },
+ {
+ displayName: 'Operation',
+ name: 'operation',
+ type: 'options',
+ displayOptions: {
+ show: {
+ resource: ['list'],
+ },
+ },
+ options: [
+ {
+ name: 'Create',
+ value: 'create',
+ description: 'Create a list',
+ action: 'Create a list',
+ },
+ ],
+ default: 'create',
+ noDataExpression: true,
+ },
+ {
+ displayName: 'Operation',
+ name: 'operation',
+ type: 'options',
+ displayOptions: {
+ show: {
+ resource: ['item'],
+ },
+ },
+ options: [
+ {
+ name: 'Create',
+ value: 'add',
+ description: 'Create an item',
+ action: 'Create a item',
+ },
+ ],
+ default: 'add',
+ noDataExpression: true,
+ },
+ {
+ displayName: 'Type',
+ name: 'type',
+ type: 'options',
+ required: true,
+ displayOptions: {
+ show: {
+ resource: ['item'],
+ },
+ },
+ options: [
+ {
+ name: 'Company',
+ value: 'COMPANY',
+ description: 'List of company',
+ },
+ {
+ name: 'Contact',
+ value: 'PEOPLE',
+ description: 'List of contact',
+ },
+ ],
+ default: 'PEOPLE',
+ description: 'Type of your list',
+ noDataExpression: true,
+ },
+ {
+ displayName: 'List Name or ID',
+ name: 'list',
+ type: 'options',
+ displayOptions: {
+ show: {
+ resource: ['item'],
+ },
+ },
+ typeOptions: {
+ loadOptionsMethod: 'getLists',
+ loadOptionsDependsOn: ['type'],
+ },
+ default: '',
+ description:
+ 'Choose from the list, or specify an ID using an expression',
+ required: true,
+ },
+ {
+ displayName: 'First Name',
+ name: 'first_name',
+ type: 'string',
+ displayOptions: {
+ show: {
+ operation: ['add'],
+ resource: ['item'],
+ type: ['PEOPLE'],
+ },
+ },
+ default: '',
+ description: 'Contact first name',
+ required: true,
+ },
+ {
+ displayName: 'Last Name',
+ name: 'last_name',
+ type: 'string',
+ displayOptions: {
+ show: {
+ operation: ['add'],
+ resource: ['item'],
+ type: ['PEOPLE'],
+ },
+ },
+ default: '',
+ description: 'Contact last name',
+ required: true,
+ },
+
+ {
+ displayName: 'Company Name',
+ name: 'company_name',
+ type: 'string',
+ displayOptions: {
+ show: {
+ operation: ['add'],
+ resource: ['item'],
+ type: ['COMPANY'],
+ },
+ },
+ default: '',
+ description: 'Contact company name',
+ },
+
+ {
+ displayName: 'Additional Fields',
+ name: 'peopleAdditionalFields',
+ type: 'collection',
+ placeholder: 'Add Field',
+ default: {},
+ displayOptions: {
+ show: {
+ operation: ['add'],
+ resource: ['item'],
+ type: ['PEOPLE'],
+ },
+ },
+ options: [
+ {
+ displayName: 'Full Name',
+ name: 'full_name',
+ type: 'string',
+ default: '',
+ description: 'Contact full name',
+ },
+ {
+ displayName: 'Contact Email',
+ name: 'email',
+ type: 'string',
+ placeholder: 'name@email.com',
+ default: '',
+ description: 'Contact email',
+ },
+ {
+ displayName: 'Company Name',
+ name: 'company_name',
+ type: 'string',
+ default: '',
+ description: 'Contact company name',
+ },
+ {
+ displayName: 'Current Position',
+ name: 'current_position',
+ type: 'string',
+ default: '',
+ description: 'Contact current position',
+ },
+ {
+ displayName: 'Company Domain',
+ name: 'domain',
+ type: 'string',
+ default: '',
+ description: 'Contact company domain',
+ },
+ {
+ displayName: 'Linkedin Url',
+ name: 'linkedin_url',
+ type: 'string',
+ default: '',
+ description: 'Contact Linkedin URL',
+ },
+ {
+ displayName: 'Contact Location',
+ name: 'location',
+ type: 'string',
+ default: '',
+ },
+ {
+ displayName: 'Contact ID',
+ name: 'contact_id',
+ type: 'string',
+ default: '',
+ description: 'Contact ID from your source',
+ },
+ ],
+ },
+ {
+ displayName: 'Additional Fields',
+ name: 'companyAdditionalFields',
+ type: 'collection',
+ placeholder: 'Add Field',
+ default: {},
+ displayOptions: {
+ show: {
+ operation: ['add'],
+ resource: ['item'],
+ type: ['COMPANY'],
+ },
+ },
+ options: [
+ {
+ displayName: 'Linkedin Url',
+ name: 'linkedin_url',
+ type: 'string',
+ default: '',
+ description: 'Company Linkedin URL',
+ },
+ {
+ displayName: 'Company Domain',
+ name: 'domain',
+ type: 'string',
+ default: '',
+ description: 'Company company domain',
+ },
+ {
+ displayName: 'Contact Location',
+ name: 'location',
+ type: 'string',
+ default: '',
+ },
+ {
+ displayName: 'Contact ID',
+ name: 'contact_id',
+ type: 'string',
+ default: '',
+ description: 'Contact ID from your source',
+ },
+ ],
+ },
+ {
+ displayName: 'Name',
+ name: 'name',
+ type: 'string',
+ required: true,
+ displayOptions: {
+ show: {
+ operation: ['create'],
+ resource: ['list'],
+ },
+ },
+ default: '',
+ placeholder: 'list name',
+ description: 'Name of your list',
+ },
+ {
+ displayName: 'Type',
+ name: 'type',
+ type: 'options',
+ required: true,
+ displayOptions: {
+ show: {
+ operation: ['create'],
+ resource: ['list'],
+ },
+ },
+ options: [
+ {
+ name: 'Company',
+ value: 'COMPANY',
+ description: 'Create a list of companies',
+ action: 'Create a list of companies',
+ },
+ {
+ name: 'Contact',
+ value: 'PEOPLE',
+ description: 'Create a list of contacts',
+ action: 'Create a list of contacts',
+ },
+ ],
+ default: 'COMPANY',
+ description: 'Type of your list',
+ noDataExpression: true,
+ },
+ ],
+ };
+
+ methods = {
+ loadOptions: {
+ async getLists(this: ILoadOptionsFunctions): Promise {
+ const type = this.getNodeParameter('type') as string;
+ const data = await lonescaleApiRequest.call(this, 'GET', '/lists', {}, { entity: type });
+ return (data as { list: Array<{ name: string; id: string; entity: string }> })?.list
+ ?.filter((l) => l.entity === type)
+ .map((d) => ({
+ name: d.name,
+ value: d.id,
+ }));
+ },
+ },
+ };
+
+ async execute(this: IExecuteFunctions): Promise {
+ const items = this.getInputData();
+ let responseData;
+ const returnData = [];
+ const resource = this.getNodeParameter('resource', 0);
+ const operation = this.getNodeParameter('operation', 0);
+
+ for (let i = 0; i < items.length; i++) {
+ try {
+ if (resource === 'list') {
+ if (operation === 'create') {
+ const name = this.getNodeParameter('name', i) as string;
+ const entity = this.getNodeParameter('type', i) as string;
+ const body: IDataObject = {
+ name,
+ entity,
+ };
+
+ responseData = await lonescaleApiRequest.call(this, 'POST', '/lists', body);
+ const executionData = this.helpers.constructExecutionMetaData(
+ this.helpers.returnJsonArray(responseData),
+ { itemData: { item: i } },
+ );
+ returnData.push(...executionData);
+ }
+ }
+ if (resource === 'item') {
+ if (operation === 'add') {
+ let firstName = '';
+ let lastName = '';
+ let currentPosition = '';
+ let fullName = '';
+ let email = '';
+ let linkedinUrl = '';
+ let companyName = '';
+ let domain = '';
+ let location = '';
+ let contactId = '';
+
+ const entity = this.getNodeParameter('type', i) as string;
+ const listId = this.getNodeParameter('list', i) as string;
+ if (entity === 'PEOPLE') {
+ const peopleAdditionalFields = this.getNodeParameter('peopleAdditionalFields', i) as {
+ email: string;
+ full_name: string;
+ current_position: string;
+ linkedin_url: string;
+ company_name: string;
+ domain: string;
+ location: string;
+ contact_id: string;
+ };
+ firstName = this.getNodeParameter('first_name', i) as string;
+ lastName = this.getNodeParameter('last_name', i) as string;
+ fullName = peopleAdditionalFields?.full_name;
+ currentPosition = peopleAdditionalFields?.current_position;
+ email = peopleAdditionalFields?.email;
+ linkedinUrl = peopleAdditionalFields?.linkedin_url;
+ companyName = peopleAdditionalFields?.company_name;
+ domain = peopleAdditionalFields?.domain;
+ location = peopleAdditionalFields?.location;
+ contactId = peopleAdditionalFields?.contact_id;
+ }
+ if (entity === 'COMPANY') {
+ const companyAdditionalFields = this.getNodeParameter(
+ 'companyAdditionalFields',
+ i,
+ ) as {
+ linkedin_url: string;
+ domain: string;
+ location: string;
+ contact_id: string;
+ };
+ companyName = this.getNodeParameter('company_name', i) as string;
+ linkedinUrl = companyAdditionalFields?.linkedin_url;
+ domain = companyAdditionalFields?.domain;
+ location = companyAdditionalFields?.location;
+ contactId = companyAdditionalFields?.contact_id;
+ }
+
+ const body: IDataObject = {
+ ...(firstName && { first_name: firstName }),
+ ...(lastName && { last_name: lastName }),
+ ...(fullName && { full_name: fullName }),
+ ...(linkedinUrl && { linkedin_url: linkedinUrl }),
+ ...(companyName && { company_name: companyName }),
+ ...(currentPosition && { current_position: currentPosition }),
+ ...(domain && { domain }),
+ ...(location && { location }),
+ ...(email && { email }),
+ ...(contactId && { contact_id: contactId }),
+ };
+
+ responseData = await lonescaleApiRequest.call(
+ this,
+ 'POST',
+ `/lists/${listId}/item`,
+ body,
+ );
+ const executionData = this.helpers.constructExecutionMetaData(
+ this.helpers.returnJsonArray(responseData),
+ { itemData: { item: i } },
+ );
+ returnData.push(...executionData);
+ }
+ }
+ } catch (error) {
+ if (this.continueOnFail()) {
+ const executionData = this.helpers.constructExecutionMetaData(
+ this.helpers.returnJsonArray({ error: error.message }),
+ { itemData: { item: i } },
+ );
+ returnData.push(...executionData);
+ continue;
+ }
+ throw error;
+ }
+ }
+ return this.prepareOutputData(returnData);
+ }
+}
diff --git a/packages/nodes-base/nodes/LoneScale/LoneScaleTrigger.node.json b/packages/nodes-base/nodes/LoneScale/LoneScaleTrigger.node.json
new file mode 100644
index 0000000000..fa23dc1452
--- /dev/null
+++ b/packages/nodes-base/nodes/LoneScale/LoneScaleTrigger.node.json
@@ -0,0 +1,18 @@
+{
+ "node": "n8n-nodes-base.lonescaleTrigger",
+ "nodeVersion": "1.0",
+ "codexVersion": "1.0",
+ "categories": ["Sales"],
+ "resources": {
+ "credentialDocumentation": [
+ {
+ "url": "https://docs.n8n.io/credentials/lonescale"
+ }
+ ],
+ "primaryDocumentation": [
+ {
+ "url": "https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.lonescaletrigger/"
+ }
+ ]
+ }
+}
diff --git a/packages/nodes-base/nodes/LoneScale/LoneScaleTrigger.node.ts b/packages/nodes-base/nodes/LoneScale/LoneScaleTrigger.node.ts
new file mode 100644
index 0000000000..a6d89a6f44
--- /dev/null
+++ b/packages/nodes-base/nodes/LoneScale/LoneScaleTrigger.node.ts
@@ -0,0 +1,131 @@
+import type { IWebhookFunctions } from 'n8n-core';
+
+import type {
+ IDataObject,
+ IHookFunctions,
+ ILoadOptionsFunctions,
+ INodePropertyOptions,
+ INodeType,
+ INodeTypeDescription,
+ IWebhookResponseData,
+} from 'n8n-workflow';
+
+import { lonescaleApiRequest } from './GenericFunctions';
+
+export class LoneScaleTrigger implements INodeType {
+ description: INodeTypeDescription = {
+ displayName: 'LoneScale Trigger',
+ name: 'loneScaleTrigger',
+ icon: 'file:lonescale-logo.svg',
+ group: ['trigger'],
+ version: 1,
+ description: 'Trigger LoneScale Workflow',
+ defaults: {
+ name: 'LoneScale Trigger',
+ },
+ inputs: [],
+ outputs: ['main'],
+ credentials: [
+ {
+ name: 'loneScaleApi',
+ required: true,
+ },
+ ],
+ webhooks: [
+ {
+ name: 'default',
+ httpMethod: 'POST',
+ responseMode: 'onReceived',
+ path: 'webhook',
+ },
+ ],
+
+ properties: [
+ {
+ // eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
+ displayName: 'Workflow Name',
+ name: 'workflow',
+ type: 'options',
+ noDataExpression: true,
+ typeOptions: {
+ loadOptionsMethod: 'getWorkflows',
+ },
+ default: '',
+ // eslint-disable-next-line n8n-nodes-base/node-param-description-missing-final-period, n8n-nodes-base/node-param-description-wrong-for-dynamic-options
+ description: 'Select one workflow. Choose from the list',
+ required: true,
+ },
+ ],
+ };
+
+ methods = {
+ loadOptions: {
+ async getWorkflows(this: ILoadOptionsFunctions): Promise {
+ const data = await lonescaleApiRequest.call(this, 'GET', '/workflows');
+ return (data as Array<{ title: string; id: string }>)?.map((d) => ({
+ name: d.title,
+ value: d.id,
+ }));
+ },
+ },
+ };
+
+ webhookMethods = {
+ default: {
+ async checkExists(this: IHookFunctions): Promise {
+ const webhookData = this.getWorkflowStaticData('node');
+ const webhookUrl = this.getNodeWebhookUrl('default');
+ const workflowId = this.getNodeParameter('workflow') as string;
+ const webhook = await lonescaleApiRequest.call(
+ this,
+ 'GET',
+ `/workflows/${workflowId}/hook?type=n8n`,
+ );
+ if (webhook.target_url === webhookUrl) {
+ webhookData.webhookId = webhook.webhook_id;
+ return true;
+ }
+ return false;
+ },
+ async create(this: IHookFunctions): Promise {
+ const webhookUrl = this.getNodeWebhookUrl('default');
+ const webhookData = this.getWorkflowStaticData('node');
+ const workflowId = this.getNodeParameter('workflow') as string;
+ const body: IDataObject = {
+ type: 'n8n',
+ target_url: webhookUrl,
+ };
+ const webhook = await lonescaleApiRequest.call(
+ this,
+ 'POST',
+ `/workflows/${workflowId}/hook`,
+ body,
+ );
+ webhookData.webhookId = webhook.webhook_id;
+ return true;
+ },
+ async delete(this: IHookFunctions): Promise {
+ const webhookData = this.getWorkflowStaticData('node');
+ try {
+ await lonescaleApiRequest.call(
+ this,
+ 'DELETE',
+ `/workflows/${webhookData.webhookId}/hook?type=n8n`,
+ );
+ } catch (error) {
+ return false;
+ }
+ delete webhookData.webhookId;
+ return true;
+ },
+ },
+ };
+
+ async webhook(this: IWebhookFunctions): Promise {
+ const req = this.getRequestObject();
+
+ return {
+ workflowData: [this.helpers.returnJsonArray(req.body)],
+ };
+ }
+}
diff --git a/packages/nodes-base/nodes/LoneScale/constants.ts b/packages/nodes-base/nodes/LoneScale/constants.ts
new file mode 100644
index 0000000000..0f25710c86
--- /dev/null
+++ b/packages/nodes-base/nodes/LoneScale/constants.ts
@@ -0,0 +1 @@
+export const BASE_URL = 'https://public-api.lonescale.com';
diff --git a/packages/nodes-base/nodes/LoneScale/lonescale-logo.svg b/packages/nodes-base/nodes/LoneScale/lonescale-logo.svg
new file mode 100644
index 0000000000..ecbedb6813
--- /dev/null
+++ b/packages/nodes-base/nodes/LoneScale/lonescale-logo.svg
@@ -0,0 +1,8 @@
+
diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json
index f2cf9b4227..150ef66100 100644
--- a/packages/nodes-base/package.json
+++ b/packages/nodes-base/package.json
@@ -176,6 +176,7 @@
"dist/credentials/LineNotifyOAuth2Api.credentials.js",
"dist/credentials/LingvaNexApi.credentials.js",
"dist/credentials/LinkedInOAuth2Api.credentials.js",
+ "dist/credentials/LoneScaleApi.credentials.js",
"dist/credentials/Magento2Api.credentials.js",
"dist/credentials/MailcheckApi.credentials.js",
"dist/credentials/MailchimpApi.credentials.js",
@@ -542,6 +543,8 @@
"dist/nodes/LingvaNex/LingvaNex.node.js",
"dist/nodes/LinkedIn/LinkedIn.node.js",
"dist/nodes/LocalFileTrigger/LocalFileTrigger.node.js",
+ "dist/nodes/LoneScale/LoneScaleTrigger.node.js",
+ "dist/nodes/LoneScale/LoneScaleList.node.js",
"dist/nodes/Magento/Magento2.node.js",
"dist/nodes/Mailcheck/Mailcheck.node.js",
"dist/nodes/Mailchimp/Mailchimp.node.js",