feat(n8nApi node): add core node for consuming the n8n API (#4076)

* feat(n8n node): create n8n core node for consuming the n8n API

Co-authored-by: Michael Kret <michael.k@radency.com>
This commit is contained in:
Mike Arvela 2022-09-27 12:05:51 +03:00 committed by GitHub
parent 67513e191d
commit 929315f9e4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 1206 additions and 0 deletions

View file

@ -0,0 +1,43 @@
import {
IAuthenticateGeneric,
ICredentialTestRequest,
ICredentialType,
INodeProperties,
} from 'n8n-workflow';
export class N8nApi implements ICredentialType {
name = 'n8nApi';
displayName = 'n8n API';
documentationUrl = 'n8nApi';
properties: INodeProperties[] = [
{
displayName: 'API Key',
name: 'apiKey',
type: 'string',
default: '',
description: 'The API key for the n8n instance',
},
{
displayName: 'Base URL',
name: 'baseUrl',
type: 'string',
default: '',
placeholder: 'https://<name>.app.n8n.cloud/api/v1',
description: 'The API URL of the n8n instance',
},
];
authenticate: IAuthenticateGeneric = {
type: 'generic',
properties: {
headers: {
'X-N8N-API-KEY': '={{ $credentials.apiKey }}',
},
},
};
test: ICredentialTestRequest = {
request: {
baseURL: '={{ $credentials.baseUrl }}',
url: '/workflows?limit=5',
},
};
}

View file

@ -0,0 +1,168 @@
import { INodeProperties } from 'n8n-workflow';
import { parseAndSetBodyJson } from './GenericFunctions';
export const credentialOperations: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
default: 'create',
displayOptions: {
show: {
resource: ['credential'],
},
},
options: [
{
name: 'Create',
value: 'create',
action: 'Create a credential',
routing: {
request: {
method: 'POST',
url: '/credentials',
},
},
},
{
name: 'Delete',
value: 'delete',
action: 'Delete a credential',
routing: {
request: {
method: 'DELETE',
url: '=/credentials/{{ $parameter.credentialId }}',
},
},
},
{
name: 'Get Schema',
value: 'getSchema',
action: 'Get credential data schema for type',
routing: {
request: {
method: 'GET',
url: '=/credentials/schema/{{ $parameter.credentialTypeName }}',
},
},
},
],
},
];
const createOperation: INodeProperties[] = [
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
placeholder: 'e.g. n8n account',
required: true,
displayOptions: {
show: {
resource: ['credential'],
operation: ['create'],
},
},
routing: {
request: {
body: {
name: '={{ $value }}',
},
},
},
description: 'Name of the new credential',
},
{
displayName: 'Credential Type',
name: 'credentialTypeName',
type: 'string',
placeholder: 'e.g. n8nApi',
default: '',
required: true,
displayOptions: {
show: {
resource: ['credential'],
operation: ['create'],
},
},
routing: {
request: {
body: {
type: '={{ $value }}',
},
},
},
description:
"The available types depend on nodes installed on the n8n instance. Some built-in types include e.g. 'githubApi', 'notionApi', and 'slackApi'.",
},
{
displayName: 'Data',
name: 'data',
type: 'json',
default: '',
placeholder:
'// e.g. for n8nApi \n{\n "apiKey": "my-n8n-api-key",\n "baseUrl": "https://<name>.app.n8n.cloud/api/v1",\n}',
required: true,
typeOptions: {
alwaysOpenEditWindow: true,
},
displayOptions: {
show: {
resource: ['credential'],
operation: ['create'],
},
},
routing: {
send: {
// Validate that the 'data' property is parseable as JSON and
// set it into the request as body.data.
preSend: [parseAndSetBodyJson('data', 'data')],
},
},
description:
"A valid JSON object with properties required for this Credential Type. To see the expected format, you can use 'Get Schema' operation.",
},
];
const deleteOperation: INodeProperties[] = [
{
displayName: 'Credential ID',
name: 'credentialId',
type: 'string',
required: true,
default: '',
displayOptions: {
show: {
resource: ['credential'],
operation: ['delete'],
},
},
},
];
const getSchemaOperation: INodeProperties[] = [
{
displayName: 'Credential Type',
name: 'credentialTypeName',
default: '',
placeholder: 'e.g. n8nApi',
required: true,
type: 'string',
displayOptions: {
show: {
resource: ['credential'],
operation: ['getSchema'],
},
},
description:
"The available types depend on nodes installed on the n8n instance. Some built-in types include e.g. 'githubApi', 'notionApi', and 'slackApi'.",
},
];
export const credentialFields: INodeProperties[] = [
...createOperation,
...deleteOperation,
...getSchemaOperation,
];

