This workflow corresponds to n8n.io template #13828 — 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 →
{
"meta": {
"templateCredsSetupCompleted": true
},
"nodes": [
{
"id": "5f046833-986d-4b88-8047-e6bec5e006b6",
"name": "Keephub Form Webhook",
"type": "n8n-nodes-base.webhook",
"position": [
3696,
3392
],
"parameters": {
"path": "keephub-form-to-task",
"options": {},
"httpMethod": "POST"
},
"typeVersion": 2.1
},
{
"id": "747fd501-63b4-4996-bd5d-d3ffb1778ea2",
"name": "\ud83d\udccb Overview",
"type": "n8n-nodes-base.stickyNote",
"position": [
3344,
2816
],
"parameters": {
"width": 540,
"height": 800,
"content": "## \ud83d\udccb Keephub Form \u2192 AI Task Creator\n\n**What it does:**\nAutomatically creates intelligent follow-up tasks for managers when employees submit forms. AI analyzes the submission and generates a contextual task with relevant fields\u2014no manual work required.\n\n**How it works:**\n1. Employee submits any Keephub form\n2. Workflow identifies their manager (parent org node)\n3. AI analyzes submission data and designs an appropriate task\n4. Task is created and assigned to the manager\n\n**Quick Setup:**\n1. **Credentials:** Connect Keephub Login (4 nodes), Keephub Bearer (Create Task), and OpenAI\n2. **Config:** Set `groupId` to target specific user groups (e.g., only managers)\n3. **Test:** Submit a form and watch the magic happen\n\n**Pro tip:** Works with ANY Keephub form\u2014no per-form configuration needed."
},
"typeVersion": 1
},
{
"id": "9863427c-44d8-44fc-9c3d-d383cbddec57",
"name": "\ud83d\udea6 Gate",
"type": "n8n-nodes-base.stickyNote",
"position": [
3888,
3136
],
"parameters": {
"color": 7,
"width": 440,
"height": 476,
"content": "## \ud83d\udea6 Webhook Filter\n\n**Purpose:** Ensures only NEW form submissions trigger task creation.\n\n**Filters out:**\n\u2022 Status changes (approvals/rejections)\n\u2022 Edits to existing submissions\n\u2022 Any other webhook noise\n\n**Result:** Clean, predictable workflow execution."
},
"typeVersion": 1
},
{
"id": "509db577-2764-4119-a3c0-477c9d413ef5",
"name": "\ud83d\udd0d Context Resolution",
"type": "n8n-nodes-base.stickyNote",
"position": [
4320,
3056
],
"parameters": {
"color": 7,
"width": 2264,
"height": 904,
"content": "## \ud83d\udd0d Data Collection & Target Resolution\n\n**1. Extract Form Data**\nNormalizes all field types (NodeSelector, Radio, Checkbox, Date, etc.) into a clean, indexed structure.\n\n**2. Get Submitter**\nFetches the employee's profile and organizational unit memberships.\n\n**3. Resolve Org Node**\nDetermines which org node the submission belongs to:\n\u2022 Uses NodeSelector field value (if present in form)\n\u2022 Falls back to submitter's primary orgunit\n\u2022 Flexible: supports both explicit and automatic assignment\n\n**4. Get Parent Node**\nFinds the manager one level up in the hierarchy (with error handling).\n\n**5. Get Root Node** (fallback)\nIf no parent exists (submitter is at/near top), fetches the actual organizational root to prevent targeting errors.\n\n**6. Get Form Schema**\nRetrieves field labels and structure for building human-readable AI prompts.\n\n**Result:** Clean context ready for AI analysis."
},
"typeVersion": 1
},
{
"id": "36b456a7-ab81-4e8c-806b-e326f9337b04",
"name": "\ud83e\udd16 AI Task Design",
"type": "n8n-nodes-base.stickyNote",
"position": [
7056,
3056
],
"parameters": {
"color": 5,
"width": 1184,
"height": 1136,
"content": "## \ud83e\udd16 AI-Powered Task Generation\n\n**1. Build AI Prompt**\nCreates a structured prompt with:\n\u2022 Form title and all submitted field values\n\u2022 Task design rules (language, field types, validation)\n\u2022 Instructions for contextual, actionable output\n\n\ud83d\udca1 **Customization tip:** For single-form use, edit the prompt in \"Build AI Prompt\" node to specify exactly which fields you need.\n\n**2. Design Task with AI**\nGPT-4.1 analyzes the submission and generates:\n\u2022 Context-aware task title (references specifics from form)\n\u2022 Manager instructions paragraph\n\u2022 3-6 actionable form fields (RadioButtons, TextArea, DatePicker, etc.)\n\n**3. Compose Task**\n\u2022 Validates AI output (checks for required fields, malformed data)\n\u2022 Generates cryptographically strong field IDs\n\u2022 URL-encodes link back to original submission\n\u2022 Builds full Keephub API payload with dates, targeting, and permissions\n\n**4. Create Task**\nPosts to Keephub API. Mission accomplished. \u2705"
},
"typeVersion": 1
},
{
"id": "5bb25165-3a6e-458c-9363-07196a1fd88b",
"name": "\u2699\ufe0f Config Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
6560,
3056
],
"parameters": {
"color": 4,
"width": 500,
"height": 644,
"content": "## \u2699\ufe0f Configuration\n\nEdit these values in the **\u2699\ufe0f Config** node:\n\n**`groupId`** (string, recommended)\nKeephub group ID to target (e.g., \"65f8a3b2c1234567890abcde\").\n\u2022 **With groupId:** Task assigned only to users in that group\n\u2022 **Without groupId:** Task assigned to EVERYONE on the parent node\n\u2022 Find group IDs in Keephub admin panel\n\n**`taskDueDaysFromNow`** (number, default: 1)\nDays from now until task is due.\n\u2022 Must be \u2265 0 (validated)\n\u2022 Examples: 1 = tomorrow, 7 = next week\n\n**`workflow2WebhookUrl`** (string, optional)\nWebhook URL to chain a follow-up workflow.\n\u2022 Attached as hookurl to created task\n\u2022 Leave empty if not needed"
},
"typeVersion": 1
},
{
"id": "57e1bcea-03b3-412b-bb3f-978c3d967c8a",
"name": "Status Change?",
"type": "n8n-nodes-base.if",
"position": [
3920,
3392
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "cond-status",
"operator": {
"type": "boolean",
"operation": "exists",
"singleValue": true
},
"leftValue": "={{ $json.body.data.statusChanged }}",
"rightValue": ""
}
]
}
},
"typeVersion": 2.2
},
{
"id": "a6a5dfb7-2433-4811-bad6-b7a200f8c5fb",
"name": "Is Edit?",
"type": "n8n-nodes-base.if",
"position": [
4112,
3472
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "cond-edited",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
},
"leftValue": "={{ $json.body.data.isEdited }}",
"rightValue": ""
}
]
}
},
"typeVersion": 2.2
},
{
"id": "f237ae01-ee10-446a-8e58-282537441177",
"name": "Extract Form Data",
"type": "n8n-nodes-base.code",
"position": [
4384,
3632
],
"parameters": {
"jsCode": "const raw = $json;\nconst body = raw.body ?? raw;\nconst data = body.data ?? body;\n\nif (!data || typeof data !== 'object') {\n throw new Error('Unexpected webhook payload. Got: ' + JSON.stringify(raw).substring(0, 300));\n}\n\nconst meta = {\n formSubmissionId: data._id ?? null,\n contentRef: data.contentRef ?? null,\n submittedById: data.updatedBy ?? null,\n submittedAt: data.updatedAt ?? null,\n};\n\nfunction normalize(field) {\n const el = field.element;\n const v = field.value;\n let value;\n if (v == null) {\n value = null;\n } else if (el === 'NodeSelector') {\n value = typeof v === 'object' && !Array.isArray(v)\n ? { id: v.value, label: v.label ?? null, descendants: v.descendants ?? [] }\n : v;\n } else if (['RadioButtons', 'CheckboxGroup', 'Dropdown'].includes(el)) {\n value = Array.isArray(v) ? v.map(o => o.value || o.text?.nl || o.key) : v;\n } else if (el === 'Stars' || el === 'Smileys') {\n value = typeof v === 'number' ? v : parseInt(v, 10) || v;\n } else {\n value = v;\n }\n return { ffId: field.ffId ?? null, element: el ?? 'Unknown', value, raw: v };\n}\n\nconst fields = (data.values ?? []).map(normalize);\nconst byFfId = Object.fromEntries(fields.filter(f => f.ffId).map(f => [f.ffId, f]));\n\nreturn [{ json: { ...meta, fields, byFfId } }];\n"
},
"typeVersion": 2
},
{
"id": "35c784a5-9e78-4458-af10-60fa3ba40868",
"name": "Get Submitter",
"type": "n8n-nodes-keephub.keephub",
"position": [
4624,
3632
],
"parameters": {
"userId": "={{ $json.submittedById }}",
"resource": "user",
"authentication": "loginCredentials"
},
"typeVersion": 1
},
{
"id": "2885478b-5607-467a-8626-4bb031c6d2f0",
"name": "Resolve Org Node",
"type": "n8n-nodes-base.code",
"position": [
4864,
3632
],
"parameters": {
"jsCode": "const formData = $('Extract Form Data').first().json;\nconst userData = $input.first().json;\n\n// Strategy 1: first NodeSelector in form fields\nconst ns = formData.fields.find(f => f.element === 'NodeSelector');\nlet orgNodeId = null;\nlet resolvedFrom = null;\n\nif (ns && ns.raw) {\n orgNodeId = typeof ns.raw === 'object' ? ns.raw.value : String(ns.raw);\n resolvedFrom = 'NodeSelector';\n}\n\n// Strategy 2: submitter's first orgunit\nif (!orgNodeId && userData.orgunits && userData.orgunits.length > 0) {\n const first = userData.orgunits[0];\n orgNodeId = typeof first === 'string' ? first : (first._id || first.id || String(first));\n resolvedFrom = 'userProfile';\n}\n\nif (!orgNodeId) {\n throw new Error(\n 'No org node found. No NodeSelector in form and user ' +\n (userData._id || 'unknown') + ' has no orgunits.'\n );\n}\n\nreturn [{ json: { orgNodeId, resolvedFrom } }];\n"
},
"typeVersion": 2
},
{
"id": "e254d82c-1c2d-4f3f-9e25-c366d17c0cba",
"name": "Get Parent Node",
"type": "n8n-nodes-keephub.keephub",
"onError": "continueErrorOutput",
"position": [
5104,
3632
],
"parameters": {
"nodeId": "={{ $json.orgNodeId }}",
"resource": "orgchart",
"operation": "getParent",
"authentication": "loginCredentials"
},
"typeVersion": 1
},
{
"id": "993a4a4f-3e92-4fea-bd99-f57528dc44c8",
"name": "Get Form Schema",
"type": "n8n-nodes-keephub.keephub",
"position": [
6304,
3552
],
"parameters": {
"contentId": "={{ $('Extract Form Data').item.json.contentRef }}",
"operation": "getById",
"authentication": "loginCredentials"
},
"typeVersion": 1
},
{
"id": "2ee0543f-fb0c-4c0b-8f2d-6117340b9cae",
"name": "\u2699\ufe0f Config",
"type": "n8n-nodes-base.set",
"position": [
6768,
3552
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "cfg-001",
"name": "groupId",
"type": "string",
"value": ""
},
{
"id": "cfg-002",
"name": "taskDueDaysFromNow",
"type": "number",
"value": 1
},
{
"id": "cfg-003",
"name": "workflow2WebhookUrl",
"type": "string",
"value": ""
}
]
}
},
"typeVersion": 3.4
},
{
"id": "e7335a47-6017-4b2d-a712-47c9b04db9f1",
"name": "Build AI Prompt",
"type": "n8n-nodes-base.code",
"position": [
7216,
3552
],
"parameters": {
"jsCode": "const formData = $('Extract Form Data').first().json;\nconst schema = $('Get Form Schema').first().json;\n\nconst schemaFields = schema.form?.fields || schema.template?.form?.fields || [];\n\n// Lookup by ffId for precise matching\nconst schemaByFfId = {};\nschemaFields.forEach(sf => {\n const key = sf.ffId || sf.field_name;\n if (key) schemaByFfId[key] = sf;\n});\n\n// Fallback: sequential index over non-decorator fields\nconst inputFields = schemaFields.filter(\n sf => !['Paragraph', 'Header', 'Section'].includes(sf.element)\n);\n\nconst rawTitle = schema.title || schema.template?.title || {};\nconst formTitle = Object.values(rawTitle)[0] || 'Form submission';\n\nconst fieldLines = formData.fields.map((field, i) => {\n const sf = (field.ffId && schemaByFfId[field.ffId]) || inputFields[i];\n const label = sf?.label ? Object.values(sf.label)[0] : `Field ${i + 1}`;\n const el = field.element;\n let val;\n if (field.value == null) {\n val = '(not filled)';\n } else if (el === 'NodeSelector' && typeof field.value === 'object') {\n val = field.value.label || field.value.id;\n } else if (Array.isArray(field.value)) {\n val = field.value.join(', ');\n } else {\n val = String(field.value);\n }\n return `- ${label} [${el}]: ${val}`;\n}).join('\\n');\n\nconst prompt = `You are designing a follow-up task in Keephub for a retail manager.\\n\\nA \\\"${formTitle}\\\" was just submitted:\\n${fieldLines}\\n\\nDesign ONE actionable task the manager must complete in response.\\n\\nLanguage rules:\\n- Detect the language from the data (most likely \\\"nl\\\" or \\\"en\\\")\\n- Use the detected BCP-47 code as key in every title/label object\\n- All your text must be in that language\\n\\nTask design rules:\\n1. Title must reference specifics from this submission (name, store, form type) \u2014 never generic\\n2. First field must be a Paragraph briefly explaining what the manager must do\\n3. Only add fields the manager needs to fill in \u2014 never repeat submitted data\\n4. Labels must be specific and actionable (not \\\"Notes\\\" but \\\"Reason for approval\\\")\\n5. Match field types to content: RadioButtons for yes/no, DatePicker for dates, TextArea for justifications\\n6. 3\u20136 fields maximum\\n\\nAvailable elements (exact strings):\\n- \\\"TextInput\\\" \\\"TextArea\\\" \\\"NumberInput\\\" \\\"DatePicker\\\"\\n- \\\"RadioButtons\\\" \\\"Checkboxes\\\" \\\"Dropdown\\\" (these require options array)\\n- \\\"Stars\\\" \\\"Smileys\\\" \\\"Paragraph\\\" (Paragraph = static text, content goes in label)\\n\\nReturn ONLY valid JSON, no markdown fences:\\n{\\n \\\"title\\\": { \\\"<lang>\\\": \\\"...\\\" },\\n \\\"introText\\\": { \\\"<lang>\\\": \\\"One sentence context for the manager.\\\" },\\n \\\"fields\\\": [\\n { \\\"element\\\": \\\"Paragraph\\\", \\\"label\\\": { \\\"<lang>\\\": \\\"...\\\" } },\\n { \\\"element\\\": \\\"RadioButtons\\\", \\\"label\\\": { \\\"<lang>\\\": \\\"...\\\" }, \\\"required\\\": true, \\\"options\\\": [{ \\\"text\\\": { \\\"<lang>\\\": \\\"A\\\" } }, { \\\"text\\\": { \\\"<lang>\\\": \\\"B\\\" } }] }\\n ]\\n}`;\n\nreturn [{ json: { prompt } }];\n"
},
"typeVersion": 2
},
{
"id": "3315bc2d-0dc6-4838-a9b0-b4cf1a5c1696",
"name": "Design Task with AI",
"type": "@n8n/n8n-nodes-langchain.openAi",
"position": [
7408,
3552
],
"parameters": {
"modelId": {
"__rl": true,
"mode": "list",
"value": "gpt-4.1",
"cachedResultName": "GPT-4.1"
},
"options": {},
"responses": {
"values": [
{
"content": "={{ $json.prompt }}"
}
]
},
"builtInTools": {}
},
"typeVersion": 2.1
},
{
"id": "c7a8f42f-9eb3-4e43-89ef-0c76537c4615",
"name": "Compose Task",
"type": "n8n-nodes-base.code",
"position": [
7696,
3552
],
"parameters": {
"jsCode": "const cfg = $('\u2699\ufe0f Config').first().json;\nconst formData = $('Extract Form Data').first().json;\n\n// --- Parse AI response ---\n// Long extraction chain handles different OpenAI node output formats across versions\n// (structured output, message.content array, legacy choices[], raw text)\nconst aiContent = $input.first().json;\nconst aiRaw =\n aiContent?.output?.[0]?.content?.find(b => b.type === 'output_text')?.text ||\n aiContent?.output?.[0]?.content?.[0]?.text ||\n (Array.isArray(aiContent?.message?.content)\n ? aiContent.message.content.find(b => b.type === 'text')?.text\n : aiContent?.message?.content) ||\n aiContent?.choices?.[0]?.message?.content ||\n aiContent?.text ||\n null;\n\nif (!aiRaw) throw new Error('AI returned no content: ' + JSON.stringify(aiContent).substring(0, 300));\n\nlet aiTask;\ntry {\n aiTask = JSON.parse(aiRaw.replace(/```json|```/g, '').trim());\n} catch (e) {\n throw new Error('AI returned invalid JSON: ' + String(aiRaw).substring(0, 300));\n}\nif (Array.isArray(aiTask)) {\n aiTask = { title: { en: 'Review submission' }, introText: { en: 'Please review.' }, fields: aiTask };\n}\nif (!aiTask.title || typeof aiTask.title !== 'object') throw new Error('Missing title');\nif (!aiTask.introText || typeof aiTask.introText !== 'object') throw new Error('Missing introText');\nif (!Array.isArray(aiTask.fields) || !aiTask.fields.length) throw new Error('Missing fields');\n\n// --- Config validation ---\nconst OBJECT_ID = /^[a-f0-9]{24}$/i;\nconst dueDays = cfg.taskDueDaysFromNow ?? 1;\nif (typeof dueDays !== 'number' || dueDays < 0) {\n throw new Error(`taskDueDaysFromNow must be >= 0, got: ${dueDays}`);\n}\n\n// Validate groupId \u2014 invalid non-empty strings will broadcast org-wide\nlet groupId = null;\nif (cfg.groupId && typeof cfg.groupId === 'string' && cfg.groupId.trim()) {\n if (OBJECT_ID.test(cfg.groupId)) {\n groupId = cfg.groupId;\n } else {\n throw new Error(\n `Invalid groupId \"${cfg.groupId}\" \u2014 must be a 24-char hex ObjectID. ` +\n `Without a valid groupId, task targets EVERYONE on the node. ` +\n `Check Keephub admin for the correct group ID.`\n );\n }\n}\n\nconst webhookUrl = cfg.workflow2WebhookUrl || '';\n\nconst contentRef = formData.contentRef || '';\nconst formSubmissionId = formData.formSubmissionId || '';\n\n// --- Org target (from parent/root path) ---\n// Merge both paths: try Extract Parent Target first, fall back to Extract Root Target\nlet target;\ntry {\n target = $('Extract Parent Target').first().json;\n if (!target || !target.targetNodeId) throw new Error('No targetNodeId in parent');\n} catch {\n try {\n target = $('Extract Root Target').first().json;\n if (!target || !target.targetNodeId) throw new Error('No targetNodeId in root');\n } catch (err2) {\n throw new Error(\n 'No target node resolved from either parent or root path. ' +\n 'Check Get Parent Node / Get Root Node outputs.'\n );\n }\n}\nconst targetId = target.targetNodeId;\nconst targetDescendants = target.descendants || [];\nconst resolvedGroups = groupId ? [groupId] : [];\n\nif (!targetId) throw new Error('No target node resolved');\n\n// --- Dates (n8n global DateTime = Luxon) ---\nconst now = DateTime.now();\nconst startDate = now.startOf('day').plus({ minutes: 1 });\nconst dueDate = now.plus({ days: dueDays }).endOf('day');\nconst timezone = now.zoneName;\n\n// --- Helpers ---\n// Generate cryptographically strong ObjectID-like strings\nfunction objectId() {\n const buf = crypto.randomBytes(12);\n return buf.toString('hex');\n}\n\nconst langs = Object.keys(aiTask.title);\nconst linkLabels = { nl: 'Bekijk de originele inzending', en: 'View the original submission' };\n\n// --- Intro paragraph with link to submission (URL-encoded) ---\nconst encodedContentRef = encodeURIComponent(contentRef);\nconst encodedSubmissionId = encodeURIComponent(formSubmissionId);\nconst introField = {\n id: objectId(), required: false, element: 'Paragraph',\n static: true, bold: false, italic: false, content: '',\n text: Object.fromEntries(langs.map(lang => [\n lang,\n `<p>${aiTask.introText[lang] || ''} <a href=\\\"/system/content/${encodedContentRef}/formValueId/${encodedSubmissionId}/view\\\">${linkLabels[lang] || linkLabels.en}</a>.</p>`\n ])),\n type: '', editingFieldId: '', sectionIndex: null, insideSection: null, orderIndex: 0\n};\n\n// --- Build dynamic fields with validation ---\nconst OPTION_ELEMENTS = ['RadioButtons', 'Checkboxes', 'Dropdown'];\n\nconst dynamicFields = aiTask.fields\n .filter(f => f.element && (f.label || f.element === 'Paragraph'))\n .map((f, index) => {\n const id = objectId();\n const isParagraph = f.element === 'Paragraph';\n \n // Validate Paragraph has label\n if (isParagraph && (!f.label || typeof f.label !== 'object' || !Object.keys(f.label).length)) {\n throw new Error(`AI returned Paragraph field at index ${index} with no label. All Paragraph fields must have a label object.`);\n }\n \n const field = {\n id, required: f.required ?? false, element: f.element,\n type: '', editingFieldId: '', sectionIndex: null, insideSection: null,\n orderIndex: index + 1\n };\n if (isParagraph) {\n Object.assign(field, { static: true, bold: false, italic: false, content: '', text: f.label });\n } else {\n field.field_name = id;\n field.label = f.label;\n }\n if (OPTION_ELEMENTS.includes(f.element)) {\n // Validate options array\n if (!Array.isArray(f.options) || !f.options.length) {\n throw new Error(\n `AI returned ${f.element} field at index ${index} with no options array. ` +\n `${f.element} requires at least one option.`\n );\n }\n // Validate each option has text object\n f.options.forEach((opt, i) => {\n if (!opt.text || typeof opt.text !== 'object' || !Object.keys(opt.text).length) {\n throw new Error(\n `AI returned ${f.element} field at index ${index}, option ${i} with malformed text. ` +\n `Expected {\"<lang>\": \"...\"}, got: ${JSON.stringify(opt.text)}`\n );\n }\n });\n field.shouldTranslate = false;\n field.hiddenValues = false;\n field.options = f.options.map(opt => ({\n value: '', text: opt.text, showRemark: false, remark: '', key: objectId()\n }));\n if (f.element !== 'Dropdown') field.hiddenRemarks = false;\n }\n if (f.element === 'TextArea') field.multiline = true;\n if (f.element === 'NumberInput') { if (f.min != null) field.min = f.min; if (f.max != null) field.max = f.max; }\n if (f.element === 'DatePicker') {\n Object.assign(field, {\n readOnly: false, defaultToday: false, dateFormat: 'MM/dd/yyyy',\n timeFormat: 'hh:mm aa', showTimeSelect: false, showTimeSelectOnly: false\n });\n }\n return field;\n });\n\n// --- Final payload (single task, no repeat) ---\nreturn [{ json: {\n sendPushNotification: true,\n orgchartSelection: { include: [targetId], exclude: targetDescendants },\n parentRef: null,\n orgchartAttrSelection: [],\n groups: resolvedGroups,\n type: 'single',\n template: {\n originLanguage: langs[0],\n startDate: startDate.toISO(),\n dueDate: dueDate.toISO(),\n completionType: 'group',\n timezone,\n endGroup: 'never',\n title: aiTask.title,\n attachments: Object.fromEntries(langs.map(l => [l, []])),\n relatedTags: [],\n form: { active: true, fields: [introField, ...dynamicFields] },\n hookurls: webhookUrl ? [{ url: webhookUrl, active: true }] : [],\n highlighted: false,\n templateEndDate: null\n }\n}}];\n"
},
"typeVersion": 2
},
{
"id": "1e90ce6e-bc35-4ce1-99e4-6ed2383ca0bc",
"name": "Create Task",
"type": "n8n-nodes-keephub.keephub",
"position": [
7936,
3552
],
"parameters": {
"resource": "task",
"taskJsonBody": "={{ $node[\"Compose Task\"].json }}",
"defineTaskInput": "json"
},
"typeVersion": 1
},
{
"id": "86f33f87-089d-4681-beba-0e7330a0a875",
"name": "Get Root Node",
"type": "n8n-nodes-keephub.keephub",
"position": [
5344,
3696
],
"parameters": {
"nodeId": "={{ $('Resolve Org Node').item.json.orgNodeId }}",
"resource": "orgchart",
"operation": "getAncestors",
"authentication": "loginCredentials",
"additionalFields": {}
},
"credentials": {},
"typeVersion": 1
},
{
"id": "11ae0758-4053-424a-80cf-af2d725f2154",
"name": "\u26a0\ufe0f Group Targeting",
"type": "n8n-nodes-base.stickyNote",
"position": [
6560,
3696
],
"parameters": {
"color": 3,
"width": 500,
"height": 496,
"content": "## \u26a0\ufe0f Critical: Group Targeting\n\n**Without `groupId` \u2192 task assigned to EVERYONE**\nManagers, employees, interns, contractors\u2014all users on the parent node will see the task.\n\n**To target specific groups (recommended):**\n1. Open Keephub admin panel\n2. Navigate to Groups section\n3. Copy the 24-character group ID (e.g., \"65f8a3b2c1234567890abcde\")\n4. Paste into `groupId` field in **\u2699\ufe0f Config** node\n\n**Why manual configuration?**\nKeephub API returns groups as opaque IDs with no human-readable names. Automatic detection would be unreliable and could cause targeting errors.\n\n**Invalid `groupId` = workflow fails** (validates format to prevent silent broadcasting).\n"
},
"typeVersion": 1
},
{
"id": "1ce770a8-8ffb-40f2-8275-9d1753099a5b",
"name": "Extract Parent Target",
"type": "n8n-nodes-base.code",
"position": [
5344,
3552
],
"parameters": {
"jsCode": "const result = $input.first().json;\nconst parent = result.parent || result;\nif (!parent._id) throw new Error('Parent node has no _id');\nreturn [{ json: { targetNodeId: parent._id, descendants: parent.descendants || [], isRoot: false } }];\n"
},
"typeVersion": 2
},
{
"id": "fb288916-ad61-4d6a-bba3-a957fb9026bf",
"name": "Extract Root Target",
"type": "n8n-nodes-base.code",
"position": [
5584,
3696
],
"parameters": {
"jsCode": "// Get ancestors returns array from current node up to root\n// The last item is the actual organizational root\nconst response = $input.first().json;\nlet root;\nif (Array.isArray(response)) {\n // If ancestors array is returned, take the last (topmost) ancestor as root\n root = response.length > 0 ? response[response.length - 1] : null;\n} else if (response.ancestors && Array.isArray(response.ancestors)) {\n root = response.ancestors.length > 0 ? response.ancestors[response.ancestors.length - 1] : null;\n} else {\n root = response;\n}\nif (!root || (!root._id && !root.id)) {\n throw new Error('No root node found in ancestors response');\n}\nreturn [{ json: { targetNodeId: root._id || root.id, descendants: root.descendants || [], isRoot: true } }];\n"
},
"typeVersion": 2
}
],
"connections": {
"Is Edit?": {
"main": [
[],
[
{
"node": "Extract Form Data",
"type": "main",
"index": 0
}
]
]
},
"Compose Task": {
"main": [
[
{
"node": "Create Task",
"type": "main",
"index": 0
}
]
]
},
"Get Root Node": {
"main": [
[
{
"node": "Extract Root Target",
"type": "main",
"index": 0
}
]
]
},
"Get Submitter": {
"main": [
[
{
"node": "Resolve Org Node",
"type": "main",
"index": 0
}
]
]
},
"\u2699\ufe0f Config": {
"main": [
[
{
"node": "Build AI Prompt",
"type": "main",
"index": 0
}
]
]
},
"Status Change?": {
"main": [
[],
[
{
"node": "Is Edit?",
"type": "main",
"index": 0
}
]
]
},
"Build AI Prompt": {
"main": [
[
{
"node": "Design Task with AI",
"type": "main",
"index": 0
}
]
]
},
"Get Form Schema": {
"main": [
[
{
"node": "\u2699\ufe0f Config",
"type": "main",
"index": 0
}
]
]
},
"Get Parent Node": {
"main": [
[
{
"node": "Extract Parent Target",
"type": "main",
"index": 0
}
],
[
{
"node": "Get Root Node",
"type": "main",
"index": 0
}
]
]
},
"Resolve Org Node": {
"main": [
[
{
"node": "Get Parent Node",
"type": "main",
"index": 0
}
]
]
},
"Extract Form Data": {
"main": [
[
{
"node": "Get Submitter",
"type": "main",
"index": 0
}
]
]
},
"Design Task with AI": {
"main": [
[
{
"node": "Compose Task",
"type": "main",
"index": 0
}
]
]
},
"Extract Root Target": {
"main": [
[
{
"node": "Get Form Schema",
"type": "main",
"index": 0
}
]
]
},
"Keephub Form Webhook": {
"main": [
[
{
"node": "Status Change?",
"type": "main",
"index": 0
}
]
]
},
"Extract Parent Target": {
"main": [
[
{
"node": "Get Form Schema",
"type": "main",
"index": 0
}
]
]
}
}
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Organizations using Keephub for form management and task tracking. Perfect for HR teams handling employee requests (leave, equipment, onboarding), retail managers reviewing store submissions, or any workflow where form responses need manager follow-up.
Source: https://n8n.io/workflows/13828/ — 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 powerful n8n automation workflow is designed to execute advanced B2B lead enrichment and hyper-personalization for cold email outreach. By orchestrating a complex chain of data scraping, AI analy
This workflow bridges the gap between raw product data and revenue sales tools. It automates the entire Product Qualified Lead (PQL) lifecycle—from real-time intent routing to churn prevention—reducin
User Signup & Verification: The workflow starts when a user signs up. It generates a verification code and sends it via SMS using Twilio. Code Validation: The user replies with the code. The workflow
Generates a wordlist of 1,000–15,000 subdomains created by an AI agent by correlating detected technologies and recurring patterns.
This template is perfect for e-commerce entrepreneurs, marketers, agencies, and creative teams who want to turn simple product photos and short descriptions into professional flyers or product videos—