{
  "id": "MQ8sMd0nIn7Zq3KZ",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "Zone2_run_train_v2",
  "tags": [],
  "nodes": [
    {
      "id": "8204e5bc-af06-44b6-a9bc-75050d378d56",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2800,
        -208
      ],
      "parameters": {
        "width": 480,
        "height": 896,
        "content": "## Zone2_run_train_v2\n\n### How it works\n\nThis workflow runs once every 24 hours to pull a runner's activities from Strava, process each activity, and update a Google Sheet. It calculates pace, routes activities through classification branches, merges the prepared data, and writes updates back to the spreadsheet before continuing to the next activity.\n\n### Setup steps\n\n- Configure the Schedule Trigger interval if the workflow should run more or less often than every 24 hours.\n- Add valid Strava API authentication to the HTTP request node, including an access token with permission to read athlete activities.\n- Configure the Google Sheets credentials for both the row lookup and row update nodes.\n- Verify the target spreadsheet, sheet name, lookup columns, and update columns match the fields produced by the code nodes.\n- Review the custom code nodes to ensure pace calculation, race classification, and dispatch logic match the structure of the Strava response and spreadsheet.\n\n### Customization\n\nYou can adjust the Switch rules to change which activities are processed, modify the cleaning/classification code for different training categories, or update the schedule and Strava request parameters to control the activity lookback window."
      },
      "typeVersion": 1
    },
    {
      "id": "410eea2c-2ad2-417b-8576-beb8c68ac795",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2240,
        208
      ],
      "parameters": {
        "color": 7,
        "width": 640,
        "height": 320,
        "content": "## Schedule and fetch activities\n\nStarts the workflow every 24 hours, retrieves the runner's recent activities from the Strava API, and feeds the returned activities into a batch iterator for one-by-one processing."
      },
      "typeVersion": 1
    },
    {
      "id": "7ff397df-d5c9-447b-b52e-22c104b30549",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1568,
        16
      ],
      "parameters": {
        "color": 7,
        "width": 416,
        "height": 320,
        "content": "## Calculate and route activity\n\nCalculates pace in minutes per kilometer for each activity and uses a switch to route the item into the appropriate processing branch."
      },
      "typeVersion": 1
    },
    {
      "id": "2efce1a0-b100-46ea-b40c-1454faba9928",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1120,
        -208
      ],
      "parameters": {
        "color": 7,
        "width": 416,
        "height": 720,
        "content": "## Prepare sheet data\n\nHandles the central branching cluster: it can read existing Google Sheets rows, clean and classify normal race data, clean and classify test race data, then merge those outputs into a single stream for downstream handling."
      },
      "typeVersion": 1
    },
    {
      "id": "f2217029-4772-4f2a-84ae-c3df9315f21d",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -672,
        0
      ],
      "parameters": {
        "color": 7,
        "width": 416,
        "height": 528,
        "content": "## Dispatch and update sheet\n\nDecides how the merged activity data should be written and updates the corresponding row in Google Sheets, then loops back to the iterator to continue with the next activity batch."
      },
      "typeVersion": 1
    },
    {
      "id": "0d3b59ec-3d13-471f-9870-72c686cf5d4a",
      "name": "Calculate Pace Min per Km",
      "type": "n8n-nodes-base.code",
      "position": [
        -1520,
        176
      ],
      "parameters": {
        "jsCode": "const item = $input.first().json;\n\nconst paceDecimal = (item.moving_time / 60) / (item.distance / 1000);\n\nreturn [{\n  json: {\n    ...item,\n    pace_min_km: paceDecimal,  // ej: 6.44\n    strava_id: item.id_str\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "3da998b9-4e8e-4643-8106-50a1305712f1",
      "name": "Fetch Runner Activities",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -1968,
        368
      ],
      "parameters": {
        "url": "https://www.strava.com/api/v3/athlete/activities",
        "options": {},
        "sendQuery": true,
        "authentication": "predefinedCredentialType",
        "queryParameters": {
          "parameters": [
            {}
          ]
        },
        "nodeCredentialType": "stravaOAuth2Api"
      },
      "credentials": {
        "stravaOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.4
    },
    {
      "id": "f05554a7-d0ce-42f3-a671-13d683e2f929",
      "name": "Loop Over Activities",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [
        -1744,
        368
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 3
    },
    {
      "id": "6b17c8f2-7710-4a9c-acaf-80c1dafb08f9",
      "name": "Update Row in Sheet",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        -400,
        368
      ],
      "parameters": {
        "columns": {
          "value": {
            "row_number": 0
          },
          "schema": [
            {
              "id": "CICLO",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "CICLO",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Tirada 1",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Tirada 1",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Tirada 2",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Tirada 2",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Tirada 3",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Tirada 3",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Tirada 4",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Tirada 4",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Tirada 5",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Tirada 5",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Test 5k",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Test 5k",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "row_number",
              "type": "number",
              "display": true,
              "removed": false,
              "readOnly": true,
              "required": false,
              "displayName": "row_number",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "autoMapInputData",
          "matchingColumns": [
            "row_number"
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {
          "locationDefine": {
            "values": {}
          }
        },
        "operation": "update",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "gid=0",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/10AEB5bqyrd8ocz-I_A5RBKhIaxe9-B3-nfpvT4MmiSY/edit#gid=0",
          "cachedResultName": "Hoja 1"
        },
        "documentId": {
          "__rl": true,
          "mode": "url",
          "value": "https://docs.google.com/spreadsheets/d/10AEB5bqyrd8ocz-I_A5RBKhIaxe9-B3-nfpvT4MmiSY/edit?gid=0#gid=0"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "000e8157-1b13-433f-a41a-eea222a58d1e",
      "name": "Route by Custom Rules",
      "type": "n8n-nodes-base.switch",
      "position": [
        -1296,
        176
      ],
      "parameters": {
        "rules": {
          "values": [
            {
              "outputKey": "Zona 2",
              "conditions": {
                "options": {
                  "version": 3,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "b79d2801-9c7a-4260-b6da-65e36841480f",
                    "operator": {
                      "type": "boolean",
                      "operation": "true",
                      "singleValue": true
                    },
                    "leftValue": "={{ $json.sport_type == \"Run\" && $json.elapsed_time > 4800 && 6.3 <= $json.pace_min_km <= 7  }}",
                    "rightValue": 4800
                  }
                ]
              },
              "renameOutput": true
            },
            {
              "outputKey": "Test Run",
              "conditions": {
                "options": {
                  "version": 3,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "3284ba6c-5a9f-4ef5-91c0-16357d8e8053",
                    "operator": {
                      "type": "boolean",
                      "operation": "true",
                      "singleValue": true
                    },
                    "leftValue": "={{ $json.sport_type == \"Run\" && (4000 <= $json.distance <= 8000) && ($json.pace_min_km <= 6)}}",
                    "rightValue": ""
                  }
                ]
              },
              "renameOutput": true
            }
          ]
        },
        "options": {}
      },
      "typeVersion": 3.4
    },
    {
      "id": "fa1c6446-b629-4224-a174-35adfb08086e",
      "name": "Clean and Classify Race Data",
      "type": "n8n-nodes-base.code",
      "position": [
        -1072,
        176
      ],
      "parameters": {
        "jsCode": "const item = $input.first().json;\n\n// Calcular pace formateado\nconst paceDecimal = (item.moving_time / 60) / (item.distance / 1000);\nconst paceMin = Math.floor(paceDecimal);\nconst paceSeg = Math.round((paceDecimal - paceMin) * 60);\nconst ritmo = `${paceMin}:${String(paceSeg).padStart(2, '0')} min/km`;\n\n// Fecha formateada\nconst fecha = new Date(item.start_date_local);\nconst dia  = String(fecha.getDate()).padStart(2, '0');\nconst mes  = String(fecha.getMonth() + 1).padStart(2, '0');\nconst anio = fecha.getFullYear();\nconst fechaFormateada = `${dia}-${mes}-${anio}`;\n\n// Tiempo formateado\nconst totalSeg = item.moving_time;\nconst horas    = Math.floor(totalSeg / 3600);\nconst minutos  = Math.floor((totalSeg % 3600) / 60);\nconst segundos = totalSeg % 60;\nconst tiempo   = `${String(horas).padStart(2,'0')}:${String(minutos).padStart(2,'0')}:${String(segundos).padStart(2,'0')}`;\n\n// Distancia formateada\nconst distancia = `${(item.distance / 1000).toFixed(2)} km`;\n\n// ID de la actividad\nconst strava_id = item.id\n\n// Tipo\nconst tipo = \"ZONA 2\"\n\nreturn [{\n  json: {\n    strava_id: strava_id,\n    tipo: tipo,\n    fecha: fechaFormateada,\n    ritmo: ritmo,\n    tiempo: tiempo,\n    distancia: distancia\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "c41623e2-f859-4670-8be9-f0b69928b17b",
      "name": "Clean and Classify Race Test Data",
      "type": "n8n-nodes-base.code",
      "position": [
        -1072,
        352
      ],
      "parameters": {
        "jsCode": "const item = $input.first().json;\n\n// Calcular pace formateado\nconst paceDecimal = (item.moving_time / 60) / (item.distance / 1000);\nconst paceMin = Math.floor(paceDecimal);\nconst paceSeg = Math.round((paceDecimal - paceMin) * 60);\nconst ritmo = `${paceMin}:${String(paceSeg).padStart(2, '0')} min/km`;\n\n// Fecha formateada\nconst fecha = new Date(item.start_date_local);\nconst dia  = String(fecha.getDate()).padStart(2, '0');\nconst mes  = String(fecha.getMonth() + 1).padStart(2, '0');\nconst anio = fecha.getFullYear();\nconst fechaFormateada = `${dia}-${mes}-${anio}`;\n\n// Tiempo formateado\nconst totalSeg = item.moving_time;\nconst horas    = Math.floor(totalSeg / 3600);\nconst minutos  = Math.floor((totalSeg % 3600) / 60);\nconst segundos = totalSeg % 60;\nconst tiempo   = `${String(horas).padStart(2,'0')}:${String(minutos).padStart(2,'0')}:${String(segundos).padStart(2,'0')}`;\n\n// Distancia formateada\nconst distancia = `${(item.distance / 1000).toFixed(2)} km`;\n\n// ID Actividad\nconst strava_id = item.id\n\n// Tipo\nconst tipo = \"TEST 5KM\"\n\nreturn [{\n  json: {\n    strava_id: strava_id,\n    tipo: tipo,\n    fecha: fechaFormateada,\n    ritmo: ritmo,\n    tiempo: tiempo,\n    distancia: distancia\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "0fbfa0f7-2119-4b43-82c7-a296db150b21",
      "name": "Merge Race Results",
      "type": "n8n-nodes-base.merge",
      "position": [
        -848,
        160
      ],
      "parameters": {
        "numberInputs": 3
      },
      "typeVersion": 3.2
    },
    {
      "id": "63eef49f-941b-4f9f-abb5-aa38b2e962d5",
      "name": "Daily Schedule Trigger",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        -2192,
        368
      ],
      "parameters": {
        "rule": {
          "interval": [
            {}
          ]
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "fd2ec6cd-dfb9-4fb1-93eb-4cde41e9e0f4",
      "name": "Read Rows from Sheet",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        -1072,
        0
      ],
      "parameters": {
        "options": {
          "outputFormatting": {
            "values": {
              "date": "FORMATTED_STRING",
              "general": "UNFORMATTED_VALUE"
            }
          },
          "dataLocationOnSheet": {
            "values": {
              "range": "B1:I14",
              "rangeDefinition": "specifyRangeA1"
            }
          }
        },
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "gid=0",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/10AEB5bqyrd8ocz-I_A5RBKhIaxe9-B3-nfpvT4MmiSY/edit#gid=0",
          "cachedResultName": "Hoja 1"
        },
        "documentId": {
          "__rl": true,
          "mode": "url",
          "value": "https://docs.google.com/spreadsheets/d/10AEB5bqyrd8ocz-I_A5RBKhIaxe9-B3-nfpvT4MmiSY/edit?gid=0#gid=0"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.7,
      "alwaysOutputData": false
    },
    {
      "id": "539f3ac3-f4a3-4c56-a019-59e2c629ca00",
      "name": "Dispatch Sheet Data",
      "type": "n8n-nodes-base.code",
      "position": [
        -624,
        176
      ],
      "parameters": {
        "jsCode": "const sheetData = $input.all().filter(i => i.json.row_number !== undefined);\nconst carreras = $input.all().filter(i => i.json.tipo !== undefined);\nconst resultados = [];\n\nconst ciclosFilas = [2, 6, 10];\nconst columnasZona2 = [\"Tirada 1\", \"Tirada 2\", \"Tirada 3\", \"Tirada 4\", \"Tirada 5\"];\n\n// Leer IDs desde I2\nconst filaIDs = sheetData.find(i => i.json.row_number === 2);\n// const idsTexto = filaIDs?.json[\"IDs Registrados\"] || \"\";\nconst idsTexto = String(filaIDs?.json[\"IDs Registrados\"] || \"\");\nconst idsRegistrados = new Set(idsTexto.split(\",\").filter(Boolean));\n\n// IDs nuevos a agregar\nconst nuevosIds = carreras\n\n  .filter(c => !idsRegistrados.has(\"id_\" + String(c.json.strava_id)))\n  .map(c => \"id_\" + String(c.json.strava_id))\n  // .filter(c => !idsRegistrados.has(String(c.json.strava_id)))\n  // // .map(c => String(c.json.strava_id));\n  // .map(c => \"id_\" + String(c.json.strava_id))\n\nconst idsActualizados = [...idsRegistrados, ...nuevosIds].join(\",\");\n\nfor (const carreraItem of carreras) {\n  const carrera = carreraItem.json;\n  // if (idsRegistrados.has(String(carrera.strava_id))) continue;\n  if (idsRegistrados.has(\"id_\" + String(carrera.strava_id))) continue;\n\n  let columnaDestino = null;\n  let baseRow = null;\n\n  if (carrera.tipo === \"ZONA 2\") {\n    for (let filaBase of ciclosFilas) {\n      const filaFecha = sheetData.find(i => i.json.row_number === filaBase);\n      for (let col of columnasZona2) {\n        if (!filaFecha || !filaFecha.json[col] || filaFecha.json[col] === \"\") {\n          columnaDestino = col;\n          baseRow = filaBase;\n          break;\n        }\n      }\n      if (baseRow) break;\n    }\n  } else if (carrera.tipo === \"TEST 5KM\") {\n    for (let filaBase of ciclosFilas) {\n      const filaFecha = sheetData.find(i => i.json.row_number === filaBase);\n      if (!filaFecha || !filaFecha.json[\"Test 5km\"] || filaFecha.json[\"Test 5km\"] === \"\") {\n        columnaDestino = \"Test 5km\";\n        baseRow = filaBase;\n        break;\n      }\n    }\n  }\n\n  if (!baseRow) throw new Error(`No hay espacio disponible para ${carrera.tipo}`);\n\n  resultados.push({ json: { row_number: baseRow,     [columnaDestino]: carrera.fecha } });\n  resultados.push({ json: { row_number: baseRow + 1, [columnaDestino]: carrera.ritmo } });\n  resultados.push({ json: { row_number: baseRow + 2, [columnaDestino]: carrera.tiempo } });\n  resultados.push({ json: { row_number: baseRow + 3, [columnaDestino]: carrera.distancia } });\n}\n\n// Guardar todos los IDs en I2\nresultados.push({ json: { row_number: 2, \"IDs Registrados\": idsActualizados } });\n\nreturn resultados;"
      },
      "typeVersion": 2
    }
  ],
  "active": true,
  "settings": {
    "binaryMode": "separate",
    "callerPolicy": "workflowsFromSameOwner",
    "timeSavedMode": "fixed",
    "availableInMCP": false,
    "executionOrder": "v1"
  },
  "versionId": "74c937bf-805f-4381-ac6a-d6767da5cecc",
  "connections": {
    "Merge Race Results": {
      "main": [
        [
          {
            "node": "Dispatch Sheet Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Dispatch Sheet Data": {
      "main": [
        [
          {
            "node": "Update Row in Sheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Update Row in Sheet": {
      "main": [
        [
          {
            "node": "Loop Over Activities",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Loop Over Activities": {
      "main": [
        [],
        [
          {
            "node": "Calculate Pace Min per Km",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read Rows from Sheet": {
      "main": [
        [
          {
            "node": "Merge Race Results",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Route by Custom Rules": {
      "main": [
        [
          {
            "node": "Clean and Classify Race Data",
            "type": "main",
            "index": 0
          },
          {
            "node": "Read Rows from Sheet",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Clean and Classify Race Test Data",
            "type": "main",
            "index": 0
          },
          {
            "node": "Read Rows from Sheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Daily Schedule Trigger": {
      "main": [
        [
          {
            "node": "Fetch Runner Activities",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Runner Activities": {
      "main": [
        [
          {
            "node": "Loop Over Activities",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Calculate Pace Min per Km": {
      "main": [
        [
          {
            "node": "Route by Custom Rules",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Clean and Classify Race Data": {
      "main": [
        [
          {
            "node": "Merge Race Results",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Clean and Classify Race Test Data": {
      "main": [
        [
          {
            "node": "Merge Race Results",
            "type": "main",
            "index": 2
          }
        ]
      ]
    }
  }
}