AutomationFlowsGeneral › Complete Backup Solution for N8n Workflows & Credentials (local/ftp)

Complete Backup Solution for N8n Workflows & Credentials (local/ftp)

ByFlorent @florent on n8n.io

Complete backup solution that saves both workflows and credentials to local/server disk with optional FTP upload for off-site redundancy.

Cron / scheduled trigger★★★★☆ complexity28 nodesRead Write FileEmail SendExecute Commandn8nStop And ErrorFtp
General Trigger: Cron / scheduled Nodes: 28 Complexity: ★★★★☆ Added:

This workflow corresponds to n8n.io template #9151 — we link there as the canonical source.

This workflow follows the Executecommand → Readwritefile recipe pattern — see all workflows that pair these two integrations.

The workflow JSON

Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →

Download .json
{
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "nodes": [
    {
      "id": "",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -112,
        -1408
      ],
      "parameters": {
        "width": 1264,
        "height": 528,
        "content": "## Backup Worflows & Credentials to Drive\nConfigure \"Init\" node in 2 sections.\n### Workflow Standard Configuration\n```\n// Admin email for notifications\nconst N8N_ADMIN_EMAIL = $env.N8N_ADMIN_EMAIL || 'youremail@world.com';\n   \n// Workflow name (auto-detected)\nconst WORKFLOW_NAME = $workflow.name;\n   \n// Projects root directory on your server (see file structure in node)\nconst N8N_PROJECTS_DIR = $env.N8N_PROJECTS_DIR || '/files/n8n-projects-data';\n   \n// Project folder name for this backup workflow\nconst PROJECT_FOLDER_NAME = \"Workflow-backups\";  // \u26a0\ufe0f Your project folder name\n```\n### Workflow Custom Configuration\n```\n// Base backup path using your Docker and FTP volume configuration (folder must exist)\nconst BACKUP_FOLDER = $env.N8N_BACKUP_FOLDER || '/files/n8n-backups'; // \u26a0\ufe0f Change the default value for your n8n backup folder\nconst FTP_BACKUP_FOLDER = $env.N8N_FTP_BACKUP_FOLDER || '/n8n-backups';\nconst FTPName = 'Your FTP Server Name'; // FTP server name for logging (display purposes only)\n \nconst credentials_backup_folder = \"n8n-credentials\"; // Credentials backup path\n```"
      },
      "typeVersion": 1
    },
    {
      "id": "",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -112,
        -816
      ],
      "parameters": {
        "color": 4,
        "width": 624,
        "height": 832,
        "content": "## Initialisation"
      },
      "typeVersion": 1
    },
    {
      "id": "",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        576,
        -816
      ],
      "parameters": {
        "color": 3,
        "width": 1232,
        "height": 272,
        "content": "## Credentials\u2019 backup & FTP upload"
      },
      "typeVersion": 1
    },
    {
      "id": "",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        576,
        -480
      ],
      "parameters": {
        "color": 5,
        "width": 1232,
        "height": 496,
        "content": "## Workflows\u2019 backup & FTP upload"
      },
      "typeVersion": 1
    },
    {
      "id": "",
      "name": "Sticky Note6",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1872,
        -816
      ],
      "parameters": {
        "color": 6,
        "width": 816,
        "height": 832,
        "content": "## Finalisation"
      },
      "typeVersion": 1
    },
    {
      "id": "",
      "name": "Backup Summary",
      "type": "n8n-nodes-base.code",
      "onError": "continueErrorOutput",
      "position": [
        2112,
        -544
      ],
      "parameters": {
        "jsCode": "// Generate backup summary and statistics with binary data for log file\n// \u2705 Enhanced to handle both credentials and workflows FTP upload results\n// \u2705 Adapted for Aggregate + Merge structure\n// \u2705 Handles disabled FTP nodes gracefully\n\n// \u26a0\ufe0f CONFIGURE THIS NODE WITH:\n// SETTINGS > ON ERROR > CONTINUE (USING ERROR OUTPUT)\n\nconst allItems = $input.all();\nconst summaryDataInput = $(\"Init\").first().json;\n\nconsole.log('[Backup Summary] Total items received:', allItems.length);\n\n// Separate different data types from Merge\nconst credentialsFtpItems = allItems.filter(item => \n  item.json.credentialsFtpUpload !== undefined\n);\n\nconst workflowsFtpItems = allItems.filter(item => \n  item.json.workflowsFtpUpload !== undefined\n);\n\nconst aggregatedWorkflowItems = allItems.filter(item => \n  item.json.workflows !== undefined\n);\n\nconsole.log('[Backup Summary] Credentials FTP items:', credentialsFtpItems.length);\nconsole.log('[Backup Summary] Workflows FTP items:', workflowsFtpItems.length);\nconsole.log('[Backup Summary] Aggregated workflow items:', aggregatedWorkflowItems.length);\n\n// Get credentials FTP upload data\nconst credentialsFtpData = credentialsFtpItems.length > 0 ? \n  credentialsFtpItems[0].json.credentialsFtpUpload : null;\n\n// Get workflows FTP upload data\nconst workflowsFtpData = workflowsFtpItems.length > 0 ? \n  workflowsFtpItems[0].json.workflowsFtpUpload : null;\n\n// Get aggregated workflows data\nconst workflowsData = aggregatedWorkflowItems.length > 0 ? \n  aggregatedWorkflowItems[0].json.workflows : [];\n\n// Get original workflow count from Fetch Workflows node\nconst workflows = $(\"Fetch Workflows\").all();\nconst totalWorkflows = workflows.length;\n\nconsole.log('[Backup Summary] Total workflows fetched:', totalWorkflows);\nconsole.log('[Backup Summary] Aggregated workflows found:', workflowsData.length);\nconsole.log('[Backup Summary] Credentials FTP data found:', credentialsFtpData !== null);\nconsole.log('[Backup Summary] Workflows FTP data found:', workflowsFtpData !== null);\n\n// Get credentials export result directly from the Export Credentials node\nconst credentialsResult = $(\"Export Credentials\").first()?.json || null;\n\n// Process workflow data from aggregated structure with binary data\nconst successfulWrites = [];\nconst failedWrites = [];\nlet totalSize = 0;\n\n// Get aggregated item with binary data\nconst aggregatedItem = aggregatedWorkflowItems[0];\nconst binaryData = aggregatedItem?.binary || {};\n\n// Since we have aggregated data, we assume all workflows were written successfully\n// (failed writes would have been caught by error handling in previous nodes)\nworkflowsData.forEach((workflowFile, index) => {\n  const fileName = workflowFile.fileName;\n  const workflowName = fileName ? fileName.split('/').pop().replace('.json', '') : `workflow_${index + 1}`;\n  \n  // Try to match with original workflow data to get more details\n  const originalWorkflow = workflows.find(w => {\n    const cleanedName = workflowName.replace(/[^a-zA-Z0-9]/g, '_');\n    return w.json.cleanedFileName === cleanedName || \n           w.json.name === workflowName;\n  });\n  \n  const nodesCount = originalWorkflow ? \n    (originalWorkflow.json.nodes ? originalWorkflow.json.nodes.length : 0) : 0;\n  \n  // Get real file size from binary data\n  let actualSize = 0;\n  const binaryKey = `data_${index}` || `data${index + 1}` || Object.keys(binaryData)[index];\n  \n  if (binaryData[binaryKey] && binaryData[binaryKey].data) {\n    // Calculate size from base64 data (base64 is ~33% larger than original)\n    const base64Data = binaryData[binaryKey].data;\n    actualSize = Math.floor(base64Data.length * 0.75); // Convert base64 length to actual bytes\n  }\n  \n  console.log(`[Backup Summary] Workflow ${workflowName}: ${actualSize} bytes`);\n  \n  successfulWrites.push({\n    fileName: fileName,\n    workflowName: workflowName,\n    filePath: fileName,\n    contentSize: actualSize,\n    nodesCount: nodesCount\n  });\n  \n  totalSize += actualSize;\n});\n\nconsole.log(`[Backup Summary] Processed ${successfulWrites.length} successful workflow writes`);\n\n// Use time data from Init node instead of recalculating\nconst initTimeData = summaryDataInput.timeData;\nconst endTime = new Date();\nconst startTime = new Date(initTimeData.startTime);\nconst durationSeconds = Math.round((endTime.getTime() - startTime.getTime()) / 1000);\n\n// Process credentials export result\nconst credentialsExportSuccess = credentialsResult && \n  credentialsResult.exitCode !== undefined && \n  credentialsResult.exitCode === 0;\n\nconst credentialsExport = {\n  attempted: credentialsResult !== null,\n  success: credentialsExportSuccess,\n  exitCode: credentialsResult?.exitCode,\n  message: credentialsExportSuccess ? \n    'Credentials exported successfully' : \n    `Credentials export failed with exit code ${credentialsResult?.exitCode}`,\n  stdout: credentialsResult?.stdout || '',\n  stderr: credentialsResult?.stderr || ''\n};\n\n// Process credentials FTP upload results with disabled node detection\nconst credentialsFtpSuccess = credentialsFtpData && \n  credentialsFtpData.summary && \n  credentialsFtpData.summary.overallSuccess;\n\nconst credentialsFtpUpload = {\n  attempted: credentialsFtpData !== null,\n  disabled: credentialsFtpData === null,\n  success: credentialsFtpSuccess,\n  totalFiles: credentialsFtpData?.totalFiles || 0,\n  successfulUploads: credentialsFtpData?.successfulUploads || 0,\n  failedUploads: credentialsFtpData?.failedUploads || 0,\n  message: credentialsFtpData === null ? \n    '\u26a0\ufe0f Warning: no FTP data received (node likely disabled)' :\n    (credentialsFtpSuccess ? \n      credentialsFtpData.summary.summary :\n      credentialsFtpData?.summary?.summary || 'Credentials FTP upload failed')\n};\n\n// Process workflows FTP upload results with disabled node detection\nconst workflowsFtpSuccess = workflowsFtpData && \n  workflowsFtpData.summary && \n  workflowsFtpData.summary.overallSuccess;\n\nconst workflowsFtpUpload = {\n  attempted: workflowsFtpData !== null,\n  disabled: workflowsFtpData === null,\n  success: workflowsFtpSuccess,\n  totalFiles: workflowsFtpData?.totalFiles || 0,\n  successfulUploads: workflowsFtpData?.successfulUploads || 0,\n  failedUploads: workflowsFtpData?.failedUploads || 0,\n  message: workflowsFtpData === null ? \n    '\u26a0\ufe0f Warning: no FTP data received (node likely disabled)' :\n    (workflowsFtpSuccess ? \n      workflowsFtpData.summary.summary :\n      workflowsFtpData?.summary?.summary || 'Workflows FTP upload failed')\n};\n\nconsole.log('[Backup Summary] Credentials export status:', credentialsExport.success ? '\u2705 Success' : '\u274c Failed');\nconsole.log('[Backup Summary] Credentials FTP status:', credentialsFtpUpload.disabled ? '\u26a0\ufe0f Disabled' : (credentialsFtpUpload.success ? '\u2705 Success' : '\u274c Failed'));\nconsole.log('[Backup Summary] Workflows FTP status:', workflowsFtpUpload.disabled ? '\u26a0\ufe0f Disabled' : (workflowsFtpUpload.success ? '\u2705 Success' : '\u274c Failed'));\nconsole.log(`[Backup Summary] Final counts - Successful: ${successfulWrites.length}, Failed: ${failedWrites.length}`);\n\n// Create a backupSummary structure\nconst backupSummary = {\n  totalWorkflows: totalWorkflows,\n  errorCount: failedWrites.length,\n  errors: failedWrites.map(fail => ({\n    workflowName: fail.workflowName,\n    error: fail.error\n  }))\n};\n\n// Determine FTP folder status icon\nconst ftpFolderStatusIcon = (credentialsFtpUpload.disabled || workflowsFtpUpload.disabled) ? \n  '\u26a0\ufe0f' : \n  ((credentialsFtpUpload.success && workflowsFtpUpload.success) ? '\u2705' : '\u274c');\n\n// Create comprehensive backup log using Init variables\nconst backupLog = {\n  timestamp: endTime.toISOString(),\n  localTimestamp: endTime.toLocaleString('en-GB', { timeZone: initTimeData.localTimezone }),\n  localTimezone: initTimeData.localTimezone,\n  backupFolder: summaryDataInput.customConfig.backupFolder,\n  dateFolder: initTimeData.datePrefix,\n  duration: {\n    seconds: durationSeconds,\n    formatted: `${Math.floor(durationSeconds / 60)}m ${durationSeconds % 60}s`,\n    startTime: initTimeData.startTime,\n    endTime: endTime.toISOString()\n  },\n  statistics: {\n    totalWorkflows: backupSummary.totalWorkflows,\n    successfulBackups: successfulWrites.length,\n    failedBackups: failedWrites.length,\n    preparationErrors: backupSummary.errorCount,\n    credentialsExported: credentialsExport.success,\n    credentialsFtpUploaded: credentialsFtpUpload.success,\n    credentialsFtpDisabled: credentialsFtpUpload.disabled,\n    credentialsFtpFiles: credentialsFtpUpload.totalFiles,\n    workflowsFtpUploaded: workflowsFtpUpload.success,\n    workflowsFtpDisabled: workflowsFtpUpload.disabled,\n    workflowsFtpFiles: workflowsFtpUpload.totalFiles,\n    totalSizeMB: Math.round(totalSize / 1024 / 1024 * 100) / 100,\n    averageSizeKB: successfulWrites.length > 0 ? Math.round(totalSize / successfulWrites.length / 1024) : 0\n  },\n  credentialsExport,\n  credentialsFtpUpload,\n  workflowsFtpUpload,\n  successfulFiles: successfulWrites.map(file => ({\n    fileName: file.fileName,\n    workflowName: file.workflowName,\n    contentSize: file.contentSize,\n    nodesCount: file.nodesCount\n  })),\n  failedFiles: failedWrites.map(file => ({\n    fileName: file.fileName,\n    workflowName: file.workflowName,\n    error: file.error\n  })),\n  preparationErrors: backupSummary.errors || [],\n  status: failedWrites.length === 0 && \n          backupSummary.errorCount === 0 && \n          credentialsExport.success && \n          (!credentialsFtpUpload.attempted || credentialsFtpUpload.success) &&\n          (!workflowsFtpUpload.attempted || workflowsFtpUpload.success) ? \n    'success' : 'partial_success',\n  configuration: {\n    backupConfig: summaryDataInput.customConfig.backupConfig,\n    workflowName: summaryDataInput.workflowConfig.WORKFLOW_NAME,\n    adminEmail: summaryDataInput.workflowConfig.N8N_ADMIN_EMAIL\n  },\n  debug: {\n    totalItemsReceived: allItems.length,\n    credentialsFtpItemsFound: credentialsFtpItems.length,\n    workflowsFtpItemsFound: workflowsFtpItems.length,\n    aggregatedWorkflowItemsFound: aggregatedWorkflowItems.length,\n    workflowsInAggregate: workflowsData.length,\n    totalWorkflowsFetched: totalWorkflows,\n    credentialsResultFound: credentialsResult !== null\n  }\n};\n\n// Convert backup log to binary data for Write Files from Disk node\nconst logJsonContent = JSON.stringify(backupLog, null, 2);\nconst logBinaryData = Buffer.from(logJsonContent, 'utf8').toString('base64');\n\n// Create console & email output lines array using Init variables\nconst startTimeLocal = new Date(initTimeData.startTime).toLocaleString('sv-SE', { timeZone: initTimeData.localTimezone });\nconst endTimeLocal = endTime.toLocaleString('sv-SE', { timeZone: initTimeData.localTimezone });\n\n// Determine status text for FTP uploads\nconst credentialsFtpStatus = credentialsFtpUpload.disabled ? '\u26a0\ufe0f Disabled' : \n  (credentialsFtpUpload.success ? '\u2705 Yes' : '\u274c Failed');\n\nconst workflowsFtpStatus = workflowsFtpUpload.disabled ? '\u26a0\ufe0f Disabled' : \n  (workflowsFtpUpload.success ? '\u2705 Yes' : '\u274c Failed');\n\nconst consoleLines = [\n  '=================================',\n  '\ud83d\udcc1 N8N WORKFLOWS BACKUP SUMMARY',\n  '=================================',\n  `\ud83d\udcc5 Date: ${initTimeData.localTimeFormatted}`,\n  `\ud83c\udf0d Timezone: ${initTimeData.localTimezone}`,\n  `\u23f1\ufe0f Duration: ${backupLog.duration.formatted}`,\n  `   \u2022 Start time (local): ${startTimeLocal}`,\n  `   \u2022 End time (local): ${endTimeLocal}`,\n  `\ud83d\udcc2 Backup folder: ${summaryDataInput.customConfig.backupFolder}/${initTimeData.datePrefix} ${successfulWrites.length > 0 ? '\u2705' : '\u274c'}`,\n  `\ud83d\udce4 FTP folder (${summaryDataInput.customConfig.FTPName || 'FTP Server'}): ${summaryDataInput.customConfig.FTP_BACKUP_FOLDER}/${initTimeData.datePrefix} ${ftpFolderStatusIcon}`,\n  `\ud83d\udcca Workflows found: ${backupLog.statistics.totalWorkflows}`,\n  `\u2705 Successfully backed up: ${backupLog.statistics.successfulBackups}`,\n  `\u274c Failed backups: ${backupLog.statistics.failedBackups}`,\n  `\u26a0\ufe0f Preparation errors: ${backupLog.statistics.preparationErrors}`,\n  `\ud83d\udd10 Credentials exported: ${credentialsExport.success ? '\u2705 Yes' : '\u274c Failed'}`,\n  `\ud83d\udce4 Credentials FTP uploaded: ${credentialsFtpStatus}`,\n  `\ud83d\udce4 Workflows FTP uploaded: ${workflowsFtpStatus}`,\n  `\ud83d\udcbe Total size: ${backupLog.statistics.totalSizeMB} MB`,\n  `\ud83d\udcc8 Status: ${backupLog.status.toUpperCase()}`,\n  '================================='\n];\n\n// Console summary - display all lines\nconsoleLines.forEach(line => console.log(line));\n\n// Add error details to console lines if any\nif (failedWrites.length > 0) {\n  console.log('\u274c Failed files:');\n  consoleLines.push('\u274c Failed files:');\n  failedWrites.forEach(fail => {\n    const errorLine = `  - ${fail.fileName}: ${fail.error}`;\n    console.log(errorLine);\n    consoleLines.push(errorLine);\n  });\n}\n\n// Add credentials export details - only show errors, not success details\nif (!credentialsExport.success) {\n  console.log('\ud83d\udd10 Credentials export error:');\n  consoleLines.push('\ud83d\udd10 Credentials export error:');\n  consoleLines.push(`  - Exit code: ${credentialsExport.exitCode}`);\n  if (credentialsExport.stderr) {\n    consoleLines.push(`  - Error: ${credentialsExport.stderr}`);\n  }\n}\n\n// Add credentials FTP upload details\nif (credentialsFtpUpload.disabled) {\n  console.log('Credentials FTP upload disabled:');\n  consoleLines.push('\u26a0\ufe0f Credentials FTP upload:');\n  consoleLines.push(`  - Warning: no FTP data received (node likely disabled)`);\n} else if (!credentialsFtpUpload.success && credentialsFtpUpload.attempted) {\n  console.log('Credentials FTP upload errors:');\n  consoleLines.push('\u274c Credentials FTP upload errors:');\n  consoleLines.push(`  - ${credentialsFtpUpload.message}`);\n  \n  if (credentialsFtpData && credentialsFtpData.uploads) {\n    const failedUploads = credentialsFtpData.uploads.filter(upload => !upload.success);\n    failedUploads.forEach(upload => {\n      const errorLine = `    \u2022 ${upload.fileName}: ${upload.error || 'Upload failed'}`;\n      console.log(errorLine);\n      consoleLines.push(errorLine);\n    });\n  }\n}\n\n// Add workflows FTP upload details\nif (workflowsFtpUpload.disabled) {\n  console.log('Workflows FTP upload disabled:');\n  consoleLines.push('\u26a0\ufe0f Workflows FTP upload:');\n  consoleLines.push(`  - Warning: no FTP data received (node likely disabled)`);\n} else if (!workflowsFtpUpload.success && workflowsFtpUpload.attempted) {\n  console.log('Workflows FTP upload errors:');\n  consoleLines.push('\u274c Workflows FTP upload errors:');\n  consoleLines.push(`  - ${workflowsFtpUpload.message}`);\n  \n  if (workflowsFtpData && workflowsFtpData.uploads) {\n    const failedUploads = workflowsFtpData.uploads.filter(upload => !upload.success);\n    failedUploads.forEach(upload => {\n      const errorLine = `    \u2022 ${upload.fileName || upload.workflowName}: ${upload.error || 'Upload failed'}`;\n      console.log(errorLine);\n      consoleLines.push(errorLine);\n    });\n  }\n}\n\n// Create email log file content as plain text\nconst emailLogText = consoleLines.join('\\n');\n\n// Convert execution log to binary data as plain text\nconst emailLogBinaryData = Buffer.from(emailLogText, 'utf8').toString('base64'); \n\n// Create execution log file name using Init variables\nconst emailLogFileName = `${initTimeData.localDateTime}_exec_log.txt`;\nconst emailLogFilePath = `${summaryDataInput.workflowConfig.LOGS_PATH}/${emailLogFileName}`;\n\nconst emailLog = {\n  data: emailLogBinaryData,\n  mimeType: 'text/plain', \n  fileName: emailLogFileName,\n  fileExtension: 'txt'\n}\n\nconst summaryData = {\n  backupLog,\n  emailLogText,\n  logPathFile: summaryDataInput.workflowConfig.logPathFileName,\n  emailLogPath: emailLogFilePath,\n  emailLogFileName: emailLogFileName,\n  backupCompleted: true,\n  workflowStep: 'completed'\n};\n\n// Return with binary data for BOTH log files\nreturn [{\n  json: summaryData,\n  binary: {\n    data: {\n      data: logBinaryData,\n      mimeType: 'application/json',\n      fileName: summaryDataInput.workflowConfig.logFileName,\n      fileExtension: 'json'\n    },\n    emailLog\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "",
      "name": "Write Backup Log",
      "type": "n8n-nodes-base.readWriteFile",
      "position": [
        2320,
        -560
      ],
      "parameters": {
        "options": {},
        "fileName": "={{ $json.logPathFile }}",
        "operation": "write"
      },
      "typeVersion": 1
    },
    {
      "id": "",
      "name": "Send email",
      "type": "n8n-nodes-base.emailSend",
      "position": [
        2528,
        -560
      ],
      "parameters": {
        "text": "=Workflow:  {{ $workflow.name }}\nWorkflow has executed sucessfully.\n\n{{ $json.emailLogText }}\n\nFind the exported Workflow JSON files attached to this email.",
        "options": {
          "attachments": "data",
          "appendAttribution": false
        },
        "subject": "=n8n SUCCESS: {{ $workflow.name }}",
        "toEmail": "={{ $env.N8N_ADMIN_EMAIL }}",
        "fromEmail": "admin <admin@example.com>",
        "emailFormat": "text"
      },
      "typeVersion": 2.1
    },
    {
      "id": "",
      "name": "Write Email Log",
      "type": "n8n-nodes-base.readWriteFile",
      "position": [
        2320,
        -752
      ],
      "parameters": {
        "options": {},
        "fileName": "={{ $json.emailLogPath }}",
        "operation": "write",
        "dataPropertyName": "emailLog"
      },
      "typeVersion": 1
    },
    {
      "id": "",
      "name": "Create Date Folder",
      "type": "n8n-nodes-base.executeCommand",
      "position": [
        368,
        -560
      ],
      "parameters": {
        "command": "=mkdir -p \"{{ $json.customConfig.backupFolder }}/{{ $json.customConfig.datePrefix }}\""
      },
      "typeVersion": 1
    },
    {
      "id": "",
      "name": "Init",
      "type": "n8n-nodes-base.code",
      "position": [
        160,
        -560
      ],
      "parameters": {
        "jsCode": "// Init Workflow Variables - Local timezone version\n// \u2705 All dates/times handled in local timezone\n\n// YOUR_AWS_SECRET_KEY_HERE==\n// \ud83d\udcc5 LOCAL DATE/TIME INITIALIZATION\n// YOUR_AWS_SECRET_KEY_HERE==\n\nconst now = new Date();\n\n// YOUR_AWS_SECRET_KEY_HERE==\n// \ud83c\udf0d USER-DEFINED TIMEZONE CONFIGURATION\n// YOUR_AWS_SECRET_KEY_HERE==\n// \u26a0\ufe0f  IMPORTANT: This code runs on the n8n SERVER, not in your browser!\n// \u26a0\ufe0f  Configure the timezone where you want executions to be scheduled,\n// \u26a0\ufe0f  regardless of where your n8n server is physically located.\n//\n// \ud83d\udccd Common timezone examples:\n// - 'Europe/Paris'       \u2192 Central European Time (CET/CEST)\n// - 'America/New_York'   \u2192 Eastern Time (EST/EDT)\n// - 'America/Chicago'    \u2192 Central Time (CST/CDT)\n// - 'America/Los_Angeles'\u2192 Pacific Time (PST/PDT)\n// - 'Asia/Tokyo'         \u2192 Japan Standard Time (JST)\n// - 'Asia/Shanghai'      \u2192 China Standard Time (CST)\n// - 'Australia/Sydney'   \u2192 Australian Eastern Time (AET)\n// - 'UTC'                \u2192 Coordinated Universal Time\n//\n// \ud83d\udd27 TO CONFIGURE FOR YOUR USE CASE:\n// 1. Uncomment and edit the line below with your desired timezone\n// 2. Comment out the automatic detection line\n//\n// To use user-defined timezone instead, UNCOMMENT these lines:\n// const USER_TIMEZONE = 'Europe/Paris';  // \ud83d\udc48 EDIT THIS for your location\n// const LOCAL_TIMEZONE = USER_TIMEZONE;  // \ud83d\udc48 EDIT THIS for your location\n//\n// For automatic detection (uses server timezone or environment variable):\nconst LOCAL_TIMEZONE = $env.TZ || 'Europe/Paris';  // \ud83d\udfe2 Default fallback\n// To use user-defined timezone instead, comment out the above line\n\n// Local date in YYYY-MM-DD format\nconst localDate = now.toLocaleDateString('sv-SE', { timeZone: LOCAL_TIMEZONE });\n\n// Local datetime in YYYY-MM-DD_HH-MM-SS format\nconst localDateTime = now.toLocaleString('sv-SE', { \n  timeZone: LOCAL_TIMEZONE,\n  year: 'numeric',\n  month: '2-digit',\n  day: '2-digit',\n  hour: '2-digit',\n  minute: '2-digit',\n  second: '2-digit'\n}).replace(' ', '_').replace(/:/g, '-');\n\n// Local formatted time for display\nconst localTimeFormatted = now.toLocaleString('en-GB', { \n  timeZone: LOCAL_TIMEZONE,\n  year: 'numeric',\n  month: '2-digit',\n  day: '2-digit',\n  hour: '2-digit',\n  minute: '2-digit',\n  second: '2-digit'\n});\n\nconst timestampUTC = now.toISOString().replace(/[:.]/g, '-').slice(0, 19); // 2025-01-20T09-30-45\n\nconsole.log('\ud83d\ude80 USER-DEFINE TIMEZONE VARIABLES INITIALISED (Local timezone):');\nconsole.log(`   \ud83c\udf0d Timezone: ${LOCAL_TIMEZONE}`);\nconsole.log(`   \ud83d\udcc5 Local date: ${localDate}`);\nconsole.log(`   \ud83d\udcc5 Local date/time: ${localDateTime}`);\nconsole.log(`   \ud83d\udd50 Local time: ${localTimeFormatted}`);\n\n// Create structure for time data output\nconst timeData = {\n  nowUTC: now.toISOString(),        // UTC for calculations\n  localDate: localDate,             // YYYY-MM-DD format\n  localDateTime: localDateTime,     // YYY-MM-DD_HH-MM-SS format\n  localTimeFormatted: localTimeFormatted, // Human readable\n  localTimezone: LOCAL_TIMEZONE,         // For reference\n  datePrefix: localDate,\n  timestampUTC,\n  startTime: now.toISOString()\n}\n\n// YOUR_AWS_SECRET_KEY_HERE==\n// \ud83d\udcdd WORKFLOW STANDARD CONFIGURATION\n// YOUR_AWS_SECRET_KEY_HERE==\n\nconst N8N_ADMIN_EMAIL = $env.N8N_ADMIN_EMAIL || 'user@example.com';\nconst WORKFLOW_NAME = $workflow.name;\nconst N8N_PROJECTS_DIR = $env.N8N_PROJECTS_DIR || '/files/n8n-projects-data'; // \u26a0\ufe0f Your projects\u2019 ROOT folder here\n// projects-root-folder/\n//   \u2514\u2500\u2500 Your-project-folder-name/\n//       \u251c\u2500\u2500 logs/\n//       \u251c\u2500\u2500 reports/\n//       \u251c\u2500\u2500 ...\n//       \u2514\u2500\u2500 [other project files]\nconst PROJECT_FOLDER_NAME = \"Workflow-backups\"; // \u26a0\ufe0f Your project folder name\nconst PROJECT_ROOT_PATH = `${N8N_PROJECTS_DIR}/${PROJECT_FOLDER_NAME}`;\n// const N8N_MEDIA_ROOT_PATH = $env.N8N_MEDIA_SHARED || '/files/n8n-media-shared'; // \u26a0\ufe0f Your public folder, accessible from inet\n// const mediaOutputFolder = \"/output\";\n// const mediaTempFolder = \"/temp\";\n// const N8N_FILE_SERVER_PURL = $env.N8N_FILE_SERVER_PURL || 'https://files.example.com'; // File serve public url\n\nconst LOGS_PATH = `${PROJECT_ROOT_PATH}/logs`;\n// const logFileName = `${localDateTime2}-${WORKFLOW_NAME.replace(/[^a-zA-Z0-9]/g, '_')}_logs.json`;  // \u26a0\ufe0f Your log file name\nconst logFileName = `${localDateTime}-backup_logs.json`;  // \u26a0\ufe0f Your log file name\nconst logPathFileName = `${LOGS_PATH}/${logFileName}`;\n\n// Configuration for report generation\nconst REPORTS_PATH = `${PROJECT_ROOT_PATH}/reports`;\nconst reportFileName = `${localDateTime}-report.txt`;\nconst reportPathFileName = `${REPORTS_PATH}/${reportFileName}`;\n\n// Console output\nconsole.log('\ud83e\uddfe STANDARD WORKFLOW VARIABLES INITIALISED:');\nconsole.log(`   \ud83d\udcc1 Admin email: ${N8N_ADMIN_EMAIL}`);\nconsole.log(`   \ud83d\udcc1 Workflow name: ${WORKFLOW_NAME}`);\nconsole.log(`   \ud83d\udcc1 Projects folder: ${N8N_PROJECTS_DIR}`);\nconsole.log(`   \ud83d\udcc1 Project folder name: ${PROJECT_FOLDER_NAME}`);\nconsole.log(`   \ud83d\udcc1 Project root path: ${PROJECT_ROOT_PATH}`);\n// console.log(`   \ud83d\udcc1 Media root path: ${N8N_MEDIA_ROOT_PATH}`);\n// console.log(`   \ud83d\udcc1 File server public URL: ${N8N_FILE_SERVER_PURL}`);\nconsole.log(`   \ud83d\udcc1 Log path: ${LOGS_PATH}`);\nconsole.log(`   \ud83d\udcc1 Log file name: ${logFileName}`);\nconsole.log(`   \ud83d\udcc1 Log file path: ${logPathFileName}`);\nconsole.log(`   \ud83d\udcc1 Reports path: ${REPORTS_PATH}`);\nconsole.log(`   \ud83d\udcc1 Report file name: ${reportFileName}`);\nconsole.log(`   \ud83d\udcc1 Report file path: ${reportPathFileName}`);\n\n// Create structure for workflow configuration output\nconst workflowConfig = {\n  workflowStep: 'init',\n  N8N_ADMIN_EMAIL,\n  WORKFLOW_NAME,\n  N8N_PROJECTS_DIR,\n  PROJECT_FOLDER_NAME,\n  PROJECT_ROOT_PATH,\n  // N8N_MEDIA_ROOT_PATH,\n  // mediaOutputFolder,\n  // mediaTempFolder,\n  // N8N_FILE_SERVER_PURL,\n  LOGS_PATH,\n  logFileName,\n  logPathFileName,\n  REPORTS_PATH,\n  reportFileName,\n  reportPathFileName\n}\n\n// YOUR_AWS_SECRET_KEY_HERE==\n// \ud83d\udcdd WORKFLOW CUSTOM CONFIGURATION\n// YOUR_AWS_SECRET_KEY_HERE==\n// \u26a0\ufe0f  INSERT HERE: your workflow custom configuration variables\n\n// Base backup path using your Docker and FTP volume configuration (folder must exist)\nconst BACKUP_FOLDER = $env.N8N_BACKUP_FOLDER || '/files/n8n-backups'; // \u26a0\ufe0f Change the default value for your n8n backup folder\nconst FTP_BACKUP_FOLDER = $env.N8N_FTP_BACKUP_FOLDER || '/n8n-backups';\nconst FTPName = 'Your FTP Server Name'; // FTP server name for logging (display purposes only)\n\nconst credentials_backup_folder = \"n8n-credentials\"; // Credentials backup path\n\n// Configuration for the backup process\nconst backupConfig = {\n  datePrefix: localDate, // Prefix for all backup files\n  fileNaming: 'name_only', // Options: 'name_only', 'id_only', 'name_and_id'\n  maxRetries: 3,\n  timeout: 30000,\n  includeCredentials: false, // Security: Don't export credential data\n  compression: false // Future enhancement\n};\n\n// Console output\nconsole.log('\ud83e\uddfe CUSTOM WORKFLOW VARIABLES INITIALISED:');\nconsole.log(`   \ud83d\udcbe Backup folder: ${BACKUP_FOLDER}`);\nconsole.log('      File prefix:', localDate);\nconsole.log('      Timestamp (UTC):', timestampUTC);\n\nconst customConfig = {\n  backupFolder: BACKUP_FOLDER,\n  FTP_BACKUP_FOLDER,\n  FTPName,\n  credentials: credentials_backup_folder,\n  ...backupConfig\n}\n\n// YOUR_AWS_SECRET_KEY_HERE==\n// \ud83d\udcca OUTPUT DATA\n// YOUR_AWS_SECRET_KEY_HERE==\n\nconst initData = {\n  timeData,\n  workflowConfig,\n  customConfig\n};\n\nreturn [{ json: initData }];"
      },
      "typeVersion": 2
    },
    {
      "id": "",
      "name": "Daily Backup",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        -48,
        -560
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "triggerAtHour": 4
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "",
      "name": "Convert to File",
      "type": "n8n-nodes-base.convertToFile",
      "position": [
        1040,
        -336
      ],
      "parameters": {
        "mode": "each",
        "options": {
          "format": true,
          "fileName": "={{ $json.name }}"
        },
        "operation": "toJson",
        "binaryPropertyName": "=data"
      },
      "typeVersion": 1.1
    },
    {
      "id": "",
      "name": "Fetch Workflows",
      "type": "n8n-nodes-base.n8n",
      "position": [
        624,
        -336
      ],
      "parameters": {
        "filters": {},
        "requestOptions": {}
      },
      "typeVersion": 1
    },
    {
      "id": "",
      "name": "Clean Filename",
      "type": "n8n-nodes-base.code",
      "onError": "continueErrorOutput",
      "position": [
        832,
        -336
      ],
      "parameters": {
        "jsCode": "// YOUR_AWS_SECRET_KEY_HERE==\n// \ud83e\uddf9 FLEXIBLE FILENAME CLEANER\n// YOUR_AWS_SECRET_KEY_HERE==\n// Purpose: Clean filename for cross-platform compatibility\n// Usage: Can work with current input OR reference another node\n//\n// Recommended Configuration:\n// **Node Settings**:\n// * \u2611\ufe0f **On Error**: Continue (using error output)\n// * \u2611\ufe0f **Error Workflow**: Your notification workflow\n// The node stops at the first error and immediately triggers your notification system! \ud83d\udea8\n\n// \u2699\ufe0f CONFIGURATION\nconst SOURCE_FIELD = 'name';           // Field containing the filename\nconst SOURCE_NODE = 'Fetch Workflows';              // Set to node name like 'Fetch Workflows' OR null to use current input\n                                       // Examples: 'Fetch Workflows', 'HTTP Request', 'Set'\n\n// Get input items\nconst inputItems = $input.all();\nconst itemCount = inputItems.length;\n\nconsole.log(`Flexible Filename Cleaner: Processing ${itemCount} item(s)`);\nconsole.log(`Configuration: SOURCE_FIELD=\"${SOURCE_FIELD}\", SOURCE_NODE=${SOURCE_NODE || 'current input'}`);\n\nif (itemCount === 0) {\n  console.error('No items to process');\n  throw new Error('No items to process');\n}\n\nconst outputItems = [];\n\ninputItems.forEach((item, index) => {\n  try {\n    let originalFileName;\n    let sourceDescription;\n    \n    // Determine where to get the filename from\n    if (SOURCE_NODE) {\n      // Get from specified node\n      try {\n        // Use the corresponding item from the source node (same index)\n        const sourceItems = $(SOURCE_NODE).all();\n        if (sourceItems[index]) {\n          originalFileName = sourceItems[index].json[SOURCE_FIELD];\n          sourceDescription = `${SOURCE_NODE}[${index}].${SOURCE_FIELD}`;\n        } else {\n          // Fallback to first item if index doesn't exist\n          originalFileName = $(SOURCE_NODE).first().json[SOURCE_FIELD];\n          sourceDescription = `${SOURCE_NODE}[0].${SOURCE_FIELD} (fallback)`;\n        }\n      } catch (nodeError) {\n        const errorMsg = `Cannot access node '${SOURCE_NODE}': ${nodeError.message}`;\n        console.error(errorMsg);\n        throw new Error(errorMsg);\n      }\n    } else {\n      // Get from current input item\n      originalFileName = item.json[SOURCE_FIELD];\n      sourceDescription = `current_input[${index}].${SOURCE_FIELD}`;\n    }\n    \n    if (!originalFileName) {\n      const errorMsg = `No filename found in ${sourceDescription}`;\n      console.error(errorMsg);\n      throw new Error(errorMsg);\n    }\n    \n    console.log(`Item ${index + 1}/${itemCount}: \"${originalFileName}\" from ${sourceDescription}`);\n    \n    // Clean filename for cross-platform compatibility\n    const cleanedFileName = originalFileName\n      // Remove forbidden characters for Windows/Linux\n      .replace(/[<>:\"/\\\\|?*\\x00-\\x1F\\x7F]/g, '_')\n      // Replace multiple spaces/dots with single underscore\n      .replace(/[\\s.]+/g, '_')\n      // Remove consecutive underscores\n      .replace(/_+/g, '_')\n      // Remove leading/trailing underscores\n      .replace(/^_+|_+$/g, '')\n      // Limit length to 180 chars\n      .substring(0, 180);\n    \n    // Fallback if name becomes empty after cleaning\n    const finalCleanedFileName = cleanedFileName || `file_${index + 1}`;\n    \n    console.log(`\u2705 Cleaned: \"${finalCleanedFileName}\"`);\n    \n    // Pass through all original data + add cleaned filename\n    outputItems.push({\n      json: {\n        ...item.json,\n        cleanedFileName: finalCleanedFileName\n      },\n      binary: item.binary || {}\n    });\n    \n  } catch (error) {\n    console.error(`\u274c Error processing item ${index + 1}:`, error.message);\n    \n    // Log error and re-throw to trigger error workflow\n    throw new Error(`Failed to process item ${index + 1}: ${error.message}`);\n  }\n});\n\nconsole.log(`\u2705 Successfully processed ${outputItems.length} item(s)`);\nreturn outputItems;"
      },
      "typeVersion": 2
    },
    {
      "id": "",
      "name": "Write Each Workflow To Disk",
      "type": "n8n-nodes-base.readWriteFile",
      "position": [
        1264,
        -240
      ],
      "parameters": {
        "options": {
          "append": false
        },
        "fileName": "={{ $('Init').first().json.customConfig.backupFolder }}/{{ $('Init').first().json.customConfig.datePrefix }}/{{ $('Clean Filename').all()[$itemIndex].json.cleanedFileName }}.json",
        "operation": "write"
      },
      "typeVersion": 1
    },
    {
      "id": "",
      "name": "ERROR:\u202fBackup Summary",
      "type": "n8n-nodes-base.stopAndError",
      "position": [
        2336,
        -368
      ],
      "parameters": {
        "errorMessage": "={{ $json.error }}"
      },
      "typeVersion": 1
    },
    {
      "id": "",
      "name": "ERROR: Clean Filename",
      "type": "n8n-nodes-base.stopAndError",
      "position": [
        1040,
        -144
      ],
      "parameters": {
        "errorMessage": "={{ $json.error }}"
      },
      "typeVersion": 1
    },
    {
      "id": "",
      "name": "Read/Write Files from Disk",
      "type": "n8n-nodes-base.readWriteFile",
      "position": [
        1248,
        -736
      ],
      "parameters": {
        "options": {},
        "fileSelector": "={{ $json.localPath }}"
      },
      "typeVersion": 1
    },
    {
      "id": "",
      "name": "List Credential Files",
      "type": "n8n-nodes-base.executeCommand",
      "position": [
        832,
        -736
      ],
      "parameters": {
        "command": "=# List all JSON files in credentials backup folder\n# Command for Execute Command node\n\nls -1 \"{{ $('Init').first().json.customConfig.backupFolder }}/{{ $('Init').first().json.customConfig.datePrefix }}/{{ $('Init').first().json.customConfig.credentials }}\"/*.json 2>/dev/null || echo \"No JSON files found\""
      },
      "typeVersion": 1
    },
    {
      "id": "",
      "name": "Output Credential Items",
      "type": "n8n-nodes-base.code",
      "position": [
        1040,
        -736
      ],
      "parameters": {
        "jsCode": "// Process file list from Execute Command and create items for each file\n// \u2705 Converts file list into individual items for Read Files node\n\n// \u26a0\ufe0f CONFIGURE THIS NODE WITH:\n// SETTINGS > ON ERROR > CONTINUE (USING ERROR OUTPUT)\n\nconst initData = $('Init').first().json;\nconst commandResult = $input.first().json;\nconst exportResult = $('Export Credentials').first().json;\n\nconsole.log('[Process File List] Command result:', commandResult);\nconsole.log('[Process File List] Command stdout:', commandResult.stdout);\n\n// Check if command was successful\nif (commandResult.exitCode !== 0) {\n  const errorMsg = `List files command failed with exit code ${commandResult.exitCode}`;\n  console.error(`[Process File List] ${errorMsg}`);\n  console.error(`[Process File List] Command stderr:`, commandResult.stderr);\n  throw new Error(errorMsg);\n}\n\n// Parse the file list from stdout\nconst stdout = commandResult.stdout.trim();\n\nif (!stdout || stdout === \"No JSON files found\") {\n  console.log('[Process File List] No credentials files found');\n  return [{ json: { workflowStep: 'no_credentials_files', message: 'No credentials files found' } }];\n}\n\n// Split file paths and filter out empty lines\nconst filePaths = stdout.split('\\n').filter(path => path.trim().length > 0);\n\nconsole.log(`[Process File List] Found ${filePaths.length} credentials file(s)`);\n\nconst outputItems = filePaths.map((fullPath, index) => {\n  // Extract filename from full path\n  const fileName = fullPath.split('/').pop();\n  \n  // Build FTP remote path\n  const ftpPath = `${initData.customConfig.FTP_BACKUP_FOLDER}/${initData.customConfig.datePrefix}/${initData.customConfig.credentials}/${fileName}`;\n  \n  console.log(`[Process File List] File ${index + 1}: ${fileName}`);\n  console.log(`  Local path: ${fullPath}`);\n  console.log(`  FTP path: ${ftpPath}`);\n  \n  return {\n    json: {\n      fileName: fileName,\n      localPath: fullPath,\n      ftpPath: ftpPath,\n      fileType: 'credentials_file',\n      workflowStep: 'credentials_file_read',\n      fileIndex: index + 1,\n      totalFiles: filePaths.length,\n      exportResult: {\n        exitCode: exportResult.exitCode,\n        stdout: exportResult.stdout,\n        stderr: exportResult.stderr\n      }\n    }\n  };\n});\n\nconsole.log(`[Process File List] \u2705 Created ${outputItems.length} items for file reading`);\n\nreturn outputItems;"
      },
      "typeVersion": 2
    },
    {
      "id": "",
      "name": "Aggregate",
      "type": "n8n-nodes-base.aggregate",
      "position": [
        1472,
        -240
      ],
      "parameters": {
        "options": {
          "includeBinaries": true
        },
        "aggregate": "aggregateAllItemData",
        "destinationFieldName": "workflows"
      },
      "typeVersion": 1
    },
    {
      "id": "",
      "name": "Merge",
      "type": "n8n-nodes-base.merge",
      "position": [
        1920,
        -560
      ],
      "parameters": {
        "numberInputs": 3
      },
      "typeVersion": 3.2
    },
    {
      "id": "",
      "name": "Upload Credentials To FTP",
      "type": "n8n-nodes-base.ftp",
      "onError": "continueRegularOutput",
      "disabled": true,
      "position": [
        1456,
        -736
      ],
      "parameters": {
        "path": "={{ $('Output Credential Items').item.json.ftpPath }}",
        "operation": "upload"
      },
      "typeVersion": 1
    },
    {
      "id": "",
      "name": "FTP Logger (credentials)",
      "type": "n8n-nodes-base.code",
      "disabled": true,
      "position": [
        1664,
        -736
      ],
      "parameters": {
        "jsCode": "// FTP Upload Results Logger for Credentials Files\n// \u2705 Processes FTP upload results from individual credentials files and creates comprehensive logging data\n// \u2705 Enhanced error handling for FTP connection timeouts and server errors\n\n// \u26a0\ufe0f CONFIGURE THIS NODE WITH:\n// SETTINGS > ON ERROR > CONTINUE (USING ERROR OUTPUT)\n\nconst allFtpResults = $input.all();\nconst initData = $('Init').first().json;\n\nconsole.log('[FTP Logger] Processing FTP upload results for credentials files...');\nconsole.log(`[FTP Logger] Received ${allFtpResults.length} FTP result item(s)`);\n\nconst ftpUploadResults = {\n  timestamp: new Date().toISOString(),\n  localTimestamp: new Date().toLocaleString('en-GB', { timeZone: initData.timeData.timezone }),\n  totalFiles: allFtpResults.length,\n  successfulUploads: 0,\n  failedUploads: 0,\n  uploads: []\n};\n\n// Process each FTP result\nallFtpResults.forEach((result, index) => {\n  try {\n    // Handle different result structures (success vs error outputs)\n    let uploadData;\n    let isConnectionError = false;\n    let connectionErrorMessage = null;\n    \n    // Check if this is an error result from FTP node\n    if (result.error) {\n      // This comes from the error output of FTP node\n      isConnectionError = true;\n      connectionErrorMessage = result.error;\n      uploadData = result.json || {};\n      console.log(`[FTP Logger] FTP node error detected: ${connectionErrorMessage}`);\n    } else if (result.json && result.json.error) {\n      // Error embedded in JSON response\n      isConnectionError = true;\n      connectionErrorMessage = result.json.error;\n      uploadData = result.json;\n      console.log(`[FTP Logger] FTP JSON error detected: ${connectionErrorMessage}`);\n    } else {\n      // Normal success/failure response\n      uploadData = result.json || {};\n    }\n    \n    // Determine if upload was successful\n    const isSuccess = !isConnectionError && !uploadData.error && uploadData.success !== false;\n    \n    // Extract file information with fallbacks\n    const fileName = uploadData.fileName || uploadData.path || `credentials_file_${index + 1}`;\n    const ftpPath = uploadData.ftpPath || uploadData.path || 'unknown';\n    \n    const uploadLog = {\n      fileName: fileName,\n      fileType: uploadData.fileType || 'credentials_file',\n      localPath: uploadData.localPath || 'unknown',\n      ftpPath: ftpPath,\n      success: isSuccess,\n      uploadTime: new Date().toISOString(),\n      error: connectionErrorMessage || uploadData.error || null,\n      ftpResponse: uploadData.response || null,\n      fileSize: uploadData.fileSize || null,\n      fileIndex: uploadData.fileIndex || index + 1,\n      connectionError: isConnectionError\n    };\n    \n    if (isSuccess) {\n      ftpUploadResults.successfulUploads++;\n      console.log(`[FTP Logger] \u2705 Successfully uploaded: ${uploadLog.fileName} (${uploadLog.fileIndex}/${uploadData.totalFiles || ftpUploadResults.totalFiles})`);\n    } else {\n      ftpUploadResults.failedUploads++;\n      if (isConnectionError) {\n        console.error(`[FTP Logger] \ud83d\udd0c Connection error for: ${uploadLog.fileName} - ${connectionErrorMessage}`);\n      } else {\n        console.error(`[FTP Logger] \u274c Failed to upload: ${uploadLog.fileName} (${uploadLog.fileIndex}/${uploadData.totalFiles || ftpUploadResults.totalFiles})`);\n      }\n      if (uploadLog.error) {\n        console.error(`[FTP Logger] Error details: ${uploadLog.error}`);\n      }\n    }\n    \n    ftpUploadResults.uploads.push(uploadLog);\n    \n  } catch (error) {\n    console.error(`[FTP Logger] Error processing result ${index + 1}:`, error.message);\n    \n    // Add error entry for processing failures\n    ftpUploadResults.uploads.push({\n      fileName: `unknown_file_${index + 1}`,\n      fileType: 'credentials_file',\n      localPath: 'unknown',\n      ftpPath: 'unknown',\n      success: false,\n      error: `Processing error: ${error.message}`,\n      uploadTime: new Date().toISOString(),\n      fileIndex: index + 1,\n      connectionError: false\n    });\n    \n    ftpUploadResults.failedUploads++;\n  }\n});\n\n// Create simplified summary\nconst connectionErrors = ftpUploadResults.uploads.filter(upload => upload.connectionError);\nconst hasConnectionErrors = connectionErrors.length > 0;\n\n// Create simplified summary\nconst ftpSummary = {\n  overallSuccess: ftpUploadResults.failedUploads === 0,\n  summary: hasConnectionErrors ? \n    `FTP server connection failed - credentials files could not be uploaded due to server connectivity issues` :\n    `${ftpUploadResults.successfulUploads}/${ftpUploadResults.totalFiles} credentials files uploaded successfully`\n};\n\nconsole.log('[FTP Logger] Credentials Upload Summary:');\nconsole.log(`  Total credentials files: ${ftpUploadResults.totalFiles}`);\nconsole.log(`  Successful uploads: ${ftpUploadResults.successfulUploads}`);\nconsole.log(`  Failed uploads: ${ftpUploadResults.failedUploads}`);\n\nif (hasConnectionErrors) {\n  console.log('[FTP Logger] \ud83d\udd0c FTP server connection failed');\n}\n\n// Add summary to results\nftpUploadResults.summary = ftpSummary;\n\n// Create output data that will be merged with Backup Summary\nconst credentialsFtpLog = {\n  credentialsFtpUpload: ftpUploadResults\n};\n\nconsole.log('[FTP Logger] \u2705 Credentials FTP upload logging completed');\n\nreturn [{ json: credentialsFtpLog }];"
      },
      "typeVersion": 2
    },
    {
      "id": "",
      "name": "FTP Logger (workflows)",
      "type": "n8n-nodes-base.code",
      "disabled": true,
      "position": [
        1472,
        -432
      ],
      "parameters": {
        "jsCode": "// FTP Upload Results Logger for Workflow Files\n// \u2705 Processes FTP upload results from workflow files and creates summary for Backup Summary\n// \u2705 Enhanced error handling for FTP connection timeouts and server errors\n\n// \u26a0\ufe0f CONFIGURE THIS NODE WITH:\n// SETTINGS > ON ERROR > CONTINUE (USING ERROR OUTPUT)\n\nconst allFtpResults = $input.all();\nconst initData = $('Init').first().json;\n\nconsole.log('[FTP Workflow Logger] Processing FTP upload results for workflow files...');\nconsole.log(`[FTP Workflow Logger] Received ${allFtpResults.length} FTP result item(s)`);\n\nconst ftpUploadResults = {\n  timestamp: new Date().toISOString(),\n  localTimestamp: new Date().toLocaleString('en-GB', { timeZone: initData.timeData.localTimezone }),\n  totalFiles: allFtpResults.length,\n  successfulUploads: 0,\n  failedUploads: 0,\n  uploads: []\n};\n\n// Process each FTP result\nallFtpResults.forEach((result, index) => {\n  try {\n    // Handle different result structures (success vs error outputs)\n    let uploadData;\n    let isConnectionError = false;\n    let connectionErrorMessage = null;\n    \n    // Check if this is an error result from FTP node\n    if (result.error) {\n      // This comes from the error output of FTP node\n      isConnectionError = true;\n      connectionErrorMessage = result.error;\n      uploadData = result.json || {};\n      console.log(`[FTP Workflow Logger] FTP node error detected: ${connectionErrorMessage}`);\n    } else if (result.json && result.json.error) {\n      // Error embedded in JSON response\n      isConnectionError = true;\n      connectionErrorMessage = result.json.error;\n      uploadData = result.json;\n      console.log(`[FTP Workflow Logger] FTP JSON error detected: ${connectionErrorMessage}`);\n    } else {\n      // Normal success/failure response\n      uploadData = result.json || {};\n    }\n    \n    // Determine if upload was successful\n    const isSuccess = !isConnectionError && !uploadData.error && uploadData.success !== false;\n    \n    // Extract file information with fallbacks\n    // For workflows, try to get the filename from Clean Filename node (which has both name and cleanedFileName)\n    let originalWorkflowName = `workflow_${index + 1}`;\n    let cleanedFileName = `workflow_${index + 1}`;\n    \n    try {\n      const cleanFilenameItems = $('Clean Filename').all();\n      if (cleanFilenameItems[index]) {\n        originalWorkflowName = cleanFilenameItems[index].json.name || `workflow_${index + 1}`;\n        cleanedFileName = cleanFilenameItems[index].json.cleanedFileName || `workflow_${index + 1}`;\n      }\n    } catch (error) {\n      console.log(`[FTP Workflow Logger] Could not access Clean Filename node for item ${index + 1}, using fallback`);\n    }\n    \n    const fileName = uploadData.fileName || uploadData.path || `${cleanedFileName}.json`;\n    const ftpPath = uploadData.ftpPath || uploadData.path || 'unknown';\n    \n    const uploadLog = {\n      fileName: fileName,\n      workflowName: originalWorkflowName,\n      cleanedFileName: cleanedFileName,\n      fileType: 'workflow_file',\n      localPath: uploadData.localPath || 'unknown',\n      ftpPath: ftpPath,\n      success: isSuccess,\n      uploadTime: new Date().toISOString(),\n      error: connectionErrorMessage || uploadData.error || null,\n      ftpResponse: uploadData.response || null,\n      fileSize: uploadData.fileSize || null,\n      fileIndex: uploadData.fileIndex || index + 1,\n      connectionError: isConnectionError\n    };\n    \n    if (isSuccess) {\n      ftpUploadResults.successfulUploads++;\n      console.log(`[FTP Workflow Logger] \u2705 Successfully uploaded: ${originalWorkflowName} (${uploadLog.fileIndex}/${ftpUploadResults.totalFiles})`);\n    } else {\n      ftpUploadResults.failedUploads++;\n      if (isConnectionError) {\n        console.error(`[FTP Workflow Logger] \ud83d\udd0c Connection error for: ${originalWorkflowName} - ${connectionErrorMessage}`);\n      } else {\n        console.error(`[FTP Workflow Logger] \u274c Failed to upload: ${originalWorkflowName} (${uploadLog.fileIndex}/${ftpUploadResults.totalFiles})`);\n      }\n      if (uploadLog.error) {\n        console.error(`[FTP Workflow Logger] Error details: ${uploadLog.error}`);\n      }\n    }\n    \n    ftpUploadResults.uploads.push(uploadLog);\n    \n  } catch (error) {\n    console.error(`[FTP Workflow Logger] Error processing result ${index + 1}:`, error.message);\n    \n    // Add error entry for processing failures\n    ftpUploadResults.uploads.push({\n      fileName: `unknown_workflow_${index + 1}`,\n      workflowName: `unknown_workflow_${index + 1}`,\n      fileType: 'workflow_file',\n      localPath: 'unknown',\n      ftpPath: 'unknown',\n      success: false,\n      error: `Processing error: ${error.message}`,\n      uploadTime: new Date().toISOString(),\n      fileIndex: index + 1,\n      connectionError: false\n    });\n    \n    ftpUploadResults.failedUploads++;\n  }\n});\n\n// Check for connection errors specifically\nconst connectionErrors = ftpUploadResults.uploads.filter(upload => upload.connectionError);\nconst hasConnectionErrors = connectionErrors.length > 0;\n\n// Create simplified summary\nconst ftpSummary = {\n  overallSuccess: ftpUploadResults.failedUploads === 0,\n  summary: hasConnectionErrors ? \n    `FTP server connection failed - workflow files could not be uploaded due to server connectivity issues` :\n    `${ftpUploadResults.successfulUploads}/${ftpUploadResults.totalFiles} workflow files uploaded successfully`\n};\n\nconsole.log('[FTP Workflow Logger] Workflow Upload Summary:');\nconsole.log(`  Total workflow files: ${ftpUploadResults.totalFiles}`);\nconsole.log(`  Successful uploads: ${ftpUploadResults.successfulUploads}`);\nconsole.log(`  Failed uploads: ${ftpUploadResults.failedUploads}`);\n\nif (hasConnectionErrors) {\n  console.log('[FTP Workflow Logger] \ud83d\udd0c FTP server connection failed');\n}\n\n// Add summary to results\nftpUploadResults.summary = ftpSummary;\n\n// Create output data that will be merged with Backup Summary\nconst workflowsFtpLog = {\n  workflowsFtpUpload: ftpUploadResults\n};\n\nconsole.log('[FTP Workflow Logger] \u2705 Workflow FTP upload logging completed');\n\nreturn [{ json: workflowsFtpLog }];"
      },
      "typeVersion": 2
    },
    {
      "id": "",
      "name": "Export Credentials",
      "type": "n8n-nodes-base.executeCommand",
      "position": [
        624,
        -736
      ],
      "parameters": {
        "command": "=# Export des credentials avec backup automatique\nn8n export:credentials --backup --output={{ $('Init').item.json.customConfig.backupFolder }}/{{ $('Init').item.json.customConfig.datePrefix }}/{{ $('Init').item.json.customConfig.credentials }}"
      },
      "typeVersion": 1
    },
    {
      "id": "",
      "name": "Upload Workflows To FTP",
      "type": "n8n-nodes-base.ftp",
      "onError": "continueRegularOutput",
      "disabled": true,
      "position": [
        1264,
        -432
      ],
      "parameters": {
        "path": "={{ $('Init').first().json.customConfig.FTP_BACKUP_FOLDER }}/{{ $('Init').first().json.customConfig.datePrefix }}/{{ $('Clean Filename').all()[$itemIndex].json.cleanedFileName }}.json",
        "operation": "upload"
      },
      "credentials": {
        "ftp": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    }
  ],
  "connections": {
    "Init": {
      "main": [
        [
          {
            "node": "Create Date Folder",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge": {
      "main": [
        [
          {
            "node": "Backup Summary",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Aggregate": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 2
          }
        ]
      ]
    },
    "Daily Backup": {
      "main": [
        [
          {
            "node": "Init",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Backup Summary": {
      "main": [
        [
          {
            "node": "Write Backup Log",
            "type": "main",
            "index": 0
          },
          {
            "node": "Write Email Log",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "ERROR:\u202fBackup Summary",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Clean Filename": {
      "main": [
        [
          {
            "node": "Convert to File",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "ERROR: Clean Filename",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Convert to File": {
      "main": [
        [
          {
            "node": "Upload Workflows To FTP",
            "type": "main",
            "index": 0
          },
          {
            "node": "Write Each Workflow To Disk",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Workflows": {
      "main": [
        [
          {
            "node": "Clean Filename",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Write Backup Log": {
      "main": [
        [
          {
            "node": "Send email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create Date Folder": {
      "main": [
        [
          {
            "node": "Fetch Workflows",
            "type": "main",
            "index": 0
          },
          {
            "node": "Export Credentials",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Export Credentials": {
      "main": [
        [
          {
            "node": "List Credential Files",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "List Credential Files": {
      "main": [
        [
          {
            "node": "Output Credential Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "FTP Logger (workflows)": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Output Credential Items": {
      "main": [
        [
          {
            "node": "Read/Write Files from Disk",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Upload Workflows To FTP": {
      "main": [
        [
          {
            "node": "FTP Logger (workflows)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "FTP Logger (credentials)": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Upload Credentials To FTP": {
      "main": [
        [
          {
            "node": "FTP Logger (credentials)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read/Write Files from Disk": {
      "main": [
        [
          {
            "node": "Upload Credentials To FTP",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Write Each Workflow To Disk": {
      "main": [
        [
          {
            "node": "Aggregate",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}

Credentials you'll need

Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.

Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

Complete backup solution that saves both workflows and credentials to local/server disk with optional FTP upload for off-site redundancy.

Source: https://n8n.io/workflows/9151/ — original creator credit. Request a take-down →

More General workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

General

Perfect for content publishing with organic scheduling patterns, social media automation, API systems that need to avoid rate limiting, or any automation requiring randomised timing control across mul

n8n, Read Write File, Stop And Error +1
General

Perfect for disaster recovery or migrating between environments, this workflow automatically identifies your most recent FTP backup and provides a manual restore capability that intelligently excludes

Email Send, Ftp, Read Write File +2
General

Perfect for disaster recovery or migrating between environments, this workflow automatically identifies your most recent backup and provides a manual restore capability that intelligently excludes the

Email Send, Execute Command, Stop And Error
General

This n8n workflow automates the secure transfer of files between FTP servers on a scheduled basis, providing enterprise-grade reliability with comprehensive error handling and dual notification system

Ftp, Email Send
General

This workflow automatically handles errors in your n8n workflows by: Detecting when an error occurs and capturing the error details Sending an email notification with the error message and affected no

Error Trigger, Email Send, n8n