View file

@ -0,0 +1,252 @@
/* eslint-disable n8n-nodes-base/node-param-default-missing */
import { getCursorPaginator } from './GenericFunctions';
import { INodeProperties } from 'n8n-workflow';
import { workflowIdLocator } from './WorkflowLocator';
export const executionOperations: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
default: 'getAll',
displayOptions: {
show: {
resource: ['execution'],
},
},
options: [
{
name: 'Get',
value: 'get',
action: 'Get an execution',
routing: {
request: {
method: 'GET',
url: '=/executions/{{ $parameter.executionId }}',
},
},
},
{
name: 'Get Many',
value: 'getAll',
action: 'Get many executions',
routing: {
request: {
method: 'GET',
url: '/executions',
},
send: {
paginate: true,
},
operations: {
pagination: getCursorPaginator(),
},
},
},
{
name: 'Delete',
value: 'delete',
action: 'Delete an execution',
routing: {
request: {
method: 'DELETE',
url: '=/executions/{{ $parameter.executionId }}',
},
},
},
],
},
];
const deleteOperation: INodeProperties[] = [
{
displayName: 'Execution ID',
name: 'executionId',
type: 'string',
required: true,
displayOptions: {
show: {
resource: ['execution'],
operation: ['delete'],
},
},
default: '',
},
];
const getAllOperation: INodeProperties[] = [
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
default: false,
displayOptions: {
show: {
resource: ['execution'],
operation: ['getAll'],
},
},
description: 'Whether to return all results or only up to a given limit',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
default: 100,
typeOptions: {
minValue: 1,
maxValue: 250,
},
displayOptions: {
show: {
resource: ['execution'],
operation: ['getAll'],
returnAll: [false],
},
},
routing: {
request: {
qs: {
limit: '={{ $value }}',
},
},
},
description: 'Max number of results to return',
},
{
displayName: 'Filters',
name: 'filters',
type: 'collection',
placeholder: 'Add Filter',
default: {},
displayOptions: {
show: {
resource: ['execution'],
operation: ['getAll'],
},
},
options: [
{
// Use the common workflowIdLocator, but provide a custom routing
...workflowIdLocator,
routing: {
send: {
type: 'query',
property: 'workflowId',
value: '={{ $value || undefined }}',
},
},
description: 'Workflow to filter the executions by',
},
{
displayName: 'Status',
name: 'status',
type: 'options',
options: [
{
name: 'Error',
value: 'error',
},
{
name: 'Success',
value: 'success',
},
{
name: 'Waiting',
value: 'waiting',
},
],
default: 'success',
routing: {
send: {
type: 'query',
property: 'status',
value: '={{ $value }}',
},
},
description: 'Status to filter the executions by',
},
],
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
default: {},
placeholder: 'Add Option',
displayOptions: {
show: {
resource: ['execution'],
operation: ['getAll'],
},
},
options: [
{
displayName: 'Include Execution Details',
name: 'activeWorkflows',
type: 'boolean',
default: false,
routing: {
send: {
type: 'query',
property: 'includeData',
value: '={{ $value }}',
},
},
description: 'Whether to include the detailed execution data',
},
],
},
];
const getOperation: INodeProperties[] = [
{
displayName: 'Execution ID',
name: 'executionId',
type: 'string',
required: true,
default: '',
displayOptions: {
show: {
resource: ['execution'],
operation: ['get'],
},
},
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
default: {},
placeholder: 'Add Option',
displayOptions: {
show: {
resource: ['execution'],
operation: ['get'],
},
},
options: [
{
displayName: 'Include Execution Details',
name: 'activeWorkflows',
type: 'boolean',
default: false,
routing: {
send: {
type: 'query',
property: 'includeData',
value: '={{ $value }}',
},
},
description: 'Whether to include the detailed execution data',
},
],
},
];
export const executionFields: INodeProperties[] = [
...deleteOperation,
...getAllOperation,
...getOperation,
];

