AutomationFlowsGeneral › Clean Up Archived Workflows with the N8n API on a Schedule

Clean Up Archived Workflows with the N8n API on a Schedule

ByAlvaro Pérez @junforever on n8n.io

This workflow runs daily and uses the n8n API to find archived workflows older than a retention threshold, optionally deleting unprotected ones in batches while producing a detailed preview or deletion summary. Runs every day at 09:00 (workflow timezone) on a schedule. Fetches…

Cron / scheduled trigger★★★★☆ complexity17 nodesn8nStop And Error
General Trigger: Cron / scheduled Nodes: 17 Complexity: ★★★★☆ Added:
Clean Up Archived Workflows with the N8n API on a Schedule — n8n workflow card showing n8n, Stop And Error integration

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

The workflow JSON

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

Download .json
{
  "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
          }
        ]
      ]
    }
  }
}

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

This workflow runs daily and uses the n8n API to find archived workflows older than a retention threshold, optionally deleting unprotected ones in batches while producing a detailed preview or deletion summary. Runs every day at 09:00 (workflow timezone) on a schedule. Fetches…

Source: https://n8n.io/workflows/17533/ — 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

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

Read Write File, Email Send, Execute Command +3
General

Workflow 2469. Uses moveBinaryData, googleDrive, itemLists, n8n. Scheduled trigger; 33 nodes.

Move Binary Data, Google Drive, Item Lists +1
General

&gt; v2: Now it can read multiple types of LLM usages. Better dynamic approach for reading model usage.

n8n, Execute Workflow Trigger, Stop And Error
General

Click here to access this Workflow for free.

Google Drive, n8n