{
  "id": "aicp-drive-sync",
  "name": "Drive \u2192 workspace sync (scrubbed)",
  "meta": {
    "description": "Pulls changed files from a scope's Google Drive folder, runs them through the PII scrubber, and lands tokenized text in workspace/scopes/<chain>/ingest/. Attach your Google Drive OAuth2 credential to the two HTTP nodes. Set FOLDER_ID + scope in the Config node. Activate for the schedule + last-sync memory to work."
  },
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "minutes",
              "minutesInterval": 15
            }
          ]
        }
      },
      "id": "eeeeeeee-0000-0000-0000-000000000001",
      "name": "Every 15 min",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        220,
        300
      ]
    },
    {
      "parameters": {
        "jsCode": "// CONFIG: which Drive folder feeds which scope. One folder->one scope per\n// workflow; duplicate the workflow per mapping (keeps failures isolated).\nconst FOLDER_ID = $env.DRIVE_FOLDER_ID;\nif (!FOLDER_ID) { throw new Error('DRIVE_FOLDER_ID is not set in the n8n environment. Set it in .env and recreate the n8n container. Refusing to run against an unset Drive folder rather than querying a bogus id.'); }\nconst SCOPE = 'data-team';\n// Last-sync memory (persists on ACTIVE schedule runs; manual runs re-sync 7d).\nconst sd = $getWorkflowStaticData('global');\nconst since = sd.lastSync || new Date(Date.now() - 7*24*3600*1000).toISOString();\nreturn [{ json: {\n  scope: SCOPE,\n  q: `'${FOLDER_ID}' in parents and trashed=false and modifiedTime > '${since}'`,\n  runStartedAt: new Date().toISOString()\n} }];"
      },
      "id": "eeeeeeee-0000-0000-0000-000000000002",
      "name": "Config + last sync",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        420,
        300
      ]
    },
    {
      "parameters": {
        "url": "https://www.googleapis.com/drive/v3/files",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "googleDriveOAuth2Api",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "q",
              "value": "={{ $json.q }}"
            },
            {
              "name": "fields",
              "value": "files(id,name,mimeType,modifiedTime)"
            },
            {
              "name": "pageSize",
              "value": "100"
            },
            {
              "name": "supportsAllDrives",
              "value": "true"
            },
            {
              "name": "includeItemsFromAllDrives",
              "value": "true"
            }
          ]
        },
        "options": {}
      },
      "id": "eeeeeeee-0000-0000-0000-000000000003",
      "name": "List changed files",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        620,
        300
      ]
    },
    {
      "parameters": {
        "jsCode": "// One item per file, with the right download strategy per mime type.\n// Google-native docs export as text; plain text/markdown/csv download raw.\n// Everything else (images, video, binaries) is skipped EXPLICITLY \u2014 no silent gaps.\nconst cfg = $('Config + last sync').first().json;\nconst files = $input.first().json.files || [];\nconst out = [], skipped = [];\nfor (const f of files) {\n  if (f.mimeType === 'application/vnd.google-apps.document') {\n    out.push({ json: { ...cfg, ...f, url: `https://www.googleapis.com/drive/v3/files/${f.id}/export`, exportMime: 'text/plain' } });\n  } else if (/^text\\/|^application\\/(json|xml)/.test(f.mimeType)) {\n    out.push({ json: { ...cfg, ...f, url: `https://www.googleapis.com/drive/v3/files/${f.id}`, exportMime: null } });\n  } else {\n    skipped.push(`${f.name} (${f.mimeType})`);\n  }\n}\nif (!out.length) return [{ json: { ...cfg, empty: true, skipped } }];\nout[0].json.skipped = skipped;\nreturn out;"
      },
      "id": "eeeeeeee-0000-0000-0000-000000000004",
      "name": "Plan downloads",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        820,
        300
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true
          },
          "conditions": [
            {
              "leftValue": "={{ $json.empty }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "notEquals"
              }
            }
          ]
        },
        "options": {}
      },
      "id": "eeeeeeee-0000-0000-0000-000000000005",
      "name": "Has files?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1020,
        300
      ]
    },
    {
      "parameters": {
        "url": "={{ $json.url }}",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "googleDriveOAuth2Api",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "mimeType",
              "value": "={{ $json.exportMime }}"
            },
            {
              "name": "alt",
              "value": "={{ $json.exportMime ? undefined : 'media' }}"
            },
            {
              "name": "supportsAllDrives",
              "value": "true"
            }
          ]
        },
        "options": {
          "response": {
            "response": {
              "responseFormat": "text",
              "outputPropertyName": "content"
            }
          }
        }
      },
      "id": "eeeeeeee-0000-0000-0000-000000000006",
      "name": "Download text",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1220,
        200
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "http://scrubber:8080/ingest",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "x-scrubber-token",
              "value": "change-me-to-a-long-random-secret"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ { \"scope\": $json.scope, \"filename\": $json.name, \"text\": $json.content } }}",
        "options": {}
      },
      "id": "eeeeeeee-0000-0000-0000-000000000007",
      "name": "Scrub + ingest",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1420,
        200
      ]
    },
    {
      "parameters": {
        "jsCode": "// Persist the sync watermark and produce a summary (feed into Notify if desired).\nconst sd = $getWorkflowStaticData('global');\nconst cfg = $('Config + last sync').first().json;\nsd.lastSync = cfg.runStartedAt;\nconst items = $input.all().map(i => i.json);\nconst files = items.filter(i => i.written);\nconst findings = {};\nfor (const f of files) for (const [k,v] of Object.entries(f.findings||{})) findings[k]=(findings[k]||0)+v;\nreturn [{ json: {\n  severity: 'info',\n  title: `Drive sync: ${files.length} file(s) \u2192 scope ${cfg.scope}`,\n  message: `Ingested (tokenized): ${files.map(f=>f.written).join(', ') || 'none'}\\nPII tokenized: ${JSON.stringify(findings)}`,\n  source: 'drive-sync'\n} }];"
      },
      "id": "eeeeeeee-0000-0000-0000-000000000008",
      "name": "Watermark + summary",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1620,
        200
      ]
    },
    {
      "parameters": {
        "source": "database",
        "workflowId": {
          "__rl": true,
          "value": "aicp-notify",
          "mode": "id"
        },
        "options": {
          "waitForSubWorkflow": true
        },
        "workflowInputs": {
          "mappingMode": "autoMapInputData",
          "value": {},
          "matchingColumns": [],
          "schema": [
            {
              "id": "severity",
              "displayName": "severity",
              "type": "string",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "canBeUsedToMatch": true
            },
            {
              "id": "title",
              "displayName": "title",
              "type": "string",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "canBeUsedToMatch": true
            },
            {
              "id": "message",
              "displayName": "message",
              "type": "string",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "canBeUsedToMatch": true
            },
            {
              "id": "source",
              "displayName": "source",
              "type": "string",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "canBeUsedToMatch": true
            }
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": true
        }
      },
      "id": "n0t1fy00-0000-0000-0000-303362112884",
      "name": "Notify",
      "type": "n8n-nodes-base.executeWorkflow",
      "typeVersion": 1.2,
      "position": [
        1840,
        200
      ]
    },
    {
      "id": "ondemand",
      "name": "Run on demand",
      "type": "n8n-nodes-base.executeWorkflowTrigger",
      "typeVersion": 1.1,
      "position": [
        220,
        460
      ],
      "parameters": {
        "inputSource": "passthrough"
      }
    }
  ],
  "connections": {
    "Every 15 min": {
      "main": [
        [
          {
            "node": "Config + last sync",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Config + last sync": {
      "main": [
        [
          {
            "node": "List changed files",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "List changed files": {
      "main": [
        [
          {
            "node": "Plan downloads",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Plan downloads": {
      "main": [
        [
          {
            "node": "Has files?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Has files?": {
      "main": [
        [
          {
            "node": "Download text",
            "type": "main",
            "index": 0
          }
        ],
        []
      ]
    },
    "Download text": {
      "main": [
        [
          {
            "node": "Scrub + ingest",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Scrub + ingest": {
      "main": [
        [
          {
            "node": "Watermark + summary",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Watermark + summary": {
      "main": [
        [
          {
            "node": "Notify",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Run on demand": {
      "main": [
        [
          {
            "node": "Config + last sync",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "errorWorkflow": "aicp-error-trigger"
  },
  "active": false
}