{
  "name": "FLUF + Airtable \u2014 current stock status",
  "nodes": [
    {
      "id": "bf1f15d1-d180-5d3b-b620-1e220f678343",
      "name": "Overview",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -660,
        -440
      ],
      "parameters": {
        "content": "## Update Airtable availability from FLUF out-of-stock events\n\n### How it works\nWhen FLUF reports an item out of stock or sold out on your marketplaces, this workflow reads the item's current state from FLUF and updates the matching Inventory row. It responds to out-of-stock and sold-out events only; it does not track restocks or other quantity changes.\n\n### Setup\n1. Copy the Airtable template (link below) or use your own base with the same field names.\n2. Open **Configure workspace** and paste your base ID and the Inventory, Sales and Tasks table IDs.\n3. Select your Airtable personal access token credential on every Airtable request node.\n4. On FLUF HTTP request nodes use Header Auth: `Authorization` = `Bearer YOUR_FLUF_PAT`.\n5. Select your FLUF Connect API credential on each FLUF trigger.\n6. In FLUF, choose n8n as the automation owner for this base (one owner per base).\n7. Test with zero-stock drafts first, then activate.\n\n### Requirements\nYour own n8n workspace, an Airtable base and a paid FLUF plan with API write access (not included in the trial).\n\nTemplate base: https://airtable.com/appGvPOBL2b7Lfxwg/shryPFF1iDosEzitJ\nGuide: https://fluf.io/support/connect-airtable/",
        "height": 1000,
        "width": 560,
        "color": 4
      }
    },
    {
      "id": "cae1933a-2ea4-5ffe-b407-170cb04e6189",
      "name": "Note 1",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -60,
        -260
      ],
      "parameters": {
        "content": "## 1. Stock event\nTwo FLUF Connect triggers: out-of-stock alert and listing sold out. **Configure workspace** holds your base and table IDs.",
        "height": 580,
        "width": 520,
        "color": 7
      }
    },
    {
      "id": "6352cfa1-9fe2-579e-bd80-64aece532ba0",
      "name": "Note 2",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        540,
        -260
      ],
      "parameters": {
        "content": "## 2. Read FLUF state\nFetches the item's current FLUF ID, SKU, FLUF status and available quantity from FLUF.\nAirtable and FLUF requests make up to three attempts in total, waiting 35 seconds after a rate limit (429) or server error. Other errors stop the run so you can see them in the execution.",
        "height": 620,
        "width": 940,
        "color": 7
      }
    },
    {
      "id": "d3a443fd-ad09-56dd-a13d-597c6fb4bcf9",
      "name": "Note 3",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        1580,
        -260
      ],
      "parameters": {
        "content": "## 3. Find the Inventory row\nLooks up the row by its Airtable record ID, or by FLUF ID. Two rows with the same FLUF ID stop the run until you merge them.",
        "height": 620,
        "width": 940,
        "color": 7
      }
    },
    {
      "id": "00c2d77e-8d9e-5365-8f20-2d80787faaed",
      "name": "Note 4",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        2620,
        -260
      ],
      "parameters": {
        "content": "## 4. Write to Airtable\n**Prepare inventory writes** builds the Inventory update (or a new row) and, for crosslistings, a Listing outcome task. A row whose FLUF ID was changed by hand stops the run.",
        "height": 620,
        "width": 1240,
        "color": 7
      }
    },
    {
      "id": "616db20b-d80d-57a8-bde3-cde8ed1e18ca",
      "name": "Oos Alert",
      "type": "n8n-nodes-fluf-connect.flufConnectTrigger",
      "typeVersion": 1,
      "position": [
        0,
        0
      ],
      "parameters": {
        "event": "oos_alert"
      },
      "webhookId": "cc0ab206-56a4-50d0-a0c2-444ad6cad543"
    },
    {
      "id": "8e67c51c-45a5-5b19-a738-5d13546446d8",
      "name": "Listing Sold Out",
      "type": "n8n-nodes-fluf-connect.flufConnectTrigger",
      "typeVersion": 1,
      "position": [
        0,
        160
      ],
      "parameters": {
        "event": "listing_sold_out"
      },
      "webhookId": "453f327f-ddb2-5076-b119-451eb2e39642"
    },
    {
      "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": {
    "Oos Alert": {
      "main": [
        [
          {
            "node": "Configure workspace",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Listing Sold Out": {
      "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": []
}
