{
  "name": "Logixb2b — Sync CRM → hojas de cálculo",
  "meta": {
    "templateCredsSetupCompleted": false,
    "description": "Cada hora: GET CRM → append/update Sheets (clave email). Si falla → Slack. Unidireccional CRM→Sheets."
  },
  "settings": {
    "executionOrder": "v1",
    "timezone": "Europe/Madrid"
  },
  "active": false,
  "pinData": {},
  "tags": [
    {
      "name": "Logixb2b"
    },
    {
      "name": "recursos"
    },
    {
      "name": "crm"
    },
    {
      "name": "sheets"
    }
  ],
  "nodes": [
    {
      "id": "sticky-instrucciones-logix",
      "name": "INSTRUCCIONES (conectar + VPS)",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -480,
        40
      ],
      "parameters": {
        "content": "## INSTRUCCIONES — Sync CRM → hojas de cálculo\n\n**Recurso:** `/recursos/sync-crm-hojas-calculo`\n**Guía:** /sincronizar-leads-crm-hojas-calculo\n**Zona:** Europe/Madrid · `active: false` al importar (tú lo activas).\n\n### 1) Importar\n1. n8n → ⋯ → **Import from File** → este JSON.\n2. El flujo llega **inactive**. No pegues secretos en el lienzo ni en el archivo.\n\n### 2) Credenciales y configuración por nodo\n- **Cada hora** (`schedule`): sin credencial. Revisa expresión cron / intervalo y zona **Europe/Madrid** (settings del workflow). Solo dispara si n8n está **Active** 24/7.\n- **Config sync** (`Set` Config): **no lleva credencial**. Aquí van URLs/placeholders (`REEMPLAZA_*`, `example.com`, `TU-CRM`). Sustituye todos antes de Active.\n- **GET contactos CRM** (`HTTP Request` CRM): Credentials → **Header Auth** / OAuth del CRM. Sustituye `crm*Url` en el nodo **Config**. Prueba con un Lead de sandbox.\n- **Unir respuesta + config** (`Set` Config): **no lleva credencial**. Aquí van URLs/placeholders (`REEMPLAZA_*`, `example.com`, `TU-CRM`). Sustituye todos antes de Active.\n- **¿CRM OK?** (`IF`): sin credencial. Comprueba la condición (email, umbral, over_limit, etc.) con datos reales de prueba.\n- **Mapear filas (email clave)** (`Set`): sin credencial. Revisa expresiones `={{ }}` y campos de salida.\n- **HTTP Sheets append/update** (`HTTP Request` Sheets): mejor migrar a nodo Google Sheets; si dejas HTTP, usa OAuth2 Bearer de Google.\n- **Estado escritura Sheets** (`Set`): sin credencial. Revisa expresiones `={{ }}` y campos de salida.\n- **¿Sheets OK?** (`IF`): sin credencial. Comprueba la condición (email, umbral, over_limit, etc.) con datos reales de prueba.\n- **Slack error Sheets** (`HTTP Request` → Slack/Teams): crea Incoming Webhook en Slack (o Adaptive Card URL en Teams). Pega la URL en **Config** o en este nodo. Auth: none (la URL ya es secreta).\n- **Slack error CRM** (`HTTP Request` → Slack/Teams): crea Incoming Webhook en Slack (o Adaptive Card URL en Teams). Pega la URL en **Config** o en este nodo. Auth: none (la URL ya es secreta).\n\n### 3) Orden recomendado de puesta en marcha\n1. Rellena el nodo **Config** (URLs reales, umbrales, webhooks de Slack).\n2. Crea Credentials en n8n (candado de cada nodo HTTP / Sheets / OpenAI) y **enlázalas**.\n3. Ejecuta **una vez** (Manual / Test URL del Webhook / \"Execute workflow\").\n4. Verifica CRM + Slack/Sheets (y que la IA no escriba en producción sin HITL).\n5. Solo entonces: interruptor **Active**.\n\n### 4) Producción 24/7 en VPS (UE)\nImportar en localhost no basta: webhooks y cron necesitan n8n encendido.\n\n1. VPS en UE (Hetzner/OVH/IONOS…) + Docker Compose + **Postgres** + HTTPS (Caddy/Nginx).\n2. Variables mínimas: `GENERIC_TIMEZONE=Europe/Madrid`, `WEBHOOK_URL=https://n8n.tudominio.com/`, `N8N_ENCRYPTION_KEY`, `N8N_HOST` / `N8N_PROTOCOL=https`.\n3. Firewall: solo 80/443 (+ SSH). Backups del volumen Postgres + export de workflows.\n4. Importa este JSON en la instancia de producción, vuelve a crear Credentials (no se copian solas), actualiza URLs del Config al dominio real.\n5. Pon **Active**. Prueba el Webhook de producción o espera el próximo cron.\n6. Alternativa: **n8n Cloud** (mismo JSON; Credentials en la UI Cloud).\n\n### 5) RGPD + EU AI Act (resumen)\nSi hay nodos IA sois **deployers** (Reglamento UE 2024/1689). Art. 4 alfabetización. **HITL** antes de outreach, scores masivos o pausas de cuenta. Minimiza PII en Slack/logs. Informativo — no es asesoramiento legal. /uso-responsable-de-ia-y-transparencia\n",
        "height": 1500,
        "width": 580,
        "color": 4
      }
    },
    {
      "id": "sched-hourly",
      "name": "Cada hora",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        0,
        560
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "hours",
              "hoursInterval": 1
            }
          ]
        }
      }
    },
    {
      "id": "set-config",
      "name": "Config sync",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        300,
        560
      ],
      "parameters": {
        "mode": "manual",
        "duplicateItem": false,
        "assignments": {
          "assignments": [
            {
              "id": "c1",
              "name": "crmContactsUrl",
              "value": "https://TU-CRM.ejemplo/api/contacts?limit=100",
              "type": "string"
            },
            {
              "id": "c2",
              "name": "sheetsWriteUrl",
              "value": "https://sheets.googleapis.com/v4/spreadsheets/TU_SHEET_ID/values/Leads!A2:append?valueInputOption=USER_ENTERED",
              "type": "string"
            },
            {
              "id": "c3",
              "name": "slackWebhookUrl",
              "value": "https://example.com/aviso-equipo-slack",
              "type": "string"
            },
            {
              "id": "c4",
              "name": "matchKey",
              "value": "email",
              "type": "string"
            },
            {
              "id": "c5",
              "name": "syncedAt",
              "value": "={{ $now.toISO() }}",
              "type": "string"
            }
          ]
        }
      }
    },
    {
      "id": "http-crm",
      "name": "GET contactos CRM",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        600,
        560
      ],
      "parameters": {
        "method": "GET",
        "url": "={{ $json.crmContactsUrl }}",
        "options": {
          "timeout": 30000,
          "response": {
            "response": {
              "fullResponse": true,
              "neverError": true
            }
          }
        }
      }
    },
    {
      "id": "set-status",
      "name": "Unir respuesta + config",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        900,
        560
      ],
      "parameters": {
        "mode": "manual",
        "duplicateItem": false,
        "assignments": {
          "assignments": [
            {
              "id": "s1",
              "name": "statusCode",
              "value": "={{ $json.statusCode }}",
              "type": "number"
            },
            {
              "id": "s2",
              "name": "crmOk",
              "value": "={{ Number($json.statusCode) >= 200 && Number($json.statusCode) < 300 }}",
              "type": "boolean"
            },
            {
              "id": "s3",
              "name": "body",
              "value": "={{ $json.body }}",
              "type": "string"
            },
            {
              "id": "s4",
              "name": "sheetsWriteUrl",
              "value": "={{ $('Config sync').item.json.sheetsWriteUrl }}",
              "type": "string"
            },
            {
              "id": "s5",
              "name": "slackWebhookUrl",
              "value": "={{ $('Config sync').item.json.slackWebhookUrl }}",
              "type": "string"
            },
            {
              "id": "s6",
              "name": "matchKey",
              "value": "={{ $('Config sync').item.json.matchKey }}",
              "type": "string"
            },
            {
              "id": "s7",
              "name": "syncedAt",
              "value": "={{ $('Config sync').item.json.syncedAt }}",
              "type": "string"
            }
          ]
        }
      }
    },
    {
      "id": "if-crm-ok",
      "name": "¿CRM OK?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1200,
        560
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "if-crm-ok-cond",
              "leftValue": "={{ $json.crmOk }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        }
      }
    },
    {
      "id": "set-rows",
      "name": "Mapear filas (email clave)",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        1500,
        300
      ],
      "parameters": {
        "mode": "manual",
        "duplicateItem": false,
        "assignments": {
          "assignments": [
            {
              "id": "r1",
              "name": "rows",
              "value": "={{ (() => { const b = $json.body; const list = Array.isArray(b) ? b : (b.contacts || b.data || b.results || []); return list.map(c => [ c.email || '', c.name || c.fullname || '', c.company || '', $('Config sync').item.json.syncedAt ]); })() }}",
              "type": "string"
            },
            {
              "id": "r2",
              "name": "rowCount",
              "value": "={{ (() => { const b = $json.body; const list = Array.isArray(b) ? b : (b.contacts || b.data || b.results || []); return list.length; })() }}",
              "type": "number"
            },
            {
              "id": "r3",
              "name": "sheetsWriteUrl",
              "value": "={{ $json.sheetsWriteUrl }}",
              "type": "string"
            },
            {
              "id": "r4",
              "name": "slackWebhookUrl",
              "value": "={{ $json.slackWebhookUrl }}",
              "type": "string"
            },
            {
              "id": "r5",
              "name": "syncedAt",
              "value": "={{ $json.syncedAt }}",
              "type": "string"
            }
          ]
        }
      }
    },
    {
      "id": "http-sheets",
      "name": "HTTP Sheets append/update",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1800,
        300
      ],
      "parameters": {
        "method": "POST",
        "url": "={{ $json.sheetsWriteUrl }}",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ values: $json.rows }) }}",
        "options": {
          "timeout": 30000,
          "response": {
            "response": {
              "fullResponse": true,
              "neverError": true
            }
          }
        }
      }
    },
    {
      "id": "set-sheet-status",
      "name": "Estado escritura Sheets",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        2100,
        300
      ],
      "parameters": {
        "mode": "manual",
        "duplicateItem": false,
        "assignments": {
          "assignments": [
            {
              "id": "t1",
              "name": "statusCode",
              "value": "={{ $json.statusCode }}",
              "type": "number"
            },
            {
              "id": "t2",
              "name": "sheetsOk",
              "value": "={{ Number($json.statusCode) >= 200 && Number($json.statusCode) < 300 }}",
              "type": "boolean"
            },
            {
              "id": "t3",
              "name": "rowCount",
              "value": "={{ $('Mapear filas (email clave)').item.json.rowCount }}",
              "type": "number"
            },
            {
              "id": "t4",
              "name": "slackWebhookUrl",
              "value": "={{ $('Mapear filas (email clave)').item.json.slackWebhookUrl }}",
              "type": "string"
            },
            {
              "id": "t5",
              "name": "syncedAt",
              "value": "={{ $('Mapear filas (email clave)').item.json.syncedAt }}",
              "type": "string"
            }
          ]
        }
      }
    },
    {
      "id": "if-sheets-ok",
      "name": "¿Sheets OK?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        2400,
        300
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "if-sheets-ok-cond",
              "leftValue": "={{ $json.sheetsOk }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        }
      }
    },
    {
      "id": "noop-ok",
      "name": "Sync OK",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [
        2700,
        40
      ],
      "parameters": {}
    },
    {
      "id": "http-slack-sheets",
      "name": "Slack error Sheets",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2700,
        560
      ],
      "parameters": {
        "method": "POST",
        "url": "={{ $json.slackWebhookUrl }}",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ text: ':warning: *Sync CRM→Sheets falló (escritura)*\\nHTTP: ' + $json.statusCode + '\\nFilas intentadas: ' + $json.rowCount + '\\nHora: ' + $json.syncedAt }) }}",
        "options": {
          "timeout": 10000
        }
      }
    },
    {
      "id": "http-slack-crm",
      "name": "Slack error CRM",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1500,
        820
      ],
      "parameters": {
        "method": "POST",
        "url": "={{ $json.slackWebhookUrl }}",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ text: ':warning: *Sync CRM→Sheets falló (lectura CRM)*\\nHTTP: ' + $json.statusCode + '\\nHora: ' + $json.syncedAt + '\\nClave matching: email' }) }}",
        "options": {
          "timeout": 10000
        }
      }
    }
  ],
  "connections": {
    "Cada hora": {
      "main": [
        [
          {
            "node": "Config sync",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Config sync": {
      "main": [
        [
          {
            "node": "GET contactos CRM",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "GET contactos CRM": {
      "main": [
        [
          {
            "node": "Unir respuesta + config",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Unir respuesta + config": {
      "main": [
        [
          {
            "node": "¿CRM OK?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "¿CRM OK?": {
      "main": [
        [
          {
            "node": "Mapear filas (email clave)",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Slack error CRM",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Mapear filas (email clave)": {
      "main": [
        [
          {
            "node": "HTTP Sheets append/update",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "HTTP Sheets append/update": {
      "main": [
        [
          {
            "node": "Estado escritura Sheets",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Estado escritura Sheets": {
      "main": [
        [
          {
            "node": "¿Sheets OK?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "¿Sheets OK?": {
      "main": [
        [
          {
            "node": "Sync OK",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Slack error Sheets",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
