{
  "name": "Cloudinary — Asset Governance & Audit (search → flag → bulk retag)",
  "nodes": [
    {
      "parameters": {},
      "id": "55550000-0000-4a01-9001-000000000001",
      "name": "Start",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        240,
        320
      ]
    },
    {
      "parameters": {
        "resource": "asset",
        "operation": "search",
        "searchExpression": "tags=n8n-blog",
        "searchResourceTypes": [
          "image",
          "video"
        ],
        "searchReturnAll": true,
        "searchAdditionalFields": {
          "sortField": "bytes",
          "sortDirection": "desc",
          "with_field": [
            "tags"
          ]
        }
      },
      "id": "55550000-0000-4a01-9001-000000000002",
      "name": "Search Library",
      "type": "n8n-nodes-cloudinary.cloudinary",
      "typeVersion": 1,
      "position": [
        520,
        320
      ]
    },
    {
      "parameters": {
        "mode": "runOnceForAllItems",
        "language": "javaScript",
        "jsCode": "// Governance rules. Flag any asset that is untagged, or larger than the budget.\nconst MAX_BYTES = 1_000_000; // 1 MB budget for delivery-ready assets\nreturn $input.all().map((item) => {\n  const a = item.json;\n  const tags = Array.isArray(a.tags) ? a.tags : [];\n  const reasons = [];\n  if (tags.length === 0) reasons.push('untagged');\n  if ((a.bytes || 0) > MAX_BYTES) reasons.push(`oversized (${Math.round((a.bytes||0)/1024)} KB)`);\n  return { json: {\n    public_id: a.public_id,\n    resource_type: a.resource_type,\n    bytes: a.bytes,\n    tags,\n    flagged: reasons.length > 0,\n    reasons: reasons.join(', '),\n  }};\n});"
      },
      "id": "55550000-0000-4a01-9001-000000000003",
      "name": "Apply Governance Rules",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        820,
        320
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "is-flagged",
              "leftValue": "={{ $json.flagged }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ]
        },
        "options": {}
      },
      "id": "55550000-0000-4a01-9001-000000000004",
      "name": "Flagged?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1120,
        320
      ]
    },
    {
      "parameters": {
        "resource": "asset",
        "operation": "updateTags",
        "tagMode": "append",
        "tagAppendPublicIds": "={{ $json.public_id }}",
        "tagAppendResourceType": "={{ $json.resource_type }}",
        "tagAppendType": "upload",
        "tags": "needs-review"
      },
      "id": "55550000-0000-4a01-9001-000000000005",
      "name": "Tag: needs-review",
      "type": "n8n-nodes-cloudinary.cloudinary",
      "typeVersion": 1,
      "position": [
        1420,
        220
      ]
    },
    {
      "parameters": {
        "mode": "runOnceForAllItems",
        "language": "javaScript",
        "jsCode": "// Build an audit report of everything that was scanned.\nconst rows = $('Apply Governance Rules').all().map(i => i.json);\nconst flagged = rows.filter(r => r.flagged);\nreturn [{ json: {\n  scanned: rows.length,\n  flagged: flagged.length,\n  clean: rows.length - flagged.length,\n  details: flagged.map(r => ({ public_id: r.public_id, reasons: r.reasons, bytes: r.bytes })),\n}}];"
      },
      "id": "55550000-0000-4a01-9001-000000000006",
      "name": "Audit Report",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1420,
        440
      ]
    },
    {
      "parameters": {
        "content": "## 🗂️ Asset Governance & Audit\n\n**Keep a sprawling media library clean — automatically.** Find assets that break your rules, flag them for review, and report on the whole library, at any scale.\n\n### What it does\n1. **Search Library** uses **Asset → Search Assets** with **Return All** on, so it paginates through *every* matching asset (here, scoped to `tags=n8n-blog`). It emits **one n8n item per asset**, and pulls each asset's **tags** (via *With Field → Tags*).\n2. **Apply Governance Rules** flags anything **untagged** or **over 1 MB** — edit the rules to match your policy.\n3. **Flagged?** splits the stream: flagged assets go one way, clean ones the other.\n4. **Tag: needs-review** appends a `needs-review` tag to each flagged asset (**Append** mode preserves existing tags). A teammate can now filter on it in the Media Library.\n5. **Audit Report** summarizes `{scanned, flagged, clean, details}` for a Slack/email digest.\n\n### Scale it up\nPoint the search at your whole library (`folder:products/*`, `resource_type:video`, `uploaded_at>30d`, …). *Return All* handles pagination and rate limits for you — this works the same on 50 assets or 50,000.\n\n### Make it yours\n- **Your rules:** edit *Apply Governance Rules* (missing alt text, wrong folder, stale upload date, missing metadata field…).\n- **Other actions:** instead of tagging, branch into **Asset → Update Structured Metadata**, or — carefully — **Asset → Delete Assets** for stale content. (Test your search expression first; deletes are permanent.)\n- **Report:** wire *Audit Report* into Slack, Gmail, or a Google Sheet.\n\n> 💡 Newly-uploaded assets can take a few seconds to appear in Search (eventual consistency). If you run this right after an upload workflow, add a short **Wait**.\n\n### Setup\n👉 Add your **Cloudinary API** credential to the Cloudinary nodes (Cloud name + API Key + Secret from the [Console](https://console.cloudinary.com/settings/api-keys)). Not included in this template.\n\n💾 **Then press `Cmd/Ctrl+S` to save before running.** That one save persists the credential to *every* Cloudinary node at once, so a full **Execute Workflow** run finds credentials everywhere. Skip the save and the run can fail node-after-node with *\"Node does not have any credentials set\"* — even though the credential looks selected. (If **Execute step** works but **Execute Workflow** doesn't, you forgot the save.)",
        "height": 1280,
        "width": 600
      },
      "id": "55550000-0000-4a01-9001-000000000007",
      "name": "README",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        200,
        560
      ]
    }
  ],
  "connections": {
    "Start": {
      "main": [
        [
          {
            "node": "Search Library",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Search Library": {
      "main": [
        [
          {
            "node": "Apply Governance Rules",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Apply Governance Rules": {
      "main": [
        [
          {
            "node": "Flagged?",
            "type": "main",
            "index": 0
          },
          {
            "node": "Audit Report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Flagged?": {
      "main": [
        [
          {
            "node": "Tag: needs-review",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "pinData": {},
  "meta": {}
}
