La deuxième boucle ne traite pas correctement les fichiers CSV multi-lignes

Hello everyone,

I have a workflow tasked with processing a list of records contained in a CSV file with this structure (header and data):

sku    --> header
SKU1;  --> data
SKU2;  --> data

output convertToCSV :

[
  {
    "sku": "SKU1"
  },
  {
    "sku": "SKU2"
  }
]

With a loop, each line of the file is saved in an external database, then another node adds information collected upstream of the CSV reading node, adds data obtained through an API call. This gives a JSON with this structure:

Fist iteration :

[
  {
    "name": "image_hd_01",
    "value": "7/f/5/9/7f592403068ab3e002b6a4ad1f8d545fb0ce960b_sku1_1.jpg",
    "sku": "SKU1",
    "rowid": "ro_ta_b9765384c9df45a98663767176a4c1ee_08e6dfec73b74bd3b7a57c0cbd9cf732"
  },
  {
    "name": "image_web_01",
    "value": "1/6/c/0/16c0ca6d4a672f66e5d255cbc76cd88aada4ed76_sku1_1.jpg",
    "sku": "SKU1",
    "rowid": "ro_ta_b9765384c9df45a98663767176a4c1ee_08e6dfec73b74bd3b7a57c0cbd9cf732"
  }
]

Second iteration:

[
  {
    "name": "image_hd_01",
    "value": "a/d/b/2/adb276b7c2ab98ad60efaf3f27251755473d9d49_300DPI_2000px_sku2.jpg",
    "sku": "SKU2",
    "rowid": "ro_ta_b9765384c9df45a98663767176a4c1ee_8b2c580ba29c467a844170b3b0786d60"
  }
]

output “loopInsertSKU”:
1 iteration in loop branch :

[
  {
    "sku": "SKU1"
  }
]

2 iteration in loop branch :

[
  {
    "sku": "SKU2"
  }
]

Last iteration in done branch:

[
  {
    "data": {
      "_id": "ro_ta_b9765384c9df45a98663767176a4c1ee_08e6dfec73b74bd3b7a57c0cbd9cf732",
      "sku": "SKU1",
      "tableId": "ta_b9765384c9df45a98663767176a4c1ee",
      "type": "row",
      "createdAt": "2026-05-19T15:24:32.228Z",
      "updatedAt": "2026-05-19T15:24:32.228Z",
      "image_hd_01": "7/f/5/9/7f592403068ab3e002b6a4ad1f8d545fb0ce960b_SKU1_1.jpg",
      "image_hd_01_filename": "SKU1-1.jpg"
    }
  },
  {
    "data": {
      "_id": "ro_ta_b9765384c9df45a98663767176a4c1ee_08e6dfec73b74bd3b7a57c0cbd9cf732",
      "sku": "SKU1",
      "tableId": "ta_b9765384c9df45a98663767176a4c1ee",
      "type": "row",
      "createdAt": "2026-05-19T15:24:32.228Z",
      "updatedAt": "2026-05-19T15:24:32.228Z",
      "image_hd_01": "7/f/5/9/7f592403068ab3e002b6a4ad1f8d545fb0ce960b_SKU1_1.jpg",
      "image_hd_01_filename": "SKU1-1.jpg"
    }
  },

/// Why doesn't this part have the same structure as the rest? ///
  {
    "name": "image_hd_01",
    "value": "a/d/b/2/adb276b7c2ab98ad60efaf3f27251755473d9d49_300DPI_2000px_SKU2.jpg",
    "sku": "SKU1",
    "rowid": "ro_ta_b9765384c9df45a98663767176a4c1ee_8b2c580ba29c467a844170b3b0786d60"
  }
]

Then another loop is launched to make another API call and save additional data in the external database. The CSV reading, saving the rows in the database, and collecting from the APIs are working fine. But the second loop is not being handled correctly in the case of a CSV file with multiple lines. The first line of the CSV is processed correctly but the second one is not, and for example, the data collected in the second loop are not in the loop branch but in the done branch.

