{
  "name": "FLUF + Airtable \u2014 inventory and listing outcomes",
  "nodes": [
    {
      "id": "3286ee74-28e7-5aa8-b602-2049c615ff78",
      "name": "Setup guide",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        0,
        -400
      ],
      "parameters": {
        "content": "## Before enabling\nRead https://fluf.io/support/connect-airtable/\n1. Set table IDs in Configure workspace.\n2. Select your Airtable PAT credential on Airtable requests.\n3. For FLUF HTTP requests use Header Auth: Authorization = Bearer YOUR_FLUF_PAT.\n4. Select FLUF credentials on each trigger.\n5. Choose n8n as workflow owner for this base.\n6. Publishing requires allowPublish=true AND Approved for publishing on the row.\nTest with zero-stock drafts first. Use the same currency as your FLUF draft currency.",
        "height": 360,
        "width": 440
      }
    },
    {
      "id": "ae10bd08-d8a6-5d0e-801d-05465734f32c",
      "name": "New Listing",
      "type": "n8n-nodes-fluf-connect.flufConnectTrigger",
      "typeVersion": 1,
      "position": [
        0,
        0
      ],
      "parameters": {
        "event": "new_listing"
      },
      "webhookId": "2f3ca938-8484-5583-9fcc-8d223d44629c"
    },
    {
      "id": "219c84f5-2c00-5af0-9576-b03dbdda2941",
      "name": "Listing Crosslisted",
      "type": "n8n-nodes-fluf-connect.flufConnectTrigger",
      "typeVersion": 1,
      "position": [
        0,
        160
      ],
      "parameters": {
        "event": "listing_crosslisted"
      },
      "webhookId": "348e96cb-cb8c-5d7b-a940-5720a4bc87e0"
    },
    {
      "id": "8a4d53f4-dff9-5df9-8ac3-212e52f75dbe",
      "name": "Configure workspace",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        300,
        0
      ],
      "parameters": {
        "jsCode": "// Choose a base and its four FLUF tables. Use standard field names from the guide.\nconst config = { baseId: 'REPLACE_BASE_ID', inventory: 'REPLACE_INVENTORY_TABLE_ID', sales: 'REPLACE_SALES_TABLE_ID', tasks: 'REPLACE_TASKS_TABLE_ID', allowPublish: false };\nif (!/^app[A-Za-z0-9]+$/.test(config.baseId)) throw new Error('Set your Airtable base ID in Configure workspace.');\nfor (const key of ['inventory','sales','tasks']) if (!/^tbl[A-Za-z0-9]+$/.test(config[key])) throw new Error('Set the '+key+' table ID.');\nreturn $input.all().map((item,i)=>({json:{...item.json,config},pairedItem:{item:i}}));"
      }
    },
    {
      "id": "accd9a13-6357-52e9-9236-fb00742d41e3",
      "name": "Request Read current FLUF state",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        840,
        0
      ],
      "parameters": {
        "method": "GET",
        "url": "={{ 'https://fluf.io/wp-json/fc/external/v1/n8n/airtable/snapshot/'+Number($json.listing_id)+'?base_id='+$json.config.baseId }}",
        "authentication": "genericCredentialType",
        "options": {
          "batching": {
            "batch": {
              "batchSize": 1,
              "batchInterval": 350
            }
          },
          "response": {
            "response": {
              "fullResponse": true,
              "neverError": true
            }
          }
        },
        "genericAuthType": "httpHeaderAuth"
      },
      "retryOnFail": false,
      "onError": "continueRegularOutput"
    },
    {
      "id": "bcf83288-10fe-5d96-bf38-1765094660f7",
      "name": "Request Find Airtable inventory",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1880,
        0
      ],
      "parameters": {
        "method": "GET",
        "url": "={{ 'https://api.airtable.com/v0/'+$('Configure workspace').item.json.config.baseId+'/'+$('Configure workspace').item.json.config.inventory+($json.record_id?'/'+$json.record_id:'?maxRecords=2&filterByFormula='+encodeURIComponent('{FLUF ID}='+JSON.stringify($json.fields['FLUF ID']))) }}",
        "authentication": "predefinedCredentialType",
        "options": {
          "batching": {
            "batch": {
              "batchSize": 1,
              "batchInterval": 350
            }
          },
          "response": {
            "response": {
              "fullResponse": true,
              "neverError": true
            }
          }
        },
        "nodeCredentialType": "airtableTokenApi"
      },
      "retryOnFail": false,
      "onError": "continueRegularOutput"
    },
    {
      "id": "c6ec4754-e7d7-5fe4-a72c-357a826cce3d",
      "name": "Prepare inventory writes",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2680,
        0
      ],
      "parameters": {
        "jsCode": "return $input.all().flatMap((item,i)=>{\n const e=$('Configure workspace').itemMatching(i).json,c=e.config;\n const snapshot=$('Read current FLUF state').itemMatching(i).json;\n if((item.json.records||[]).length>1) throw new Error('Merge duplicate FLUF IDs before retrying.');\n const existing=item.json.id?item.json:item.json.records?.[0];\n if(existing?.fields?.['FLUF ID'] && String(existing.fields['FLUF ID'])!==String(snapshot.fields['FLUF ID'])) throw new Error('Restore the original FLUF ID on this row.');\n const fields=existing?snapshot.fields:{...snapshot.preparation,...snapshot.fields};\n const writes=[{table:c.inventory,key:'FLUF ID',fields,record_id:existing?.id||snapshot.record_id}];\n if(e.event==='listing_crosslisted'||e.event_type==='listing_crosslisted') writes.push({table:c.tasks,key:'Task key',fields:{'Task key':e.event_id,'FLUF ID':String(e.listing_id),'Type':'Listing outcome','Marketplace':e.channel||'','Details':'Listed'+(e.channel_url?': '+e.channel_url:''),'Created at':e.occurred_at||e.fired_at||''}});\n return writes.map(w=>({json:{...w,config:c},pairedItem:{item:i}}));\n});"
      }
    },
    {
      "id": "85acf375-e3f9-59c7-a59b-6d34cdd7e570",
      "name": "Request Write current inventory state",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        3220,
        0
      ],
      "parameters": {
        "method": "PATCH",
        "url": "={{ 'https://api.airtable.com/v0/'+$json.config.baseId+'/'+$json.table+($json.record_id?'/'+$json.record_id:'') }}",
        "authentication": "predefinedCredentialType",
        "options": {
          "batching": {
            "batch": {
              "batchSize": 1,
              "batchInterval": 350
            }
          },
          "response": {
            "response": {
              "fullResponse": true,
              "neverError": true
            }
          }
        },
        "nodeCredentialType": "airtableTokenApi",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ $json.record_id?{fields:$json.fields}:{performUpsert:{fieldsToMergeOn:[$json.key]},records:[{fields:$json.fields}]} }}"
      },
      "retryOnFail": false,
      "onError": "continueRegularOutput"
    },
    {
      "id": "28d13347-e5af-5ea8-a2a2-1a5d9870158c",
      "name": "Attempt Read current FLUF state",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        600,
        0
      ],
      "parameters": {
        "jsCode": "return $input.all().map((item,i)=>({json:{...item.json,__airtableAttempt:Number(item.json.__airtableAttempt||0)+1},pairedItem:{item:i}}));"
      }
    },
    {
      "id": "4c4183c7-d1d6-5c62-9bf4-e1d00aec4e56",
      "name": "Read current FLUF state",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1080,
        0
      ],
      "parameters": {
        "jsCode": "return $input.all().map((item,i)=>{\n const before=$(\"Attempt Read current FLUF state\").itemMatching(i).json, response=item.json;\n const status=Number(response.statusCode||0);\n if(status>=200 && status<300) return {json:{...response.body,__airtableRetry:false},pairedItem:{item:i}};\n if((status===0||status===429||status>=500) && before.__airtableAttempt<3) return {json:{...before,__airtableRetry:true},pairedItem:{item:i}};\n const error=response.body?.message||response.body?.error?.message||'The request failed. Check the connection, field mapping and service allowance.';\n if(!false) throw new Error(String(error));\n return {json:{error:String(error),__airtableRetry:false},pairedItem:{item:i}};\n});"
      }
    },
    {
      "id": "16cae7ba-2e08-57d7-aab4-6cf09a26e646",
      "name": "Retry Read current FLUF state",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1320,
        0
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "retry",
              "leftValue": "={{ $json.__airtableRetry }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "bec90d32-8528-54ca-b3c9-f59bee0b4bb0",
      "name": "Wait for Read current FLUF state",
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1.1,
      "position": [
        1320,
        200
      ],
      "parameters": {
        "amount": 35,
        "unit": "seconds"
      },
      "webhookId": "b50160f5-81de-5fed-890e-22c502dcd3bd"
    },
    {
      "id": "8f745722-b1e6-5060-bba7-235fe9c270ab",
      "name": "Attempt Find Airtable inventory",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1640,
        0
      ],
      "parameters": {
        "jsCode": "return $input.all().map((item,i)=>({json:{...item.json,__airtableAttempt:Number(item.json.__airtableAttempt||0)+1},pairedItem:{item:i}}));"
      }
    },
    {
      "id": "4ec2a0d0-9e68-52df-85e3-b75622401382",
      "name": "Find Airtable inventory",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2120,
        0
      ],
      "parameters": {
        "jsCode": "return $input.all().map((item,i)=>{\n const before=$(\"Attempt Find Airtable inventory\").itemMatching(i).json, response=item.json;\n const status=Number(response.statusCode||0);\n if(status>=200 && status<300) return {json:{...response.body,__airtableRetry:false},pairedItem:{item:i}};\n if((status===0||status===429||status>=500) && before.__airtableAttempt<3) return {json:{...before,__airtableRetry:true},pairedItem:{item:i}};\n const error=response.body?.message||response.body?.error?.message||'The request failed. Check the connection, field mapping and service allowance.';\n if(!false) throw new Error(String(error));\n return {json:{error:String(error),__airtableRetry:false},pairedItem:{item:i}};\n});"
      }
    },
    {
      "id": "78db91c7-dd57-5709-9f93-0af86b1e0d8b",
      "name": "Retry Find Airtable inventory",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        2360,
        0
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "retry",
              "leftValue": "={{ $json.__airtableRetry }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "f541bf49-c4ea-54df-88a7-fa77b4abaa98",
      "name": "Wait for Find Airtable inventory",
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1.1,
      "position": [
        2360,
        200
      ],
      "parameters": {
        "amount": 35,
        "unit": "seconds"
      },
      "webhookId": "a4fd8de0-a391-580e-955f-433ccf210431"
    },
    {
      "id": "bbbc89ca-a802-571f-a8ce-08115096829b",
      "name": "Attempt Write current inventory state",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2980,
        0
      ],
      "parameters": {
        "jsCode": "return $input.all().map((item,i)=>({json:{...item.json,__airtableAttempt:Number(item.json.__airtableAttempt||0)+1},pairedItem:{item:i}}));"
      }
    },
    {
      "id": "2b7840b8-2729-5aa6-9172-a82e8c861283",
      "name": "Write current inventory state",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3460,
        0
      ],
      "parameters": {
        "jsCode": "return $input.all().map((item,i)=>{\n const before=$(\"Attempt Write current inventory state\").itemMatching(i).json, response=item.json;\n const status=Number(response.statusCode||0);\n if(status>=200 && status<300) return {json:{...response.body,__airtableRetry:false},pairedItem:{item:i}};\n if((status===0||status===429||status>=500) && before.__airtableAttempt<3) return {json:{...before,__airtableRetry:true},pairedItem:{item:i}};\n const error=response.body?.message||response.body?.error?.message||'The request failed. Check the connection, field mapping and service allowance.';\n if(!false) throw new Error(String(error));\n return {json:{error:String(error),__airtableRetry:false},pairedItem:{item:i}};\n});"
      }
    },
    {
      "id": "f06536bc-7c99-5bc1-bd5c-45fc4b5fa2c3",
      "name": "Retry Write current inventory state",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        3700,
        0
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "retry",
              "leftValue": "={{ $json.__airtableRetry }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "3382b813-8a26-5362-8300-b77612c2d049",
      "name": "Wait for Write current inventory state",
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1.1,
      "position": [
        3700,
        200
      ],
      "parameters": {
        "amount": 35,
        "unit": "seconds"
      },
      "webhookId": "7d17c86d-a6f7-5bb8-b5d9-2fa5a43461ed"
    }
  ],
  "connections": {
    "New Listing": {
      "main": [
        [
          {
            "node": "Configure workspace",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Listing Crosslisted": {
      "main": [
        [
          {
            "node": "Configure workspace",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Configure workspace": {
      "main": [
        [
          {
            "node": "Attempt Read current FLUF state",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare inventory writes": {
      "main": [
        [
          {
            "node": "Attempt Write current inventory state",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Attempt Read current FLUF state": {
      "main": [
        [
          {
            "node": "Request Read current FLUF state",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Request Read current FLUF state": {
      "main": [
        [
          {
            "node": "Read current FLUF state",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read current FLUF state": {
      "main": [
        [
          {
            "node": "Retry Read current FLUF state",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait for Read current FLUF state": {
      "main": [
        [
          {
            "node": "Attempt Read current FLUF state",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Attempt Find Airtable inventory": {
      "main": [
        [
          {
            "node": "Request Find Airtable inventory",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Request Find Airtable inventory": {
      "main": [
        [
          {
            "node": "Find Airtable inventory",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Find Airtable inventory": {
      "main": [
        [
          {
            "node": "Retry Find Airtable inventory",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait for Find Airtable inventory": {
      "main": [
        [
          {
            "node": "Attempt Find Airtable inventory",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Attempt Write current inventory state": {
      "main": [
        [
          {
            "node": "Request Write current inventory state",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Request Write current inventory state": {
      "main": [
        [
          {
            "node": "Write current inventory state",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Write current inventory state": {
      "main": [
        [
          {
            "node": "Retry Write current inventory state",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait for Write current inventory state": {
      "main": [
        [
          {
            "node": "Attempt Write current inventory state",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Retry Read current FLUF state": {
      "main": [
        [
          {
            "node": "Wait for Read current FLUF state",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Attempt Find Airtable inventory",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Retry Find Airtable inventory": {
      "main": [
        [
          {
            "node": "Wait for Find Airtable inventory",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Prepare inventory writes",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Retry Write current inventory state": {
      "main": [
        [
          {
            "node": "Wait for Write current inventory state",
            "type": "main",
            "index": 0
          }
        ],
        []
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1",
    "executionTimeout": 300
  },
  "tags": []
}
