This workflow corresponds to n8n.io template #9397 — we link there as the canonical source.
This workflow follows the Google Sheets → OpenAI recipe pattern — see all workflows that pair these two integrations.
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": "REDACTED_WORKFLOW_ID",
"meta": {
"templateCredsSetupCompleted": true
},
"name": "CyberPulse Compliance \u2013 v2 batch pipeline",
"tags": [],
"nodes": [
{
"id": "c38c20d9-247b-4cda-b487-3cae9c648bf0",
"name": "Manual Trigger",
"type": "n8n-nodes-base.manualTrigger",
"position": [
-1200,
304
],
"parameters": {},
"typeVersion": 1
},
{
"id": "978dc46e-747b-4960-b233-958f13971896",
"name": "Append row in sheet",
"type": "n8n-nodes-base.googleSheets",
"position": [
2016,
576
],
"parameters": {
"columns": {
"value": {
"score": "={{ $json.score }}",
"status": "={{ $json.status }}",
"rationale": "={{ $json.rationale }}",
"timestamp": "={{ $json.timestamp }}",
"ai_summary": "={{ $json.ai_summary }}",
"categories": "={{ $json.categories }}",
"confidence": "={{ $json.confidence }}",
"control_id": "={{ $json.control_id }}",
"evaluation": "={{ $json.evaluation }}",
"ai_findings": "={{ $json.ai_findings }}",
"mapped_count": "={{ $json.mapped_count }}",
"mapping_flat": "={{ $json.mapping_flat }}",
"response_text": "={{ $json.response_text }}",
"engine_version": "={{ $json.engine_version }}",
"evidence_count": "={{ $json.evidence_count }}",
"evidence_url_1": "={{ $json.evidence_url_1 }}",
"evidence_url_2": "={{ $json.evidence_url_2 }}",
"evidence_url_3": "={{ $json.evidence_url_3 }}",
"evidence_url_4": "={{ $json.evidence_url_4 }}",
"ai_recommendations": "={{ $json.ai_recommendations }}",
"control_description": "={{ $json.control_description }}",
"frameworks_selected": "={{ $json.frameworks_selected }}",
"implementation_notes": "={{ $json.implementation_notes }}"
},
"schema": [
{
"id": "timestamp",
"type": "string",
"display": true,
"required": false,
"displayName": "timestamp",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "control_id",
"type": "string",
"display": true,
"required": false,
"displayName": "control_id",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "control_description",
"type": "string",
"display": true,
"required": false,
"displayName": "control_description",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "response_text",
"type": "string",
"display": true,
"required": false,
"displayName": "response_text",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "implementation_notes",
"type": "string",
"display": true,
"required": false,
"displayName": "implementation_notes",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "evidence_url_1",
"type": "string",
"display": true,
"required": false,
"displayName": "evidence_url_1",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "evidence_url_2",
"type": "string",
"display": true,
"required": false,
"displayName": "evidence_url_2",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "evidence_url_3",
"type": "string",
"display": true,
"required": false,
"displayName": "evidence_url_3",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "evidence_url_4",
"type": "string",
"display": true,
"required": false,
"displayName": "evidence_url_4",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "status",
"type": "string",
"display": true,
"required": false,
"displayName": "status",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "evaluation",
"type": "string",
"display": true,
"required": false,
"displayName": "evaluation",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "score",
"type": "string",
"display": true,
"required": false,
"displayName": "score",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "confidence",
"type": "string",
"display": true,
"required": false,
"displayName": "confidence",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "rationale",
"type": "string",
"display": true,
"required": false,
"displayName": "rationale",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "categories",
"type": "string",
"display": true,
"required": false,
"displayName": "categories",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "evidence_count",
"type": "string",
"display": true,
"required": false,
"displayName": "evidence_count",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "mapped_count",
"type": "string",
"display": true,
"required": false,
"displayName": "mapped_count",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "mapping_flat",
"type": "string",
"display": true,
"required": false,
"displayName": "mapping_flat",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "frameworks_selected",
"type": "string",
"display": true,
"required": false,
"displayName": "frameworks_selected",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "engine_version",
"type": "string",
"display": true,
"required": false,
"displayName": "engine_version",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "ai_summary",
"type": "string",
"display": true,
"required": false,
"displayName": "ai_summary",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "ai_findings",
"type": "string",
"display": true,
"required": false,
"displayName": "ai_findings",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "ai_recommendations",
"type": "string",
"display": true,
"required": false,
"displayName": "ai_recommendations",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "priority",
"type": "string",
"display": true,
"required": false,
"displayName": "priority",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "owner",
"type": "string",
"display": true,
"required": false,
"displayName": "owner",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "due_date",
"type": "string",
"display": true,
"required": false,
"displayName": "due_date",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "ticket_id",
"type": "string",
"display": true,
"required": false,
"displayName": "ticket_id",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "next_action",
"type": "string",
"display": true,
"required": false,
"displayName": "next_action",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "evidence_gap_flag",
"type": "string",
"display": true,
"required": false,
"displayName": "evidence_gap_flag",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "policy_gap_flag",
"type": "string",
"display": true,
"required": false,
"displayName": "policy_gap_flag",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "last_run_id",
"type": "string",
"display": true,
"required": false,
"displayName": "last_run_id",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "source_sheet_row",
"type": "string",
"display": true,
"required": false,
"displayName": "source_sheet_row",
"defaultMatch": false,
"canBeUsedToMatch": true
}
],
"mappingMode": "defineBelow",
"matchingColumns": [],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {},
"operation": "append",
"sheetName": {
"__rl": true,
"mode": "list",
"value": 1117838353,
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/REDACTED_SHEET_ID_1/edit#gid=1117838353",
"cachedResultName": "controls_results_template.csv"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "REDACTED_SHEET_ID_1",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/REDACTED_SHEET_ID_1/edit?usp=drivesdk",
"cachedResultName": "controls_results_template"
}
},
"typeVersion": 4.7
},
{
"id": "8cc0a735-cfdb-47cd-8807-680a84230570",
"name": "Get row(s) in sheet",
"type": "n8n-nodes-base.googleSheets",
"position": [
-1184,
544
],
"parameters": {
"options": {},
"sheetName": {
"__rl": true,
"mode": "list",
"value": 1182991363,
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/REDACTED_SHEET_ID_2/edit#gid=1182991363",
"cachedResultName": "Sheet1"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "REDACTED_SHEET_ID_2",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/REDACTED_SHEET_ID_2/edit?usp=drivesdk",
"cachedResultName": "controls_input_mock_100_rows"
}
},
"typeVersion": 4.7
},
{
"id": "fc5a7cd1-f2a3-4711-9e62-c82e559f95e3",
"name": "Explain & Recommend",
"type": "@n8n/n8n-nodes-langchain.openAi",
"position": [
448,
624
],
"parameters": {
"modelId": {
"__rl": true,
"mode": "list",
"value": "gpt-4o-mini",
"cachedResultName": "GPT-4O-MINI"
},
"options": {},
"messages": {
"values": [
{
"role": "system",
"content": "=Use only the provided fields. Do not invent evidence, numbers, or frameworks.\nReturn a JSON object with keys exactly:\n- ai_summary (string)\n- ai_findings (array of 3 short strings), no bullets, no dashes, no numbering, no checkboxes.\n- ai_recommendations (array of 3 short, actionable strings), no bullets, no dashes, no numbering, no checkboxes.\nNo other keys.\n"
},
{
"content": "={\n \"timestamp\": \"{{ $('Edit Fields').item.json.timestamp }}\",\n \"control_id\": \"{{ $('Edit Fields').item.json.control_id }}\",\n \"control_description\": \"{{ $('Edit Fields').item.json.control_description }}\",\n \"response_text\": \"{{ $('Edit Fields').item.json.response_text }}\",\n \"implementation_notes\": \"{{$json.implementation_notes || ''}}\",\n\n \"status\": \"{{$json.status}}\",\n \"evaluation\": \"{{$json.evaluation}}\",\n \"score\": {{ +$json.score }},\n \"confidence\": {{ +$json.confidence }},\n \"rationale\": \"{{$json.rationale}}\",\n\n \"evidence_count\": {{\n Array.isArray($json.evidence)\n ? $json.evidence.filter(u => u && String(u).trim()).length\n : ([\n $json.evidence_url_1,\n $json.evidence_url_2,\n $json.evidence_url_3,\n $json.evidence_url_4\n ].filter(u => u && String(u).trim()).length || ($json.evidence_count ?? 0))\n}},\n\n \"mapped_count\": {{ Array.isArray($json.mapped_requirements) ? $json.mapped_requirements.length : ($json.mapped_count ?? 0) }},\n \"mapping_flat\": \"{{ Array.isArray($json.mapped_requirements) ? $json.mapped_requirements.map(m => [m.framework, m.clause, m.title].filter(Boolean).join(': ')).join(' | ') : ($json.mapping_flat || '') }}\",\n \"categories\": \"{{ Array.isArray($json.categories) ? $json.categories.join(', ') : ($json.categories || '') }}\",\n \"frameworks_selected\": \"{{ Array.isArray($json.mapped_requirements) ? [...new Set($json.mapped_requirements.map(m => m.framework).filter(Boolean))].join(', ') : ($json.frameworks_selected || '') }}\",\n \"engine_version\": \"{{$json.engine_version || ''}}\",\n\n \"format_instructions\": \"Return ai_summary exactly as: 'Status: {status}. Evaluation: {evaluation}. Score: {score}. Confidence: {confidence}. Evidence items: {evidence_count}. Categories: {categories}. Mappings: {mapping_flat}'. Then return ai_findings as a single string with 3 short bullets (prefix each with '\u2022 ') grounded in rationale/categories. Then return ai_recommendations as a single string with 3 short, actionable bullets (prefix each with '\u2022 ') tied to mapping_flat and evidence_count. Also compute priority, next_action, evidence_gap_flag, policy_gap_flag using the rules below.\\n\\nRules:\\n- priority:\\n - P1 if (status == 'Non-Compliant' && mapped_count >= 3) OR (score < 50 && evidence_count == 0)\\n - P2 if (status in ['Non-Compliant','Partial'] && (score >= 50 && score < 75)) OR (evidence_count <= 1)\\n - P3 if (status == 'Compliant' && (confidence < 60 || evidence_count < 2))\\n - else P4\\n- next_action:\\n - 'implement' if status == 'Non-Compliant'\\n - 'remediate' if status == 'Partial'\\n - 'monitor' if status == 'Compliant' and (confidence < 60 || evidence_count < 2)\\n - else 'review'\\n- evidence_gap_flag: 'yes' if evidence_count == 0 OR evidence_count == 1, else 'no'\\n- policy_gap_flag: 'yes' if ('policy' appears in (response_text or rationale) case-insensitive) OR categories contains 'policy', else 'no'\"\n}\n"
}
]
},
"jsonOutput": true
},
"typeVersion": 1.8
},
{
"id": "242c3fcf-4293-4a1b-b229-a1c9e0938ae2",
"name": "Parse + attach to each item",
"type": "n8n-nodes-base.code",
"position": [
1504,
576
],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "// Helpers\nfunction isNonEmpty(x){ if(x===undefined||x===null) return false; if(typeof x==='number') return true; return String(x).trim()!==''; }\nfunction prefer(...vals){ for(const v of vals){ if(isNonEmpty(v)) return v; } return ''; }\nfunction safeNum(x, def=''){ const n = Number(x); return Number.isFinite(n) ? n : def; }\n\n// Strip any leading bullets / dashes / numbers / checkboxes before we add our own bullets\nfunction stripLeadingMarkers(s){\n return String(s).replace(/^\\s*(?:[-*\u2022\\u2022]+|\\d+[.)]|[\u2713\u2714\u2717\u2718xX\\[\\]\\(\\)])\\s*/u, '');\n}\nfunction asArray(x){\n if(!x) return [];\n if(Array.isArray(x)) return x.filter(Boolean).map(v => stripLeadingMarkers(v));\n return String(x).split(/\\r?\\n+/).map(v => stripLeadingMarkers(v)).filter(Boolean);\n}\nfunction bullets(x){\n const a = asArray(x);\n return a.length ? a.map(s => `\u2022 ${s}`).join('\\n') : '';\n}\n\n// Current item from Merge\nconst i = $json;\n// Originals straight from \"Edit Fields\" (for fields the merge might not include)\nconst src = $(\"Edit Fields\").item?.json ?? {};\n\n// ---- Ingest (preserve originals)\nconst timestamp = prefer(i.timestamp, src.timestamp, new Date().toISOString());\nconst control_id = prefer(i.control_id, src.control_id);\nconst control_description = prefer(i.control_description, src.control_description);\nconst response_text = prefer(i.response_text, src.response_text);\nconst implementation_notes= prefer(i.implementation_notes, src.implementation_notes);\nconst evidence_url_1 = prefer(i.evidence_url_1, src.evidence_url_1);\nconst evidence_url_2 = prefer(i.evidence_url_2, src.evidence_url_2);\nconst evidence_url_3 = prefer(i.evidence_url_3, src.evidence_url_3);\nconst evidence_url_4 = prefer(i.evidence_url_4, src.evidence_url_4);\n\n// ---- Scoring & mapping\nconst status = prefer(i.status, 'Unknown');\nconst evaluation = prefer(i.evaluation, status);\nconst score = safeNum(i.score);\nconst confidence = safeNum(i.confidence);\nconst rationaleIn = prefer(i.rationale, i.reason);\n\n// categories may be array or string\nconst categoriesStr = Array.isArray(i.categories) ? i.categories.join(', ') : prefer(i.categories);\n\n// evidence: prefer i.evidence (array), else derive from URL fields, then filter empties\nlet evidenceArr = [];\nif (Array.isArray(i.evidence)) {\n evidenceArr = i.evidence.filter(u => u && String(u).trim());\n} else {\n evidenceArr = [evidence_url_1, evidence_url_2, evidence_url_3, evidence_url_4]\n .filter(u => u && String(u).trim());\n}\nconst evidence_count = isNonEmpty(i.evidence_count) ? safeNum(i.evidence_count, evidenceArr.length) : evidenceArr.length;\n\n// mapped requirements\nconst mappedReqs = Array.isArray(i.mapped_requirements) ? i.mapped_requirements : [];\nconst mapped_count = isNonEmpty(i.mapped_count) ? safeNum(i.mapped_count, mappedReqs.length) : mappedReqs.length;\nconst mapping_flat = isNonEmpty(i.mapping_flat)\n ? String(i.mapping_flat)\n : mappedReqs.map(m => [m.framework, m.clause, m.title].filter(Boolean).join(': ')).join(' | ');\n\n// frameworks selected (pretty commas)\nconst frameworks_selected = (isNonEmpty(i.frameworks_selected)\n ? String(i.frameworks_selected)\n : [...new Set(mappedReqs.map(m => m.framework).filter(Boolean))].join(', ')\n).replace(/,\\s*/g, ', ');\n\nconst engine_version = prefer(i.engine_version, i.version);\n\n// ---- AI outputs (handle message.content object OR JSON string; also accept top-level)\nlet ai_summary = i.ai_summary;\nlet ai_findings_any = i.ai_findings;\nlet ai_recommendations_any = i.ai_recommendations;\n\nif ((!ai_summary || (!ai_findings_any && !ai_recommendations_any)) && i.message?.content) {\n if (typeof i.message.content === 'object') {\n const c = i.message.content;\n ai_summary = ai_summary ?? c.ai_summary;\n ai_findings_any = ai_findings_any ?? c.ai_findings;\n ai_recommendations_any = ai_recommendations_any ?? c.ai_recommendations;\n } else {\n try {\n const parsed = JSON.parse(i.message.content);\n ai_summary = ai_summary ?? parsed.ai_summary;\n ai_findings_any = ai_findings_any ?? parsed.ai_findings;\n ai_recommendations_any = ai_recommendations_any ?? parsed.ai_recommendations;\n } catch {}\n }\n}\n\n// Normalize to single strings for the sheet (no double bullets)\nconst ai_findings = bullets(ai_findings_any);\nconst ai_recommendations = bullets(ai_recommendations_any);\n\n// Sync the item count inside rationale (e.g., replace \"(4 items)\" with \"(3 items)\")\nlet rationale = rationaleIn;\nif (isNonEmpty(rationale)) {\n rationale = rationale.replace(/\\(\\s*\\d+\\s*items?\\s*\\)/i, `(${evidence_count} items)`);\n}\n\n// Synthesize summary if missing (deterministic)\nif (!isNonEmpty(ai_summary)) {\n ai_summary = `Status: ${status}. Evaluation: ${evaluation}. Score: ${score}. `\n + `Confidence: ${confidence}. Evidence items: ${evidence_count}. `\n + `Categories: ${categoriesStr}. Mappings: ${mapping_flat}`;\n}\n\n// Return final row payload\nreturn {\n json: {\n // Ingest\n timestamp,\n control_id,\n control_description,\n response_text,\n implementation_notes,\n evidence_url_1,\n evidence_url_2,\n evidence_url_3,\n evidence_url_4,\n\n // Scoring & mapping\n status,\n evaluation,\n score,\n confidence,\n rationale,\n categories: categoriesStr,\n evidence_count,\n mapped_count,\n mapping_flat,\n frameworks_selected,\n engine_version,\n\n // AI (strings)\n ai_summary,\n ai_findings,\n ai_recommendations\n }\n};"
},
"typeVersion": 2
},
{
"id": "3df145fc-4393-4bd9-85c2-8b69ce7b1b1e",
"name": "Loop Over Items",
"type": "n8n-nodes-base.splitInBatches",
"position": [
-32,
544
],
"parameters": {
"options": {}
},
"typeVersion": 3
},
{
"id": "4e2b71c8-a9ac-4433-96a8-f53097334081",
"name": "Edit Fields",
"type": "n8n-nodes-base.set",
"position": [
-928,
560
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "87318d44-cd2c-47d8-850a-c0b152d6b455",
"name": "row_number",
"type": "string",
"value": "={{ $json.row_number }}"
},
{
"id": "9eab7a02-e58e-46a8-85cb-d2a41693759c",
"name": "timestamp",
"type": "string",
"value": "={{ $json.timestamp }}"
},
{
"id": "6d3fa2f3-5e8b-45c6-80f6-e9b713fd82e4",
"name": "control_description",
"type": "string",
"value": "={{ $json.control_description }}"
},
{
"id": "e0b7a487-bc52-40d9-80a6-5bc2a13c19a0",
"name": "response_text",
"type": "string",
"value": "={{ $json.response_text }}"
},
{
"id": "b6347812-76af-454b-8869-81c47f4c90a8",
"name": "evidence_url_1",
"type": "string",
"value": "={{ $json.evidence_url_1 }}"
},
{
"id": "f8784a98-ead8-4af2-a1ca-42aa2fcc6032",
"name": "evidence_url_2",
"type": "string",
"value": "={{ $json.evidence_url_2 }}"
},
{
"id": "d91dceb0-e1a7-4322-89a9-041a34e00f52",
"name": "evidence_url_3",
"type": "string",
"value": "={{ $json.evidence_url_3 }}"
},
{
"id": "14d6374b-5191-4f4f-85b0-d443a9e404ee",
"name": "evidence_url_4",
"type": "string",
"value": "={{ $json.evidence_url_4 }}"
},
{
"id": "b8bd82a7-c19c-458b-a0cb-5cf2941efc0c",
"name": "implementation_notes",
"type": "string",
"value": "={{ $json.implementation_notes }}"
},
{
"id": "fb10d1d9-da75-4f06-8a12-57cbda7c39b2",
"name": "evidenceUrls_clean",
"type": "array",
"value": "={{ [$json.evidence_url_1, $json.evidence_url_2, $json.evidence_url_3, $json.evidence_url_4].filter(url => url != null && url !== '').map(url => String(url).trim()).filter(url => url.length > 0 && url.startsWith('http')) }}"
}
]
},
"includeOtherFields": true
},
"typeVersion": 3.4
},
{
"id": "3b945dd3-e849-4264-bacf-760802595467",
"name": "Merge1",
"type": "n8n-nodes-base.merge",
"position": [
1024,
576
],
"parameters": {
"mode": "combine",
"options": {
"includeUnpaired": true
},
"combineBy": "combineByPosition"
},
"typeVersion": 3.2
},
{
"id": "f3debc88-ff96-40b4-aa79-71d9b5507a4d",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1520,
208
],
"parameters": {
"color": 4,
"width": 528,
"height": 512,
"content": "\n\n\ud83d\udfe2 Manual Trigger \u2014 Start/diagnostics\n\nReceives POST and starts the run; echoes runId for tracing\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\ud83d\udfe9 Get row(s) in sheet \u2014 Read inputs\n\nLoads model/routing settings from\n the config sheet."
},
"typeVersion": 1
},
{
"id": "532fb675-a2f1-4c21-943a-b8b92de4e31a",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
-976,
336
],
"parameters": {
"color": 5,
"width": 400,
"height": 384,
"content": "\n\n\n\n\n\ud83d\udfe6 Edit Fields \u2014 Normalize columns\n\nMaps incoming keys, trims text, and sets safe defaults."
},
"typeVersion": 1
},
{
"id": "41740561-c43b-46e3-9794-892d6720d070",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"position": [
-560,
336
],
"parameters": {
"width": 432,
"height": 384,
"content": "\n\n\ud83d\udfe8 CyberPulse Compliance (Dev) \u2014 Score + map\n\nScores and maps each control using control text + implementation notes + up to 4 evidence URLs and selected frameworks.\nOutputs score (0\u2013100), status, confidence, binary evaluation, categories, crosswalk mappings, and adds gaps/actions when evidence is weak or missing."
},
"typeVersion": 1
},
{
"id": "789faf46-fd9f-4c9c-8316-00954cd4b10b",
"name": "Sticky Note3",
"type": "n8n-nodes-base.stickyNote",
"position": [
-112,
336
],
"parameters": {
"color": 6,
"width": 432,
"height": 384,
"content": "\n\n\ud83d\udfea Loop Over Items \u2014 Iterate per control\n\nIterates each control independently to run LLM \u2192 parse \u2192 append, preserving per-row context and emitting one result per input."
},
"typeVersion": 1
},
{
"id": "18d9e2a5-a850-4cc7-a60c-1af8e3ba6790",
"name": "Sticky Note4",
"type": "n8n-nodes-base.stickyNote",
"position": [
336,
336
],
"parameters": {
"color": 5,
"width": 512,
"height": 384,
"content": "\n\n\ud83d\udfe6 Explain & Recommend (Message Model) \u2014 Exec summary\n\nGenerates a strict-JSON executive summary\u2014ai_summary, three ai_findings, and three ai_recommendations\u2014from the control\u2019s status, score, confidence, categories, evidence, and framework mappings."
},
"typeVersion": 1
},
{
"id": "7c3b398d-4f6f-4712-aa1d-21e971abc634",
"name": "Sticky Note5",
"type": "n8n-nodes-base.stickyNote",
"position": [
864,
336
],
"parameters": {
"color": 3,
"width": 464,
"height": 384,
"content": "\n\n\ud83d\udfe7 Merge1 \u2014 Combine model + original\n\nCombines CyberPulse scoring/mapping with the LLM summary by position, producing one merged item per row."
},
"typeVersion": 1
},
{
"id": "3b2fdc19-9052-4d39-b6e7-b68f85e5bd82",
"name": "Sticky Note6",
"type": "n8n-nodes-base.stickyNote",
"position": [
1344,
336
],
"parameters": {
"width": 464,
"height": 384,
"content": "\n\n\ud83d\udfe8 Parse + attach to each item \u2014 Final shaping\n\nMerges CyberPulse scores/mappings with the LLM output by index into a single unified row."
},
"typeVersion": 1
},
{
"id": "2e250890-eac2-4c90-acfc-dd074c2fa9ab",
"name": "Sticky Note7",
"type": "n8n-nodes-base.stickyNote",
"position": [
1824,
336
],
"parameters": {
"color": 4,
"width": 496,
"height": 384,
"content": "\n\n\ud83d\udfe9 Append row in sheet \u2014 Write results\n\nAppends one result row per item to the results sheet with core, scoring, mapping, and AI fields, leaving future columns blank."
},
"typeVersion": 1
},
{
"id": "95c58b60-8383-4297-9185-d9b1f053ae66",
"name": "Sticky Note8",
"type": "n8n-nodes-base.stickyNote",
"position": [
0,
0
],
"parameters": {
"color": 7,
"width": 784,
"height": 288,
"content": "\n\nWhat is CyberPulse Agent workflow ?\n\nAutomates evidence-aware control scoring (0\u2013100) with deterministic gates and confidence from evidence/text quality.\nReads controls from Google Sheets (text, up to 4 evidence URLs, notes) and classifies, scores, and maps via the CyberPulse node.\nGenerates board-ready AI outputs per control: one-paragraph summary, 3 findings, and 3 actionable recommendations.\nWrites normalized, analytics-ready rows back to a results sheet with flattened framework mappings and detected categories.\nCovers ISO 27001, NIST CSF, SOC 2, PCI DSS, Essential Eight, GDPR; secure, scalable in n8n with tunable weights/thresholds."
},
"typeVersion": 1
},
{
"id": "632ae2b0-f5a7-4b6a-9296-99f30a3eb817",
"name": "Evaluate compliance control",
"type": "CUSTOM.cyberPulseCompliance",
"position": [
-416,
560
],
"parameters": {
"controlText": "=={{ \n ($json.control_description || '') + ' ' +\n ($json.response_text || '') +\n ($json.implementation_notes ? (' ' + $json.implementation_notes) : '')\n}}\n",
"crosswalkUrl": "https://example.com/path/to/crosswalk.json",
"evidenceUrls": [
"={{ $json.evidenceUrls_clean[0] || '' }}",
"={{ $json.evidenceUrls_clean[1] || '' }}",
"={{ $json.evidenceUrls_clean[2] || '' }}",
"={{ $json.evidenceUrls_clean[3] || '' }}"
]
},
"typeVersion": 1
},
{
"id": "cb29029a-a50e-40c0-a89d-5979a2e8c6d2",
"name": "Code in JavaScript",
"type": "n8n-nodes-base.code",
"position": [
-704,
560
],
"parameters": {
"jsCode": "// Ultra-defensive evidence URL cleaning\nreturn items.map(item => {\n let evidenceUrls = item.json.evidenceUrls_clean;\n \n // Extra safety checks\n if (!Array.isArray(evidenceUrls)) {\n evidenceUrls = [];\n }\n \n // Force every element to be a valid string\n evidenceUrls = evidenceUrls\n .filter(url => url != null && url !== undefined)\n .map(url => {\n // Force to string and trim\n const str = String(url).trim();\n return str;\n })\n .filter(url => {\n // Only keep valid HTTP(S) URLs\n return url.length > 0 && \n (url.startsWith('http://') || url.startsWith('https://'));\n });\n \n return {\n json: {\n ...item.json,\n evidenceUrls_clean: evidenceUrls\n }\n };\n});"
},
"typeVersion": 2
}
],
"active": false,
"settings": {
"availableInMCP": false,
"executionOrder": "v1"
},
"versionId": "REDACTED_VERSION_ID",
"connections": {
"Merge1": {
"main": [
[
{
"node": "Parse + attach to each item",
"type": "main",
"index": 0
}
]
]
},
"Edit Fields": {
"main": [
[
{
"node": "Code in JavaScript",
"type": "main",
"index": 0
}
]
]
},
"Manual Trigger": {
"main": [
[
{
"node": "Get row(s) in sheet",
"type": "main",
"index": 0
}
]
]
},
"Loop Over Items": {
"main": [
[],
[
{
"node": "Explain & Recommend",
"type": "main",
"index": 0
},
{
"node": "Merge1",
"type": "main",
"index": 0
}
]
]
},
"Code in JavaScript": {
"main": [
[
{
"node": "Evaluate compliance control",
"type": "main",
"index": 0
}
]
]
},
"Append row in sheet": {
"main": [
[
{
"node": "Loop Over Items",
"type": "main",
"index": 0
}
]
]
},
"Explain & Recommend": {
"main": [
[
{
"node": "Merge1",
"type": "main",
"index": 1
}
]
]
},
"Get row(s) in sheet": {
"main": [
[
{
"node": "Edit Fields",
"type": "main",
"index": 0
}
]
]
},
"Evaluate compliance control": {
"main": [
[
{
"node": "Loop Over Items",
"type": "main",
"index": 0
}
]
]
},
"Parse + attach to each item": {
"main": [
[
{
"node": "Append row in sheet",
"type": "main",
"index": 0
}
]
]
}
}
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
What this template does
Source: https://n8n.io/workflows/9397/ — 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.
Ask questions like “How much did I spend on food last month?” and get instant answers from your financial data — directly in Telegram.
The Problem That it Solves
This intelligent email automation workflow helps you maximize engagement through domain-based outreach. It utilizes AI-powered personalization and strategic follow-ups to increase response rates. The
Note: Now includes an Apify alternative for Rapid API (Some users can't create new accounts on Rapid API, so I have added an alternative for you. But immediately you are able to get access to Rapid AP
Scrape ads – Pulls Facebook Ad Library data for "ai automation" keywords using Apify Filter & sort – Filters ads by page likes (>1,000) and separates into videos, images, and text ads Analyze creat