1 iteration in “Loop Over Items1” in loop branch:

[
  {
    "name": "image_hd_01",
    "value": "7/f/5/9/7f592403068ab3e002b6a4ad1f8d545fb0ce960b_SKU1_1.jpg",
    "sku": "SKU1",
    "rowid": "ro_ta_b9765384c9df45a98663767176a4c1ee_08e6dfec73b74bd3b7a57c0cbd9cf732"
  }
]

2 iteration in “Loop over items1” in done branch (?)

[
  {
    "data": {
      "_id": "ro_ta_b9765384c9df45a98663767176a4c1ee_08e6dfec73b74bd3b7a57c0cbd9cf732",
      "sku": "SKU1",
      "tableId": "ta_b9765384c9df45a98663767176a4c1ee",
      "type": "row",
      "createdAt": "2026-05-19T15:24:32.228Z",
      "updatedAt": "2026-05-19T15:24:32.228Z",
      "image_hd_01": "7/f/5/9/7f592403068ab3e002b6a4ad1f8d545fb0ce960b_SKU1_1.jpg",
      "image_hd_01_filename": "SKU1-1.jpg"
    }
  }
]

Last iteration in “Loop over items1” in done branch:

[
  {
    "data": {
      "_id": "ro_ta_b9765384c9df45a98663767176a4c1ee_08e6dfec73b74bd3b7a57c0cbd9cf732",
      "sku": "SKU1,
      "tableId": "ta_b9765384c9df45a98663767176a4c1ee",
      "type": "row",
      "createdAt": "2026-05-19T15:24:32.228Z",
      "updatedAt": "2026-05-19T15:24:32.228Z",
      "image_hd_01": "7/f/5/9/7f592403068ab3e002b6a4ad1f8d545fb0ce960b_SKU1_1.jpg",
      "image_hd_01_filename": "SKU1-1.jpg"
    }
  },

/// Why doesn't this part have the same structure as the rest? ///
  {
    "name": "image_hd_01",
    "value": "a/d/b/2/adb276b7c2ab98ad60efaf3f27251755473d9d49_300DPI_2000px_SKU2.jpg",
    "sku": "SKU2",
    "rowid": "ro_ta_b9765384c9df45a98663767176a4c1ee_8b2c580ba29c467a844170b3b0786d60"
  }
]

Please share your workflow

Information on your n8n setup

  • n8n version: 2.20.9
  • Database : SQLite
  • n8n EXECUTIONS_PROCESS setting (default: own, main):
  • Running n8n via : Docker
  • Operating system: Linux

Pouvez-vous essayer ceci ?

