# n8n Tag Janitor Workflow ## Overview Daily automated workflow to clean up duplicate/similar tags using AI semantic analysis. ## Workflow Structure [Cron] → [HTTP GET /api/tags] → [Code: Preprocess] → [AI: Generate Plan] → [Code: Validate JSON] → [HTTP POST /api/tags/maintenance] → [Notification] ## Node Configuration ### 1. Schedule Trigger (Cron) - Trigger: Daily at 03:00 UTC - Timezone: UTC ### 2. HTTP Request - Fetch Tags - Method: GET - URL: `{{$env.SITE_BASE_URL}}/api/tags` - Response: JSON with `.tags` array ### 3. Code Node - Preprocess Purpose: Format tags for LLM input, handle chunking if >200 tags ```javascript const tags = $input.first().json.tags; const formatted = tags.map((t) => ({ id: t.id, name: t.name, nameEn: t.nameEn || "", projectCount: t._count.projects, })); // Sort by projectCount desc for prioritization formatted.sort((a, b) => b.projectCount - a.projectCount); return [{ json: { tags: formatted, total: formatted.length } }]; ``` ### 4. AI Node - Generate Merge Plan Model: GPT-4o / Claude 3.5 Sonnet Temperature: 0.1 (deterministic) Prompt Template: ``` You are a tag management expert. Analyze these tags and identify: 1. Semantic duplicates that should be merged (e.g., "机器学习" and "ML" → keep "机器学习" with nameEn "Machine Learning") 2. Tags missing English names that need nameEn补全 Tags (JSON): {{$json.tags}} Output STRICT JSON (no markdown, no explanation): { "merges": [ { "target": { "name": "保留的标签名", "nameEn": "Canonical English Name" }, "sourceTagIds": ["id1", "id2"] } ], "updates": [ { "tagId": "id", "nameEn": "English Name" } ] } Rules: - Keep the tag with higher projectCount as target - For merges, target can be { "id": "existing_id" } if keeping existing tag, or { "name": "...", "nameEn": "..." } to create new - Only include tags that NEED action (empty arrays if nothing to do) - nameEn should be proper English, not pinyin - Common tech terms: 机器学习=Machine Learning, 深度学习=Deep Learning, 自然语言处理=NLP ``` ### 5. Code Node - Validate & Parse ```javascript const response = $input.first().json; let plan; try { plan = typeof response === "string" ? JSON.parse(response) : response; } catch (e) { throw new Error("Invalid JSON from AI: " + e.message); } // Validate structure if (!Array.isArray(plan.merges)) plan.merges = []; if (!Array.isArray(plan.updates)) plan.updates = []; // Self-merge check: target.id cannot be in sourceTagIds for (const merge of plan.merges) { if (merge.target.id && merge.sourceTagIds.includes(merge.target.id)) { throw new Error("Self-merge detected: " + merge.target.id); } } return [{ json: plan }]; ``` ### 6. HTTP Request - Execute Maintenance - Method: POST - URL: `{{$env.SITE_BASE_URL}}/api/tags/maintenance` - Body: ```json { "apiKey": "{{$env.WEBHOOK_API_KEY}}", "updates": {{$json.updates}}, "merges": {{$json.merges}} } ``` ### 7. Notification (Slack/Email/Webhook) Send summary: - Tags merged: X - Tags deleted: Y - Names updated: Z - Errors: [list] ## Environment Variables Required - `SITE_BASE_URL`: https://your-site.com - `WEBHOOK_API_KEY`: API key for authentication ## Chunking Strategy (for >200 tags) 1. Split tags into batches of 100 2. Process each batch sequentially 3. Aggregate results before final notification ## Error Handling - Retry HTTP requests 3x with exponential backoff - On AI parse failure: skip and alert - On maintenance failure: log error, continue with notification ## Testing 1. Dry run: Comment out HTTP POST node, check AI output only 2. Real run: Enable all nodes, monitor /api/tags count before/after