View file

@ -0,0 +1,209 @@
import {
DeclarativeRestApiSettings,
IDataObject,
IExecuteFunctions,
IExecutePaginationFunctions,
IExecuteSingleFunctions,
IHookFunctions,
IHttpRequestOptions,
ILoadOptionsFunctions,
INodeExecutionData,
JsonObject,
NodeApiError,
NodeOperationError,
PreSendAction,
} from 'n8n-workflow';
import { OptionsWithUri } from 'request';
/**
* A custom API request function to be used with the resourceLocator lookup queries.
*/
export async function apiRequest(
this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions,
method: string,
endpoint: string,
body: object,
query?: IDataObject,
): Promise<unknown> {
query = query || {};
type N8nApiCredentials = {
apiKey: string;
baseUrl: string;
};
const credentials = (await this.getCredentials('n8nApi')) as N8nApiCredentials;
const baseUrl = credentials.baseUrl;
const options: OptionsWithUri = {
method,
body,
qs: query,
uri: `${baseUrl}/${endpoint}`,
json: true,
};
try {
return await this.helpers.requestWithAuthentication.call(this, 'n8nApi', options);
} catch (error) {
if (error instanceof NodeApiError) {
throw error;
}
throw new NodeApiError(this.getNode(), error as JsonObject);
}
}
/**
* Get a cursor-based paginator to use with n8n 'getAll' type endpoints.
*
* It will look up a 'nextCursor' in the response and if the node has
* 'returnAll' set to true, will consecutively include it as the 'cursor' query
* parameter for the next request, effectively getting everything in slices.
*
* Prequisites:
* - routing.send.paginate must be set to true, for all requests to go through here
* - node is expected to have a boolean parameter 'returnAll'
* - no postReceive action setting the rootProperty, to get the items mapped
*
* @returns A ready-to-use cursor-based paginator function.
*/
export const getCursorPaginator = () => {
return async function cursorPagination(
this: IExecutePaginationFunctions,
requestOptions: DeclarativeRestApiSettings.ResultOptions,
): Promise<INodeExecutionData[]> {
if (!requestOptions.options.qs) {
requestOptions.options.qs = {};
}
let executions: INodeExecutionData[] = [];
let responseData: INodeExecutionData[];
let nextCursor: string | undefined = undefined;
const returnAll = this.getNodeParameter('returnAll', true) as boolean;
do {
requestOptions.options.qs.cursor = nextCursor;
responseData = await this.makeRoutingRequest(requestOptions);
// Check for another page of items
const lastItem = responseData[responseData.length - 1].json;
nextCursor = lastItem.nextCursor as string | undefined;
responseData.forEach((page) => {
const items = page.json.data as IDataObject[];
if (items) {
// Extract the items themselves
executions = executions.concat(items.map((item) => ({ json: item })));
}
});
// If we don't return all, just return the first page
} while (returnAll && nextCursor);
return executions;
};
};
/**
* A helper function to parse a node parameter as JSON and set it in the request body.
* Throws a NodeOperationError is the content is not valid JSON or it cannot be set.
*
* Currently, parameters with type 'json' are not validated automatically.
* Also mapping the value for 'body.data' declaratively has it treated as a string,
* but some operations (e.g. POST /credentials) don't work unless it is set as an object.
* To get the JSON-body operations to work consistently, we need to parse and set the body
* manually.
*
* @param parameterName The name of the node parameter to parse
* @param setAsBodyProperty An optional property name to set the parsed data into
* @returns The requestOptions with its body replaced with the contents of the parameter
*/
export const parseAndSetBodyJson = (
parameterName: string,
setAsBodyProperty?: string,
): PreSendAction => {
return async function (
this: IExecuteSingleFunctions,
requestOptions: IHttpRequestOptions,
): Promise<IHttpRequestOptions> {
try {
const rawData = this.getNodeParameter(parameterName, '{}') as string;
const parsedObject = JSON.parse(rawData);
// Set the parsed object to either as the request body directly, or as its sub-property
if (setAsBodyProperty === undefined) {
requestOptions.body = parsedObject;
} else {
requestOptions.body = Object.assign({}, requestOptions.body, {
[setAsBodyProperty]: parsedObject,
});
}
} catch (err) {
throw new NodeOperationError(
this.getNode(),
`The '${parameterName}' property must be valid JSON, but cannot be parsed: ${err}`,
);
}
return requestOptions;
};
};
/**
* A helper function to prepare the workflow object data for creation. It only sets
* known workflow properties, for pre-emptively avoiding a HTTP 400 Bad Request
* response until we have a better client-side schema validation mechanism.
*
* NOTE! This expects the requestOptions.body to already be set as an object,
* so take care to first call parseAndSetBodyJson().
*/
export const prepareWorkflowCreateBody: PreSendAction = async function (
this: IExecuteSingleFunctions,
requestOptions: IHttpRequestOptions,
): Promise<IHttpRequestOptions> {
const body = requestOptions.body as IDataObject;
const newBody: IDataObject = {};
newBody.name = body.name || 'My workflow';
newBody.nodes = body.nodes || [];
newBody.settings = body.settings || {};
newBody.connections = body.connections || {};
newBody.staticData = body.staticData || null;
requestOptions.body = newBody;
return requestOptions;
};
/**
* A helper function to prepare the workflow object data for update.
*
* NOTE! This expects the requestOptions.body to already be set as an object,
* so take care to first call parseAndSetBodyJson().
*/
export const prepareWorkflowUpdateBody: PreSendAction = async function (
this: IExecuteSingleFunctions,
requestOptions: IHttpRequestOptions,
): Promise<IHttpRequestOptions> {
const body = requestOptions.body as IDataObject;
const newBody: IDataObject = {};
if (body.name) {
newBody.name = body.name;
}
if (body.nodes) {
newBody.nodes = body.nodes;
}
if (body.settings) {
newBody.settings = body.settings;
}
if (body.connections) {
newBody.connections = body.connections;
}
if (body.staticData) {
newBody.staticData = body.staticData;
}
requestOptions.body = newBody;
return requestOptions;
};

