{
  "id": "6Yk9irtx0X2IIDne",
  "meta": {
    "builderVariant": "mcp",
    "aiBuilderAssisted": true
  },
  "name": "Archived Workflow Cleanup",
  "tags": [],
  "nodes": [
    {
      "id": "15055dc0-d5a1-44e3-9013-4903483a9a00",
      "name": "get_workflows",
      "type": "n8n-nodes-base.n8n",
      "position": [
        -480,
        832
      ],
      "parameters": {
        "filters": {
          "projectId": "={{ $('config').first().json.project_id || undefined }}",
          "excludePinnedData": true
        },
        "requestOptions": {
          "timeout": 30000
        }
      },
      "credentials": {
        "n8nApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "a4722029-e0aa-43b8-bf8c-b4aadf7597aa",
      "name": "delete_workflows_in_batches",
      "type": "n8n-nodes-base.n8n",
      "onError": "continueRegularOutput",
      "position": [
        1248,
        832
      ],
      "parameters": {
        "operation": "delete",
        "workflowId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $json.target.id }}"
        },
        "requestOptions": {
          "timeout": 30000,
          "batching": {
            "batch": {
              "batchSize": "={{ $('config').first().json.batch_size }}",
              "batchInterval": "={{ $('config').first().json.batch_interval_ms }}"
            }
          }
        }
      },
      "credentials": {
        "n8nApi": {
          "name": "<your credential>"
        }
      },
      "retryOnFail": false,
      "typeVersion": 1,
      "alwaysOutputData": true
    },
    {
      "id": "fc9ee699-bc47-47a2-ae2f-1b71e294f4ce",
      "name": "workflow_overview_note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2368,
        608
      ],
      "parameters": {
        "color": 5,
        "width": 1080,
        "height": 372,
        "content": "# Workflow overview\n\nSafely previews and optionally deletes archived n8n workflows older than a configurable retention period.\n\n**Flow:** schedule \u2192 retrieve workflows \u2192 keep archived \u2192 classify \u2192 preview/delete \u2192 summarize.\n\n**Safety:** `dry_run` is enabled by default, `project_id` is required for live mode, `protected_tag` prevents deletion, and deletions are batched.\n\n**Tradeoffs**\n- The n8n API returns all workflows in scope before the Filter node, so large instances may use more memory and execution time.\n\n"
      },
      "typeVersion": 1
    },
    {
      "id": "85c861a8-cd78-4dfd-9127-31b97e4b791b",
      "name": "daily_cleanup_schedule",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        -1088,
        832
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "triggerAtHour": 9
            }
          ]
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "1e2d722f-f748-4ed7-a17a-671a5397f2df",
      "name": "config",
      "type": "n8n-nodes-base.set",
      "position": [
        -816,
        832
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "retention_days",
              "name": "retention_days",
              "type": "number",
              "value": 7
            },
            {
              "id": "dry_run",
              "name": "dry_run",
              "type": "boolean",
              "value": true
            },
            {
              "id": "project_id",
              "name": "project_id",
              "type": "string",
              "value": ""
            },
            {
              "id": "protected_tag",
              "name": "protected_tag",
              "type": "string",
              "value": "keep"
            },
            {
              "id": "batch_size",
              "name": "batch_size",
              "type": "number",
              "value": 10
            },
            {
              "id": "batch_interval_ms",
              "name": "batch_interval_ms",
              "type": "number",
              "value": 1000
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "38c8d221-0575-4da6-9aee-c60fb86f2b3d",
      "name": "select_candidates",
      "type": "n8n-nodes-base.code",
      "onError": "continueErrorOutput",
      "position": [
        128,
        832
      ],
      "parameters": {
        "jsCode": "// Clasifica los workflows archivados seg\u00fan retenci\u00f3n y etiqueta de protecci\u00f3n.\nconst config = $('config').first().json;\nconst retention_days = Number(config.retention_days);\nconst project_id = String(config.project_id ?? '').trim();\nconst protected_tag = String(config.protected_tag ?? 'keep').trim().toLowerCase();\n\n// Valida la configuraci\u00f3n antes de evaluar los workflows archivados.\nif (!Number.isFinite(retention_days) || retention_days < 1) {\n  throw new Error('retention_days must be a number greater than 0');\n}\nif (!config.dry_run && !project_id) {\n  throw new Error('Set project_id before changing dry_run to false');\n}\n\nconst archived_workflows = $input.all();\nconst now_ms = Date.now();\nconst cutoff_ms = now_ms - retention_days * 86_400_000;\nconst stats = {\n  total_archived: archived_workflows.length,\n  to_delete: 0,\n  not_deleted_by_retention: 0,\n  not_deleted_by_protected_tag: 0,\n};\nconst candidates = [];\n\n// Asigna cada workflow archivado a una \u00fanica categor\u00eda estad\u00edstica.\nfor (const input_item of archived_workflows) {\n  const workflow_data = input_item.json;\n  const workflow_id = String(workflow_data.id ?? '').trim();\n  const updated_at_ms = Date.parse(workflow_data.updatedAt);\n\n  if (!workflow_id || !Number.isFinite(updated_at_ms)) {\n    throw new Error('Every archived workflow must include id and updatedAt');\n  }\n\n  if (updated_at_ms >= cutoff_ms) {\n    stats.not_deleted_by_retention += 1;\n    continue;\n  }\n\n  // Normaliza las etiquetas para aplicar la protecci\u00f3n sin distinguir may\u00fasculas.\n  const tags = (Array.isArray(workflow_data.tags) ? workflow_data.tags : [])\n    .map((tag) => String(typeof tag === 'string' ? tag : tag?.name ?? '').trim().toLowerCase())\n    .filter(Boolean);\n\n  if (protected_tag && tags.includes(protected_tag)) {\n    stats.not_deleted_by_protected_tag += 1;\n    continue;\n  }\n\n  stats.to_delete += 1;\n  candidates.push({\n    id: workflow_id,\n    name: String(workflow_data.name ?? ''),\n    updated_at: workflow_data.updatedAt,\n    age_days: Math.floor((now_ms - updated_at_ms) / 86_400_000),\n    tags,\n  });\n}\n\n// Devuelve candidatos y estad\u00edsticas que forman una partici\u00f3n del total archivado.\nreturn [{\n  json: {\n    cutoff_at: new Date(cutoff_ms).toISOString(),\n    stats,\n    candidates,\n  },\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "2bc2928f-5a03-48a5-918c-77297d5144ba",
      "name": "has_candidates",
      "type": "n8n-nodes-base.if",
      "position": [
        384,
        816
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "has_candidates",
              "operator": {
                "type": "number",
                "operation": "gt"
              },
              "leftValue": "={{ $json.stats.to_delete }}",
              "rightValue": 0
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "d36b68dd-4672-4785-a168-0114bceeb949",
      "name": "dry_run",
      "type": "n8n-nodes-base.if",
      "position": [
        640,
        720
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "dry_run",
              "operator": {
                "type": "boolean",
                "operation": "equals"
              },
              "leftValue": "={{ $('config').first().json.dry_run }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "d118ed2e-de8b-4a87-9506-0995416b17df",
      "name": "split_candidates",
      "type": "n8n-nodes-base.splitOut",
      "position": [
        992,
        832
      ],
      "parameters": {
        "include": "allOtherFields",
        "options": {
          "destinationFieldName": "target"
        },
        "fieldToSplitOut": "candidates"
      },
      "typeVersion": 1
    },
    {
      "id": "0bfada89-5a65-4d06-ab6b-55590ac44d50",
      "name": "cleanup_summary",
      "type": "n8n-nodes-base.code",
      "position": [
        1504,
        640
      ],
      "parameters": {
        "jsCode": "// Resume la simulaci\u00f3n o eliminaci\u00f3n con estad\u00edsticas exclusivas de workflows archivados.\nconst config = $('config').first().json;\nconst selection = $('select_candidates').first().json;\n\n// Normaliza los resultados recibidos desde la rama ejecutada.\nconst results = $input.all().map((input_item) => input_item.json);\nconst deletion_enabled = !config.dry_run && selection.stats.to_delete > 0;\n\n// Separa resultados correctos y fallidos cuando la eliminaci\u00f3n est\u00e1 habilitada.\nconst failures = deletion_enabled ? results.filter((result) => result?.error) : [];\nconst successes = deletion_enabled ? results.filter((result) => !result?.error) : [];\nconst not_processed = deletion_enabled\n  ? Math.max(selection.stats.to_delete - results.length, 0)\n  : 0;\n\n// Calcula el estado final usando directamente el valor de dry_run.\nconst status = selection.stats.to_delete === 0\n  ? 'NOTHING_TO_DO'\n  : config.dry_run\n    ? 'PREVIEW'\n    : failures.length || not_processed\n      ? 'PARTIAL'\n      : 'SUCCESS';\n\n// Extrae mensajes legibles de los errores devueltos por n8n.\nconst error_messages = failures.map((result) => result.error?.message ?? String(result.error));\n\nreturn [{\n  json: {\n    status,\n    dry_run: config.dry_run,\n    generated_at: new Date().toISOString(),\n    retention_days: config.retention_days,\n    project_id: config.project_id,\n    protected_tag: config.protected_tag,\n    cutoff_at: selection.cutoff_at,\n    stats: selection.stats,\n    attempted_count: deletion_enabled ? results.length : 0,\n    deleted_count: successes.length,\n    failed_count: failures.length,\n    not_processed_count: not_processed,\n    candidates: selection.candidates,\n    errors: error_messages,\n  },\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "c082f21f-d936-4873-8af5-27ccbf0dce43",
      "name": "validation_error",
      "type": "n8n-nodes-base.stopAndError",
      "position": [
        384,
        1024
      ],
      "parameters": {
        "errorMessage": "={{ 'Hubo un error al procesar los flujos. Detalle: ' + ($json.error?.message ?? $json.error ?? $json.message ?? 'sin detalle') }}"
      },
      "typeVersion": 1
    },
    {
      "id": "f3ff19f4-4d82-4f54-a257-17e53cd0e5ab",
      "name": "filter_archived_workflows",
      "type": "n8n-nodes-base.filter",
      "position": [
        -224,
        832
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "is_archived",
              "operator": {
                "type": "boolean",
                "operation": "equals"
              },
              "leftValue": "={{ $json.isArchived }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "7ab39741-bfb3-4631-8c84-71652d473ebc",
      "name": "title_note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2016,
        480
      ],
      "parameters": {
        "color": 5,
        "width": 296,
        "height": 84,
        "content": "# Archived Workflow Cleanup\n"
      },
      "typeVersion": 1
    },
    {
      "id": "71af3978-c904-4a00-b1a7-0baf164f3b40",
      "name": "configuration_note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1168,
        368
      ],
      "parameters": {
        "color": 4,
        "width": 600,
        "height": 876,
        "content": "# Configuration\n\nChange these values in `config`:\n\n- `retention_days`: minimum archived age. Default: `7`.\n- `dry_run`: `true` previews; `false` enables deletion.\n- `project_id`: optional scope in preview; required for live deletion.\n- `protected_tag`: tag that prevents deletion. Default: `keep`.\n- `batch_size`: deletions per batch. Default: `10`.\n- `batch_interval_ms`: pause between batches. Default: `1000` ms.\n\nSelect an **n8n API credential** in both n8n nodes. Review a preview before enabling live mode.\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n"
      },
      "typeVersion": 1
    },
    {
      "id": "3c94e411-7265-4bd8-a33e-2ae0f010c265",
      "name": "discovery_note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -544,
        368
      ],
      "parameters": {
        "color": 4,
        "width": 600,
        "height": 876,
        "content": "# Step 1 \u2014 Schedule and discover\n\n- `daily_cleanup_schedule` runs daily at 09:00 in the workflow timezone.\n- `get_workflows` retrieves workflows from `project_id` when provided.\n- Pinned data is excluded.\n- `filter_archived_workflows` sends only archived workflows to classification.\n\n**Important:** non-archived workflows are discarded before custom code runs.\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n"
      },
      "typeVersion": 1
    },
    {
      "id": "e00279b3-65f8-4893-9083-a0d055bf76bd",
      "name": "classification_note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        80,
        368
      ],
      "parameters": {
        "color": 4,
        "width": 820,
        "height": 868,
        "content": "# Step 2 \u2014 Classify and decide\n\n- `select_candidates` validates configuration and workflow fields.\n- Every archived workflow enters one category: delete, retained by age, or protected by tag.\n- `has_candidates` skips deletion when the candidate count is zero.\n- `dry_run` routes candidates to preview or live deletion.\n- Validation failures stop in `validation_error`.\n\n**Invariant:** the three categories always add up to `total_archived`.\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n"
      },
      "typeVersion": 1
    },
    {
      "id": "85d81e2e-741e-4dc1-9ac2-f7283fdb4bfd",
      "name": "deletion_note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        944,
        368
      ],
      "parameters": {
        "color": 4,
        "width": 920,
        "height": 876,
        "content": "# Step 3 \u2014 Delete and summarize\n\n- `split_candidates` creates one item per workflow candidate.\n- `delete_workflows_in_batches` deletes using `batch_size` and `batch_interval_ms`.\n- `cleanup_summary` reports status, archived totals, candidates, successes, and failures.\n\n**Important:** deletion is permanent. Keep `dry_run = true` until the preview is approved.\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n"
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "timezone": "America/Bogota",
    "binaryMode": "separate",
    "callerPolicy": "workflowsFromSameOwner",
    "timeSavedMode": "fixed",
    "availableInMCP": true,
    "executionOrder": "v1",
    "executionTimeout": 300,
    "saveManualExecutions": true,
    "saveDataErrorExecution": "all",
    "saveDataSuccessExecution": "all"
  },
  "versionId": "a4d8b71b-9347-4b43-87aa-754df6dbc15c",
  "nodeGroups": [],
  "connections": {
    "config": {
      "main": [
        [
          {
            "node": "get_workflows",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "dry_run": {
      "main": [
        [
          {
            "node": "cleanup_summary",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "split_candidates",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "get_workflows": {
      "main": [
        [
          {
            "node": "filter_archived_workflows",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "has_candidates": {
      "main": [
        [
          {
            "node": "dry_run",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "cleanup_summary",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "split_candidates": {
      "main": [
        [
          {
            "node": "delete_workflows_in_batches",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "select_candidates": {
      "main": [
        [
          {
            "node": "has_candidates",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "validation_error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "daily_cleanup_schedule": {
      "main": [
        [
          {
            "node": "config",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "filter_archived_workflows": {
      "main": [
        [
          {
            "node": "select_candidates",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "delete_workflows_in_batches": {
      "main": [
        [
          {
            "node": "cleanup_summary",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}