This workflow corresponds to n8n.io template #16639 — we link there as the canonical source.
This workflow follows the Google Sheets → HTTP Request recipe pattern — see all workflows that pair these two integrations.
The workflow JSON
Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →
{
"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
}
]
]
}
}
}
Credentials you'll need
Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.
googleSheetsOAuth2ApistravaOAuth2Api
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This scheduled workflow pulls your latest running activities from Strava, classifies them as Zone 2 runs or 5K tests based on pace and distance, and writes the formatted results into specific cells in a Google Sheets training log while tracking processed activity IDs. Runs on a…
Source: https://n8n.io/workflows/16639/ — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
Complaint Triage Orchestrator (S2.3). Uses httpRequest, googleSheets. Scheduled trigger; 65 nodes.
This workflow automates video distribution to 9 social platforms simultaneously using Blotato's API. It includes both a scheduled publisher (checks Google Sheets for videos marked "Ready") and a subwo
YogiAI. Uses googleSheets, googleSheetsTool, httpRequest, stopAndError. Scheduled trigger; 61 nodes.
This workflow monitors Google Calendar for events indicating that a customer will visit the company today or the next day, retrieves the required details, and sends reminder notifications to the relev
ofn hook v0.24.0 beta. Uses start, httpRequest, functionItem, itemLists. Scheduled trigger; 42 nodes.