{"workflow":{"id":14040,"name":"Migrate ClickUp list or task tree to Nextcloud Deck as a one-off import","views":14,"recentViews":0,"totalViews":14,"createdAt":"2026-03-14T13:21:19.634Z","description":"**Move your ClickUp List or Task Tree to Nextcloud Deck**\n\nMove a ClickUp list, view, or a single root task tree into Nextcloud Deck as a one-off board migration. This workflow is for people who want to preserve structure, status, comments, subtasks, labels, due dates, and completion state when moving work from ClickUp into Nextcloud Deck, without doing the world’s saddest copy-paste marathon.\n\n## How it works\n\n- Validates that the target Nextcloud Deck board is reachable before starting the import. You have to create the Deck Board first.\n- Supports two source modes: a) import from a ClickUp view/list, and b) import from a single ClickUp root task tree into the same downstream Deck flow.\n- Resolves the real ClickUp home list, fetches tasks page by page, and includes closed tasks and subtasks.\n- Converts ClickUp statuses into Deck stacks using a configurable status-to-stack mapping.\n- Imports parent tasks as Deck cards and appends subtasks into the card description instead of creating separate child cards.\n- Pulls ClickUp task comments and appends them to each card description for better context in Deck.\n- Creates Deck labels from ClickUp metadata such as OKR, Progress, and Priority, then assigns those labels to the created cards.\n- Sends due dates to Deck in ISO format and marks imported cards as done when the ClickUp task is already complete.\n- Produces a final summary showing attempted and successful card creation, done-state updates, and label assignments.\n\n## Set up steps\n1. Connect your Nextcloud account in n8n using HTTP Basic Auth and make sure Deck is enabled on the target instance. You’ll also need the Nextcloud base URL and the target Deck board ID. You can get these from the URL addresses in Nextcloud.\n2. Add your ClickUp API token and choose your source:\n\t- a ClickUp view/list ID for list import\n\t- or a root task ID for task-tree import.\n3. Review the configuration values for status mapping, max pages to fetch, and the subtask title prefix so the imported board matches your workflow style.\n4. Run the workflow manually for a one-off migration into a clean or lightly used Deck board, then review the result summary and the created stacks, labels, and cards.\n\n## Requirements\n- ClickUp account and app API key\n- Nextcloud account and Deck installed\n- Target Deck Board created in Nextcloud\n","workflow":{"id":"QiHX0W61otMoi1AP","meta":{"instanceId":"85603f2c9b847af4278f163086ceccba635983e17b019aef1c45034467adacd5"},"name":"ClickUp List to Nextcloud Deck (one-off)","tags":[{"id":"8bXQxvAHY2O7MTAL","name":"BizLog","createdAt":"2026-02-21T07:36:16.153Z","updatedAt":"2026-02-21T07:36:16.153Z"}],"nodes":[{"id":"8530c39c-04c9-4ed4-b40c-1cdeded0be8b","name":"Manual Trigger","type":"n8n-nodes-base.manualTrigger","notes":"Starts the one-off migration manually.\n\nUse this workflow for ad hoc imports, not for a recurring sync.\nBefore running, decide which config node you want active:\n- **Set Config - View** for importing everything visible in a ClickUp view\n- **Set Config - Task Root** for importing one root task tree\n\nThis trigger currently feeds the task-root config branch. To run in view mode, connect/activate the view config path instead.","position":[-1216,672],"parameters":{},"notesInFlow":false,"typeVersion":1},{"id":"d9d51e75-2997-4701-ba9d-548b9a88f631","name":"Sticky Note","type":"n8n-nodes-base.stickyNote","notes":"ClickUp View/List → Nextcloud Deck (one-off import)\n\nThis workflow supports two source modes:\n1. **view** = resolve the real home list from a ClickUp view, then import parent tasks from that list\n2. **task_root** = fetch one ClickUp root task, discover its home list, collect its descendants, and import only that subtree\n\nShared behavior:\n- parent tasks become Deck cards\n- subtasks are appended into the parent card description\n- ClickUp comments are appended after subtasks/checklists\n- Deck stacks are created from mapped ClickUp statuses\n- Deck labels are created from OKR, Progress, Priority, and Finished\n- completed ClickUp tasks are marked done in Deck after card creation","position":[-1280,32],"parameters":{"color":5,"width":430,"height":420,"content":"ClickUp View/List → Nextcloud Deck (one-off import)\n\nHow this version works:\n1. Uses the ClickUp view id from Set Config to resolve the real home list id.\n2. Fetches the full list with subtasks=true and include_timl=true.\n3. Imports only parent tasks as Deck cards and appends subtasks into the parent card description.\n4. Appends task comments to the end of the card description.\n5. Creates Deck labels from ClickUp OKR, Progress, and Priority, then assigns them to cards.\n6. Sends due dates to Deck in ISO-8601 format.\n\nBefore you run:\n- Keep the Set Config values as they are unless you want a different view/board.\n- Attach the same Nextcloud HTTP Basic Auth credential to the Deck HTTP nodes.\n- Best used as a one-off migration on a clean or lightly used board.\n\nNew in this version:\n7. A second manual trigger can import one ClickUp root task tree into the same downstream Deck migration flow."},"notesInFlow":false,"typeVersion":1},{"id":"cf17069e-29a8-4be9-86cc-fee725178c8e","name":"Deck - Validate Board","type":"n8n-nodes-base.httpRequest","notes":"Validates the Nextcloud Deck target before any ClickUp-heavy work starts.\n\nWhat it checks:\n- base URL resolves\n- Deck app API path is correct\n- board ID exists\n- attached HTTP Basic Auth credential can access the board\n\nIf this node fails, fix the Nextcloud URL, board ID, or credential attachment before debugging anything else.","position":[-448,576],"parameters":{"url":"={{ $('Set Config').first().json.nextcloud_base_url.replace(/\\/$/, '') + '/index.php/apps/deck/api/v1.0/boards/' + $('Set Config').first().json.nextcloud_deck_board_id }}","options":{},"sendHeaders":true,"authentication":"genericCredentialType","genericAuthType":"httpBasicAuth","headerParameters":{"parameters":[{"name":"OCS-APIRequest","value":"true"},{"name":"Accept","value":"application/json"}]}},"credentials":{"httpBasicAuth":{"id":"VvRycBRBshAOeOEYy","name":"Nextcloud app password"}},"notesInFlow":false,"typeVersion":4.2},{"id":"bb4bb97f-987c-45c8-a7a4-df6b2748ed80","name":"Build Page Numbers","type":"n8n-nodes-base.code","notes":"View-mode bootstrap.\n\nCreates a single seed item:\n- `page = 0`\n- `clickup_list_id` from config\n\nWhy this exists:\n- the view endpoint is queried first only to discover the actual home list ID behind the view\n- full pagination starts later after the real list ID is known","position":[48,384],"parameters":{"jsCode":"const cfg = $('Set Config').first().json;\nreturn [{ json: { page: 0, clickup_list_id: cfg.clickup_list_id } }];"},"notesInFlow":false,"typeVersion":2},{"id":"0482a998-f170-4c87-887b-0528cd37650c","name":"ClickUp - Get View Tasks Page","type":"n8n-nodes-base.httpRequest","notes":"Calls ClickUp's **view tasks** endpoint for page 0.\n\nRequest behavior:\n- includes subtasks\n- includes closed tasks\n\nPurpose:\n- gather enough task data to infer the real ClickUp home list ID used by the view\n- this is not the full import yet; it is a discovery step","position":[288,384],"parameters":{"url":"={{ $('Set Config').first().json.clickup_base_url.replace(/\\/$/, '') + '/api/v2/view/' + $('Set Config').first().json.clickup_list_id + '/task' }}","options":{},"sendQuery":true,"sendHeaders":true,"queryParameters":{"parameters":[{"name":"page","value":"={{ $json.page }}"},{"name":"subtasks","value":"true"},{"name":"include_closed","value":"true"}]},"headerParameters":{"parameters":[{"name":"Authorization","value":"={{ $('Set Config').first().json.clickup_api_token }}"},{"name":"Accept","value":"application/json"}]}},"notesInFlow":false,"typeVersion":4.2},{"id":"c00b4025-1c7b-43ee-8b80-6723d6f934c0","name":"Append Comments To Task","type":"n8n-nodes-base.code","notes":"Merges ClickUp comments into each prepared task description.\n\nFormatting behavior:\n- comments are normalized into markdown\n- author and date are included when available\n- comments are appended under a `Comments` section after the existing description\n\nOutput is the final card content used for card creation.","position":[4064,464],"parameters":{"jsCode":"function toText(value) {\n  if (value === null || value === undefined) return '';\n  return String(value);\n}\n\nfunction normalizeBreaks(text) {\n  return toText(text).replace(/\\r\\n/g, '\\n').trim();\n}\n\nfunction extractComments(payload) {\n  if (Array.isArray(payload?.comments)) return payload.comments;\n  if (Array.isArray(payload?.data?.comments)) return payload.data.comments;\n  if (Array.isArray(payload?.comment_response?.comments)) return payload.comment_response.comments;\n  return [];\n}\n\nfunction extractCommentText(comment) {\n  return normalizeBreaks(\n    comment?.comment_text ??\n    comment?.comment ??\n    comment?.text ??\n    comment?.text_content ??\n    ''\n  );\n}\n\nfunction extractCommentAuthor(comment) {\n  return (\n    comment?.user?.username ||\n    comment?.user?.name ||\n    comment?.user?.email ||\n    'Unknown'\n  );\n}\n\nfunction extractCommentDateIso(comment) {\n  const raw =\n    comment?.date ??\n    comment?.date_created ??\n    comment?.date_added ??\n    null;\n\n  if (raw === null || raw === undefined || raw === '') return null;\n\n  const n = Number(raw);\n  if (Number.isFinite(n)) return new Date(n).toISOString();\n\n  const d = new Date(raw);\n  if (!Number.isNaN(d.getTime())) return d.toISOString();\n\n  return null;\n}\n\nfunction buildCommentsMarkdown(comments) {\n  const rows = comments\n    .map((comment) => {\n      const text = extractCommentText(comment);\n      if (!text) return null;\n\n      const author = extractCommentAuthor(comment);\n      const dateIso = extractCommentDateIso(comment);\n      const header = `- **${author}**${dateIso ? ` (${dateIso})` : ''}`;\n      const body = text.replace(/\\n/g, '\\n  ');\n\n      return `${header}\\n\\n  ${body}`;\n    })\n    .filter(Boolean);\n\n  if (!rows.length) return '';\n\n  return `## Comments\\n\\n${rows.join('\\n\\n')}`;\n}\n\nconst preparedCards = $('Prepare Task Work Items').all().map(item => item.json ?? {});\nconst commentItems = $input.all();\n\nreturn commentItems.map((item, index) => {\n  const base = preparedCards[index] ?? {};\n  const payload = item.json ?? {};\n  const comments = extractComments(payload);\n\n  const existingDescription = normalizeBreaks(base.description ?? base.description_base ?? '');\n  const commentsMd = buildCommentsMarkdown(comments);\n\n  const mergedDescription = commentsMd\n    ? (existingDescription ? `${existingDescription}\\n\\n${commentsMd}` : commentsMd)\n    : existingDescription;\n\n  return {\n    json: {\n      ...base,\n      comments,\n      description: mergedDescription,\n    },\n    pairedItem: item.pairedItem ?? { item: index },\n  };\n});"},"notesInFlow":false,"typeVersion":2},{"id":"86ef54e9-41fc-44e0-ab78-0a2a8afdb57e","name":"Build Stack Items","type":"n8n-nodes-base.code","notes":"Collects the Deck stacks that should exist.\n\nLogic:\n- starts from `status_stack_map_json`\n- also adds unmapped statuses observed in normalized tasks\n- normalizes keys and sorts stacks by configured order\n\nResult:\n- one item per target Deck stack to create/read back later","position":[1664,464],"parameters":{"jsCode":"const cfg = $('Set Config').first().json;\nconst tasks = $('Normalize ClickUp Parent Tasks').all().map(item => item.json ?? {});\n\nfunction normalizeStatusKey(value) {\n  return String(value || '')\n    .trim()\n    .toLowerCase()\n    .replace(/\\s+/g, ' ');\n}\n\nfunction titleCase(value) {\n  return String(value || '')\n    .split(' ')\n    .filter(Boolean)\n    .map(word => word.charAt(0).toUpperCase() + word.slice(1))\n    .join(' ') || 'Other';\n}\n\nlet statusMap = [];\ntry {\n  statusMap = JSON.parse(cfg.status_stack_map_json || '[]');\n} catch (error) {\n  throw new Error('status_stack_map_json is not valid JSON.');\n}\n\nconst byKey = new Map();\n\nfor (const item of statusMap) {\n  const key = normalizeStatusKey(item.status_key);\n  if (!key) continue;\n  byKey.set(key, {\n    stack_key: key,\n    stack_title: item.stack_title || titleCase(key),\n    order: Number(item.order || 999),\n  });\n}\n\nfor (const task of tasks) {\n  const key = normalizeStatusKey(task.stack_key || task.stack_title);\n  if (!key) continue;\n  if (!byKey.has(key)) {\n    byKey.set(key, {\n      stack_key: key,\n      stack_title: task.stack_title || titleCase(key),\n      order: Number(task.stack_order || 900),\n    });\n  }\n}\n\nreturn Array.from(byKey.values())\n  .sort((a, b) => (a.order || 999) - (b.order || 999))\n  .map(item => ({ json: item }));"},"notesInFlow":false,"typeVersion":2},{"id":"833bc781-60ef-49ce-9975-5c305fbb0566","name":"Deck - Create Stack","type":"n8n-nodes-base.httpRequest","notes":"Creates each target stack in Nextcloud Deck.\n\nImplementation details:\n- sends one request per stack\n- `continueOnFail=true` so reruns or duplicate-stack situations do not stop the workflow\n- the workflow later re-reads stacks from Deck, so exact create responses are not the final source of truth","position":[1904,464],"parameters":{"url":"={{ $('Set Config').first().json.nextcloud_base_url.replace(/\\/$/, '') + '/index.php/apps/deck/api/v1.0/boards/' + $('Set Config').first().json.nextcloud_deck_board_id + '/stacks' }}","body":"={{ JSON.stringify({ title: $json.stack_title, order: Number($json.order || 999) }) }}","method":"POST","options":{"batching":{"batch":{"batchSize":1,"batchInterval":100}}},"sendBody":true,"contentType":"raw","sendHeaders":true,"authentication":"genericCredentialType","rawContentType":"application/json","genericAuthType":"httpBasicAuth","headerParameters":{"parameters":[{"name":"OCS-APIRequest","value":"true"},{"name":"Accept","value":"application/json"},{"name":"Content-Type","value":"application/json"}]}},"notesInFlow":false,"typeVersion":4.2,"continueOnFail":true},{"id":"11f5cec7-c260-4d30-bc6f-76ed7a77a844","name":"After Stack Creation - Emit Once","type":"n8n-nodes-base.code","notes":"Collapses the per-stack stream back to a single item.\n\nPurpose:\n- later phases only need to know that stack creation was attempted\n- prevents label creation from running once per stack","position":[2144,464],"parameters":{"jsCode":"return [{\n  json: {\n    stack_create_attempts: $input.all().length\n  }\n}];"},"notesInFlow":false,"typeVersion":2},{"id":"0c0846e8-3b6e-4fdf-a60d-aeadb2c0d4d7","name":"Deck - Get Stacks","type":"n8n-nodes-base.httpRequest","notes":"Reads the board's stacks after stack creation.\n\nWhy this matters:\n- stack IDs are required to create cards in the correct stack\n- this read is the authoritative stack source, which is safer than trusting create responses during reruns","position":[3344,464],"parameters":{"url":"={{ $('Set Config').first().json.nextcloud_base_url.replace(/\\/$/, '') + '/index.php/apps/deck/api/v1.0/boards/' + $('Set Config').first().json.nextcloud_deck_board_id + '/stacks' }}","options":{},"sendHeaders":true,"authentication":"genericCredentialType","genericAuthType":"httpBasicAuth","headerParameters":{"parameters":[{"name":"OCS-APIRequest","value":"true"},{"name":"Accept","value":"application/json"}]}},"notesInFlow":false,"typeVersion":4.2},{"id":"537c05f3-531f-42f5-9062-9e68f759b53b","name":"Prepare Task Work Items","type":"n8n-nodes-base.code","notes":"Combines normalized tasks with live Deck board data.\n\nMain jobs:\n- map each task to the correct `stack_id`\n- map label titles to actual `label_id` values from the board\n- create a stable card order within each stack\n- carry forward title, description, due date, and label metadata for later steps\n\nIf expected stacks are missing, this node throws an error early.","position":[3584,464],"parameters":{"jsCode":"const tasks = $('Normalize ClickUp Parent Tasks').all().map(item => item.json ?? {});\nconst stackItems = $('Deck - Get Stacks').all();\nconst board = $('Deck - Get Board After Labels').first().json ?? {};\n\nfunction normalizeKey(value) {\n  return String(value || '').trim().toLowerCase().replace(/\\s+/g, ' ');\n}\nfunction extractStacks(items) {\n  const out = [];\n  for (const item of items) {\n    const j = item.json ?? item ?? {};\n    if (Array.isArray(j)) { out.push(...j); continue; }\n    const nested = [j.stacks, j.data?.stacks, j.data, j.items, j.results].find(v => Array.isArray(v));\n    if (nested) { out.push(...nested); continue; }\n    if (j && typeof j === 'object' && j.id && j.title) out.push(j);\n  }\n  return out;\n}\n\nconst stacks = extractStacks(stackItems);\nif (!stacks.length) throw new Error('No Deck stacks were returned. Check board permissions, base URL, or Deck API path.');\nconst stackByTitle = new Map(stacks.map(stack => [normalizeKey(stack.title), stack]));\nconst labelByTitle = new Map((Array.isArray(board.labels) ? board.labels : []).map(label => [normalizeKey(label.title), label]));\nconst missingTitles = Array.from(new Set(tasks.map(task => task.stack_title).filter(title => !stackByTitle.has(normalizeKey(title)))));\nif (missingTitles.length) throw new Error(`These Deck stacks were not found after creation: ${missingTitles.join(', ')}`);\n\nconst orderCounters = new Map();\nconst out = [];\nfor (const task of tasks) {\n  const stack = stackByTitle.get(normalizeKey(task.stack_title));\n  const currentOrder = (orderCounters.get(stack.id) || 0) + 1000;\n  orderCounters.set(stack.id, currentOrder);\n  out.push({\n    json: {\n      ...task,\n      stack_id: stack.id,\n      description: task.description || task.description_base || '',\n      order: currentOrder,\n      label_ids: (Array.isArray(task.deck_labels) ? task.deck_labels : [])\n        .map(label => labelByTitle.get(normalizeKey(label.title))?.id)\n        .filter(Boolean),\n      label_titles: (Array.isArray(task.deck_labels) ? task.deck_labels : []).map(label => label.title),\n    }\n  });\n}\nreturn out;"},"notesInFlow":false,"typeVersion":2},{"id":"e222f2e2-eebe-4169-bf9e-3b6c058bc7ed","name":"Deck - Create Card","type":"n8n-nodes-base.httpRequest","notes":"Creates one Deck card per prepared parent task.\n\nCurrent mapping:\n- title\n- description\n- order\n- due date\n- card type = `plain`\n\nNotes:\n- only parent tasks become cards\n- subtasks remain embedded inside the description\n- `continueOnFail=true` allows the workflow to continue into summary/reporting even if some card creates fail","position":[4304,464],"parameters":{"url":"={{ $('Set Config').first().json.nextcloud_base_url.replace(/\\/$/, '') + '/index.php/apps/deck/api/v1.0/boards/' + $('Set Config').first().json.nextcloud_deck_board_id + '/stacks/' + $json.stack_id + '/cards' }}","body":"={{ JSON.stringify({ title: $json.title, type: 'plain', order: Number($json.order || 999), description: $json.description || '', duedate: $json.duedate || null }) }}","method":"POST","options":{"batching":{"batch":{"batchSize":1,"batchInterval":100}}},"sendBody":true,"contentType":"raw","sendHeaders":true,"authentication":"genericCredentialType","rawContentType":"application/json","genericAuthType":"httpBasicAuth","headerParameters":{"parameters":[{"name":"OCS-APIRequest","value":"true"},{"name":"Accept","value":"application/json"},{"name":"Content-Type","value":"application/json"}]}},"notesInFlow":false,"typeVersion":4.2,"continueOnFail":true},{"id":"9b7e54b8-e8bb-4ad4-a517-90ba4454fd74","name":"Result Summary","type":"n8n-nodes-base.code","notes":"Produces a human-readable migration summary.\n\nReported metrics include:\n- parent tasks attempted\n- cards created and failed\n- done-mark attempts and failures\n- label assignment attempts and failures\n- target board ID\n- original ClickUp source ID\n\nUse this node first when validating a run or troubleshooting partial imports.","position":[5024,464],"parameters":{"jsCode":"const createdCardItems = $('Deck - Create Card').all();\nconst preparedCardItems = $('Append Comments To Task').all();\nconst labelAssignItems = $('Deck - Assign Label').all();\nconst attemptedCards = preparedCardItems.length;\nconst failedCards = createdCardItems.filter(item => item.json?.error || item.error || !item.json?.id).length;\nconst createdCards = createdCardItems.filter(item => item.json?.id).length;\nconst attemptedLabelAssignments = $('Prepare Label Assignments').all().length;\nconst failedLabelAssignments = labelAssignItems.filter(item => item.json?.error || item.error).length;\nconst successfulLabelAssignments = attemptedLabelAssignments - failedLabelAssignments;\n\nlet attemptedDoneMarks = 0;\nlet failedDoneMarks = 0;\nlet successfulDoneMarks = 0;\ntry {\n  attemptedDoneMarks = $('Prepare Completed Card Updates').all().length;\n  const doneItems = $('Deck - Mark Card Done').all();\n  failedDoneMarks = doneItems.filter(item => item.json?.error || item.error).length;\n  successfulDoneMarks = attemptedDoneMarks - failedDoneMarks;\n} catch (e) {}\n\nreturn [{\n  json: {\n    message: `Deck import finished. Parent tasks attempted ${attemptedCards}. Cards created ${createdCards}. Card failures ${failedCards}. Cards marked done attempted ${attemptedDoneMarks}. Cards marked done succeeded ${successfulDoneMarks}. Done-mark failures ${failedDoneMarks}. Label assignments attempted ${attemptedLabelAssignments}. Label assignments succeeded ${successfulLabelAssignments}. Label assignment failures ${failedLabelAssignments}.`,\n    attempted_cards: attemptedCards,\n    created_cards: createdCards,\n    failed_cards: failedCards,\n    attempted_done_marks: attemptedDoneMarks,\n    successful_done_marks: successfulDoneMarks,\n    failed_done_marks: failedDoneMarks,\n    attempted_label_assignments: attemptedLabelAssignments,\n    successful_label_assignments: successfulLabelAssignments,\n    failed_label_assignments: failedLabelAssignments,\n    board_id: $('Set Config').first().json.nextcloud_deck_board_id,\n    clickup_source_id: $('Set Config').first().json.clickup_list_id,\n  }\n}];"},"notesInFlow":false,"typeVersion":2},{"id":"cdb88ee9-b32f-4b1d-b76a-a2bca4779554","name":"Resolve ClickUp Home List ID","type":"n8n-nodes-base.code","notes":"Extracts the real ClickUp list ID from the view results.\n\nOutput:\n- `clickup_actual_list_id`\n- `clickup_actual_list_name`\n- `visible_task_count`\n- original view ID for traceability\n\nFallback behavior:\n- if tasks do not reveal a list ID, the config value is used only when it already looks like a numeric list ID\n- otherwise the workflow throws an error","position":[528,384],"parameters":{"jsCode":"const cfg = $('Set Config').first().json;\nconst inputItems = $input.all();\nfunction extractTasks(itemJson) {\n  if (!itemJson) return [];\n  if (Array.isArray(itemJson)) return itemJson;\n  const candidates = [itemJson.tasks, itemJson.data?.tasks, itemJson.data, itemJson.items, itemJson.results];\n  return candidates.find(v => Array.isArray(v)) || [];\n}\nlet actualListId = null;\nlet actualListName = null;\nlet visibleTaskCount = 0;\nfor (const item of inputItems) {\n  const tasks = extractTasks(item.json ?? {});\n  visibleTaskCount += tasks.length;\n  for (const task of tasks) {\n    const listId = task?.list?.id || task?.list_id || null;\n    const listName = task?.list?.name || task?.list_name || null;\n    if (listId) {\n      actualListId = String(listId);\n      actualListName = listName ? String(listName) : null;\n      break;\n    }\n  }\n  if (actualListId) break;\n}\nif (!actualListId) {\n  const fallback = String(cfg.clickup_list_id || '').trim();\n  if (/^\\d+$/.test(fallback)) actualListId = fallback;\n  else throw new Error('Could not resolve the real ClickUp home list id from the view results.');\n}\nreturn [{ json: { clickup_view_id: String(cfg.clickup_list_id || ''), clickup_actual_list_id: actualListId, clickup_actual_list_name: actualListName, visible_task_count: visibleTaskCount } }];"},"notesInFlow":false,"typeVersion":2},{"id":"06b2a66c-6a3d-42d6-8018-e5f797f24ab6","name":"Build List Page Numbers","type":"n8n-nodes-base.code","notes":"Creates one item per ClickUp list page for view mode.\n\nUses:\n- `clickup_actual_list_id`\n- `max_pages`\n\nOutput shape:\n- page number\n- actual list ID/name\n- original view ID\n\nThis is the real pagination fan-out for the shared import pipeline.","position":[768,384],"parameters":{"jsCode":"const cfg = $('Set Config').first().json;\nconst src = $input.first().json ?? {};\nconst maxPages = Math.max(1, Number(cfg.max_pages || 10));\nconst listId = String(src.clickup_actual_list_id || '').trim();\nif (!listId) throw new Error('clickup_actual_list_id is missing.');\nreturn Array.from({ length: maxPages }, (_, i) => ({\n  json: {\n    page: i,\n    clickup_actual_list_id: listId,\n    clickup_actual_list_name: src.clickup_actual_list_name || null,\n    clickup_view_id: src.clickup_view_id || cfg.clickup_list_id || null,\n  }\n}));"},"notesInFlow":false,"typeVersion":2},{"id":"3de10cd3-c39b-45b1-9d20-19911e835915","name":"ClickUp - Get List Tasks Page","type":"n8n-nodes-base.httpRequest","notes":"Fetches paged tasks from the resolved ClickUp list.\n\nRequest options:\n- `subtasks=true`\n- `include_closed=true`\n- `include_timl=true`\n\nPurpose:\n- pull the list contents that will be normalized into parent Deck cards\n- multiple pages are requested based on `max_pages`","position":[1008,384],"parameters":{"url":"={{ $('Set Config').first().json.clickup_base_url.replace(/\\/$/, '') + '/api/v2/list/' + $json.clickup_actual_list_id + '/task' }}","options":{},"sendQuery":true,"sendHeaders":true,"queryParameters":{"parameters":[{"name":"page","value":"={{ $json.page }}"},{"name":"subtasks","value":"true"},{"name":"include_closed","value":"true"},{"name":"include_timl","value":"true"}]},"headerParameters":{"parameters":[{"name":"Authorization","value":"={{ $('Set Config').first().json.clickup_api_token }}"},{"name":"Accept","value":"application/json"}]}},"notesInFlow":false,"typeVersion":4.2},{"id":"dcc46523-411d-4a45-8bcd-f39d1dd1bfc1","name":"ClickUp - Get Task Comments","type":"n8n-nodes-base.httpRequest","notes":"Fetches comments for each normalized parent task.\n\nImportant settings:\n- batched one at a time\n- 800 ms interval to be gentle with ClickUp\n- `continueOnFail=true` so comment failures do not block card creation\n\nComments are appended into the Deck card description in the next node.","position":[3824,464],"parameters":{"url":"={{ $('Set Config').first().json.clickup_base_url.replace(/\\/$/, '') + '/api/v2/task/' + $json.task_id + '/comment' }}","options":{"batching":{"batch":{"batchSize":1,"batchInterval":800}}},"sendHeaders":true,"headerParameters":{"parameters":[{"name":"Authorization","value":"={{ $('Set Config').first().json.clickup_api_token }}"},{"name":"Accept","value":"application/json"}]}},"notesInFlow":false,"typeVersion":4.2,"continueOnFail":true},{"id":"29fb5c0d-445a-472a-a6ba-4850fe612e66","name":"Build Label Items","type":"n8n-nodes-base.code","notes":"Collects unique Deck labels from normalized tasks.\n\nLabels currently come from:\n- OKR custom field\n- Progress custom field\n- Priority\n- Finished\n\nIt also normalizes colors to 6-digit hex values accepted by Deck.","position":[2384,464],"parameters":{"jsCode":"const tasks = $('Normalize ClickUp Parent Tasks').all().map(item => item.json ?? {});\nconst byTitle = new Map();\nfunction normalizeColor(value, fallback = '808080') {\n  let color = String(value || fallback).trim().replace(/^#/, '').toUpperCase();\n  if (/^[0-9A-F]{8}$/.test(color)) color = color.slice(2);\n  if (!/^[0-9A-F]{6}$/.test(color)) color = fallback;\n  return color;\n}\nfor (const task of tasks) {\n  for (const label of Array.isArray(task.deck_labels) ? task.deck_labels : []) {\n    const title = String(label?.title || '').trim();\n    if (!title) continue;\n    if (!byTitle.has(title)) {\n      byTitle.set(title, { title, color: normalizeColor(label?.color, '808080') });\n    }\n  }\n}\nreturn Array.from(byTitle.values()).map(label => ({ json: label }));"},"notesInFlow":false,"typeVersion":2},{"id":"4f0ad243-60d3-449b-8aaa-b6f2f7612dc9","name":"Deck - Create Label","type":"n8n-nodes-base.httpRequest","notes":"Creates each label in the target Deck board.\n\nImplementation details:\n- one request per label\n- `continueOnFail=true` so reruns or duplicate labels do not halt the workflow\n- actual label IDs are retrieved later by re-reading the board","position":[2624,464],"parameters":{"url":"={{ $('Set Config').first().json.nextcloud_base_url.replace(/\\/$/, '') + '/index.php/apps/deck/api/v1.0/boards/' + $('Set Config').first().json.nextcloud_deck_board_id + '/labels' }}","body":"={{ JSON.stringify({ title: $json.title, color: $json.color || '808080' }) }}","method":"POST","options":{"batching":{"batch":{"batchSize":1,"batchInterval":100}}},"sendBody":true,"contentType":"raw","sendHeaders":true,"authentication":"genericCredentialType","rawContentType":"application/json","genericAuthType":"httpBasicAuth","headerParameters":{"parameters":[{"name":"OCS-APIRequest","value":"true"},{"name":"Accept","value":"application/json"},{"name":"Content-Type","value":"application/json"}]}},"notesInFlow":false,"typeVersion":4.2,"continueOnFail":true},{"id":"561e5564-7037-4ad9-9225-4d193ccc7de7","name":"After Label Creation - Emit Once","type":"n8n-nodes-base.code","notes":"Collapses label-create results to one item.\n\nPurpose:\n- continue the workflow once after the full label batch\n- avoid repeating downstream read/preparation work for each label","position":[2864,464],"parameters":{"jsCode":"return [{ json: { label_create_attempts: $input.all().length } }];"},"notesInFlow":false,"typeVersion":2},{"id":"5b55512a-41a9-4c15-a821-1a72861e5393","name":"Deck - Get Board After Labels","type":"n8n-nodes-base.httpRequest","notes":"Reads the board again after label creation.\n\nWhy this matters:\n- Deck label IDs are needed for assignment later\n- the board response is treated as the authoritative label list after any create/duplicate behavior","position":[3104,464],"parameters":{"url":"={{ $('Set Config').first().json.nextcloud_base_url.replace(/\\/$/, '') + '/index.php/apps/deck/api/v1.0/boards/' + $('Set Config').first().json.nextcloud_deck_board_id }}","options":{},"sendHeaders":true,"authentication":"genericCredentialType","genericAuthType":"httpBasicAuth","headerParameters":{"parameters":[{"name":"OCS-APIRequest","value":"true"},{"name":"Accept","value":"application/json"}]}},"notesInFlow":false,"typeVersion":4.2},{"id":"6e0a1544-be12-4b78-b27e-6a45b32d5c96","name":"Prepare Label Assignments","type":"n8n-nodes-base.code","notes":"Builds one output item per `(card, label)` assignment.\n\nInput assumptions:\n- the card was successfully created\n- label IDs were already resolved from the board\n\nThis fan-out happens after card creation because Deck labels are attached to existing cards, not sent inline during create.","position":[4544,464],"parameters":{"jsCode":"const createdCards = $input.all();\nconst preparedCards = $('Append Comments To Task').all();\nconst board = $('Deck - Get Board After Labels').first().json ?? {};\n\nfunction normalizeKey(value) {\n  return String(value || '').trim().toLowerCase().replace(/\\s+/g, ' ');\n}\n\nconst labelById = new Map((Array.isArray(board.labels) ? board.labels : []).map(label => [Number(label.id), label]));\n\nconst out = [];\nfor (const created of createdCards) {\n  const card = created.json ?? {};\n  if (card?.error || !card?.id) continue;\n\n  const pairedIndex = created.pairedItem?.item ?? null;\n  const source = (pairedIndex !== null && preparedCards[pairedIndex]) ? (preparedCards[pairedIndex].json ?? {}) : {};\n  const labelIds = Array.isArray(source.label_ids) ? source.label_ids : [];\n  const stackId = card.stackId || source.stack_id || null;\n\n  for (const labelId of labelIds) {\n    if (!stackId) continue;\n    const label = labelById.get(Number(labelId));\n    out.push({\n      json: {\n        task_id: source.task_id || null,\n        card_id: card.id,\n        stack_id: stackId,\n        label_id: Number(labelId),\n        label_title: label?.title || null,\n      }\n    });\n  }\n}\nreturn out;"},"notesInFlow":false,"typeVersion":2},{"id":"b1ce5c62-0084-4279-8147-15a0a8ceca0e","name":"Deck - Assign Label","type":"n8n-nodes-base.httpRequest","notes":"Assigns one label at a time to a created Deck card.\n\nImplementation details:\n- uses board ID, stack ID, card ID, and label ID\n- `continueOnFail=true` so a single label problem does not stop the summary\n- success/failure counts are reported at the end","position":[4784,464],"parameters":{"url":"={{ $('Set Config').first().json.nextcloud_base_url.replace(/\\/$/, '') + '/index.php/apps/deck/api/v1.0/boards/' + $('Set Config').first().json.nextcloud_deck_board_id + '/stacks/' + $json.stack_id + '/cards/' + $json.card_id + '/assignLabel' }}","body":"={{ JSON.stringify({ labelId: Number($json.label_id) }) }}","method":"PUT","options":{"batching":{"batch":{"batchSize":1,"batchInterval":100}}},"sendBody":true,"contentType":"raw","sendHeaders":true,"authentication":"genericCredentialType","rawContentType":"application/json","genericAuthType":"httpBasicAuth","headerParameters":{"parameters":[{"name":"OCS-APIRequest","value":"true"},{"name":"Accept","value":"application/json"},{"name":"Content-Type","value":"application/json"}]}},"notesInFlow":false,"typeVersion":4.2,"continueOnFail":true},{"id":"8e0d1695-b328-475e-90b4-e020bb245815","name":"Prepare Completed Card Updates","type":"n8n-nodes-base.code","notes":"Builds updates for cards that should be marked done.\n\nOnly tasks recognized as complete in ClickUp are included.\nThe payload carries:\n- board/stack/card IDs\n- title/description/order\n- due date\n- `done` timestamp\n- owner fallback information\n\nCommunity note:\n- this logic references `nextcloud_username` as a fallback owner, but that field is not defined in the current config nodes. It usually still works if Deck returns an owner on card creation; if not, add that config field before sharing widely.","position":[4544,688],"parameters":{"jsCode":"const createdCards = $input.all();\nconst sourceItems = $('Append Comments To Task').all();\nconst fallbackOwner = $('Set Config').first().json.nextcloud_username;\n\nconst out = [];\n\nfor (const created of createdCards) {\n  const card = created.json ?? {};\n  if (card?.error || !card?.id) continue;\n\n  const pairedIndex = created.pairedItem?.item ?? null;\n  const source = (pairedIndex !== null && sourceItems[pairedIndex])\n    ? (sourceItems[pairedIndex].json ?? {})\n    : {};\n\n  if (!source.clickup_is_complete) continue;\n\n  const owner =\n    card.owner ||\n    source.owner ||\n    fallbackOwner;\n\n  out.push({\n    json: {\n      board_id: Number($('Set Config').first().json.nextcloud_deck_board_id),\n      stack_id: Number(card.stackId || source.stack_id),\n      card_id: Number(card.id),\n      title: source.title || card.title || '',\n      description: source.description || '',\n      order: Number(source.order || card.order || 999),\n      duedate: source.duedate || null,\n      done: source.clickup_done_at || new Date().toISOString(),\n      owner,\n      task_id: source.task_id || null,\n    }\n  });\n}\n\nreturn out;"},"notesInFlow":false,"typeVersion":2},{"id":"54792a24-e7d8-42e6-9d98-d1f39c439778","name":"Deck - Mark Card Done","type":"n8n-nodes-base.httpRequest","notes":"Updates already-created Deck cards to mark them done.\n\nWhy this is a separate step:\n- cards are first created normally\n- completed ClickUp tasks are then patched with a `done` timestamp\n\nThe request also re-sends title/description/order (and due date when present) to keep the update payload complete.","position":[4784,688],"parameters":{"url":"={{ $('Set Config').first().json.nextcloud_base_url.replace(/\\/$/, '') + '/index.php/apps/deck/api/v1.0/boards/' + $('Set Config').first().json.nextcloud_deck_board_id + '/stacks/' + $json.stack_id + '/cards/' + $json.card_id }}","body":"={{ (() => {\n  const payload = {\n    title: $json.title || '',\n    description: $json.description || '',\n    type: 'plain',\n    order: Number($json.order || 999),\n    owner: $json.owner || $('Set Config').first().json.nextcloud_username,\n    done: $json.done,\n  };\n\n  if ($json.duedate) {\n    payload.duedate = $json.duedate;\n  }\n\n  return JSON.stringify(payload);\n})() }}","method":"PUT","options":{"batching":{"batch":{"batchSize":1,"batchInterval":100}}},"sendBody":true,"contentType":"raw","sendHeaders":true,"authentication":"genericCredentialType","rawContentType":"application/json","genericAuthType":"httpBasicAuth","headerParameters":{"parameters":[{"name":"OCS-APIRequest","value":"true"},{"name":"Accept","value":"application/json"},{"name":"Content-Type","value":"application/json"}]}},"notesInFlow":false,"typeVersion":4.2,"continueOnFail":true},{"id":"c00ce336-398c-49d2-8a45-00c68af881fb","name":"Set Config - View","type":"n8n-nodes-base.set","notes":"Configuration for **view mode**.\n\nFill these placeholders before running:\n- `clickup_list_id` = **ClickUp view ID** in this mode (the field name is legacy)\n- `clickup_api_token` = ClickUp personal/team API token\n- `nextcloud_base_url` = your Nextcloud base URL without extra path\n- `nextcloud_deck_board_id` = target Deck board ID\n- `max_pages` = maximum number of ClickUp list pages to read\n- `status_stack_map_json` = mapping from ClickUp status to Deck stack title/order\n- `subtask_title_prefix` = prefix shown before subtask names in the card description\n\nImportant:\n- `source_mode` must stay `view`\n- this node contains placeholders, not real secrets\n- downstream nodes expect `status_stack_map_json` to be valid JSON","position":[-944,544],"parameters":{"options":{},"assignments":{"assignments":[{"id":"548d43a2-d8b4-4150-91ee-56cc49bd29b6","name":"clickup_base_url","type":"string","value":"https://api.clickup.com"},{"id":"94250d4a-f257-4e07-aa7b-24a3e913884a","name":"clickup_list_id","type":"string","value":"INSERT-LIST-ID-HERE"},{"id":"3ffa7913-4920-4a59-a3a2-92c1846fe1d8","name":"clickup_api_token","type":"string","value":"INSERT-API-KEY-HERE"},{"id":"b75d855a-03fb-4a90-84b6-79f1ca4510bc","name":"nextcloud_base_url","type":"string","value":"INSERT-BASE-URL-HERE"},{"id":"d789fa71-3b70-4fc6-989e-2ef583da8fa0","name":"nextcloud_deck_board_id","type":"string","value":"INSERT-DECK-ID-HERE"},{"id":"0c9b7f45-2241-4e1e-a35c-085d3a32a040","name":"max_pages","type":"string","value":"10"},{"id":"28598936-b7c3-42c1-adaf-bb51e2407a6d","name":"status_stack_map_json","type":"string","value":"[{\"status_key\": \"to do\", \"stack_title\": \"To do\", \"order\": 100}, {\"status_key\": \"in progress\", \"stack_title\": \"In progress\", \"order\": 200}, {\"status_key\": \"complete\", \"stack_title\": \"Complete\", \"order\": 300}]"},{"id":"69fcb569-c377-48d0-8df0-477509c62e8a","name":"subtask_title_prefix","type":"string","value":"↳ "},{"id":"36d3033f-d3f7-4821-ae5c-a9ab1ccac993","name":"source_mode","type":"string","value":"view"}]}},"notesInFlow":false,"typeVersion":3.4},{"id":"1d39725f-7407-4845-ac15-5075b0ec6262","name":"Normalize ClickUp Parent Tasks - View","type":"n8n-nodes-base.code","notes":"Transforms the full list payload from view mode into the workflow's shared task schema.\n\nMain jobs:\n- deduplicate tasks across pages\n- keep only parent tasks as card candidates\n- map ClickUp statuses to Deck stacks\n- build markdown description with metadata, description, checklists, and subtasks\n- derive Deck labels from OKR, Progress, Priority, and completion state\n- convert due/completion dates to ISO format\n\nOutput fields are designed for downstream stack/label/card creation.","position":[1248,384],"parameters":{"jsCode":"const cfg = $('Set Config - View').first().json;\nconst inputItems = $input.all();\n\nfunction asArray(value) {\n  return Array.isArray(value) ? value : [];\n}\nfunction pick(obj, keys, fallback = null) {\n  for (const key of keys) {\n    if (obj && obj[key] !== undefined && obj[key] !== null && obj[key] !== '') return obj[key];\n  }\n  return fallback;\n}\nfunction firstNonEmpty(...values) {\n  for (const value of values) {\n    if (value !== undefined && value !== null && String(value).trim() !== '') return value;\n  }\n  return null;\n}\nfunction normalizeStatusKey(value) {\n  return String(value || '').trim().toLowerCase().replace(/\\s+/g, ' ');\n}\nfunction titleCase(value) {\n  return String(value || '')\n    .split(' ')\n    .filter(Boolean)\n    .map(word => word.charAt(0).toUpperCase() + word.slice(1))\n    .join(' ') || 'Other';\n}\nfunction truncate(value, maxLength) {\n  const str = String(value || '');\n  return str.length <= maxLength ? str : str.slice(0, Math.max(0, maxLength - 1)).trimEnd() + '…';\n}\nfunction toMs(value) {\n  if (value === null || value === undefined || value === '') return null;\n  const n = Number(value);\n  if (!Number.isFinite(n) || n <= 0) return null;\n  return n;\n}\nfunction toIso(value) {\n  const ms = toMs(value);\n  if (!ms) return null;\n  const unixMs = ms > 9999999999 ? ms : ms * 1000;\n  try {\n    return new Date(unixMs).toISOString();\n  } catch {\n    return null;\n  }\n}\nfunction namesFromUsers(users) {\n  return asArray(users)\n    .map(user => user?.username || user?.email || user?.initials || user?.id)\n    .filter(Boolean)\n    .join(', ');\n}\nfunction namesFromTags(tags) {\n  return asArray(tags)\n    .map(tag => tag?.name || tag?.tag || tag)\n    .filter(Boolean)\n    .join(', ');\n}\nfunction priorityText(priority) {\n  if (!priority) return null;\n  if (typeof priority === 'string') return priority;\n  return priority.priority || priority.name || priority.orderindex || null;\n}\nfunction normalizeColor(value, fallback = '808080') {\n  let color = String(value || fallback).trim().replace(/^#/, '').toUpperCase();\n  if (/^[0-9A-F]{8}$/.test(color)) color = color.slice(2);\n  if (!/^[0-9A-F]{6}$/.test(color)) color = fallback;\n  return color;\n}\nfunction checklistMarkdown(checklists) {\n  const groups = [];\n  for (const checklist of asArray(checklists)) {\n    const title = checklist?.name || checklist?.title || 'Checklist';\n    const items = asArray(checklist?.items).map(item => {\n      const label = item?.name || item?.label || item?.text || 'Item';\n      const checked = item?.resolved || item?.checked || item?.completed || false;\n      return `- [${checked ? 'x' : ' '}] ${label}`;\n    });\n    if (items.length) groups.push(`### ${title}\\n` + items.join('\\n'));\n  }\n  return groups.join('\\n\\n');\n}\nfunction getCustomField(task, fieldName) {\n  return asArray(task.custom_fields).find(field => String(field?.name || '').trim().toLowerCase() === String(fieldName).trim().toLowerCase()) || null;\n}\nfunction getDropdownFieldInfo(field) {\n  if (!field) return null;\n  const raw = field.value;\n  const options = asArray(field.type_config?.options);\n  if (raw === null || raw === undefined || raw === '') return null;\n  const match = options.find(option =>\n    String(option?.id) === String(raw) ||\n    String(option?.orderindex) === String(raw) ||\n    Number(option?.orderindex) === Number(raw)\n  );\n  if (!match) return null;\n  return {\n    name: String(match.name || '').trim(),\n    color: normalizeColor(match.color, '808080'),\n  };\n}\nfunction getProgressToOkrText(field) {\n  if (!field || !field.value) return null;\n  const value = field.value;\n  if (typeof value.percent_completed === 'number') return `${Math.round(value.percent_completed * 100)}%`;\n  if (value.current !== null && value.current !== undefined && value.current !== '') return `${value.current}%`;\n  return null;\n}\nfunction extractTasks(itemJson) {\n  if (!itemJson) return [];\n  if (Array.isArray(itemJson)) return itemJson;\n  const candidates = [itemJson.tasks, itemJson.data?.tasks, itemJson.data, itemJson.items, itemJson.results];\n  return candidates.find(v => Array.isArray(v)) || [];\n}\n\nlet statusMap = [];\ntry {\n  statusMap = JSON.parse(cfg.status_stack_map_json || '[]');\n} catch (error) {\n  throw new Error('status_stack_map_json is not valid JSON.');\n}\nconst statusMapping = new Map(\n  statusMap.map(item => [\n    normalizeStatusKey(item.status_key),\n    {\n      stack_title: item.stack_title || titleCase(item.status_key),\n      stack_order: Number(item.order || 999),\n    }\n  ])\n);\n\nconst byId = new Map();\nfor (const item of inputItems) {\n  const tasks = extractTasks(item.json ?? {});\n  for (const task of tasks) {\n    const id = String(task?.id || task?.task_id || '').trim();\n    if (!id) continue;\n    if (!byId.has(id)) byId.set(id, task);\n  }\n}\n\nconst allTasks = Array.from(byId.values());\nconst parentTasks = allTasks.filter(task => {\n  const parentId = firstNonEmpty(task.parent, task.parent_id);\n  return !(parentId && String(parentId).trim() !== '');\n});\n\nfunction buildSubtaskMarkdown(subtasks) {\n  if (!subtasks.length) return null;\n  const prefix = String(cfg.subtask_title_prefix || '↳ ');\n  return subtasks.map(subtask => {\n    const statusText = firstNonEmpty(subtask?.status?.status, subtask?.status?.name, subtask?.status, 'other');\n    const priority = priorityText(subtask.priority);\n    const dueDateIso = toIso(firstNonEmpty(subtask.due_date, subtask.duedate));\n    const isDone = normalizeStatusKey(statusText) === 'complete' || subtask?.status?.type === 'closed' || !!firstNonEmpty(subtask.date_closed, subtask.done);\n    const details = [\n      statusText ? `status: ${statusText}` : null,\n      priority ? `priority: ${priority}` : null,\n      dueDateIso ? `due: ${dueDateIso}` : null\n    ].filter(Boolean).join(' | ');\n    return `- [${isDone ? 'x' : ' '}] ${prefix}${subtask.name || subtask.id}${details ? ` — ${details}` : ''}`;\n  }).join('\\n');\n}\n\nconst out = [];\nfor (const task of parentTasks) {\n  const taskId = String(task.id || task.task_id);\n  const taskName = String(firstNonEmpty(task.name, task.task_name, `Task ${taskId}`)).trim();\n  const rawStatus = firstNonEmpty(task?.status?.status, task?.status?.name, task?.status, 'other');\n  const statusKey = normalizeStatusKey(rawStatus);\n  const mappedStatus = statusMapping.get(statusKey) || {\n    stack_title: titleCase(statusKey),\n    stack_order: 900,\n  };\n  const createdAtMs = toMs(firstNonEmpty(task.date_created, task.created_at));\n  const dueDateIso = toIso(firstNonEmpty(task.due_date, task.duedate));\n  const startDateIso = toIso(firstNonEmpty(task.start_date));\n  const doneAtIso = toIso(firstNonEmpty(task.date_closed, task.done));\n  const isComplete = task?.status?.type === 'closed' || statusKey === 'complete' || !!doneAtIso;\n  const nativeTags = namesFromTags(task.tags);\n  const assignees = namesFromUsers(task.assignees);\n  const priority = priorityText(task.priority);\n  const checklistMd = checklistMarkdown(task.checklists);\n  const descriptionMd = String(firstNonEmpty(task.markdown_description, task.description, task.text_content, '') || '');\n  const clickupUrl = firstNonEmpty(task.url, `https://app.clickup.com/t/${taskId}`);\n  const timeEstimateMs = toMs(firstNonEmpty(task.time_estimate));\n  const timeSpentMs = toMs(firstNonEmpty(task.time_spent));\n  const timeEstimateHours = timeEstimateMs ? (timeEstimateMs / 3600000).toFixed(2) : null;\n  const timeSpentHours = timeSpentMs ? (timeSpentMs / 3600000).toFixed(2) : null;\n  const okrInfo = getDropdownFieldInfo(getCustomField(task, 'OKR'));\n  const progressInfo = getDropdownFieldInfo(getCustomField(task, 'Progress'));\n  const progressToOkrText = getProgressToOkrText(getCustomField(task, 'Progress to OKR'));\n  const deckLabels = [\n    okrInfo?.name ? { title: `OKR: ${okrInfo.name}`, color: okrInfo.color } : null,\n    progressInfo?.name ? { title: `Progress: ${progressInfo.name}`, color: progressInfo.color } : null,\n    priority ? { title: `Priority: ${String(priority).trim().toLowerCase()}`, color: normalizeColor(task?.priority?.color, '808080') } : null,\n    isComplete ? { title: 'Finished', color: '31CC7C' } : null,\n  ].filter(Boolean);\n\n  const subtasks = allTasks\n    .filter(candidate => String(firstNonEmpty(candidate.parent, candidate.parent_id) || '') === taskId)\n    .sort((a, b) => {\n      const aMs = toMs(firstNonEmpty(a.date_created, a.created_at)) || 0;\n      const bMs = toMs(firstNonEmpty(b.date_created, b.created_at)) || 0;\n      if (aMs !== bMs) return aMs - bMs;\n      return String(a.name || a.id).localeCompare(String(b.name || b.id));\n    });\n  const subtasksMd = buildSubtaskMarkdown(subtasks);\n\n  const metaLines = [\n    `- **ClickUp task ID:** \\`${taskId}\\``,\n    `- **Status:** ${mappedStatus.stack_title}`,\n    isComplete ? `- **Completed in ClickUp:** Yes` : null,\n    doneAtIso ? `- **Completed at:** ${doneAtIso}` : null,\n    priority ? `- **Priority:** ${priority}` : null,\n    okrInfo?.name ? `- **OKR:** ${okrInfo.name}` : null,\n    progressInfo?.name ? `- **Progress:** ${progressInfo.name}` : null,\n    progressToOkrText ? `- **Progress to OKR:** ${progressToOkrText}` : null,\n    assignees ? `- **Assignees:** ${assignees}` : null,\n    nativeTags ? `- **Tags:** ${nativeTags}` : null,\n    startDateIso ? `- **Start date:** ${startDateIso}` : null,\n    dueDateIso ? `- **Due date:** ${dueDateIso}` : null,\n    timeEstimateHours ? `- **Time estimate (h):** ${timeEstimateHours}` : null,\n    timeSpentHours ? `- **Time spent (h):** ${timeSpentHours}` : null,\n    clickupUrl ? `- **ClickUp URL:** ${clickupUrl}` : null,\n  ].filter(Boolean);\n\n  const sections = [\n    `Imported once from ClickUp source \\`${cfg.clickup_list_id}\\`.`,\n    '',\n    ...metaLines,\n    '',\n    descriptionMd ? `## Description\\n\\n${descriptionMd}` : null,\n    checklistMd ? `## Checklists\\n\\n${checklistMd}` : null,\n    subtasksMd ? `## Subtasks\\n\\n${subtasksMd}` : null,\n  ].filter(section => section !== null);\n\n  const description = sections.join('\\n');\n\n  out.push({\n    json: {\n      task_id: taskId,\n      task_name: taskName,\n      stack_key: statusKey,\n      stack_title: mappedStatus.stack_title,\n      stack_order: mappedStatus.stack_order,\n      title: truncate(taskName, 255),\n      description,\n      description_base: description,\n      duedate: dueDateIso,\n      created_at_ms: createdAtMs || 0,\n      deck_labels: deckLabels,\n      clickup_is_complete: isComplete,\n      clickup_done_at: doneAtIso,\n    }\n  });\n}\n\nout.sort((a, b) => {\n  const aj = a.json;\n  const bj = b.json;\n  if ((aj.stack_order || 999) !== (bj.stack_order || 999)) return (aj.stack_order || 999) - (bj.stack_order || 999);\n  if ((aj.created_at_ms || 0) !== (bj.created_at_ms || 0)) return (aj.created_at_ms || 0) - (bj.created_at_ms || 0);\n  return String(aj.title || '').localeCompare(String(bj.title || ''));\n});\n\nif (!out.length) {\n  throw new Error('No parent tasks were produced from ClickUp list pages. Check the list endpoint output.');\n}\nreturn out;"},"notesInFlow":false,"typeVersion":2},{"id":"773e4122-8f19-4f7e-ae20-117f1ddfbe8e","name":"Set Config - Task Root","type":"n8n-nodes-base.set","notes":"Configuration for **task_root mode**.\n\nFill these placeholders before running:\n- `clickup_task_id` = the ClickUp root task ID whose full subtree should be imported\n- `clickup_api_token` = ClickUp personal/team API token\n- `nextcloud_base_url` = your Nextcloud base URL\n- `nextcloud_deck_board_id` = target Deck board ID\n- `max_pages` = how many pages of the home list to scan while finding descendants\n- `status_stack_map_json` = status-to-stack mapping\n- `subtask_title_prefix` = prefix added before subtask names\n\nImportant:\n- `source_mode` must stay `task_root`\n- this path is useful when one list is huge but you only want one project/task tree\n- this node contains placeholders, not real secrets","position":[-944,768],"parameters":{"options":{},"assignments":{"assignments":[{"id":"e3d5b9f4-5f86-41b2-ae3e-d30cdc3e1e5f","name":"clickup_base_url","type":"string","value":"https://api.clickup.com"},{"id":"bcd206f3-80e4-405f-bf52-fb3be523cdc8","name":"clickup_api_token","type":"string","value":"INSERT-API-KEY-HERE"},{"id":"43d5f028-ebb2-404a-8bcf-71bdf6e6c54e","name":"nextcloud_base_url","type":"string","value":"INSERT-URL-HERE"},{"id":"a928aca4-ddee-4aee-b786-37ce6f2841dc","name":"nextcloud_deck_board_id","type":"string","value":"INSERT-BOARD-ID-HERE"},{"id":"67eba8a4-e439-430a-82fc-50cb998b2241","name":"max_pages","type":"string","value":"10"},{"id":"e40cc21d-de9a-414c-86bc-91f290f133c0","name":"status_stack_map_json","type":"string","value":"[{\"status_key\": \"to do\", \"stack_title\": \"To do\", \"order\": 100}, {\"status_key\": \"in progress\", \"stack_title\": \"In progress\", \"order\": 200}, {\"status_key\": \"complete\", \"stack_title\": \"Complete\", \"order\": 300}]"},{"id":"6c558d06-0c1b-47cd-9be6-cc768192bf69","name":"subtask_title_prefix","type":"string","value":"↳ "},{"id":"9d36b548-a05d-42b6-88d9-c94d353ec8ed","name":"clickup_task_id","type":"string","value":"INSERT-TASK-ID-HERE"},{"id":"ca4b3c36-547c-4276-8db0-3b4337bdddcf","name":"source_mode","type":"string","value":"task_root"}]}},"notesInFlow":false,"typeVersion":3.4},{"id":"82738d9c-445a-443d-bc56-04284080d2ca","name":"Set Config","type":"n8n-nodes-base.code","notes":"Pass-through normalizer for the chosen config node.\n\nPurpose:\n- whichever config node runs upstream, this code node copies its JSON forward unchanged\n- downstream nodes always read settings from **Set Config**, so both source modes can share the same later logic","position":[-624,576],"parameters":{"jsCode":"return $input.all().map(item => ({ json: { ...(item.json ?? {}) } }));"},"notesInFlow":false,"typeVersion":2},{"id":"4f5a0530-9982-4629-a80f-a520a7c52382","name":"IF - Task Root Mode","type":"n8n-nodes-base.if","notes":"Routes execution by `source_mode`.\n\nBranches:\n- **false / upper branch**: view mode → use ClickUp view to discover the real list, then import all parent tasks from that list\n- **true / lower branch**: task-root mode → fetch the chosen root task, discover its home list, then keep only that task subtree\n\nThis node is the switch that allows one shared downstream migration pipeline.","position":[-240,576],"parameters":{"options":{},"conditions":{"options":{"version":2,"leftValue":"","caseSensitive":true,"typeValidation":"strict"},"combinator":"and","conditions":[{"id":"86c6f764-d440-43e9-bae8-b0668920735b","operator":{"type":"string","operation":"equal"},"leftValue":"={{ $json.source_mode }}","rightValue":"task_root"}]}},"notesInFlow":false,"typeVersion":2.2},{"id":"9f4c1feb-5317-419b-984d-1544ef73ecc5","name":"ClickUp - Get Root Task","type":"n8n-nodes-base.httpRequest","notes":"Fetches the specific ClickUp task referenced by `clickup_task_id`.\n\nPurpose:\n- verify the task exists\n- get the task name/URL\n- discover the task's home list so the workflow can scan that list for descendants","position":[48,688],"parameters":{"url":"={{ $('Set Config').first().json.clickup_base_url.replace(/\\/$/, '') + '/api/v2/task/' + $('Set Config').first().json.clickup_task_id }}","options":{},"sendHeaders":true,"headerParameters":{"parameters":[{"name":"Authorization","value":"={{ $('Set Config').first().json.clickup_api_token }}"},{"name":"Accept","value":"application/json"}]}},"notesInFlow":false,"typeVersion":4.2},{"id":"bae39d2b-eab0-4eb5-b4ce-1b88bdb396ed","name":"Resolve Root Task Home List ID","type":"n8n-nodes-base.code","notes":"Extracts root-task metadata needed by the task-root branch.\n\nOutput:\n- root task ID/name/URL\n- the home list ID/name\n\nThe workflow stops here if the root task or its home list cannot be resolved.","position":[288,688],"parameters":{"jsCode":"const task = $input.first().json ?? {};\nconst listId = task?.list?.id || task?.list_id || null;\nconst listName = task?.list?.name || task?.list_name || null;\nconst rootTaskId = String(task?.id || task?.task_id || '').trim();\nconst rootTaskName = String(task?.name || task?.task_name || rootTaskId || 'Root task').trim();\nconst rootTaskUrl = task?.url || (rootTaskId ? `https://app.clickup.com/t/${rootTaskId}` : null);\nif (!rootTaskId) throw new Error('clickup_task_id did not return a valid ClickUp task.');\nif (!listId) throw new Error('Could not resolve the home list id from the root task.');\nreturn [{ json: { root_task_id: rootTaskId, root_task_name: rootTaskName, root_task_url: rootTaskUrl, clickup_actual_list_id: String(listId), clickup_actual_list_name: listName || null } }];"},"notesInFlow":false,"typeVersion":2},{"id":"1f38fef9-aed3-404e-a61d-505a94f91250","name":"Build Root Task List Page Numbers","type":"n8n-nodes-base.code","notes":"Creates page items for scanning the root task's home list.\n\nUses:\n- discovered home list ID\n- configured `max_pages`\n\nWhy scan the full list:\n- ClickUp's task API returns the root task, but descendants may need to be found from paged list results before the subtree can be isolated","position":[528,688],"parameters":{"jsCode":"const cfg = $('Set Config').first().json;\nconst src = $input.first().json ?? {};\nconst maxPages = Math.max(1, Number(cfg.max_pages || 10));\nconst listId = String(src.clickup_actual_list_id || '').trim();\nif (!listId) throw new Error('clickup_actual_list_id is missing.');\nreturn Array.from({ length: maxPages }, (_, i) => ({ json: { page: i, clickup_actual_list_id: listId, clickup_actual_list_name: src.clickup_actual_list_name || null, root_task_id: src.root_task_id, root_task_name: src.root_task_name || null, root_task_url: src.root_task_url || null } }));"},"notesInFlow":false,"typeVersion":2},{"id":"82bd02f4-fcd9-478b-a625-26746d856ccc","name":"ClickUp - Get Root Task List Tasks Page","type":"n8n-nodes-base.httpRequest","notes":"Fetches pages from the root task's home list.\n\nRequest options mirror the main list import:\n- subtasks included\n- closed tasks included\n- `include_timl=true`\n\nThese pages are later filtered down to only the chosen root task and its descendants.","position":[768,688],"parameters":{"url":"={{ $('Set Config').first().json.clickup_base_url.replace(/\\/$/, '') + '/api/v2/list/' + $json.clickup_actual_list_id + '/task' }}","options":{},"sendQuery":true,"sendHeaders":true,"queryParameters":{"parameters":[{"name":"page","value":"={{ $json.page }}"},{"name":"subtasks","value":"true"},{"name":"include_closed","value":"true"},{"name":"include_timl","value":"true"}]},"headerParameters":{"parameters":[{"name":"Authorization","value":"={{ $('Set Config').first().json.clickup_api_token }}"},{"name":"Accept","value":"application/json"}]}},"notesInFlow":false,"typeVersion":4.2},{"id":"4b00a98e-7381-4265-96d4-276115196fa1","name":"Filter Root Task Tree","type":"n8n-nodes-base.code","notes":"Builds the root-task subtree.\n\nLogic:\n- starts with the fetched root task\n- adds paged list tasks into a map\n- walks parent relationships until all descendants of the root task are included\n\nOutput:\n- one payload containing `tasks` limited to the selected subtree","position":[1008,688],"parameters":{"jsCode":"const pageItems = $input.all();\nconst rootTask = $('ClickUp - Get Root Task').first().json ?? {};\nconst rootId = String(rootTask?.id || rootTask?.task_id || '').trim();\nfunction extractTasks(itemJson) {\n  if (!itemJson) return [];\n  if (Array.isArray(itemJson)) return itemJson;\n  const candidates = [itemJson.tasks, itemJson.data?.tasks, itemJson.data, itemJson.items, itemJson.results];\n  return candidates.find(v => Array.isArray(v)) || [];\n}\nfunction firstNonEmpty(...values) {\n  for (const value of values) {\n    if (value !== undefined && value !== null && String(value).trim() !== '') return value;\n  }\n  return null;\n}\nif (!rootId) throw new Error('Root task id missing.');\nconst byId = new Map();\nbyId.set(rootId, rootTask);\nfor (const item of pageItems) {\n  const tasks = extractTasks(item.json ?? {});\n  for (const task of tasks) {\n    const id = String(task?.id || task?.task_id || '').trim();\n    if (!id) continue;\n    if (!byId.has(id)) byId.set(id, task);\n  }\n}\nconst subtreeIds = new Set([rootId]);\nlet changed = true;\nwhile (changed) {\n  changed = false;\n  for (const task of byId.values()) {\n    const id = String(task?.id || task?.task_id || '').trim();\n    const parentId = String(firstNonEmpty(task?.parent, task?.parent_id) || '').trim();\n    if (!id || !parentId) continue;\n    if (subtreeIds.has(parentId) && !subtreeIds.has(id)) {\n      subtreeIds.add(id);\n      changed = true;\n    }\n  }\n}\nconst tasks = Array.from(subtreeIds).map(id => byId.get(id)).filter(Boolean);\nreturn [{ json: { root_task_id: rootId, root_task_name: rootTask?.name || rootTask?.task_name || rootId, root_task_url: rootTask?.url || `https://app.clickup.com/t/${rootId}`, tasks } }];"},"notesInFlow":false,"typeVersion":2},{"id":"653773f4-f1e8-4ea1-a18d-42efa4e553a7","name":"Normalize ClickUp Task Tree","type":"n8n-nodes-base.code","notes":"Normalizes the task-root subtree into the same schema produced by the view branch.\n\nThis keeps downstream logic shared:\n- same task/card structure\n- same stack mapping\n- same description building\n- same label derivation\n- same completion and due-date formatting","position":[1248,688],"parameters":{"jsCode":"const cfg = $('Set Config').first().json;\nconst payload = $input.first().json ?? {};\nconst rootTask = $('ClickUp - Get Root Task').first().json ?? {};\nconst inputTasks = Array.isArray(payload.tasks) ? payload.tasks : [];\n\nfunction asArray(value) { return Array.isArray(value) ? value : []; }\nfunction pick(obj, keys, fallback = null) {\n  for (const key of keys) {\n    if (obj && obj[key] !== undefined && obj[key] !== null && obj[key] !== '') return obj[key];\n  }\n  return fallback;\n}\nfunction firstNonEmpty(...values) {\n  for (const value of values) {\n    if (value !== undefined && value !== null && String(value).trim() !== '') return value;\n  }\n  return null;\n}\nfunction normalizeStatusKey(value) {\n  return String(value || '').trim().toLowerCase().replace(/\\s+/g, ' ');\n}\nfunction titleCase(value) {\n  return String(value || '').split(' ').filter(Boolean).map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ') || 'Other';\n}\nfunction truncate(value, maxLength) {\n  const str = String(value || '');\n  return str.length <= maxLength ? str : str.slice(0, Math.max(0, maxLength - 1)).trimEnd() + '…';\n}\nfunction toMs(value) {\n  if (value === null || value === undefined || value === '') return null;\n  const n = Number(value);\n  if (!Number.isFinite(n) || n <= 0) return null;\n  return n;\n}\nfunction toIso(value) {\n  const ms = toMs(value);\n  if (!ms) return null;\n  const unixMs = ms > 9999999999 ? ms : ms * 1000;\n  try { return new Date(unixMs).toISOString(); } catch { return null; }\n}\nfunction namesFromUsers(users) { return asArray(users).map(user => user?.username || user?.email || user?.initials || user?.id).filter(Boolean).join(', '); }\nfunction namesFromTags(tags) { return asArray(tags).map(tag => tag?.name || tag?.tag || tag).filter(Boolean).join(', '); }\nfunction priorityText(priority) {\n  if (!priority) return null;\n  if (typeof priority === 'string') return priority;\n  return priority.priority || priority.name || priority.orderindex || null;\n}\nfunction normalizeColor(value, fallback = '808080') {\n  let color = String(value || fallback).trim().replace(/^#/, '').toUpperCase();\n  if (/^[0-9A-F]{8}$/.test(color)) color = color.slice(2);\n  if (!/^[0-9A-F]{6}$/.test(color)) color = fallback;\n  return color;\n}\nfunction checklistMarkdown(checklists) {\n  const groups = [];\n  for (const checklist of asArray(checklists)) {\n    const title = checklist?.name || checklist?.title || 'Checklist';\n    const items = asArray(checklist?.items).map(item => {\n      const label = item?.name || item?.label || item?.text || 'Item';\n      const checked = item?.resolved || item?.checked || item?.completed || false;\n      return `- [${checked ? 'x' : ' '}] ${label}`;\n    });\n    if (items.length) groups.push(`### ${title}\\n` + items.join('\\n'));\n  }\n  return groups.join('\\n\\n');\n}\nfunction getCustomField(task, fieldName) {\n  return asArray(task.custom_fields).find(field => String(field?.name || '').trim().toLowerCase() === String(fieldName).trim().toLowerCase()) || null;\n}\nfunction getDropdownFieldInfo(field) {\n  if (!field) return null;\n  const raw = field.value;\n  const options = asArray(field.type_config?.options);\n  if (raw === null || raw === undefined || raw === '') return null;\n  const match = options.find(option =>\n    String(option?.id) === String(raw) ||\n    String(option?.orderindex) === String(raw) ||\n    Number(option?.orderindex) === Number(raw)\n  );\n  if (!match) return null;\n  return { name: String(match.name || '').trim(), color: normalizeColor(match.color, '808080') };\n}\nfunction getProgressToOkrText(field) {\n  if (!field || !field.value) return null;\n  const value = field.value;\n  if (typeof value.percent_completed === 'number') return `${Math.round(value.percent_completed * 100)}%`;\n  if (value.current !== null && value.current !== undefined && value.current !== '') return `${value.current}%`;\n  return null;\n}\n\nlet statusMap = [];\ntry { statusMap = JSON.parse(cfg.status_stack_map_json || '[]'); } catch (error) { throw new Error('status_stack_map_json is not valid JSON.'); }\nconst statusMapping = new Map(statusMap.map(item => [normalizeStatusKey(item.status_key), { stack_title: item.stack_title || titleCase(item.status_key), stack_order: Number(item.order || 999) }]));\n\nconst byId = new Map();\nfor (const task of inputTasks) {\n  const id = String(task?.id || task?.task_id || '').trim();\n  if (!id) continue;\n  byId.set(id, task);\n}\n\nconst rootId = String(rootTask?.id || payload.root_task_id || '').trim();\nconst directChildren = Array.from(byId.values()).filter(task => String(firstNonEmpty(task.parent, task.parent_id) || '').trim() === rootId);\nconst cards = directChildren.length ? directChildren : [rootTask];\n\nfunction descendantsFor(cardId) {\n  const result = [];\n  for (const task of byId.values()) {\n    const id = String(task?.id || task?.task_id || '').trim();\n    if (!id || id === cardId) continue;\n    let parentId = String(firstNonEmpty(task.parent, task.parent_id) || '').trim();\n    const seen = new Set();\n    while (parentId && !seen.has(parentId)) {\n      seen.add(parentId);\n      if (parentId === cardId) { result.push(task); break; }\n      const parentTask = byId.get(parentId);\n      parentId = String(firstNonEmpty(parentTask?.parent, parentTask?.parent_id) || '').trim();\n    }\n  }\n  result.sort((a, b) => {\n    const aMs = toMs(firstNonEmpty(a.date_created, a.created_at)) || 0;\n    const bMs = toMs(firstNonEmpty(b.date_created, b.created_at)) || 0;\n    if (aMs !== bMs) return aMs - bMs;\n    return String(a.name || a.id).localeCompare(String(b.name || b.id));\n  });\n  return result;\n}\nfunction depthFrom(cardId, task) {\n  let depth = 1;\n  let parentId = String(firstNonEmpty(task.parent, task.parent_id) || '').trim();\n  const seen = new Set();\n  while (parentId && !seen.has(parentId)) {\n    seen.add(parentId);\n    if (parentId === cardId) return depth;\n    const parentTask = byId.get(parentId);\n    parentId = String(firstNonEmpty(parentTask?.parent, parentTask?.parent_id) || '').trim();\n    depth += 1;\n  }\n  return depth;\n}\nfunction buildSubtaskMarkdown(cardId) {\n  const descendants = descendantsFor(cardId);\n  if (!descendants.length) return null;\n  const prefix = String(cfg.subtask_title_prefix || '↳ ');\n  return descendants.map(task => {\n    const statusText = firstNonEmpty(task?.status?.status, task?.status?.name, task?.status, 'other');\n    const priority = priorityText(task.priority);\n    const dueDateIso = toIso(firstNonEmpty(task.due_date, task.duedate));\n    const isDone = normalizeStatusKey(statusText) === 'complete' || task?.status?.type === 'closed' || !!firstNonEmpty(task.date_closed, task.done);\n    const details = [statusText ? `status: ${statusText}` : null, priority ? `priority: ${priority}` : null, dueDateIso ? `due: ${dueDateIso}` : null].filter(Boolean).join(' | ');\n    const depth = depthFrom(cardId, task);\n    const indent = '  '.repeat(Math.max(0, depth - 1));\n    return `${indent}- [${isDone ? 'x' : ' '}] ${prefix}${task.name || task.id}${details ? ` — ${details}` : ''}`;\n  }).join('\\n');\n}\n\nconst out = [];\nfor (const task of cards) {\n  const taskId = String(task.id || task.task_id);\n  const taskName = String(firstNonEmpty(task.name, task.task_name, `Task ${taskId}`)).trim();\n  const rawStatus = firstNonEmpty(task?.status?.status, task?.status?.name, task?.status, 'other');\n  const statusKey = normalizeStatusKey(rawStatus);\n  const mappedStatus = statusMapping.get(statusKey) || { stack_title: titleCase(statusKey), stack_order: 900 };\n  const createdAtMs = toMs(firstNonEmpty(task.date_created, task.created_at));\n  const dueDateIso = toIso(firstNonEmpty(task.due_date, task.duedate));\n  const startDateIso = toIso(firstNonEmpty(task.start_date));\n  const doneAtIso = toIso(firstNonEmpty(task.date_closed, task.done));\n  const isComplete = task?.status?.type === 'closed' || statusKey === 'complete' || !!doneAtIso;\n  const nativeTags = namesFromTags(task.tags);\n  const assignees = namesFromUsers(task.assignees);\n  const priority = priorityText(task.priority);\n  const checklistMd = checklistMarkdown(task.checklists);\n  const descriptionMd = String(firstNonEmpty(task.markdown_description, task.description, task.text_content, '') || '');\n  const clickupUrl = firstNonEmpty(task.url, `https://app.clickup.com/t/${taskId}`);\n  const timeEstimateMs = toMs(firstNonEmpty(task.time_estimate));\n  const timeSpentMs = toMs(firstNonEmpty(task.time_spent));\n  const timeEstimateHours = timeEstimateMs ? (timeEstimateMs / 3600000).toFixed(2) : null;\n  const timeSpentHours = timeSpentMs ? (timeSpentMs / 3600000).toFixed(2) : null;\n  const okrInfo = getDropdownFieldInfo(getCustomField(task, 'OKR'));\n  const progressInfo = getDropdownFieldInfo(getCustomField(task, 'Progress'));\n  const progressToOkrText = getProgressToOkrText(getCustomField(task, 'Progress to OKR'));\n  const deckLabels = [\n    okrInfo?.name ? { title: `OKR: ${okrInfo.name}`, color: okrInfo.color } : null,\n    progressInfo?.name ? { title: `Progress: ${progressInfo.name}`, color: progressInfo.color } : null,\n    priority ? { title: `Priority: ${String(priority).trim().toLowerCase()}`, color: normalizeColor(task?.priority?.color, '808080') } : null,\n    isComplete ? { title: 'Finished', color: '31CC7C' } : null\n  ].filter(Boolean);\n\n  const subtasksMd = buildSubtaskMarkdown(taskId);\n  const rootContext = taskId !== rootId ? [`- **Root task:** ${payload.root_task_name || rootTask?.name || rootId}`, payload.root_task_url ? `- **Root task URL:** ${payload.root_task_url}` : null].filter(Boolean) : [];\n  const metaLines = [\n    ...rootContext,\n    `- **ClickUp task ID:** \\`${taskId}\\``,\n    `- **Status:** ${mappedStatus.stack_title}`,\n    isComplete ? `- **Completed in ClickUp:** Yes` : null,\n    doneAtIso ? `- **Completed at:** ${doneAtIso}` : null,\n    priority ? `- **Priority:** ${priority}` : null,\n    okrInfo?.name ? `- **OKR:** ${okrInfo.name}` : null,\n    progressInfo?.name ? `- **Progress:** ${progressInfo.name}` : null,\n    progressToOkrText ? `- **Progress to OKR:** ${progressToOkrText}` : null,\n    assignees ? `- **Assignees:** ${assignees}` : null,\n    nativeTags ? `- **Tags:** ${nativeTags}` : null,\n    startDateIso ? `- **Start date:** ${startDateIso}` : null,\n    dueDateIso ? `- **Due date:** ${dueDateIso}` : null,\n    timeEstimateHours ? `- **Time estimate (h):** ${timeEstimateHours}` : null,\n    timeSpentHours ? `- **Time spent (h):** ${timeSpentHours}` : null,\n    clickupUrl ? `- **ClickUp URL:** ${clickupUrl}` : null\n  ].filter(Boolean);\n\n  const sections = [\n    `Imported once from ClickUp root task \\`${rootId}\\`.`,\n    '',\n    ...metaLines,\n    '',\n    descriptionMd ? `## Description\\n\\n${descriptionMd}` : null,\n    checklistMd ? `## Checklists\\n\\n${checklistMd}` : null,\n    subtasksMd ? `## Subtasks\\n\\n${subtasksMd}` : null\n  ].filter(section => section !== null);\n\n  const description = sections.join('\\n');\n  out.push({\n    json: {\n      task_id: taskId,\n      task_name: taskName,\n      stack_key: statusKey,\n      stack_title: mappedStatus.stack_title,\n      stack_order: mappedStatus.stack_order,\n      title: truncate(taskName, 255),\n      description,\n      description_base: description,\n      duedate: dueDateIso,\n      created_at_ms: createdAtMs || 0,\n      deck_labels: deckLabels,\n      clickup_is_complete: isComplete,\n      clickup_done_at: doneAtIso\n    }\n  });\n}\n\nout.sort((a, b) => {\n  const aj = a.json; const bj = b.json;\n  if ((aj.stack_order || 999) !== (bj.stack_order || 999)) return (aj.stack_order || 999) - (bj.stack_order || 999);\n  if ((aj.created_at_ms || 0) !== (bj.created_at_ms || 0)) return (aj.created_at_ms || 0) - (bj.created_at_ms || 0);\n  return String(aj.title || '').localeCompare(String(bj.title || ''));\n});\n\nif (!out.length) throw new Error('No cards were produced from the root task tree.');\nreturn out;"},"notesInFlow":false,"typeVersion":2},{"id":"e579727c-6c9f-4068-9cef-8fefc16f64fc","name":"Normalize ClickUp Parent Tasks","type":"n8n-nodes-base.code","notes":"Merge point for both branches.\n\nThis node intentionally does no transformation and simply forwards all incoming items.\nIts value is structural:\n- view-mode normalized tasks and task-root normalized tasks can join here\n- downstream nodes only need one connection path","position":[1488,464],"parameters":{"jsCode":"return $input.all();"},"notesInFlow":false,"typeVersion":2},{"id":"a944e36c-e5de-4f0a-91cb-0dae12718e4b","name":"Sticky Note - Setup & Security","type":"n8n-nodes-base.stickyNote","position":[-1296,976],"parameters":{"color":4,"width":520,"height":330,"content":"Setup checklist for community use\n\n1. Replace all `INSERT-...-HERE` placeholders in the active config node.\n2. Reconnect the Nextcloud HTTP Basic Auth credential on every Deck HTTP node after import.\n3. In **view** mode, `clickup_list_id` actually expects a ClickUp **view ID**.\n4. In **task_root** mode, `clickup_task_id` is the root task whose subtree will be imported.\n5. `status_stack_map_json` must stay valid JSON.\n\nSecurity audit of this export\n- I did **not** find any real ClickUp API token, username, or password in config values.\n- The workflow contains credential **references** (`id` / `name`) for n8n credentials, not the secret values themselves.\n- The file also contains n8n metadata such as workflow IDs and instance ID; those are metadata, not passwords.\n- One credential display name includes your first name (`Nextcloud app password`). That is not the password, but you may still want to rename it before publishing the workflow screenshot/export publicly."},"notesInFlow":false,"typeVersion":1},{"id":"04da686d-ad3b-4864-9028-f89e70bdd0af","name":"Sticky Note - View Mode Branch","type":"n8n-nodes-base.stickyNote","position":[0,0],"parameters":{"color":6,"width":600,"height":300,"content":"Upper branch = View mode\n\nFlow:\nSet Config - View → Set Config → Validate Deck board → IF false branch\n→ seed page 0 on ClickUp view endpoint\n→ resolve the real home list ID\n→ paginate the list\n→ normalize parent tasks into the shared schema\n\nWhy this extra discovery step exists:\nClickUp views are not the same thing as ClickUp lists. The workflow first uses the view to discover the underlying list, then imports from the list endpoint."},"notesInFlow":false,"typeVersion":1},{"id":"1b0f44b7-42b1-477e-af5a-c26eb210f907","name":"Sticky Note - Task Root Branch","type":"n8n-nodes-base.stickyNote","position":[0,880],"parameters":{"color":7,"width":640,"height":320,"content":"Lower branch = Task root mode\n\nFlow:\nSet Config - Task Root → Set Config → Validate Deck board → IF true branch\n→ fetch one root task\n→ resolve its home list\n→ paginate that list\n→ filter down to the root task and all descendants\n→ normalize into the same shared schema as view mode\n\nUse this when you want one project/task tree instead of everything in a ClickUp view or list."},"notesInFlow":false,"typeVersion":1},{"id":"628a7735-9b02-4bb1-ac6b-4df80e659097","name":"Sticky Note - Shared Import Pipeline","type":"n8n-nodes-base.stickyNote","position":[1728,0],"parameters":{"color":3,"width":700,"height":330,"content":"Shared pipeline after both branches merge\n\n1. Build unique Deck stacks from mapped statuses\n2. Create stacks\n3. Build unique Deck labels from normalized tasks\n4. Create labels\n5. Re-read board and stacks to get authoritative IDs\n6. Prepare work items with stack IDs, label IDs, order, due date, and description\n7. Fetch ClickUp comments and append them to the card description\n\nImportant design choice:\nThe workflow trusts the **read-back** data from Deck more than the create responses. That makes reruns safer when stacks or labels already exist."},"notesInFlow":false,"typeVersion":1},{"id":"6da070a3-5581-4967-8e28-d4d0bb5ba729","name":"Sticky Note - Card Creation & Finishing","type":"n8n-nodes-base.stickyNote","position":[4144,0],"parameters":{"color":5,"width":640,"height":330,"content":"Final phase\n\n- Create one Deck card per parent task\n- Fan out label assignments and apply them\n- For tasks already complete in ClickUp, patch the created card with a `done` timestamp\n- Emit a result summary with counts and failures\n\nBehavior worth knowing\n- Subtasks are not separate cards; they are embedded in the parent card description.\n- Comment fetches, card creation, label assignment, and done updates use `continueOnFail` where appropriate so one bad item does not sink the entire run."},"notesInFlow":false,"typeVersion":1}],"active":false,"pinData":{},"settings":{"binaryMode":"separate","availableInMCP":false,"executionOrder":"v1"},"versionId":"21a696fd-11c9-422b-b6e1-40fa8d4d14eb","connections":{"Set Config":{"main":[[{"node":"Deck - Validate Board","type":"main","index":0}]]},"Manual Trigger":{"main":[[{"node":"Set Config - Task Root","type":"main","index":0}]]},"Build Label Items":{"main":[[{"node":"Deck - Create Label","type":"main","index":0}]]},"Build Stack Items":{"main":[[{"node":"Deck - Create Stack","type":"main","index":0}]]},"Deck - Get Stacks":{"main":[[{"node":"Prepare Task Work Items","type":"main","index":0}]]},"Set Config - View":{"main":[[{"node":"Set Config","type":"main","index":0}]]},"Build Page Numbers":{"main":[[{"node":"ClickUp - Get View Tasks Page","type":"main","index":0}]]},"Deck - Create Card":{"main":[[{"node":"Prepare Label Assignments","type":"main","index":0},{"node":"Prepare Completed Card Updates","type":"main","index":0}]]},"Deck - Assign Label":{"main":[[{"node":"Result Summary","type":"main","index":0}]]},"Deck - Create Label":{"main":[[{"node":"After Label Creation - Emit Once","type":"main","index":0}]]},"Deck - Create Stack":{"main":[[{"node":"After Stack Creation - Emit Once","type":"main","index":0}]]},"IF - Task Root Mode":{"main":[[{"node":"Build Page Numbers","type":"main","index":0}],[{"node":"ClickUp - Get Root Task","type":"main","index":0}]]},"Deck - Validate Board":{"main":[[{"node":"IF - Task Root Mode","type":"main","index":0}]]},"Filter Root Task Tree":{"main":[[{"node":"Normalize ClickUp Task Tree","type":"main","index":0}]]},"Set Config - Task Root":{"main":[[{"node":"Set Config","type":"main","index":0}]]},"Append Comments To Task":{"main":[[{"node":"Deck - Create Card","type":"main","index":0}]]},"Build List Page Numbers":{"main":[[{"node":"ClickUp - Get List Tasks Page","type":"main","index":0}]]},"ClickUp - Get Root Task":{"main":[[{"node":"Resolve Root Task Home List ID","type":"main","index":0}]]},"Prepare Task Work Items":{"main":[[{"node":"ClickUp - Get Task Comments","type":"main","index":0}]]},"Prepare Label Assignments":{"main":[[{"node":"Deck - Assign Label","type":"main","index":0}]]},"ClickUp - Get Task Comments":{"main":[[{"node":"Append Comments To Task","type":"main","index":0}]]},"Normalize ClickUp Task Tree":{"main":[[{"node":"Normalize ClickUp Parent Tasks","type":"main","index":0}]]},"Resolve ClickUp Home List ID":{"main":[[{"node":"Build List Page Numbers","type":"main","index":0}]]},"ClickUp - Get List Tasks Page":{"main":[[{"node":"Normalize ClickUp Parent Tasks - View","type":"main","index":0}]]},"ClickUp - Get View Tasks Page":{"main":[[{"node":"Resolve ClickUp Home List ID","type":"main","index":0}]]},"Deck - Get Board After Labels":{"main":[[{"node":"Deck - Get Stacks","type":"main","index":0}]]},"Normalize ClickUp Parent Tasks":{"main":[[{"node":"Build Stack Items","type":"main","index":0}]]},"Prepare Completed Card Updates":{"main":[[{"node":"Deck - Mark Card Done","type":"main","index":0}]]},"Resolve Root Task Home List ID":{"main":[[{"node":"Build Root Task List Page Numbers","type":"main","index":0}]]},"After Label Creation - Emit Once":{"main":[[{"node":"Deck - Get Board After Labels","type":"main","index":0}]]},"After Stack Creation - Emit Once":{"main":[[{"node":"Build Label Items","type":"main","index":0}]]},"Build Root Task List Page Numbers":{"main":[[{"node":"ClickUp - Get Root Task List Tasks Page","type":"main","index":0}]]},"Normalize ClickUp Parent Tasks - View":{"main":[[{"node":"Normalize ClickUp Parent Tasks","type":"main","index":0}]]},"ClickUp - Get Root Task List Tasks Page":{"main":[[{"node":"Filter Root Task Tree","type":"main","index":0}]]}}},"lastUpdatedBy":1,"workflowInfo":{"nodeCount":42,"nodeTypes":{"n8n-nodes-base.if":{"count":1},"n8n-nodes-base.set":{"count":2},"n8n-nodes-base.code":{"count":19},"n8n-nodes-base.stickyNote":{"count":6},"n8n-nodes-base.httpRequest":{"count":13},"n8n-nodes-base.manualTrigger":{"count":1}}},"status":"published","readyToDemo":null,"user":{"name":"BizLog.fi","username":"bizlog","bio":"","verified":false,"links":[],"avatar":"https://gravatar.com/avatar/d9dc4503eee33cd17eb55be294ec44a935fc1b1bda0435aa34a91b7f5c786793?r=pg&d=retro&size=200"},"nodes":[{"id":19,"icon":"file:httprequest.svg","name":"n8n-nodes-base.httpRequest","codex":{"data":{"alias":["API","Request","URL","Build","cURL"],"resources":{"generic":[{"url":"https://n8n.io/blog/2021-the-year-to-automate-the-new-you-with-n8n/","icon":"☀️","label":"2021: The Year to Automate the New You with n8n"},{"url":"https://n8n.io/blog/why-business-process-automation-with-n8n-can-change-your-daily-life/","icon":"🧬","label":"Why business process automation with n8n can change your daily life"},{"url":"https://n8n.io/blog/automatically-pulling-and-visualizing-data-with-n8n/","icon":"📈","label":"Automatically pulling and visualizing data with n8n"},{"url":"https://n8n.io/blog/learn-how-to-automatically-cross-post-your-content-with-n8n/","icon":"✍️","label":"Learn how to automatically cross-post your content with n8n"},{"url":"https://n8n.io/blog/automatically-adding-expense-receipts-to-google-sheets-with-telegram-mindee-twilio-and-n8n/","icon":"🧾","label":"Automatically Adding Expense Receipts to Google Sheets with Telegram, Mindee, Twilio, and n8n"},{"url":"https://n8n.io/blog/running-n8n-on-ships-an-interview-with-maranics/","icon":"🛳","label":"Running n8n on ships: An interview with Maranics"},{"url":"https://n8n.io/blog/what-are-apis-how-to-use-them-with-no-code/","icon":" 🪢","label":"What are APIs and how to use them with no code"},{"url":"https://n8n.io/blog/5-tasks-you-can-automate-with-notion-api/","icon":"⚡️","label":"5 tasks you can automate with the new Notion API "},{"url":"https://n8n.io/blog/world-poetry-day-workflow/","icon":"📜","label":"Celebrating World Poetry Day with a daily poem in Telegram"},{"url":"https://n8n.io/blog/automate-google-apps-for-productivity/","icon":"💡","label":"15 Google apps you can combine and automate to increase productivity"},{"url":"https://n8n.io/blog/automate-designs-with-bannerbear-and-n8n/","icon":"🎨","label":"Automate Designs with Bannerbear and n8n"},{"url":"https://n8n.io/blog/how-uproc-scraped-a-multi-page-website-with-a-low-code-workflow/","icon":" 🕸️","label":"How uProc scraped a multi-page website with a low-code workflow"},{"url":"https://n8n.io/blog/building-an-expense-tracking-app-in-10-minutes/","icon":"📱","label":"Building an expense tracking app in 10 minutes"},{"url":"https://n8n.io/blog/5-workflow-automations-for-mattermost-that-we-love-at-n8n/","icon":"🤖","label":"5 workflow automations for Mattermost that we love at n8n"},{"url":"https://n8n.io/blog/how-to-use-the-http-request-node-the-swiss-army-knife-for-workflow-automation/","icon":"🧰","label":"How to use the HTTP Request Node - The Swiss Army Knife for Workflow Automation"},{"url":"https://n8n.io/blog/learn-how-to-use-webhooks-with-mattermost-slash-commands/","icon":"🦄","label":"Learn how to use webhooks with Mattermost slash commands"},{"url":"https://n8n.io/blog/how-a-membership-development-manager-automates-his-work-and-investments/","icon":"📈","label":"How a Membership Development Manager automates his work and investments"},{"url":"https://n8n.io/blog/a-low-code-bitcoin-ticker-built-with-questdb-and-n8n-io/","icon":"📈","label":"A low-code bitcoin ticker built with QuestDB and n8n.io"},{"url":"https://n8n.io/blog/how-to-set-up-a-ci-cd-pipeline-with-no-code/","icon":"🎡","label":"How to set up a no-code CI/CD pipeline with GitHub and TravisCI"},{"url":"https://n8n.io/blog/automations-for-activists/","icon":"✨","label":"How Common Knowledge use workflow automation for activism"},{"url":"https://n8n.io/blog/creating-scheduled-text-affirmations-with-n8n/","icon":"🤟","label":"Creating scheduled text affirmations with n8n"},{"url":"https://n8n.io/blog/how-goomer-automated-their-operations-with-over-200-n8n-workflows/","icon":"🛵","label":"How Goomer automated their operations with over 200 n8n workflows"},{"url":"https://n8n.io/blog/aws-workflow-automation/","label":"7 no-code workflow automations for Amazon Web Services"}],"primaryDocumentation":[{"url":"https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.httprequest/"}]},"categories":["Development","Core Nodes"],"nodeVersion":"1.0","codexVersion":"1.0","subcategories":{"Core Nodes":["Helpers"]}}},"group":"[\"output\"]","defaults":{"name":"HTTP Request","color":"#0004F5"},"iconData":{"type":"file","fileBuffer":"data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHZpZXdCb3g9IjAgMCA0MCA0MCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik00MCAyMEM0MCA4Ljk1MzE0IDMxLjA0NjkgMCAyMCAwQzguOTUzMTQgMCAwIDguOTUzMTQgMCAyMEMwIDMxLjA0NjkgOC45NTMxNCA0MCAyMCA0MEMzMS4wNDY5IDQwIDQwIDMxLjA0NjkgNDAgMjBaTTIwIDM2Ljk0NThDMTguODg1MiAzNi45NDU4IDE3LjEzNzggMzUuOTY3IDE1LjQ5OTggMzIuNjk4NUMxNC43OTY0IDMxLjI5MTggMTQuMTk2MSAyOS41NDMxIDEzLjc1MjYgMjcuNjg0N0gyNi4xODk4QzI1LjgwNDUgMjkuNTQwMyAyNS4yMDQ0IDMxLjI5MDEgMjQuNTAwMiAzMi42OTg1QzIyLjg2MjIgMzUuOTY3IDIxLjExNDggMzYuOTQ1OCAyMCAzNi45NDU4Wk0xMi45MDY0IDIwQzEyLjkwNjQgMjEuNjA5NyAxMy4wMDg3IDIzLjE2NCAxMy4yMDAzIDI0LjYzMDVIMjYuNzk5N0MyNi45OTEzIDIzLjE2NCAyNy4wOTM2IDIxLjYwOTcgMjcuMDkzNiAyMEMyNy4wOTM2IDE4LjM5MDMgMjYuOTkxMyAxNi44MzYgMjYuNzk5NyAxNS4zNjk1SDEzLjIwMDNDMTMuMDA4NyAxNi44MzYgMTIuOTA2NCAxOC4zOTAzIDEyLjkwNjQgMjBaTTIwIDMuMDU0MTlDMjEuMTE0OSAzLjA1NDE5IDIyLjg2MjIgNC4wMzA3OCAyNC41MDAxIDcuMzAwMzlDMjUuMjA2NiA4LjcxNDA4IDI1LjgwNzIgMTAuNDA2NyAyNi4xOTIgMTIuMzE1M0gxMy43NTAxQzE0LjE5MzMgMTAuNDA0NyAxNC43OTQyIDguNzEyNTQgMTUuNDk5OCA3LjMwMDY0QzE3LjEzNzcgNC4wMzA4MyAxOC44ODUxIDMuMDU0MTkgMjAgMy4wNTQxOVpNMzAuMTQ3OCAyMEMzMC4xNDc4IDE4LjQwOTkgMzAuMDU0MyAxNi44NjE3IDI5LjgyMjcgMTUuMzY5NUgzNi4zMDQyQzM2LjcyNTIgMTYuODQyIDM2Ljk0NTggMTguMzk2NCAzNi45NDU4IDIwQzM2Ljk0NTggMjEuNjAzNiAzNi43MjUyIDIzLjE1OCAzNi4zMDQyIDI0LjYzMDVIMjkuODIyN0MzMC4wNTQzIDIzLjEzODMgMzAuMTQ3OCAyMS41OTAxIDMwLjE0NzggMjBaTTI2LjI3NjcgNC4yNTUxMkMyNy42MzY1IDYuMzYwMTkgMjguNzExIDkuMTMyIDI5LjM3NzQgMTIuMzE1M0gzNS4xMDQ2QzMzLjI1MTEgOC42NjggMzAuMTA3IDUuNzgzNDYgMjYuMjc2NyA0LjI1NTEyWk0xMC42MjI2IDEyLjMxNTNINC44OTI5M0M2Ljc1MTQ3IDguNjY3ODQgOS44OTM1MSA1Ljc4MzQxIDEzLjcyMzIgNC4yNTUxM0MxMi4zNjM1IDYuMzYwMjEgMTEuMjg5IDkuMTMyMDEgMTAuNjIyNiAxMi4zMTUzWk0zLjA1NDE5IDIwQzMuMDU0MTkgMjEuNjAzIDMuMjc3NDMgMjMuMTU3NSAzLjY5NDg0IDI0LjYzMDVIMTAuMTIxN0M5Ljk0NjE5IDIzLjE0MiA5Ljg1MjIyIDIxLjU5NDMgOS44NTIyMiAyMEM5Ljg1MjIyIDE4LjQwNTcgOS45NDYxOSAxNi44NTggMTAuMTIxNyAxNS4zNjk1SDMuNjk0ODRDMy4yNzc0MyAxNi44NDI1IDMuMDU0MTkgMTguMzk3IDMuMDU0MTkgMjBaTTI2LjI3NjYgMzUuNzQyN0MyNy42MzY1IDMzLjYzOTMgMjguNzExIDMwLjg2OCAyOS4zNzc0IDI3LjY4NDdIMzUuMTA0NkMzMy4yNTEgMzEuMzMyMiAzMC4xMDY4IDM0LjIxNzkgMjYuMjc2NiAzNS43NDI3Wk0xMy43MjM0IDM1Ljc0MjdDOS44OTM2OSAzNC4yMTc5IDYuNzUxNTUgMzEuMzMyNCA0Ljg5MjkzIDI3LjY4NDdIMTAuNjIyNkMxMS4yODkgMzAuODY4IDEyLjM2MzUgMzMuNjM5MyAxMy43MjM0IDM1Ljc0MjdaIiBmaWxsPSIjM0E0MkU5Ii8+Cjwvc3ZnPgo="},"displayName":"HTTP Request","typeVersion":4,"nodeCategories":[{"id":5,"name":"Development"},{"id":9,"name":"Core Nodes"}]},{"id":20,"icon":"fa:map-signs","name":"n8n-nodes-base.if","codex":{"data":{"alias":["Router","Filter","Condition","Logic","Boolean","Branch"],"details":"The IF node can be used to implement binary conditional logic in your workflow. You can set up one-to-many conditions to evaluate each item of data being inputted into the node. That data will either evaluate to TRUE or FALSE and route out of the node accordingly.\n\nThis node has multiple types of conditions: Bool, String, Number, and Date & Time.","resources":{"generic":[{"url":"https://n8n.io/blog/learn-to-automate-your-factorys-incident-reporting-a-step-by-step-guide/","icon":"🏭","label":"Learn to Automate Your Factory's Incident Reporting: A Step by Step Guide"},{"url":"https://n8n.io/blog/2021-the-year-to-automate-the-new-you-with-n8n/","icon":"☀️","label":"2021: The Year to Automate the New You with n8n"},{"url":"https://n8n.io/blog/why-business-process-automation-with-n8n-can-change-your-daily-life/","icon":"🧬","label":"Why business process automation with n8n can change your daily life"},{"url":"https://n8n.io/blog/create-a-toxic-language-detector-for-telegram/","icon":"🤬","label":"Create a toxic language detector for Telegram in 4 step"},{"url":"https://n8n.io/blog/no-code-ecommerce-workflow-automations/","icon":"store","label":"6 e-commerce workflows to power up your Shopify s"},{"url":"https://n8n.io/blog/how-to-build-a-low-code-self-hosted-url-shortener/","icon":"🔗","label":"How to build a low-code, self-hosted URL shortener in 3 steps"},{"url":"https://n8n.io/blog/automate-your-data-processing-pipeline-in-9-steps-with-n8n/","icon":"⚙️","label":"Automate your data processing pipeline in 9 steps"},{"url":"https://n8n.io/blog/how-to-get-started-with-crm-automation-and-no-code-workflow-ideas/","icon":"👥","label":"How to get started with CRM automation (with 3 no-code workflow ideas"},{"url":"https://n8n.io/blog/5-tasks-you-can-automate-with-notion-api/","icon":"⚡️","label":"5 tasks you can automate with the new Notion API "},{"url":"https://n8n.io/blog/automate-google-apps-for-productivity/","icon":"💡","label":"15 Google apps you can combine and automate to increase productivity"},{"url":"https://n8n.io/blog/automation-for-maintainers-of-open-source-projects/","icon":"🏷️","label":"How to automatically manage contributions to open-source projects"},{"url":"https://n8n.io/blog/how-uproc-scraped-a-multi-page-website-with-a-low-code-workflow/","icon":" 🕸️","label":"How uProc scraped a multi-page website with a low-code workflow"},{"url":"https://n8n.io/blog/5-workflow-automations-for-mattermost-that-we-love-at-n8n/","icon":"🤖","label":"5 workflow automations for Mattermost that we love at n8n"},{"url":"https://n8n.io/blog/why-this-product-manager-loves-workflow-automation-with-n8n/","icon":"🧠","label":"Why this Product Manager loves workflow automation with n8n"},{"url":"https://n8n.io/blog/sending-automated-congratulations-with-google-sheets-twilio-and-n8n/","icon":"🙌","label":"Sending Automated Congratulations with Google Sheets, Twilio, and n8n "},{"url":"https://n8n.io/blog/how-to-set-up-a-ci-cd-pipeline-with-no-code/","icon":"🎡","label":"How to set up a no-code CI/CD pipeline with GitHub and TravisCI"},{"url":"https://n8n.io/blog/benefits-of-automation-and-n8n-an-interview-with-hubspots-hugh-durkin/","icon":"🎖","label":"Benefits of automation and n8n: An interview with HubSpot's Hugh Durkin"},{"url":"https://n8n.io/blog/aws-workflow-automation/","label":"7 no-code workflow automations for Amazon Web Services"}],"primaryDocumentation":[{"url":"https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.if/"}]},"categories":["Core Nodes"],"nodeVersion":"1.0","codexVersion":"1.0","subcategories":{"Core Nodes":["Flow"]}}},"group":"[\"transform\"]","defaults":{"name":"If","color":"#408000"},"iconData":{"icon":"map-signs","type":"icon"},"displayName":"If","typeVersion":2,"nodeCategories":[{"id":9,"name":"Core Nodes"}]},{"id":38,"icon":"fa:pen","name":"n8n-nodes-base.set","codex":{"data":{"alias":["Set","JS","JSON","Filter","Transform","Map"],"resources":{"generic":[{"url":"https://n8n.io/blog/learn-to-automate-your-factorys-incident-reporting-a-step-by-step-guide/","icon":"🏭","label":"Learn to Automate Your Factory's Incident Reporting: A Step by Step Guide"},{"url":"https://n8n.io/blog/2021-the-year-to-automate-the-new-you-with-n8n/","icon":"☀️","label":"2021: The Year to Automate the New You with n8n"},{"url":"https://n8n.io/blog/automatically-pulling-and-visualizing-data-with-n8n/","icon":"📈","label":"Automatically pulling and visualizing data with n8n"},{"url":"https://n8n.io/blog/database-monitoring-and-alerting-with-n8n/","icon":"📡","label":"Database Monitoring and Alerting with n8n"},{"url":"https://n8n.io/blog/automatically-adding-expense-receipts-to-google-sheets-with-telegram-mindee-twilio-and-n8n/","icon":"🧾","label":"Automatically Adding Expense Receipts to Google Sheets with Telegram, Mindee, Twilio, and n8n"},{"url":"https://n8n.io/blog/no-code-ecommerce-workflow-automations/","icon":"store","label":"6 e-commerce workflows to power up your Shopify s"},{"url":"https://n8n.io/blog/how-to-build-a-low-code-self-hosted-url-shortener/","icon":"🔗","label":"How to build a low-code, self-hosted URL shortener in 3 steps"},{"url":"https://n8n.io/blog/automate-your-data-processing-pipeline-in-9-steps-with-n8n/","icon":"⚙️","label":"Automate your data processing pipeline in 9 steps"},{"url":"https://n8n.io/blog/how-to-get-started-with-crm-automation-and-no-code-workflow-ideas/","icon":"👥","label":"How to get started with CRM automation (with 3 no-code workflow ideas"},{"url":"https://n8n.io/blog/5-tasks-you-can-automate-with-notion-api/","icon":"⚡️","label":"5 tasks you can automate with the new Notion API "},{"url":"https://n8n.io/blog/automate-google-apps-for-productivity/","icon":"💡","label":"15 Google apps you can combine and automate to increase productivity"},{"url":"https://n8n.io/blog/how-uproc-scraped-a-multi-page-website-with-a-low-code-workflow/","icon":" 🕸️","label":"How uProc scraped a multi-page website with a low-code workflow"},{"url":"https://n8n.io/blog/building-an-expense-tracking-app-in-10-minutes/","icon":"📱","label":"Building an expense tracking app in 10 minutes"},{"url":"https://n8n.io/blog/the-ultimate-guide-to-automate-your-video-collaboration-with-whereby-mattermost-and-n8n/","icon":"📹","label":"The ultimate guide to automate your video collaboration with Whereby, Mattermost, and n8n"},{"url":"https://n8n.io/blog/5-workflow-automations-for-mattermost-that-we-love-at-n8n/","icon":"🤖","label":"5 workflow automations for Mattermost that we love at n8n"},{"url":"https://n8n.io/blog/learn-to-build-powerful-api-endpoints-using-webhooks/","icon":"🧰","label":"Learn to Build Powerful API Endpoints Using Webhooks"},{"url":"https://n8n.io/blog/how-a-membership-development-manager-automates-his-work-and-investments/","icon":"📈","label":"How a Membership Development Manager automates his work and investments"},{"url":"https://n8n.io/blog/a-low-code-bitcoin-ticker-built-with-questdb-and-n8n-io/","icon":"📈","label":"A low-code bitcoin ticker built with QuestDB and n8n.io"},{"url":"https://n8n.io/blog/how-to-set-up-a-ci-cd-pipeline-with-no-code/","icon":"🎡","label":"How to set up a no-code CI/CD pipeline with GitHub and TravisCI"},{"url":"https://n8n.io/blog/benefits-of-automation-and-n8n-an-interview-with-hubspots-hugh-durkin/","icon":"🎖","label":"Benefits of automation and n8n: An interview with HubSpot's Hugh Durkin"},{"url":"https://n8n.io/blog/how-goomer-automated-their-operations-with-over-200-n8n-workflows/","icon":"🛵","label":"How Goomer automated their operations with over 200 n8n workflows"},{"url":"https://n8n.io/blog/aws-workflow-automation/","label":"7 no-code workflow automations for Amazon Web Services"}],"primaryDocumentation":[{"url":"https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.set/"}]},"categories":["Core Nodes"],"nodeVersion":"1.0","codexVersion":"1.0","subcategories":{"Core Nodes":["Data Transformation"]}}},"group":"[\"input\"]","defaults":{"name":"Edit Fields"},"iconData":{"icon":"pen","type":"icon"},"displayName":"Edit Fields (Set)","typeVersion":3,"nodeCategories":[{"id":9,"name":"Core Nodes"}]},{"id":565,"icon":"fa:sticky-note","name":"n8n-nodes-base.stickyNote","codex":{"data":{"alias":["Comments","Notes","Sticky"],"categories":["Core Nodes"],"nodeVersion":"1.0","codexVersion":"1.0","subcategories":{"Core Nodes":["Helpers"]}}},"group":"[\"input\"]","defaults":{"name":"Sticky Note","color":"#FFD233"},"iconData":{"icon":"sticky-note","type":"icon"},"displayName":"Sticky Note","typeVersion":1,"nodeCategories":[{"id":9,"name":"Core Nodes"}]},{"id":834,"icon":"file:code.svg","name":"n8n-nodes-base.code","codex":{"data":{"alias":["cpde","Javascript","JS","Python","Script","Custom Code","Function"],"details":"The Code node allows you to execute JavaScript in your workflow.","resources":{"primaryDocumentation":[{"url":"https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.code/"}]},"categories":["Development","Core Nodes"],"nodeVersion":"1.0","codexVersion":"1.0","subcategories":{"Core Nodes":["Helpers","Data Transformation"]}}},"group":"[\"transform\"]","defaults":{"name":"Code"},"iconData":{"type":"file","fileBuffer":"data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTEyIiBoZWlnaHQ9IjUxMiIgdmlld0JveD0iMCAwIDUxMiA1MTIiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMF8xMTcxXzQ0MSkiPgo8cGF0aCBkPSJNMTcwLjI4MyA0OEgxOTYuNUMyMDMuMTI3IDQ4IDIwOC41IDQyLjYyNzQgMjA4LjUgMzZWMTJDMjA4LjUgNS4zNzI1OCAyMDMuMTI3IDAgMTk2LjUgMEgxNzAuMjgzQzEyNi4xIDAgOTAuMjgzIDM1LjgxNzIgOTAuMjgzIDgwVjE3NkM5MC4yODMgMjA2LjkyOCA2NS4yMTA5IDIzMiAzNC4yODMgMjMySDIzQzE2LjM3MjYgMjMyIDExIDIzNy4zNzIgMTEgMjQ0VjI2OEMxMSAyNzQuNjI3IDE2LjM3MjQgMjgwIDIyLjk5OTYgMjgwTDM0LjI4MyAyODBDNjUuMjEwOSAyODAgOTAuMjgzIDMwNS4wNzIgOTAuMjgzIDMzNlY0NDBDOTAuMjgzIDQ3OS43NjQgMTIyLjUxOCA1MTIgMTYyLjI4MyA1MTJIMTk2LjVDMjAzLjEyNyA1MTIgMjA4LjUgNTA2LjYyNyAyMDguNSA1MDBWNDc2QzIwOC41IDQ2OS4zNzMgMjAzLjEyNyA0NjQgMTk2LjUgNDY0SDE2Mi4yODNDMTQ5LjAyOCA0NjQgMTM4LjI4MyA0NTMuMjU1IDEzOC4yODMgNDQwVjMzNkMxMzguMjgzIDMwOS4wMjIgMTI4LjAxMSAyODQuNDQzIDExMS4xNjQgMjY1Ljk2MUMxMDYuMTA5IDI2MC40MTYgMTA2LjEwOSAyNTEuNTg0IDExMS4xNjQgMjQ2LjAzOUMxMjguMDExIDIyNy41NTcgMTM4LjI4MyAyMDIuOTc4IDEzOC4yODMgMTc2VjgwQzEzOC4yODMgNjIuMzI2OSAxNTIuNjEgNDggMTcwLjI4MyA0OFoiIGZpbGw9IiNGRjk5MjIiLz4KPHBhdGggZD0iTTMwNSAzNkMzMDUgNDIuNjI3NCAzMTAuMzczIDQ4IDMxNyA0OEgzNDIuOTc5QzM2MC42NTIgNDggMzc0Ljk3OCA2Mi4zMjY5IDM3NC45NzggODBWMTc2QzM3NC45NzggMjAyLjk3OCAzODUuMjUxIDIyNy41NTcgNDAyLjA5OCAyNDYuMDM5QzQwNy4xNTMgMjUxLjU4NCA0MDcuMTUzIDI2MC40MTYgNDAyLjA5OCAyNjUuOTYxQzM4NS4yNTEgMjg0LjQ0MyAzNzQuOTc4IDMwOS4wMjIgMzc0Ljk3OCAzMzZWNDMyQzM3NC45NzggNDQ5LjY3MyAzNjAuNjUyIDQ2NCAzNDIuOTc5IDQ2NEgzMTdDMzEwLjM3MyA0NjQgMzA1IDQ2OS4zNzMgMzA1IDQ3NlY1MDBDMzA1IDUwNi42MjcgMzEwLjM3MyA1MTIgMzE3IDUxMkgzNDIuOTc5QzM4Ny4xNjEgNTEyIDQyMi45NzggNDc2LjE4MyA0MjIuOTc4IDQzMlYzMzZDNDIyLjk3OCAzMDUuMDcyIDQ0OC4wNTEgMjgwIDQ3OC45NzkgMjgwSDQ5MEM0OTYuNjI3IDI4MCA1MDIgMjc0LjYyOCA1MDIgMjY4VjI0NEM1MDIgMjM3LjM3MyA0OTYuNjI4IDIzMiA0OTAgMjMyTDQ3OC45NzkgMjMyQzQ0OC4wNTEgMjMyIDQyMi45NzggMjA2LjkyOCA0MjIuOTc4IDE3NlY4MEM0MjIuOTc4IDM1LjgxNzIgMzg3LjE2MSAwIDM0Mi45NzkgMEgzMTdDMzEwLjM3MyAwIDMwNSA1LjM3MjU4IDMwNSAxMlYzNloiIGZpbGw9IiNGRjk5MjIiLz4KPC9nPgo8ZGVmcz4KPGNsaXBQYXRoIGlkPSJjbGlwMF8xMTcxXzQ0MSI+CjxyZWN0IHdpZHRoPSI1MTIiIGhlaWdodD0iNTEyIiBmaWxsPSJ3aGl0ZSIvPgo8L2NsaXBQYXRoPgo8L2RlZnM+Cjwvc3ZnPgo="},"displayName":"Code","typeVersion":2,"nodeCategories":[{"id":5,"name":"Development"},{"id":9,"name":"Core Nodes"}]},{"id":838,"icon":"fa:mouse-pointer","name":"n8n-nodes-base.manualTrigger","codex":{"data":{"resources":{"generic":[],"primaryDocumentation":[{"url":"https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.manualworkflowtrigger/"}]},"categories":["Core Nodes"],"nodeVersion":"1.0","codexVersion":"1.0"}},"group":"[\"trigger\"]","defaults":{"name":"When clicking ‘Execute workflow’","color":"#909298"},"iconData":{"icon":"mouse-pointer","type":"icon"},"displayName":"Manual Trigger","typeVersion":1,"nodeCategories":[{"id":9,"name":"Core Nodes"}]}],"categories":[{"id":46,"name":"Project Management"}],"image":[]}}