View file

@ -0,0 +1,22 @@
{
"node": "n8n-nodes-base.n8n",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": ["Development", "Core Nodes"],
"resources": {
"credentialDocumentation": [
{
"url": "https://docs.n8n.io/api/authentication/"
}
],
"primaryDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.n8n/"
}
]
},
"alias": ["Workflow", "Execution"],
"subcategories": {
"Core Nodes": ["Helpers"]
}
}

View file

@ -0,0 +1,80 @@
import { INodeType, INodeTypeDescription } from 'n8n-workflow';
import { credentialFields, credentialOperations } from './CredentialDescription';
import { executionFields, executionOperations } from './ExecutionDescription';
import { workflowFields, workflowOperations } from './WorkflowDescription';
import { searchWorkflows } from './WorkflowLocator';
/**
* The n8n node provides access to the n8n API.
*
* See: https://docs.n8n.io/api/api-reference/
*/
export class N8n implements INodeType {
description: INodeTypeDescription = {
displayName: 'n8n',
name: 'n8n',
icon: 'file:n8n.svg',
group: ['transform'],
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Consume n8n API',
defaults: {
name: 'n8n',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'n8nApi',
required: true,
},
],
requestDefaults: {
returnFullResponse: true,
baseURL: '={{ $credentials.baseUrl.replace(new RegExp("/$"), "") }}',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
},
properties: [
{
displayName: 'Resource',
name: 'resource',
type: 'options',
noDataExpression: true,
options: [
{
name: 'Credential',
value: 'credential',
},
{
name: 'Execution',
value: 'execution',
},
{
name: 'Workflow',
value: 'workflow',
},
],
default: 'workflow',
},
...credentialOperations,
...credentialFields,
...executionOperations,
...executionFields,
...workflowOperations,
...workflowFields,
],
};
methods = {
listSearch: {
// Provide workflows search capability for the workflow resourceLocator
searchWorkflows,
},
};
}

