{
  "name": "FLUF + Airtable \u2014 inventory and listing outcomes",
  "nodes": [
    {
      "id": "bf1f15d1-d180-5d3b-b620-1e220f678343",
      "name": "Overview",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -660,
        -440
      ],
      "parameters": {
        "content": "## Record FLUF marketplace listing outcomes in Airtable\n\n### How it works\nWhen a listing is created or crosslisted in FLUF, this workflow reads its current state from FLUF and writes it to the matching Inventory row (creating the row if needed). Each crosslisting also adds a Listing outcome task with the marketplace and link.\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. Listing event\nTwo FLUF Connect triggers: a new listing, and a listing crosslisted to a marketplace. **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": "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": []
}
