This workflow corresponds to n8n.io template #16226 — we link there as the canonical source.
The workflow JSON
Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →
{
"id": "xpHd9hGWINvRvc9u",
"name": "Job_ads_signals_final",
"tags": [],
"nodes": [
{
"id": "265d1925-158c-42f3-92fe-2e2c877faa88",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
576,
-240
],
"parameters": {
"width": 480,
"height": 896,
"content": "## Job_ads_signals\n\n### How it works\n\n1. Trigger the workflow manually or on a weekly schedule.\n2. Fetch and filter data from Hubspot.\n3. Search for job ads and create snapshots for comparison.\n4. Perform snapshot cleanup and validate job ad existence.\n5. Update job ads table and process company updates.\n\n### Setup steps\n\n- [ ] Set up Hubspot credentials in the n8n workspace.\n- [ ] Configure ElasticSearch settings for job ad searches.\n- [ ] Add access credentials for the data table manipulation nodes.\n- [ ] Ensure triggers (manual/weekly) have the correct configuration.\n- [ ] Code nodes require any JavaScript variables pre-defined within the workflows.\n\n### Customization\n\nYou can adjust filter criteria and comparison logic based on company-specific needs. Data snapshots and validation mechanisms can be adapted to existing database structure."
},
"typeVersion": 1
},
{
"id": "b3b427d3-f542-43f6-988e-a773c4f5dba6",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
1136,
224
],
"parameters": {
"color": 7,
"height": 496,
"content": "## Workflow triggers\n\nStart the workflow with scheduled or manual initiation."
},
"typeVersion": 1
},
{
"id": "84ec5d68-2956-4de2-8725-2354f0c9e422",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"position": [
1440,
336
],
"parameters": {
"color": 7,
"width": 592,
"height": 288,
"content": "## Initialize and fetch companies\n\nInitialize the run and get company data from Hubspot."
},
"typeVersion": 1
},
{
"id": "de9866de-d854-47a5-aa20-7038fd0d7ae8",
"name": "Sticky Note3",
"type": "n8n-nodes-base.stickyNote",
"position": [
2080,
320
],
"parameters": {
"color": 7,
"width": 432,
"height": 304,
"content": "## Filter and deduplicate companies\n\nFilter company records retrieved from Hubspot."
},
"typeVersion": 1
},
{
"id": "6af804dc-23dc-4ba8-bf09-12533dee36a7",
"name": "Sticky Note4",
"type": "n8n-nodes-base.stickyNote",
"position": [
2544,
320
],
"parameters": {
"color": 7,
"width": 368,
"height": 336,
"content": "## Loop through filtered companies\n\nProcess filtered company batches to extract domains and search for job ads."
},
"typeVersion": 1
},
{
"id": "c7a45fc3-0a95-46d9-85bf-1723f45677c0",
"name": "Sticky Note5",
"type": "n8n-nodes-base.stickyNote",
"position": [
2944,
304
],
"parameters": {
"color": 7,
"width": 432,
"height": 304,
"content": "## Search and snapshot job ads\n\nSearch for job ads and create a snapshot of data."
},
"typeVersion": 1
},
{
"id": "b6e1034b-d79b-4722-974b-f1da3e9363fd",
"name": "Sticky Note6",
"type": "n8n-nodes-base.stickyNote",
"position": [
3104,
640
],
"parameters": {
"color": 7,
"width": 400,
"height": 368,
"content": "## Snapshot comparison\n\nCompare job snapshots from current and previous runs."
},
"typeVersion": 1
},
{
"id": "21ea4fae-8dff-43d3-a05f-64f1f2c7dfc5",
"name": "Sticky Note7",
"type": "n8n-nodes-base.stickyNote",
"position": [
3568,
368
],
"parameters": {
"color": 7,
"width": 400,
"height": 272,
"content": "## Snapshot cleanup\n\nCleanup and ensure snapshot consistency."
},
"typeVersion": 1
},
{
"id": "d94ec6c2-157c-4c3a-beb5-c1a97ad871d1",
"name": "Sticky Note8",
"type": "n8n-nodes-base.stickyNote",
"position": [
4016,
368
],
"parameters": {
"color": 7,
"width": 560,
"height": 272,
"content": "## Job existence check\n\nCheck which job ads exist and update the actual snapshot."
},
"typeVersion": 1
},
{
"id": "30bddf8b-355d-4035-abcd-9b716dab6e06",
"name": "Sticky Note9",
"type": "n8n-nodes-base.stickyNote",
"position": [
4608,
352
],
"parameters": {
"color": 7,
"width": 608,
"height": 304,
"content": "## Update job ads table\n\nUpdate job ads table based on checked existence and validate removals."
},
"typeVersion": 1
},
{
"id": "8ac82212-78f8-4b5f-a165-fc1d70f9d43f",
"name": "Sticky Note10",
"type": "n8n-nodes-base.stickyNote",
"position": [
3776,
688
],
"parameters": {
"color": 7,
"width": 832,
"height": 272,
"content": "## Validate and remove jobs\n\nValidate disappeared jobs and remove them from the database."
},
"typeVersion": 1
},
{
"id": "e4401c6d-f533-47b4-a15f-8f6bf4cf99c1",
"name": "Sticky Note11",
"type": "n8n-nodes-base.stickyNote",
"position": [
5248,
368
],
"parameters": {
"color": 7,
"width": 560,
"height": 272,
"content": "## Prepare for company updates\n\nQuery rows and process data for company update actions."
},
"typeVersion": 1
},
{
"id": "3b7faa8d-85a8-457c-8862-bb1400ec06d2",
"name": "Sticky Note12",
"type": "n8n-nodes-base.stickyNote",
"position": [
5984,
-192
],
"parameters": {
"color": 7,
"width": 256,
"height": 432,
"content": "## Compare company lists\n\nCompare old and new company lists to identify updates."
},
"typeVersion": 1
},
{
"id": "7d671b01-466a-411d-8aad-353bda398703",
"name": "Sticky Note13",
"type": "n8n-nodes-base.stickyNote",
"position": [
6320,
-32
],
"parameters": {
"color": 7,
"width": 592,
"height": 720,
"content": "## Process company updates\n\nPerform updates for companies with new data or removed postings."
},
"typeVersion": 1
},
{
"id": "02cdc8e6-50ff-416c-891d-a2aa0a6d9b1e",
"name": "When Monday at 9am",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [
1184,
384
],
"parameters": {
"rule": {
"interval": [
{
"field": "cronExpression",
"expression": "0 9 * * 1"
}
]
}
},
"typeVersion": 1.2
},
{
"id": "b9163e4f-64fe-4400-aa13-aedc299f606e",
"name": "Initialize Workflow",
"type": "n8n-nodes-base.code",
"position": [
1488,
464
],
"parameters": {
"jsCode": "const now = new Date().toISOString();\nconst runId = `cs-monitor-${now}`;\nreturn [{ json: { run_id: runId, run_started_at: now } }];"
},
"typeVersion": 2
},
{
"id": "6d8034c0-ae75-4f90-bd8f-5dd63bf5ff82",
"name": "Search Jobs via Elasticsearch",
"type": "n8n-nodes-coresignal-api.coresignal",
"position": [
2992,
432
],
"parameters": {
"esQuery": "={\n \"query\": {\n \"bool\": {\n \"must\": [\n {\n \"bool\": {\n \"should\": [\n {\n \"bool\": {\n \"should\": [\n {\n \"match_phrase\": {\n \"title\": \"customer support\"\n }\n },\n {\n \"match_phrase\": {\n \"title\": \"Customer Support\"\n }\n },\n {\n \"match_phrase\": {\n \"title\": \"Customer Support.\"\n }\n },\n {\n \"match_phrase\": {\n \"title\": \"\uace0\uac1d\uc9c0\uc6d0\"\n }\n },\n {\n \"match_phrase\": {\n \"title\": \"Customersupport\"\n }\n },\n {\n \"match_phrase\": {\n \"title\": \"\u5ba2\u6237\u652f\u6301\"\n }\n },\n {\n \"match_phrase\": {\n \"title\": \"Kundst\u00f6d\"\n }\n },\n {\n \"match_phrase\": {\n \"title\": \"Kundensupport\"\n }\n },\n {\n \"match_phrase\": {\n \"title\": \"Customers Support\"\n }\n },\n {\n \"match_phrase\": {\n \"title\": \"Customer Service Support\"\n }\n },\n {\n \"match_phrase\": {\n \"title\": \"Kundesupport\"\n }\n },\n {\n \"match_phrase\": {\n \"title\": \"Soporte Al Cliente\"\n }\n },\n {\n \"match_phrase\": {\n \"title\": \"Kundsupport\"\n }\n },\n {\n \"match_phrase\": {\n \"title\": \"\u062f\u0639\u0645 \u0627\u0644\u0639\u0645\u0644\u0627\u0621\"\n }\n },\n {\n \"match_phrase\": {\n \"title\": \"Customer Services Support\"\n }\n },\n {\n \"match_phrase\": {\n \"title\": \"M\u00fc\u015fteri Destek\"\n }\n },\n {\n \"match_phrase\": {\n \"title\": \"Kunden-Support\"\n }\n },\n {\n \"match_phrase\": {\n \"title\": \"Customer Support Service\"\n }\n },\n {\n \"match_phrase\": {\n \"title\": \"Support Customer Service\"\n }\n },\n {\n \"match_phrase\": {\n \"title\": \"Customer Care Support\"\n }\n },\n {\n \"match_phrase\": {\n \"title\": \"M\u00fc\u015fteri Deste\u011fi\"\n }\n },\n {\n \"match_phrase\": {\n \"title\": \"Aten\u00e7\u00e3o Ao Cliente\"\n }\n },\n {\n \"match_phrase\": {\n \"title\": \"Atencion Cliente\"\n }\n }\n ],\n \"minimum_should_match\": 1\n }\n },\n {\n \"bool\": {\n \"should\": [\n {\n \"match_phrase\": {\n \"title\": \"customer service\"\n }\n },\n {\n \"match_phrase\": {\n \"title\": \"Customerservice\"\n }\n },\n {\n \"match_phrase\": {\n \"title\": \"Customer Service.\"\n }\n },\n {\n \"match_phrase\": {\n \"title\": \"Customer Service\"\n }\n },\n {\n \"match_phrase\": {\n \"title\": \"Kundeservice\"\n }\n },\n {\n \"match_phrase\": {\n \"title\": \"Kundenservice\"\n }\n },\n {\n \"match_phrase\": {\n \"title\": \"Atenci\u00f3n Al Cliente.\"\n }\n },\n {\n \"match_phrase\": {\n \"title\": \"\u0e1a\u0e23\u0e34\u0e01\u0e32\u0e23\u0e25\u0e39\u0e01\u0e04\u0e49\u0e32\"\n }\n },\n {\n \"match_phrase\": {\n \"title\": \"Kundtj\u00e4nst\"\n }\n },\n {\n \"match_phrase\": {\n \"title\": \"Kundservice\"\n }\n },\n {\n \"match_phrase\": {\n \"title\": \"Asiakaspalvelu\"\n }\n },\n {\n \"match_phrase\": {\n \"title\": \"Atenci\u00f3n Al Cliente\"\n }\n },\n {\n \"match_phrase\": {\n \"title\": \"Atenci\u00f3n Al Cliente\"\n }\n },\n {\n \"match_phrase\": {\n \"title\": \"Atenci\u00f3n Cliente\"\n }\n },\n {\n \"match_phrase\": {\n \"title\": \"Kundendienst\"\n }\n },\n {\n \"match_phrase\": {\n \"title\": \"Atenci\u00f3n Al Clientes\"\n }\n },\n {\n \"match_phrase\": {\n \"title\": \"\u5ba2\u6236\u670d\u52d9\"\n }\n },\n {\n \"match_phrase\": {\n \"title\": \"Servicio Al Cliente.\"\n }\n },\n {\n \"match_phrase\": {\n \"title\": \"Atenci\u00f3n Clientes\"\n }\n },\n {\n \"match_phrase\": {\n \"title\": \"Customers Service\"\n }\n },\n {\n \"match_phrase\": {\n \"title\": \"Servicio Al Cliente\"\n }\n },\n {\n \"match_phrase\": {\n \"title\": \"\u05e9\u05d9\u05e8\u05d5\u05ea \u05dc\u05e7\u05d5\u05d7\u05d5\u05ea\"\n }\n },\n {\n \"match_phrase\": {\n \"title\": \"Customer Servis\"\n }\n },\n {\n \"match_phrase\": {\n \"title\": \"Atendimento Ao Cliente.\"\n }\n },\n {\n \"match_phrase\": {\n \"title\": \"Atenci\u00f3n A Cliente\"\n }\n },\n {\n \"match_phrase\": {\n \"title\": \"Atendimento Cliente\"\n }\n },\n {\n \"match_phrase\": {\n \"title\": \"Atendimento Ao Cliente\"\n }\n },\n {\n \"match_phrase\": {\n \"title\": \"Atenci\u00f3n A Clientes.\"\n }\n },\n {\n \"match_phrase\": {\n \"title\": \"Customer-Service\"\n }\n },\n {\n \"match_phrase\": {\n \"title\": \"\u062e\u062f\u0645\u0629 \u0627\u0644\u0639\u0645\u0644\u0627\u0621\"\n }\n },\n {\n \"match_phrase\": {\n \"title\": \"Customerservices\"\n }\n },\n {\n \"match_phrase\": {\n \"title\": \"Atenci\u00f3n De Clientes\"\n }\n },\n {\n \"match_phrase\": {\n \"title\": \"Servicio Atenci\u00f3n Al Cliente\"\n }\n },\n {\n \"match_phrase\": {\n \"title\": \"Atenci\u00f3n A Clientes\"\n }\n },\n {\n \"match_phrase\": {\n \"title\": \"Pelayanan Pelanggan\"\n }\n },\n {\n \"match_phrase\": {\n \"title\": \"Customer Servise\"\n }\n },\n {\n \"match_phrase\": {\n \"title\": \"M\u00fc\u015fteri Hizmetleri\"\n }\n },\n {\n \"match_phrase\": {\n \"title\": \"Servi\u00e7o Ao Cliente\"\n }\n },\n {\n \"match_phrase\": {\n \"title\": \"Customercare\"\n }\n },\n {\n \"match_phrase\": {\n \"title\": \"Servicio De Atenci\u00f3n Al Cliente.\"\n }\n },\n {\n \"match_phrase\": {\n \"title\": \"Servicio Cliente\"\n }\n },\n {\n \"match_phrase\": {\n \"title\": \"Atencion Al Cliente.\"\n }\n },\n {\n \"match_phrase\": {\n \"title\": \"Atencion Al Cliente\"\n }\n },\n {\n \"match_phrase\": {\n \"title\": \"Atencion Al Cliente\"\n }\n },\n {\n \"match_phrase\": {\n \"title\": \"Atendimento A Cliente\"\n }\n },\n {\n \"match_phrase\": {\n \"title\": \"Servicio Atencion Al Cliente\"\n }\n },\n {\n \"match_phrase\": {\n \"title\": \"Servicio De Atenci\u00f3n Al Cliente\"\n }\n },\n {\n \"match_phrase\": {\n \"title\": \"Servcio Al Cliente\"\n }\n },\n {\n \"match_phrase\": {\n \"title\": \"Coustomer Service\"\n }\n },\n {\n \"match_phrase\": {\n \"title\": \"Customer Services\"\n }\n },\n {\n \"match_phrase\": {\n \"title\": \"Customer Services\"\n }\n },\n {\n \"match_phrase\": {\n \"title\": \"Atencion Cliente\"\n }\n },\n {\n \"match_phrase\": {\n \"title\": \"Servicio Clientes\"\n }\n },\n {\n \"match_phrase\": {\n \"title\": \"Aten\u00e7\u00e3o Ao Cliente\"\n }\n },\n {\n \"match_phrase\": {\n \"title\": \"Atencion Al Clientes\"\n }\n }\n ],\n \"minimum_should_match\": 1\n }\n },\n {\n \"bool\": {\n \"should\": [\n {\n \"match_phrase\": {\n \"title\": \"support representative\"\n }\n },\n {\n \"match_phrase\": {\n \"title\": \"Support Rep\"\n }\n }\n ],\n \"minimum_should_match\": 1\n }\n },\n {\n \"bool\": {\n \"should\": [\n {\n \"match_phrase\": {\n \"title\": \"customer representative\"\n }\n },\n {\n \"match_phrase\": {\n \"title\": \"Customer Representative\"\n }\n },\n {\n \"match_phrase\": {\n \"title\": \"Customer Rep.\"\n }\n },\n {\n \"match_phrase\": {\n \"title\": \"Customers Representative\"\n }\n },\n {\n \"match_phrase\": {\n \"title\": \"Customer Rep\"\n }\n },\n {\n \"match_phrase\": {\n \"title\": \"Representante De Cliente\"\n }\n },\n {\n \"match_phrase\": {\n \"title\": \"Customer Representive\"\n }\n },\n {\n \"match_phrase\": {\n \"title\": \"Representante De Clientes\"\n }\n },\n {\n \"match_phrase\": {\n \"title\": \"Customer Representitive\"\n }\n },\n {\n \"match_phrase\": {\n \"title\": \"\u5ba2\u670d\u4ee3\u8868\"\n }\n }\n ],\n \"minimum_should_match\": 1\n }\n }\n ],\n \"minimum_should_match\": 1\n }\n },\n {\n \"terms\": {\n \"company_domain\": \n {{ $json.domains.toJsonString() }}\n }\n }\n ],\n \"filter\": [\n {\n \"term\": {\n \"status\": 1\n }\n }\n ]\n }\n },\n \"sort\": [\n \"company_employees_count\"\n ]\n}",
"resource": "job",
"operation": "search_es_dsl"
},
"credentials": {
"coresignalApi": {
"name": "<your credential>"
}
},
"typeVersion": 1,
"alwaysOutputData": true
},
{
"id": "b3215034-faf9-403a-af91-b63b98fc04d2",
"name": "Fetch Job Details by ID",
"type": "n8n-nodes-coresignal-api.coresignal",
"onError": "continueRegularOutput",
"position": [
4256,
480
],
"parameters": {
"jobId": "={{ $json.job_id }}",
"resource": "job"
},
"credentials": {
"coresignalApi": {
"name": "<your credential>"
}
},
"typeVersion": 1,
"alwaysOutputData": true
},
{
"id": "c07057dc-708a-45c6-ba4f-2d6a12fecf67",
"name": "Wait 10 Seconds",
"type": "n8n-nodes-base.wait",
"position": [
5072,
480
],
"parameters": {
"amount": 10
},
"typeVersion": 1.1
},
{
"id": "cf2cb405-df7c-41e6-82c1-ed71f4e83942",
"name": "Retrieve Previous Run IDs",
"type": "n8n-nodes-base.dataTable",
"position": [
2944,
832
],
"parameters": {
"matchType": "allConditions",
"operation": "get",
"returnAll": true,
"dataTableId": {
"__rl": true,
"mode": "list",
"value": "TFbKeONV0qpw1fGs",
"cachedResultUrl": "/projects/d8siYNB21z0O177I/datatables/TFbKeONV0qpw1fGs",
"cachedResultName": "previous snapshot"
}
},
"typeVersion": 1.1
},
{
"id": "332e411d-0301-457c-bad1-0e778b2cdb32",
"name": "Retrieve Company List Data",
"type": "n8n-nodes-base.dataTable",
"position": [
3440,
-128
],
"parameters": {
"operation": "get",
"returnAll": true,
"dataTableId": {
"__rl": true,
"mode": "list",
"value": "UFWhZZy4KFSac27Q",
"cachedResultUrl": "/projects/d8siYNB21z0O177I/datatables/UFWhZZy4KFSac27Q",
"cachedResultName": "company_data"
}
},
"executeOnce": true,
"typeVersion": 1.1
},
{
"id": "2e162cfc-5a53-40de-ad5a-90d5d1607b7d",
"name": "Create Current Snapshot",
"type": "n8n-nodes-base.dataTable",
"position": [
3232,
432
],
"parameters": {
"columns": {
"value": {
"job_id": "={{ $json }}"
},
"schema": [
{
"id": "job_id",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "job_id",
"defaultMatch": false
},
{
"id": "domain",
"type": "string",
"display": true,
"removed": true,
"readOnly": false,
"required": false,
"displayName": "domain",
"defaultMatch": false
}
],
"mappingMode": "defineBelow",
"matchingColumns": [],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {},
"dataTableId": {
"__rl": true,
"mode": "list",
"value": "gOVbwnox6HLZa5yz",
"cachedResultUrl": "/projects/d8siYNB21z0O177I/datatables/gOVbwnox6HLZa5yz",
"cachedResultName": "current snapshot"
}
},
"typeVersion": 1.1
},
{
"id": "e84fead8-3d70-430c-86db-1157b2f2cd65",
"name": "Compare Previous and Current IDs",
"type": "n8n-nodes-base.compareDatasets",
"position": [
3360,
768
],
"parameters": {
"options": {
"skipFields": "createdAt, updatedAt, id"
},
"mergeByFields": {
"values": [
{
"field1": "job_id",
"field2": "job_id"
}
]
}
},
"executeOnce": false,
"typeVersion": 2.3
},
{
"id": "ed41de19-c3db-46f7-8663-7705c2bdae67",
"name": "Clear Current Snapshot",
"type": "n8n-nodes-base.dataTable",
"position": [
3616,
480
],
"parameters": {
"filters": {
"conditions": [
{
"keyName": "job_id",
"condition": "isNotEmpty"
}
]
},
"options": {
"dryRun": false
},
"operation": "deleteRows",
"dataTableId": {
"__rl": true,
"mode": "list",
"value": "gOVbwnox6HLZa5yz",
"cachedResultUrl": "/projects/d8siYNB21z0O177I/datatables/gOVbwnox6HLZa5yz",
"cachedResultName": "current snapshot"
}
},
"typeVersion": 1.1
},
{
"id": "ac048b69-d923-4704-94a2-df9759bac3eb",
"name": "Check Snapshot IDs in Database",
"type": "n8n-nodes-base.dataTable",
"position": [
3824,
480
],
"parameters": {
"filters": {
"conditions": [
{
"keyName": "job_id",
"keyValue": "={{ $json.job_id }}"
}
]
},
"operation": "rowNotExists",
"dataTableId": {
"__rl": true,
"mode": "list",
"value": "TFbKeONV0qpw1fGs",
"cachedResultUrl": "/projects/d8siYNB21z0O177I/datatables/TFbKeONV0qpw1fGs",
"cachedResultName": "previous snapshot"
}
},
"typeVersion": 1.1,
"alwaysOutputData": true
},
{
"id": "b51375c5-6dd6-4b17-a430-3e747e1cd0e6",
"name": "Update Active Snapshot",
"type": "n8n-nodes-base.dataTable",
"onError": "continueRegularOutput",
"position": [
4064,
480
],
"parameters": {
"columns": {
"value": {
"job_id": "={{ $json.job_id }}"
},
"schema": [
{
"id": "job_id",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "job_id",
"defaultMatch": false
},
{
"id": "domain",
"type": "string",
"display": true,
"removed": true,
"readOnly": false,
"required": false,
"displayName": "domain",
"defaultMatch": false
}
],
"mappingMode": "defineBelow",
"matchingColumns": [],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {},
"dataTableId": {
"__rl": true,
"mode": "list",
"value": "TFbKeONV0qpw1fGs",
"cachedResultUrl": "/projects/d8siYNB21z0O177I/datatables/TFbKeONV0qpw1fGs",
"cachedResultName": "previous snapshot"
}
},
"typeVersion": 1.1
},
{
"id": "9e8fde7c-562c-4e52-a144-f9c77bdc5065",
"name": "Check Job Ad Presence",
"type": "n8n-nodes-base.dataTable",
"position": [
4432,
480
],
"parameters": {
"filters": {
"conditions": [
{
"keyName": "job_id",
"keyValue": "={{ $json.id }}"
}
]
},
"operation": "rowNotExists",
"dataTableId": {
"__rl": true,
"mode": "list",
"value": "E1tGnBBo2NJauMUs",
"cachedResultUrl": "/projects/d8siYNB21z0O177I/datatables/E1tGnBBo2NJauMUs",
"cachedResultName": "job_data_table"
}
},
"typeVersion": 1.1
},
{
"id": "9bb7b78a-aef9-4adb-b80f-baaa26e5e912",
"name": "Update Job Ads Table",
"type": "n8n-nodes-base.dataTable",
"position": [
4656,
480
],
"parameters": {
"columns": {
"value": {
"url": "={{ $json.external_url }}",
"title": "={{ $json.title }}",
"job_id": "={{ $json.id }}",
"source": "={{ $json.job_sources[0].source }}",
"status": "={{ $json.status }}",
"location": "={{ $json.location }}",
"posted_at": "={{ $json.created_at }}",
"description": "={{ $json.description }}",
"company_domain": "={{ $json.company_domain }}",
"applicants_count": "={{ $json.applicants_count }}"
},
"schema": [
{
"id": "job_id",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "job_id",
"defaultMatch": false
},
{
"id": "company_domain",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "company_domain",
"defaultMatch": false
},
{
"id": "status",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "status",
"defaultMatch": false
},
{
"id": "posted_at",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "posted_at",
"defaultMatch": false
},
{
"id": "source",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "source",
"defaultMatch": false
},
{
"id": "url",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "url",
"defaultMatch": false
},
{
"id": "title",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "title",
"defaultMatch": false
},
{
"id": "description",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "description",
"defaultMatch": false
},
{
"id": "location",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "location",
"defaultMatch": false
},
{
"id": "applicants_count",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "applicants_count",
"defaultMatch": false
}
],
"mappingMode": "defineBelow",
"matchingColumns": [],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {},
"dataTableId": {
"__rl": true,
"mode": "list",
"value": "E1tGnBBo2NJauMUs",
"cachedResultUrl": "/projects/d8siYNB21z0O177I/datatables/E1tGnBBo2NJauMUs",
"cachedResultName": "job_data_table"
}
},
"typeVersion": 1.1
},
{
"id": "ea618bcf-d3ab-4d72-a372-19b6287ee57d",
"name": "Remove Obsolete Job Ads",
"type": "n8n-nodes-base.dataTable",
"position": [
3824,
800
],
"parameters": {
"filters": {
"conditions": [
{
"keyName": "job_id",
"keyValue": "={{ $json.job_id }}"
}
]
},
"options": {},
"operation": "deleteRows",
"dataTableId": {
"__rl": true,
"mode": "list",
"value": "TFbKeONV0qpw1fGs",
"cachedResultUrl": "/projects/d8siYNB21z0O177I/datatables/TFbKeONV0qpw1fGs",
"cachedResultName": "previous snapshot"
}
},
"executeOnce": false,
"typeVersion": 1.1
},
{
"id": "b74fb3c7-527a-40a8-8ac3-5b3fef29566b",
"name": "Validate Job Ad History",
"type": "n8n-nodes-base.dataTable",
"position": [
4064,
800
],
"parameters": {
"filters": {
"conditions": [
{
"keyName": "job_id",
"keyValue": "={{ $json.job_id }}"
}
]
},
"operation": "rowExists",
"dataTableId": {
"__rl": true,
"mode": "list",
"value": "E1tGnBBo2NJauMUs",
"cachedResultUrl": "/projects/d8siYNB21z0O177I/datatables/E1tGnBBo2NJauMUs",
"cachedResultName": "job_data_table"
}
},
"typeVersion": 1.1
},
{
"id": "bc21c4c3-bdf1-4f41-bca3-aace0ab4ac49",
"name": "Remove Job Ads from Table",
"type": "n8n-nodes-base.dataTable",
"position": [
4464,
800
],
"parameters": {
"filters": {
"conditions": [
{
"keyName": "job_id"
}
]
},
"options": {},
"operation": "deleteRows",
"dataTableId": {
"__rl": true,
"mode": "list",
"value": "E1tGnBBo2NJauMUs",
"cachedResultUrl": "/projects/d8siYNB21z0O177I/datatables/E1tGnBBo2NJauMUs",
"cachedResultName": "job_data_table"
}
},
"typeVersion": 1.1
},
{
"id": "469daf0a-f444-4fcb-90be-386e8e0c30f9",
"name": "Retrieve Table Rows",
"type": "n8n-nodes-base.dataTable",
"position": [
5296,
480
],
"parameters": {
"operation": "get",
"returnAll": true,
"dataTableId": {
"__rl": true,
"mode": "list",
"value": "E1tGnBBo2NJauMUs",
"cachedResultUrl": "/projects/d8siYNB21z0O177I/datatables/E1tGnBBo2NJauMUs",
"cachedResultName": "job_data_table"
}
},
"executeOnce": true,
"typeVersion": 1.1
},
{
"id": "045845f6-69be-4393-acf2-b0963ebc36e8",
"name": "Process Table Rows",
"type": "n8n-nodes-base.code",
"position": [
5488,
480
],
"parameters": {
"jsCode": "// n8n Code node\n// Mode: Run Once for All Items\n//\n// Input: one item per ACTIVE job ad\n// Output: one item per company_domain\n//\n// Expected input fields per job (best effort; code is defensive):\n// - company_domain\n// - title\n// - external_url OR url\n// - date_posted OR created_at OR posted_at\n// - location OR city / state / country\n// - job_sources (array with source fields) OR source\n// - applicants_count (optional)\n//\n// Notes:\n// - No historical storage is used.\n// - Since this is an active digest, job_active_cs_listings_total = number of active rows in input for that company.\n// - \"Hottest job\" logic:\n// 1) highest applicants_count, if at least one job for the company has a numeric applicants_count\n// 2) otherwise most recent posted date\n// - jobs_last_7_days / jobs_last_30_days are based on the best available posted date field.\n\nfunction normalizeDomain(value) {\n return String(value || '')\n .toLowerCase()\n .trim()\n .replace(/^www\\./, '');\n}\n\nfunction toDate(value) {\n if (!value) return null;\n const d = new Date(value);\n return Number.isNaN(d.getTime()) ? null : d;\n}\n\nfunction toIsoDate(value) {\n const d = toDate(value);\n return d ? d.toISOString() : '';\n}\n\nfunction getPostedDate(job) {\n return (\n job.date_posted ||\n job.posted_at ||\n job.created_at ||\n job.createdAt ||\n job.updated_at ||\n ''\n );\n}\n\nfunction parseApplicantsCount(value) {\n if (value === null || value === undefined || value === '') return null;\n\n // already numeric\n if (typeof value === 'number' && Number.isFinite(value)) return value;\n\n const str = String(value).trim().toLowerCase();\n\n // common patterns:\n // \"23\", \"23 applicants\", \"23+ applicants\", \"100+\", \"over 200 applicants\"\n const match = str.match(/(\\d+(?:[.,]\\d+)?)/);\n if (!match) return null;\n\n const num = Number(match[1].replace(',', '.'));\n return Number.isFinite(num) ? num : null;\n}\n\nfunction buildLocation(job) {\n if (job.location && String(job.location).trim()) {\n return String(job.location).trim();\n }\n\n const parts = [\n job.city,\n job.state,\n job.country\n ]\n .map(v => String(v || '').trim())\n .filter(Boolean);\n\n return parts.join(', ');\n}\n\nfunction extractSources(job) {\n const out = new Set();\n\n // single source field\n if (job.source) {\n out.add(String(job.source).trim());\n }\n\n // nested job_sources array\n if (Array.isArray(job.job_sources)) {\n for (const s of job.job_sources) {\n if (!s) continue;\n if (s.source) out.add(String(s.source).trim());\n else if (s.name) out.add(String(s.name).trim());\n }\n }\n\n return [...out].filter(Boolean);\n}\n\nfunction choosePrimaryLocation(locationCounts) {\n let bestLocation = '';\n let bestCount = -1;\n\n for (const [location, count] of Object.entries(locationCounts)) {\n if (count > bestCount) {\n bestLocation = location;\n bestCount = count;\n }\n }\n\n return bestLocation;\n}\n\nfunction chooseHottestJob(jobs) {\n // First preference: highest applicants_count if present\n const jobsWithApplicants = jobs\n .map(job => ({\n job,\n applicants_count_num: parseApplicantsCount(job.applicants_count),\n postedDate: toDate(getPostedDate(job)),\n }))\n .filter(x => x.applicants_count_num !== null);\n\n if (jobsWithApplicants.length > 0) {\n jobsWithApplicants.sort((a, b) => {\n if (b.applicants_count_num !== a.applicants_count_num) {\n return b.applicants_count_num - a.applicants_count_num;\n }\n\n const aTime = a.postedDate ? a.postedDate.getTime() : 0;\n const bTime = b.postedDate ? b.postedDate.getTime() : 0;\n return bTime - aTime;\n });\n\n return jobsWithApplicants[0].job;\n }\n\n // Fallback: most recent posted date\n const sortedByDate = [...jobs].sort((a, b) => {\n const aTime = toDate(getPostedDate(a))?.getTime() || 0;\n const bTime = toDate(getPostedDate(b))?.getTime() || 0;\n return bTime - aTime;\n });\n\n return sortedByDate[0] || null;\n}\n\nconst inputJobs = $input.all().map(i => i.json);\nconst now = new Date();\nconst ms7 = 7 * 24 * 60 * 60 * 1000;\nconst ms30 = 30 * 24 * 60 * 60 * 1000;\n\nconst companies = {};\n\n// Group jobs by company_domain\nfor (const job of inputJobs) {\n const companyDomain = normalizeDomain(job.company_domain);\n\n if (!companyDomain) continue;\n\n if (!companies[companyDomain]) {\n companies[companyDomain] = {\n company_domain: companyDomain,\n jobs: [],\n locationCounts: {},\n sourceSet: new Set(),\n latestPostedDate: null,\n earliestPostedDate: null,\n };\n }\n\n const company = companies[companyDomain];\n company.jobs.push(job);\n\n // location aggregation\n const location = buildLocation(job);\n if (location) {\n company.locationCounts[location] = (company.locationCounts[location] || 0) + 1;\n }\n\n // source aggregation\n const sources = extractSources(job);\n for (const source of sources) {\n company.sourceSet.add(source);\n }\n\n // posted date range\n const postedDate = toDate(getPostedDate(job));\n if (postedDate) {\n if (!company.latestPostedDate || postedDate > company.latestPostedDate) {\n company.latestPostedDate = postedDate;\n }\n if (!company.earliestPostedDate || postedDate < company.earliestPostedDate) {\n company.earliestPostedDate = postedDate;\n }\n }\n}\n\n// Build company digest rows\nconst result = [];\n\nfor (const companyDomain of Object.keys(companies)) {\n const company = companies[companyDomain];\n const jobs = company.jobs;\n\n const jobsLast7Days = jobs.filter(job => {\n const posted = toDate(getPostedDate(job));\n return posted ? (now - posted) <= ms7 : false;\n }).length;\n\n const jobsLast30Days = jobs.filter(job => {\n const posted = toDate(getPostedDate(job));\n return posted ? (now - posted) <= ms30 : false;\n }).length;\n\n const hottestJob = chooseHottestJob(jobs) || {};\n const hottestPosted = getPostedDate(hottestJob);\n\n result.push({\n json: {\n company_domain: company.company_domain,\n\n job_active_cs_listings_total: jobs.length,\n jobs_last_7_days: jobsLast7Days,\n jobs_last_30_days: jobsLast30Days,\n\n job_signal_detected: jobs.length > 0,\n\n top_job_title: hottestJob.title || '',\n top_job_url: hottestJob.external_url || hottestJob.url || '',\n top_job_posted_at: toIsoDate(hottestPosted),\n\n top_job_applicants_count: parseApplicantsCount(hottestJob.applicants_count),\n top_job_location: buildLocation(hottestJob),\n\n primary_location: choosePrimaryLocation(company.locationCounts),\n locations_count: Object.keys(company.locationCounts).length,\n\n sources: [...company.sourceSet],\n sources_count: company.sourceSet.size,\n\n last_job_posted_at: company.latestPostedDate ? company.latestPostedDate.toISOString() : '',\n first_job_seen_at: company.earliestPostedDate ? company.earliestPostedDate.toISOString() : '',\n }\n });\n}\n\n// Optional: sort companies by active listings desc, then recent activity desc\nresult.sort((a, b) => {\n if (b.json.job_active_cs_listings_total !== a.json.job_active_cs_listings_total) {\n return b.json.job_active_cs_listings_total - a.json.job_active_cs_listings_total;\n }\n return b.json.jobs_last_7_days - a.json.jobs_last_7_days;\n});\n\nreturn result;"
},
"typeVersion": 2
},
{
"id": "04cc241a-8761-422d-a789-a527093922a3",
"name": "Upsert Company Data",
"type": "n8n-nodes-base.dataTable",
"position": [
5664,
480
],
"parameters": {
"columns": {
"value": {
"top_job_url": "={{ $json.top_job_url }}",
"top_job_title": "={{ $json.top_job_title }}",
"company_domain": "={{ $json.company_domain }}",
"locations_count": "={{ $json.locations_count }}",
"jobs_last_7_days": "={{ $json.jobs_last_7_days }}",
"primary_location": "={{ $json.primary_location }}",
"top_job_location": "={{ $json.top_job_location }}",
"first_job_seen_at": "={{ $json.first_job_seen_at }}",
"jobs_last_30_days": "={{ $json.jobs_last_30_days }}",
"top_job_posted_at": "={{ $json.top_job_posted_at }}",
"last_job_posted_at": "={{ $json.last_job_posted_at }}",
"job_signal_detected": "={{ $json.job_signal_detected }}",
"top_job_applicants_count": "={{ $json.top_job_applicants_count }}",
"job_active_cs_listings_total": "={{ $json.job_active_cs_listings_total }}"
},
"schema": [
{
"id": "company_domain",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "company_domain",
"defaultMatch": false
},
{
"id": "job_active_cs_listings_total",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "job_active_cs_listings_total",
"defaultMatch": false
},
{
"id": "jobs_last_7_days",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "jobs_last_7_days",
"defaultMatch": false
},
{
"id": "jobs_last_30_days",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "jobs_last_30_days",
"defaultMatch": false
},
{
"id": "job_signal_detected",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "job_signal_detected",
"defaultMatch": false
},
{
"id": "top_job_title",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "top_job_title",
"defaultMatch": false
},
{
"id": "top_job_url",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "top_job_url",
"defaultMatch": false
},
{
"id": "top_job_posted_at",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "top_job_posted_at",
"defaultMatch": false
},
{
"id": "top_job_applicants_count",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "top_job_applicants_count",
"defaultMatch": false
},
{
"id": "top_job_location",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "top_job_location",
"defaultMatch": false
},
{
"id": "primary_location",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "primary_location",
"defaultMatch": false
},
{
"id": "locations_count",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "locations_count",
"defaultMatch": false
},
{
"id": "last_job_posted_at",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "last_job_posted_at",
"defaultMatch": false
},
{
"id": "first_job_seen_at",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "first_job_seen_at",
"defaultMatch": false
}
],
"mappingMode": "defineBelow",
"matchingColumns": [],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"filters": {
"conditions": [
{
"keyName": "company_domain",
"keyValue": "={{ $json.company_domain }}"
}
]
},
"options": {},
"operation": "upsert",
"dataTableId": {
"__rl": true,
"mode": "list",
"value": "UFWhZZy4KFSac27Q",
"cachedResultUrl": "/projects/d8siYNB21z0O177I/datatables/UFWhZZy4KFSac27Q",
"cachedResultName": "company_data"
}
},
"typeVersion": 1.1
},
{
"id": "899e9aeb-8131-4bcc-ab47-c47d3e1e7b43",
"name": "Compare Company Lists",
"type": "n8n-nodes-base.compareDatasets",
"position": [
6048,
-16
],
"parameters": {
"options": {
"skipFields": "updatedAt, createdAt, id"
},
"mergeByFields": {
"values": [
{
"field1": "company_domain",
"field2": "company_domain"
}
]
}
},
"typeVersion": 2.3
},
{
"id": "be2b135f-6fb3-4e73-9629-bde08c8db65b",
"name": "Identify New Companies",
"type": "n8n-nodes-base.code",
"position": [
6576,
512
],
"parameters": {
"jsCode": "// n8n Code node\n// Mode: Run Once for All Items\n//\n// Input: rows that exist only in B\n// HubSpot source node: Filter1\n\nfunction normalizeDomain(value) {\n return String(value || '')\n .toLowerCase()\n .trim()\n .replace(/^www\\./, '');\n}\n\n// Build HubSpot domain -> ids map\nconst hsItems = $items('Filter Active Companies', 0, 0) || [];\nconst hubspotByDomain = {};\n\nfor (const item of hsItems) {\n const row = item.json || {};\n const domain = normalizeDomain(row.properties.website.versions[0].value);\n if (!domain) continue;\n\n hubspotByDomain[domain] = {\n company_id: row.companyId ?? null,\n portal_id: row.portalId ?? null,\n };\n}\n\n// Join current input with HubSpot map\nconst out = [];\n\nfor (const item of $input.all()) {\n const row = { ...(item.json || {}) };\n const domain = normalizeDomain(row.company_domain);\n\n const hs = hubspotByDomain[domain] || {};\n\n row.company_domain = domain;\n row.company_id = hs.company_id ?? null;\n row.portal_id = hs.portal_id ?? null;\n\n out.push({ json: row });\n}\n\nreturn out;"
},
"typeVersion": 2
},
{
"id": "6736a184-d0cc-4e84-8ff7-b58b5e9bd77e",
"name": "Identify Updated Companies",
"type": "n8n-nodes-base.code",
"position": [
6576,
320
],
"parameters": {
"jsCode": "// n8n Code node\n// Mode: Run Once for All Items\n//\n// Input: comparison node \"Different\" output items\n// Looks up HubSpot company ID from node: Filter1\n\nfunction normalizeDomain(value) {\n return String(value || '')\n .toLowerCase()\n .trim()\n .replace(/^www\\./, '');\n}\n\nfunction toNumber(value) {\n if (value === null || value === undefined || value === '') return null;\n const num = Number(value);\n return Number.isFinite(num) ? num : null;\n}\n\nfunction toBoolean(value) {\n if (typeof value === 'boolean') return value;\n if (value === 1 || value === '1') return true;\n if (value === 0 || value === '0') return false;\n if (typeof value === 'string') {\n const v = value.toLowerCase().trim();\n if (v === 'true') return true;\n if (v === 'false') return false;\n }\n return value;\n}\n\n// --- Build HubSpot domain -> companyId map from Filter1 ---\nconst hsItems = $items('Filter Active Companies', 0, 0) || [];\nconst hubspotByDomain = {};\n\nfor (const item of hsItems) {\n const row = item.json || {};\n const domain = normalizeDomain(row.properties?.domain?.value);\n if (!domain) continue;\n\n hubspotByDomain[domain] = {\n company_id: row.companyId ?? null,\n portal_id: row.portalId ?? null,\n };\n}\n\n// --- Normalize comparison rows ---\nconst inputItems = $input.all();\nconst output = [];\n\nfor (const item of inputItems) {\n const row = item.json || {};\n const keys = row.keys || {};\n const same = row.same || {};\n const different = row.different || {};\n\n // Start from unchanged values\n const normalized = { ...same };\n\n // Overlay changed fields with inputB values\n for (const [field, diffValue] of Object.entries(different)) {\n if (diffValue && typeof diffValue === 'object' && 'inputB' in diffValue) {\n normalized[field] = diffValue.inputB;\n }\n }\n\n // Ensure company_domain exists\n if (!normalized.company_domain && keys.company_domain) {\n normalized.company_domain = keys.company_domain;\n }\n\n normalized.company_domain = normalizeDomain(normalized.company_domain);\n\n // Attach HubSpot company ID\n const hs = hubspotByDomain[normalized.company_domain] || {};\n normalized.company_id = hs.company_id ?? null;\n normalized.portal_id = hs.portal_id ?? null;\n\n // --- Change analysis ---\n // Current (B) and previous (A) active counts\n let currentActive = toNumber(normalized.job_active_cs_listings_total);\n let previousActive = currentActive;\n\n if (\n different.job_active_cs_listings_total &&\n typeof different.job_active_cs_listings_total === 'object'\n ) {\n previousActive = toNumber(different.job_active_cs_listings_total.inputA);\n currentActive = toNumber(different.job_active_cs_listings_total.inputB);\n }\n\n let delta = null;\n let trend = 'unchanged';\n let rose = false;\n let fell = false;\n\n if (previousActive !== null && currentActive !== null) {\n delta = currentActive - previousActive;\n if (delta > 0) {\n trend = 'rose';\n rose = true;\n } else if (delta < 0) {\n trend = 'fell';\n fell = true;\n }\n }\n\n // Top title change\n let previousTopTitle = normalized.top_job_title || '';\n let currentTopTitle = normalized.top_job_title || '';\n let topTitleChanged = false;\n\n if (different.top_job_title && typeof different.top_job_title === 'object') {\n previousTopTitle = different.top_job_title.inputA || '';\n currentTopTitle = different.top_job_title.inputB || '
Credentials you'll need
Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.
coresignalApihubspotOAuth2Api
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This workflow runs weekly (or manually) to pull HubSpot companies, search Coresignal job ads via Elasticsearch for customer support roles by company domain, maintain job snapshot and job tables in n8n Data Tables, and update HubSpot company properties with job-signal metrics.…
Source: https://n8n.io/workflows/16226/ — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
This workflow runs hourly to find HubSpot contacts whose lead score drops below a threshold, deduplicates alerts using Google Sheets, notifies the owner via Slack, creates a HubSpot follow-up task, se
Monitor CRM accounts for hiring spikes by enriching HubSpot companies with PredictLeads job data and alerting your team via Slack.
Zendesk Hubspot. Uses zendesk, hubspot, functionItem. Scheduled trigger; 13 nodes.
This workflow automatically syncs your Zendesk tickets to your HubSpot contacts. Every 5 minutes, your Zendesk account collects all the new or updated tickets and syncs them accordingly, with your Hub
Every time a new company is added to your HubSpot CRM, this workflow automatically enriches it with multi-source company data from Coresignal - no manual research required. It detects the creation eve