View file

@ -0,0 +1,323 @@
import { INodeProperties } from 'n8n-workflow';
import {
getCursorPaginator,
parseAndSetBodyJson,
prepareWorkflowCreateBody,
prepareWorkflowUpdateBody,
} from './GenericFunctions';
import { workflowIdLocator } from './WorkflowLocator';
export const workflowOperations: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
default: 'getAll',
displayOptions: {
show: {
resource: ['workflow'],
},
},
options: [
{
name: 'Activate',
value: 'activate',
action: 'Activate a workflow',
},
{
name: 'Create',
value: 'create',
action: 'Create a workflow',
routing: {
request: {
method: 'POST',
url: '/workflows',
},
},
},
{
name: 'Deactivate',
value: 'deactivate',
action: 'Deactivate a workflow',
},
{
name: 'Delete',
value: 'delete',
action: 'Delete a workflow',
},
{
name: 'Get',
value: 'get',
action: 'Get a workflow',
},
{
name: 'Get Many',
value: 'getAll',
action: 'Get many workflows',
routing: {
request: {
method: 'GET',
url: '/workflows',
},
send: {
paginate: true,
},
operations: {
pagination: getCursorPaginator(),
},
},
},
{
name: 'Update',
value: 'update',
action: 'Update a workflow',
},
],
},
];
const activateOperation: INodeProperties[] = [
{
...workflowIdLocator,
required: true,
displayOptions: {
show: {
resource: ['workflow'],
operation: ['activate'],
},
},
// The routing for resourceLocator-enabled properties currently needs to
// happen in the property block where the property itself is defined, or
// extractValue won't work when used with $parameter in routing.request.url.
routing: {
request: {
method: 'POST',
url: '=/workflows/{{ $value }}/activate',
},
},
},
];
const createOperation: INodeProperties[] = [
{
displayName: 'Workflow Object',
name: 'workflowObject',
type: 'json',
default: '{ "name": "My workflow", "nodes": [], "connections": {}, "settings": {} }',
placeholder:
'{\n "name": "My workflow",\n "nodes": [],\n "connections": {},\n "settings": {}\n}',
required: true,
typeOptions: {
alwaysOpenEditWindow: true,
},
displayOptions: {
show: {
resource: ['workflow'],
operation: ['create'],
},
},
routing: {
send: {
preSend: [parseAndSetBodyJson('workflowObject'), prepareWorkflowCreateBody],
},
},
description:
"A valid JSON object with required fields: 'name', 'nodes', 'connections' and 'settings'. More information can be found in the <a href=\"https://docs.n8n.io/api/api-reference/#tag/Workflow/paths/~1workflows/post\">documentation</a>.",
},
];
const deactivateOperation: INodeProperties[] = [
{
...workflowIdLocator,
required: true,
displayOptions: {
show: {
resource: ['workflow'],
operation: ['deactivate'],
},
},
routing: {
request: {
method: 'POST',
url: '=/workflows/{{ $value }}/deactivate',
},
},
},
];
const deleteOperation: INodeProperties[] = [
{
...workflowIdLocator,
required: true,
displayOptions: {
show: {
resource: ['workflow'],
operation: ['delete'],
},
},
routing: {
request: {
method: 'DELETE',
url: '=/workflows/{{ $value }}',
},
},
},
];
const getAllOperation: INodeProperties[] = [
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
default: true,
displayOptions: {
show: {
resource: ['workflow'],
operation: ['getAll'],
},
},
description: 'Whether to return all results or only up to a given limit',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
default: 100,
typeOptions: {
minValue: 1,
maxValue: 250,
},
displayOptions: {
show: {
resource: ['workflow'],
operation: ['getAll'],
returnAll: [false],
},
},
routing: {
request: {
qs: {
limit: '={{ $value }}',
},
},
},
description: 'Max number of results to return',
},
{
displayName: 'Filters',
name: 'filters',
type: 'collection',
default: {},
displayOptions: {
show: {
resource: ['workflow'],
operation: ['getAll'],
},
},
options: [
{
displayName: 'Return Only Active Workflows',
name: 'activeWorkflows',
type: 'boolean',
default: true,
routing: {
request: {
qs: {
active: '={{ $value }}',
},
},
},
},
{
displayName: 'Tags',
name: 'tags',
type: 'string',
default: '',
routing: {
// Only include the 'tags' query parameter if it's non-empty
send: {
type: 'query',
property: 'tags',
value: '={{ $value !== "" ? $value : undefined }}',
},
},
description: 'Include only workflows with these tags',
hint: 'Comma separated list of tags (empty value is ignored)',
},
],
},
];
const getOperation: INodeProperties[] = [
{
...workflowIdLocator,
required: true,
displayOptions: {
show: {
resource: ['workflow'],
operation: ['get'],
},
},
routing: {
request: {
method: 'GET',
url: '=/workflows/{{ $value }}',
},
},
},
];
const updateOperation: INodeProperties[] = [
{
...workflowIdLocator,
required: true,
displayOptions: {
show: {
resource: ['workflow'],
operation: ['update'],
},
},
routing: {
request: {
method: 'PUT',
url: '=/workflows/{{ $value }}',
},
},
},
{
displayName: 'Workflow Object',
name: 'workflowObject',
type: 'json',
default: '',
placeholder:
'{\n "name": "My workflow",\n "nodes": [],\n "connections": {},\n "settings": {}\n}',
required: true,
typeOptions: {
alwaysOpenEditWindow: true,
},
displayOptions: {
show: {
resource: ['workflow'],
operation: ['update'],
},
},
routing: {
send: {
preSend: [parseAndSetBodyJson('workflowObject'), prepareWorkflowUpdateBody],
},
},
description:
"A valid JSON object with required fields: 'name', 'nodes', 'connections' and 'settings'. More information can be found in the <a href=\"https://docs.n8n.io/api/api-reference/#tag/Workflow/paths/~1workflows~1%7Bid%7D/put\">documentation</a>.",
},
];
export const workflowFields: INodeProperties[] = [
...activateOperation,
...createOperation,
...deactivateOperation,
...deleteOperation,
...getAllOperation,
...getOperation,
...updateOperation,
];