{
  "nodes": [
    {
      "parameters": {
        "options": {
          "delimiter": ";",
          "encoding": "utf-8",
          "relaxQuotes": true,
          "headerRow": true,
          "includeEmptyCells": false,
          "rawData": false,
          "readAsString": true,
          "fromLine": 1
        }
      },
      "type": "n8n-nodes-base.extractFromFile",
      "typeVersion": 1.1,
      "position": [
        1072,
        -1040
      ],
      "id": "e6be7808-f2d0-4189-a7c8-36fc6165aee5",
      "name": "convertToCSV",
      "executeOnce": false,
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "options": {
          "reset": false
        }
      },
      "type": "n8n-nodes-base.splitInBatches",
      "typeVersion": 3,
      "position": [
        1408,
        -1056
      ],
      "id": "06a8312d-f323-4bd2-b707-bbba4faf8e7e",
      "name": "loopInsertSku"
    },
    {
      "parameters": {
        "url": "=http://myserv/api/rest/v1/products-uuid",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "search",
              "value": "={\"identifier\":[{\"operator\":\"=\",\"value\":\"{{ $json.data.sku }}\"}]}"
            },
            {
              "name": "attributes",
              "value": "={{ $('Webhook').item.json.body.attributs }}"
            },
            {
              "name": "limit",
              "value": "100"
            }
          ]
        },
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "=Bearer {{ $('setVar').item.json.access_token }}"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        1984,
        -1008
      ],
      "id": "9790bbc6-3870-4f5a-a2ec-dfc4dec0ba5e",
      "name": "getProduct1",
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "url": "=http://myserv/api/rest/v1/media-files/{{ $json.value }}",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "limit",
              "value": "1"
            }
          ]
        },
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "=Bearer {{ $('setVar').item.json.access_token }}"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        1968,
        -768
      ],
      "id": "4d59279d-4f6b-412b-a66a-e50c09dd23a9",
      "name": "getMedia1",
      "onError": "continueErrorOutput"
    },
    {
      "parameters": {
        "operation": "updaterow",
        "tableId": "={{ $('createBudibaseTable').item.json.data._id }}",
        "rowId": "={{ $('Loop Over Items1').getItem($input.item.index).json.rowid }}",
        "payload": "={\n  \"{{ $('Loop Over Items1').getItem($input.item.index).json.name }}\": \"{{ $json.code }}\",\n  \"{{ $('Loop Over Items1').getItem($input.item.index).json.name }}_filename\": \"{{ $json.original_filename }}\"\n}",
        "requestOptions": {}
      },
      "type": "n8n-nodes-budibasedb.budibaseDB",
      "typeVersion": 1,
      "position": [
        2288,
        -784
      ],
      "id": "d45b75c6-e044-a2b-bb5c-953538b3a1d4",
      "name": "updateProductRow",
      "credentials": {
        "budibaseApi": {
          "id": "HcyIW6A5n767zaCK",
          "name": "zzzzz-dev"
        }
      }
    },
    {
      "parameters": {
        "options": {}
      },
      "type": "n8n-nodes-base.splitInBatches",
      "typeVersion": 3,
      "position": [
        1600,
        -784
      ],
      "id": "6982000c-609f-4654-8ef3-4189d046c30a",
      "name": "Loop Over Items1",
      "executeOnce": false,
      "alwaysOutputData": false
    },
    {
      "parameters": {
        "tableId": "={{ $('createBudibaseTable').item.json.data._id }}",
        "payload": "={\n  \"sku\" : \"{{ $('loopInsertSku').item.json.reference }}\"\n}",
        "requestOptions": {}
      },
      "type": "n8n-nodes-budibasedb.budibaseDB",
      "typeVersion": 1,
      "position": [
        1728,
        -1008
      ],
      "id": "4a20ea09-e59c-4321-bb2c-90f0fc9acde1",
      "name": "pushSku",
      "credentials": {
        "budibaseApi": {
          "id": "HcyIW6A5n767zaCK",
          "name": "zzzzz-dev"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "const pushSkuItem = $('pushSku').getItem($input.item.index);\nconst pushSkuData = pushSkuItem.json.data;\nconst items = $input.item.json._embedded.items[0];\n\nreturn Object.entries(items.values).map(([key, arr]) =\ u003e ({\n  name: key,\n  value: arr[0].data,\n  sku: pushSkuData.sku,\n  rowid: pushSkuData._id\n}));"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2288,
        -1024
      ],
      "id": "eed26b30-d976-4414-924b-57c5f2cbd02e",
      "name": "Code in JavaScript"
    }
  ],
  "connections": {
    "convertToCSV": {
      "main": [
        [
          {
            "node": "loopInsertSku",
            "type": "main",
            "index": 0
          }
        ],
        []
      ]
    },
    "loopInsertSku": {
      "main": [
        [],
        [
          {
            "node": "pushSku",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "getProduct1": {
      "main": [
        [
          {
            "node": "Code in JavaScript",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "getMedia1": {
      "main": [
        [
          {
            "node": "updateProductRow",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "updateProductRow": {
      "main": [
        [
          {
            "node": "Loop Over Items1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Loop Over Items1": {
      "main": [
        [
          {
            "node": "loopInsertSku",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "getMedia1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "pushSku": {
      "main": [
        [
          {
            "node": "getProduct1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code in JavaScript": {
      "main": [
        [
          {
            "node": "Loop Over Items1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}

Les différences se trouvent dans :

1)Nœud Code in JavaScript

// Récupérer l'élément spécifique de pushSku qui correspond à l'élément d'entrée actuel
const pushSkuItem = $('pushSku').getItem($input.item.index);
const pushSkuData = pushSkuItem.json.data;

const items = $input.item.json._embedded.items[0];

return Object.entries(items.values).map(([key, arr]) => ({
  name: key,
  value: arr[0].data,
  sku: pushSkuData.sku,
  rowid: pushSkuData._id
}));



2)Nœud updateProductRow

{
  "{{ $('Loop Over Items1').getItem($input.item.index).json.name }}": "{{ $json.code }}",
  "{{ $('Loop Over Items1').getItem($input.item.index).json.name }}_filename": "{{ $json.original_filename }}"
}

Le nœud de boucle ne se réinitialise pas après la fin de la boucle, apparemment. Et les développeurs ont décidé que nous n’avions pas besoin de le savoir, pas assez pour le mentionner dans la documentation.

La solution de contournement consiste à mettre une expression dans l’option Réinitialiser, afin qu’elle se réinitialise en fonction de $prevNode.name par exemple.

hello @flipflip

N’utilisez pas de boucles imbriquées ; c’est trop complexe de les faire fonctionner. Utilisez plutôt une approche de sous-workflow.

La partie difficile est qu’avec les boucles imbriquées, la logique fonctionne de cette façon :

  • l’exécution entre dans la boucle externe
  • puis elle va à la boucle interne jusqu’à ce que cette boucle se termine
  • puis la boucle externe se termine aussi instantanément (ou continue avec l’index qui était le dernier dans la boucle interne) au lieu de continuer avec l’élément suivant du CSV.

@kjooleng : avec ton code javascript j’ai cette erreur :

$(...).getItem is not a function [line 2]
TypeError

@Rpahut : Je pense que l’intention est d’accumuler les données à chaque itération pour qu’elles soient utilisables après la fin de la boucle. Idéalement, il y aurait un paramètre de configuration pour contrôler ce comportement en fonction du cas d’usage spécifique.

@barn4k : ok j’essaie cette solution.

Merci à tous !

How about this?

ne fonctionnera pas :slight_smile:

Le problème, c’est que les deux nœuds Loop utiliseront la même propriété $runIndex et finiront par gâcher les entrées

J’ai essayé une boucle imbriquée basique et ça semble fonctionner. Étant donné que tu l’as configurée pour réinitialiser, comme le suggèrent les développeurs.
@barn4k qu’entends-tu par « à terme »? ne devrait-il pas échouer à chaque fois, s’il partage la propriété?

pas à chaque fois.

Cela fonctionnera s’il y a une seule entrée du tableau CSV. Sinon, les résultats seront très imprévisibles.

Voici un exemple. Vous pouvez modifier le nœud « set items » pour tester différentes options.

1er test :
1 élément dans la boucle externe, 3 éléments dans la boucle interne.
Les résultats devraient être : 1 x 3 = 3 éléments avec les indexes (externe, interne) :

0 0
0 1
0 2

celui-ci fonctionne,

Cependant,
3 éléments dans la boucle externe, 1 élément dans la boucle interne.
devrait conduire à 3 x 1 = 3 éléments avec les indexes :

0 0
1 0
2 0

Mais nous obtenons 6 éléments et un désordre avec les indexes.

Il en est de même pour 3x5 éléments. Cela devrait être 15, mais nous en obtenons 30
Ou 10x5. Cela devrait être 50, mais nous en obtenons 275…