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,25 @@
{
"node": "n8n-nodes-base.rssFeedRead",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": ["Core Nodes"],
"resources": {
"primaryDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.rssfeedread/"
}
],
"generic": [
{
"label": "Why business process automation with n8n can change your daily life",
"icon": "🧬",
"url": "https://n8n.io/blog/why-business-process-automation-with-n8n-can-change-your-daily-life/"
},
{
"label": "Why I chose n8n over Zapier in 2020",
"icon": "😍",
"url": "https://n8n.io/blog/why-i-chose-n8n-over-zapier-in-2020/"
}
]
}
}
@@ -0,0 +1,177 @@
import type {
IExecuteFunctions,
IDataObject,
INodeExecutionData,
INodeType,
INodeTypeDescription,
} from 'n8n-workflow';
import { NodeConnectionTypes, NodeOperationError } from 'n8n-workflow';
import Parser from 'rss-parser';
import { URL } from 'url';
import { generatePairedItemData } from '../../utils/utilities';
// Utility function
function validateURL(url: string) {
try {
new URL(url);
return true;
} catch (err) {
return false;
}
}
export class RssFeedRead implements INodeType {
description: INodeTypeDescription = {
displayName: 'RSS Read',
name: 'rssFeedRead',
icon: 'fa:rss',
iconColor: 'orange-red',
group: ['input'],
version: [1, 1.1, 1.2],
description: 'Reads data from an RSS Feed',
defaults: {
name: 'RSS Read',
color: '#b02020',
},
usableAsTool: true,
inputs: [NodeConnectionTypes.Main],
outputs: [NodeConnectionTypes.Main],
properties: [
{
displayName: 'URL',
name: 'url',
type: 'string',
default: '',
required: true,
description: 'URL of the RSS feed',
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add option',
default: {},
options: [
{
displayName: 'Custom Fields',
name: 'customFields',
type: 'string',
default: '',
description:
'A comma-separated list of custom fields to include in the output. For example, "author, contentSnippet".',
},
{
displayName: 'Ignore SSL Issues (Insecure)',
name: 'ignoreSSL',
type: 'boolean',
default: false,
description: 'Whether to ignore SSL/TLS certificate issues or not',
},
],
},
],
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const returnData: INodeExecutionData[] = [];
const nodeVersion = this.getNode().typeVersion;
const items = this.getInputData();
let itemsLength = items.length ? 1 : 0;
let fallbackPairedItems;
if (nodeVersion >= 1.1) {
itemsLength = items.length;
} else {
fallbackPairedItems = generatePairedItemData(items.length);
}
for (let i = 0; i < itemsLength; i++) {
try {
const url = this.getNodeParameter('url', i) as string;
const options = this.getNodeParameter('options', i);
const ignoreSSL = Boolean(options.ignoreSSL);
if (!url) {
throw new NodeOperationError(this.getNode(), 'The parameter "URL" has to be set!', {
itemIndex: i,
});
}
if (!validateURL(url)) {
throw new NodeOperationError(this.getNode(), 'The provided "URL" is not valid!', {
itemIndex: i,
});
}
const parserOptions: IDataObject = {
requestOptions: {
rejectUnauthorized: !ignoreSSL,
},
};
if (nodeVersion >= 1.2) {
parserOptions.headers = {
Accept:
'application/rss+xml, application/rdf+xml;q=0.8, application/atom+xml;q=0.6, application/xml;q=0.4, text/xml;q=0.4',
};
}
if (options.customFields) {
const customFields = options.customFields as string;
parserOptions.customFields = {
item: customFields.split(',').map((field) => field.trim()),
};
}
const parser = new Parser(parserOptions);
let feed: Parser.Output<IDataObject>;
try {
feed = await parser.parseURL(url);
} catch (error) {
if (error.code === 'ECONNREFUSED') {
throw new NodeOperationError(
this.getNode(),
`It was not possible to connect to the URL. Please make sure the URL "${url}" it is valid!`,
{
itemIndex: i,
},
);
}
throw new NodeOperationError(this.getNode(), error as Error, {
itemIndex: i,
});
}
if (feed.items) {
const feedItems = (feed.items as IDataObject[]).map((item) => ({
json: item,
})) as INodeExecutionData[];
const itemData = fallbackPairedItems ?? [{ item: i }];
const executionData = this.helpers.constructExecutionMetaData(feedItems, {
itemData,
});
returnData.push(...executionData);
}
} catch (error) {
if (this.continueOnFail()) {
returnData.push({
json: { error: error.message },
pairedItem: fallbackPairedItems ?? [{ item: i }],
});
continue;
}
throw error;
}
}
return [returnData];
}
}
@@ -0,0 +1,13 @@
{
"node": "n8n-nodes-base.rssFeedReadTrigger",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": ["Core Nodes"],
"resources": {
"primaryDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.rssfeedreadtrigger/"
}
]
}
}
@@ -0,0 +1,99 @@
import moment from 'moment-timezone';
import type {
IDataObject,
INodeExecutionData,
INodeType,
INodeTypeDescription,
IPollFunctions,
} from 'n8n-workflow';
import { NodeConnectionTypes, NodeOperationError } from 'n8n-workflow';
import Parser from 'rss-parser';
interface PollData {
lastItemDate?: string;
lastTimeChecked?: string;
}
export class RssFeedReadTrigger implements INodeType {
description: INodeTypeDescription = {
displayName: 'RSS Feed Trigger',
name: 'rssFeedReadTrigger',
icon: 'fa:rss',
iconColor: 'orange-red',
group: ['trigger'],
version: 1,
description: 'Starts a workflow when an RSS feed is updated',
subtitle: '={{$parameter["event"]}}',
defaults: {
name: 'RSS Feed Trigger',
color: '#b02020',
},
polling: true,
inputs: [],
outputs: [NodeConnectionTypes.Main],
properties: [
{
displayName: 'Feed URL',
name: 'feedUrl',
type: 'string',
default: 'https://blog.n8n.io/rss/',
required: true,
description: 'URL of the RSS feed to poll',
},
],
};
async poll(this: IPollFunctions): Promise<INodeExecutionData[][] | null> {
const pollData = this.getWorkflowStaticData('node') as PollData;
const feedUrl = this.getNodeParameter('feedUrl') as string;
const dateToCheck = Date.parse(
pollData.lastItemDate ?? pollData.lastTimeChecked ?? moment().utc().format(),
);
if (!feedUrl) {
throw new NodeOperationError(this.getNode(), 'The parameter "URL" has to be set!');
}
const parser = new Parser();
let feed: Parser.Output<IDataObject>;
try {
feed = await parser.parseURL(feedUrl);
} catch (error) {
if (error.code === 'ECONNREFUSED') {
throw new NodeOperationError(
this.getNode(),
`It was not possible to connect to the URL. Please make sure the URL "${feedUrl}" it is valid!`,
);
}
throw new NodeOperationError(this.getNode(), error as Error);
}
const returnData: IDataObject[] = [];
if (feed.items) {
if (this.getMode() === 'manual') {
return [this.helpers.returnJsonArray(feed.items[0])];
}
feed.items.forEach((item) => {
if (item.isoDate && Date.parse(item.isoDate) > dateToCheck) {
returnData.push(item);
}
});
if (feed.items.length) {
pollData.lastItemDate = feed.items.reduce((a, b) =>
new Date(a.isoDate!) > new Date(b.isoDate!) ? a : b,
).isoDate;
}
}
if (Array.isArray(returnData) && returnData.length !== 0) {
return [this.helpers.returnJsonArray(returnData)];
}
return null;
}
}
@@ -0,0 +1,79 @@
import { mock } from 'jest-mock-extended';
import { returnJsonArray } from 'n8n-core';
import type { IPollFunctions } from 'n8n-workflow';
import Parser from 'rss-parser';
import { RssFeedReadTrigger } from '../RssFeedReadTrigger.node';
jest.mock('rss-parser');
const now = new Date('2024-02-01T01:23:45.678Z');
jest.useFakeTimers({ now });
describe('RssFeedReadTrigger', () => {
describe('poll', () => {
const feedUrl = 'https://example.com/feed';
const lastItemDate = '2022-01-01T00:00:00.000Z';
const newItemDate = '2022-01-02T00:00:00.000Z';
const node = new RssFeedReadTrigger();
const pollFunctions = mock<IPollFunctions>({
helpers: mock({ returnJsonArray }),
});
it('should throw an error if the feed URL is empty', async () => {
pollFunctions.getNodeParameter.mockReturnValue('');
await expect(node.poll.call(pollFunctions)).rejects.toThrowError();
expect(pollFunctions.getNodeParameter).toHaveBeenCalledWith('feedUrl');
expect(Parser.prototype.parseURL).not.toHaveBeenCalled();
});
it('should return new items from the feed', async () => {
const pollData = mock({ lastItemDate });
pollFunctions.getNodeParameter.mockReturnValue(feedUrl);
pollFunctions.getWorkflowStaticData.mockReturnValue(pollData);
(Parser.prototype.parseURL as jest.Mock).mockResolvedValue({
items: [{ isoDate: lastItemDate }, { isoDate: newItemDate }],
});
const result = await node.poll.call(pollFunctions);
expect(result).toEqual([[{ json: { isoDate: newItemDate } }]]);
expect(pollFunctions.getWorkflowStaticData).toHaveBeenCalledWith('node');
expect(pollFunctions.getNodeParameter).toHaveBeenCalledWith('feedUrl');
expect(Parser.prototype.parseURL).toHaveBeenCalledWith(feedUrl);
expect(pollData.lastItemDate).toEqual(newItemDate);
});
it('should gracefully handle missing timestamps', async () => {
const pollData = mock();
pollFunctions.getNodeParameter.mockReturnValue(feedUrl);
pollFunctions.getWorkflowStaticData.mockReturnValue(pollData);
(Parser.prototype.parseURL as jest.Mock).mockResolvedValue({ items: [{}, {}] });
const result = await node.poll.call(pollFunctions);
expect(result).toEqual(null);
expect(pollFunctions.getWorkflowStaticData).toHaveBeenCalledWith('node');
expect(pollFunctions.getNodeParameter).toHaveBeenCalledWith('feedUrl');
expect(Parser.prototype.parseURL).toHaveBeenCalledWith(feedUrl);
});
it('should return null if the feed is empty', async () => {
const pollData = mock({ lastItemDate });
pollFunctions.getNodeParameter.mockReturnValue(feedUrl);
pollFunctions.getWorkflowStaticData.mockReturnValue(pollData);
(Parser.prototype.parseURL as jest.Mock).mockResolvedValue({ items: [] });
const result = await node.poll.call(pollFunctions);
expect(result).toEqual(null);
expect(pollFunctions.getWorkflowStaticData).toHaveBeenCalledWith('node');
expect(pollFunctions.getNodeParameter).toHaveBeenCalledWith('feedUrl');
expect(Parser.prototype.parseURL).toHaveBeenCalledWith(feedUrl);
expect(pollData.lastItemDate).toEqual(lastItemDate);
});
});
});
@@ -0,0 +1,17 @@
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
import nock from 'nock';
// eslint-disable-next-line n8n-local-rules/no-unneeded-backticks
const feed = `<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Lorem ipsum feed for an interval of 1 minutes with 3 item(s)]]></title><description><![CDATA[This is a constantly updating lorem ipsum feed]]></description><link>http://example.com/</link><generator>RSS for Node</generator><lastBuildDate>Thu, 09 Feb 2023 13:40:32 GMT</lastBuildDate><pubDate>Thu, 09 Feb 2023 13:40:00 GMT</pubDate><copyright><![CDATA[Michael Bertolacci, licensed under a Creative Commons Attribution 3.0 Unported License.]]></copyright><ttl>1</ttl><item><title><![CDATA[Lorem ipsum 2023-02-09T13:40:00Z]]></title><description><![CDATA[Fugiat excepteur exercitation tempor ut aute sunt pariatur veniam pariatur dolor.]]></description><link>http://example.com/test/1675950000</link><guid isPermaLink="true">http://example.com/test/1675950000</guid><dc:creator><![CDATA[John Smith]]></dc:creator><pubDate>Thu, 09 Feb 2023 13:40:00 GMT</pubDate><custom>custom</custom></item><item><title><![CDATA[Lorem ipsum 2023-02-09T13:39:00Z]]></title><description><![CDATA[Laboris quis nulla tempor eu ullamco est esse qui aute commodo aliqua occaecat.]]></description><link>http://example.com/test/1675949940</link><guid isPermaLink="true">http://example.com/test/1675949940</guid><dc:creator><![CDATA[John Smith]]></dc:creator><pubDate>Thu, 09 Feb 2023 13:39:00 GMT</pubDate><custom>custom</custom></item><item><title><![CDATA[Lorem ipsum 2023-02-09T13:38:00Z]]></title><description><![CDATA[Irure labore dolor dolore sint aliquip eu anim aute anim et nulla adipisicing nostrud.]]></description><link>http://example.com/test/1675949880</link><guid isPermaLink="true">http://example.com/test/1675949880</guid><dc:creator><![CDATA[John Smith]]></dc:creator><pubDate>Thu, 09 Feb 2023 13:38:00 GMT</pubDate><custom>custom</custom></item></channel></rss>`;
describe('Test RSS Feed Trigger Node', () => {
beforeAll(() => {
nock('https://lorem-rss.herokuapp.com').get('/feed?length=3').reply(200, feed);
nock('https://fake-rss-feed.com')
.get('/feed')
.reply(200, feed, { 'Content-Type': 'application/xml; charset=utf-8' });
nock('https://custom-rss-feed.com').get('/feed').reply(200, feed);
});
new NodeTestHarness().setupTests();
});
@@ -0,0 +1,209 @@
{
"name": "rss feed test",
"nodes": [
{
"parameters": {},
"id": "448372da-bc2d-4952-8d16-4b3384cd3c3d",
"name": "When clicking \"Execute Workflow\"",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [-112, -176]
},
{
"parameters": {
"url": "https://lorem-rss.herokuapp.com/feed?length=3",
"options": {}
},
"id": "13b7f0a0-db25-4ff3-9e61-4748f6910860",
"name": "RSS Read",
"type": "n8n-nodes-base.rssFeedRead",
"typeVersion": 1,
"position": [96, -176]
},
{
"parameters": {
"url": "https://fake-rss-feed.com/feed",
"options": {}
},
"type": "n8n-nodes-base.rssFeedRead",
"typeVersion": 1.2,
"position": [96, 16],
"id": "ec469874-0ca2-457e-9204-77417d08ef90",
"name": "RSS Read1"
},
{
"parameters": {
"url": "https://custom-rss-feed.com/feed",
"options": {
"customFields": "custom"
}
},
"type": "n8n-nodes-base.rssFeedRead",
"typeVersion": 1.2,
"position": [96, 176],
"id": "6b9593c7-22e0-4ed6-a2ca-bd48aecdfc59",
"name": "RSS With Custom"
}
],
"pinData": {
"RSS Read": [
{
"json": {
"creator": "John Smith",
"title": "Lorem ipsum 2023-02-09T13:40:00Z",
"link": "http://example.com/test/1675950000",
"pubDate": "Thu, 09 Feb 2023 13:40:00 GMT",
"dc:creator": "John Smith",
"content": "Fugiat excepteur exercitation tempor ut aute sunt pariatur veniam pariatur dolor.",
"contentSnippet": "Fugiat excepteur exercitation tempor ut aute sunt pariatur veniam pariatur dolor.",
"guid": "http://example.com/test/1675950000",
"isoDate": "2023-02-09T13:40:00.000Z"
}
},
{
"json": {
"creator": "John Smith",
"title": "Lorem ipsum 2023-02-09T13:39:00Z",
"link": "http://example.com/test/1675949940",
"pubDate": "Thu, 09 Feb 2023 13:39:00 GMT",
"dc:creator": "John Smith",
"content": "Laboris quis nulla tempor eu ullamco est esse qui aute commodo aliqua occaecat.",
"contentSnippet": "Laboris quis nulla tempor eu ullamco est esse qui aute commodo aliqua occaecat.",
"guid": "http://example.com/test/1675949940",
"isoDate": "2023-02-09T13:39:00.000Z"
}
},
{
"json": {
"creator": "John Smith",
"title": "Lorem ipsum 2023-02-09T13:38:00Z",
"link": "http://example.com/test/1675949880",
"pubDate": "Thu, 09 Feb 2023 13:38:00 GMT",
"dc:creator": "John Smith",
"content": "Irure labore dolor dolore sint aliquip eu anim aute anim et nulla adipisicing nostrud.",
"contentSnippet": "Irure labore dolor dolore sint aliquip eu anim aute anim et nulla adipisicing nostrud.",
"guid": "http://example.com/test/1675949880",
"isoDate": "2023-02-09T13:38:00.000Z"
}
}
],
"RSS Read1": [
{
"json": {
"creator": "John Smith",
"title": "Lorem ipsum 2023-02-09T13:40:00Z",
"link": "http://example.com/test/1675950000",
"pubDate": "Thu, 09 Feb 2023 13:40:00 GMT",
"dc:creator": "John Smith",
"content": "Fugiat excepteur exercitation tempor ut aute sunt pariatur veniam pariatur dolor.",
"contentSnippet": "Fugiat excepteur exercitation tempor ut aute sunt pariatur veniam pariatur dolor.",
"guid": "http://example.com/test/1675950000",
"isoDate": "2023-02-09T13:40:00.000Z"
}
},
{
"json": {
"creator": "John Smith",
"title": "Lorem ipsum 2023-02-09T13:39:00Z",
"link": "http://example.com/test/1675949940",
"pubDate": "Thu, 09 Feb 2023 13:39:00 GMT",
"dc:creator": "John Smith",
"content": "Laboris quis nulla tempor eu ullamco est esse qui aute commodo aliqua occaecat.",
"contentSnippet": "Laboris quis nulla tempor eu ullamco est esse qui aute commodo aliqua occaecat.",
"guid": "http://example.com/test/1675949940",
"isoDate": "2023-02-09T13:39:00.000Z"
}
},
{
"json": {
"creator": "John Smith",
"title": "Lorem ipsum 2023-02-09T13:38:00Z",
"link": "http://example.com/test/1675949880",
"pubDate": "Thu, 09 Feb 2023 13:38:00 GMT",
"dc:creator": "John Smith",
"content": "Irure labore dolor dolore sint aliquip eu anim aute anim et nulla adipisicing nostrud.",
"contentSnippet": "Irure labore dolor dolore sint aliquip eu anim aute anim et nulla adipisicing nostrud.",
"guid": "http://example.com/test/1675949880",
"isoDate": "2023-02-09T13:38:00.000Z"
}
}
],
"RSS With Custom": [
{
"json": {
"creator": "John Smith",
"title": "Lorem ipsum 2023-02-09T13:40:00Z",
"link": "http://example.com/test/1675950000",
"pubDate": "Thu, 09 Feb 2023 13:40:00 GMT",
"dc:creator": "John Smith",
"content": "Fugiat excepteur exercitation tempor ut aute sunt pariatur veniam pariatur dolor.",
"contentSnippet": "Fugiat excepteur exercitation tempor ut aute sunt pariatur veniam pariatur dolor.",
"guid": "http://example.com/test/1675950000",
"isoDate": "2023-02-09T13:40:00.000Z",
"custom": "custom"
}
},
{
"json": {
"creator": "John Smith",
"title": "Lorem ipsum 2023-02-09T13:39:00Z",
"link": "http://example.com/test/1675949940",
"pubDate": "Thu, 09 Feb 2023 13:39:00 GMT",
"dc:creator": "John Smith",
"content": "Laboris quis nulla tempor eu ullamco est esse qui aute commodo aliqua occaecat.",
"contentSnippet": "Laboris quis nulla tempor eu ullamco est esse qui aute commodo aliqua occaecat.",
"guid": "http://example.com/test/1675949940",
"isoDate": "2023-02-09T13:39:00.000Z",
"custom": "custom"
}
},
{
"json": {
"creator": "John Smith",
"title": "Lorem ipsum 2023-02-09T13:38:00Z",
"link": "http://example.com/test/1675949880",
"pubDate": "Thu, 09 Feb 2023 13:38:00 GMT",
"dc:creator": "John Smith",
"content": "Irure labore dolor dolore sint aliquip eu anim aute anim et nulla adipisicing nostrud.",
"contentSnippet": "Irure labore dolor dolore sint aliquip eu anim aute anim et nulla adipisicing nostrud.",
"guid": "http://example.com/test/1675949880",
"isoDate": "2023-02-09T13:38:00.000Z",
"custom": "custom"
}
}
]
},
"connections": {
"When clicking \"Execute Workflow\"": {
"main": [
[
{
"node": "RSS Read",
"type": "main",
"index": 0
},
{
"node": "RSS Read1",
"type": "main",
"index": 0
},
{
"node": "RSS With Custom",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1"
},
"versionId": "11023520-13ee-48bf-80b4-81821316ec5f",
"meta": {
"instanceId": "0fa937d34dcabeff4bd6480d3b42cc95edf3bc20e6810819086ef1ce2623639d"
},
"id": "hKwPhUVv6w8I1s7M",
"tags": []
}