View file

@ -0,0 +1,106 @@
import { ILoadOptionsFunctions, INodeListSearchResult, INodeProperties } from 'n8n-workflow';
import { apiRequest } from './GenericFunctions';
type DataItemsResponse<T> = {
data: T[];
};
interface PartialWorkflow {
id: number;
name: string;
}
/**
* A helper to populate workflow lists. It does a pseudo-search by
* listing available workflows and matching with the specified query.
*/
export async function searchWorkflows(
this: ILoadOptionsFunctions,
query?: string,
): Promise<INodeListSearchResult> {
const searchResults = (await apiRequest.call(
this,
'GET',
'workflows',
{},
)) as DataItemsResponse<PartialWorkflow>;
// Map the workflows list against a simple name/id filter, and sort
// with the latest on top.
const workflows = (searchResults?.data as PartialWorkflow[])
.map((w: PartialWorkflow) => ({
name: `${w.name} (#${w.id})`,
value: w.id,
}))
.filter(
(w) =>
!query ||
w.name.toLowerCase().includes(query.toLowerCase()) ||
w.value?.toString() === query,
)
.sort((a, b) => b.value - a.value);
return {
results: workflows,
};
}
/**
* A resourceLocator to enable looking up workflows by their ID.
* This object can be used as a base and then extended as needed.
*/
export const workflowIdLocator: INodeProperties = {
displayName: 'Workflow',
name: 'workflowId',
type: 'resourceLocator',
default: { mode: 'list', value: '' },
description: 'Workflow to filter the executions by',
modes: [
{
displayName: 'From List',
name: 'list',
type: 'list',
placeholder: 'Select a Workflow...',
initType: 'workflow',
typeOptions: {
searchListMethod: 'searchWorkflows',
searchFilterRequired: false,
searchable: true,
},
},
{
displayName: 'By URL',
name: 'url',
type: 'string',
placeholder: 'https://myinstance.app.n8n.cloud/workflow/1',
validation: [
{
type: 'regex',
properties: {
regex: '.*/workflow/([0-9]{1,})',
errorMessage: 'Not a valid Workflow URL',
},
},
],
extractValue: {
type: 'regex',
regex: '.*/workflow/([0-9]{1,})',
},
},
{
displayName: 'ID',
name: 'id',
type: 'string',
validation: [
{
type: 'regex',
properties: {
regex: '[0-9]{1,}',
errorMessage: 'Not a valid Workflow ID',
},
},
],
placeholder: '1',
},
],
};

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="5 5 55 55"><path d="M50.28 17.26c-2.39 0-4.4 1.64-4.97 3.86h-7.15c-2.8 0-5.08 2.28-5.08 5.08 0 1.4-1.14 2.54-2.54 2.54h-1.02a5.138 5.138 0 00-4.97-3.86c-2.39 0-4.4 1.64-4.97 3.86H15.5a5.138 5.138 0 00-4.97-3.86c-2.83 0-5.13 2.3-5.13 5.13s2.3 5.13 5.13 5.13c2.39 0 4.4-1.64 4.97-3.86h4.08a5.138 5.138 0 004.97 3.86 5.13 5.13 0 004.95-3.81h1.03c1.4 0 2.54 1.14 2.54 2.54 0 2.8 2.28 5.09 5.08 5.09h1.66a5.138 5.138 0 004.97 3.86c2.83 0 5.13-2.3 5.13-5.13s-2.3-5.13-5.13-5.13c-2.39 0-4.4 1.64-4.97 3.86h-1.66c-1.4 0-2.54-1.14-2.54-2.54 0-1.53-.68-2.9-1.76-3.84a5.092 5.092 0 001.76-3.84c0-1.4 1.14-2.54 2.54-2.54h7.15a5.138 5.138 0 004.97 3.86c2.83 0 5.13-2.3 5.13-5.13s-2.29-5.13-5.12-5.13zM10.54 32.61a2.59 2.59 0 11.001-5.181 2.59 2.59 0 01-.001 5.181zm14.02 0a2.59 2.59 0 112.59-2.59c0 1.43-1.17 2.59-2.59 2.59zm20.24 2.5a2.59 2.59 0 11-.001 5.181 2.59 2.59 0 01.001-5.181zm5.48-10.13a2.59 2.59 0 11.001-5.181 2.59 2.59 0 01-.001 5.181z" fill="#ff6d5a"/></svg>

After

Width:  |  Height:  |  Size: 1,009 B

View file

@ -208,6 +208,7 @@
"dist/credentials/Mqtt.credentials.js",
"dist/credentials/Msg91Api.credentials.js",
"dist/credentials/MySql.credentials.js",
"dist/credentials/N8nApi.credentials.js",
"dist/credentials/NasaApi.credentials.js",
"dist/credentials/NetlifyApi.credentials.js",
"dist/credentials/NextCloudApi.credentials.js",
@ -556,6 +557,7 @@
"dist/nodes/MQTT/MqttTrigger.node.js",
"dist/nodes/Msg91/Msg91.node.js",
"dist/nodes/MySql/MySql.node.js",
"dist/nodes/N8n/N8n.node.js",
"dist/nodes/N8nTrainingCustomerDatastore/N8nTrainingCustomerDatastore.node.js",
"dist/nodes/N8nTrainingCustomerMessenger/N8nTrainingCustomerMessenger.node.js",
"dist/nodes/N8nTrigger/N8nTrigger.node.js",