This workflow follows the Emailsend → OpenAI 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 →
{
"name": "QSR Inventory Reorder Alert (POS Export to Manager Approval)",
"nodes": [
{
"parameters": {},
"id": "trigger-manual",
"name": "Run Once (manual)",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
120,
220
],
"notes": "Demo entry point. In production, swap for the Schedule Trigger below."
},
{
"parameters": {
"rule": {
"interval": [
{
"field": "hours",
"hoursInterval": 24,
"triggerAtHour": 9,
"triggerAtMinute": 0
}
]
}
},
"id": "trigger-schedule",
"name": "Daily 09:00 Trigger",
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1.2,
"position": [
120,
380
]
},
{
"parameters": {
"operation": "read",
"fileSelector": "/var/qsr/inbox/{{ $now.format('yyyy_MM_dd') }}_inventory.csv",
"options": {}
},
"id": "read-file",
"name": "Read Daily POS Export",
"type": "n8n-nodes-base.readWriteFile",
"typeVersion": 1,
"position": [
360,
300
],
"notes": "Manager drops the POS export here. We chose file-drop over POS API write-access so we cannot break the live POS. For the demo, point this at the sample_pos_export.csv on your machine."
},
{
"parameters": {
"operation": "csv",
"options": {
"headerRow": true
}
},
"id": "extract-csv",
"name": "Parse CSV Rows",
"type": "n8n-nodes-base.extractFromFile",
"typeVersion": 1,
"position": [
580,
300
]
},
{
"parameters": {
"jsCode": "// Aggregate all parsed rows into a single payload for the LLM.\nconst rows = $input.all().map(i => i.json);\nreturn [{ json: { rows } }];"
},
"id": "code-aggregate",
"name": "Aggregate rows",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
800,
300
]
},
{
"parameters": {
"resource": "chat",
"operation": "message",
"modelId": {
"__rl": true,
"value": "gpt-4o-mini",
"mode": "list"
},
"messages": {
"values": [
{
"role": "system",
"content": "You normalize messy QSR POS inventory rows. Input is JSON: { rows: [...] } where each row has sku_raw, item_name_raw, uom, on_hand, par_level. Output a JSON array {canonical_sku, canonical_name, on_hand_units, par_units, deficit, action_required}. Rules: (1) Treat 'Napkins 500pk', 'Napkin 500-count', 'NPK-500', 'NAP500' as the same canonical SKU; collapse duplicates by summing on_hand. (2) Convert uom to a single canonical unit per item. (3) If on_hand or par_level is missing on ANY of the duplicate rows, set action_required='manager_input_needed'. (4) deficit = max(0, par_units - on_hand_units). (5) action_required='reorder' if deficit > 0 AND no missing input, else 'ok'. Return ONLY valid JSON. No prose, no code fences."
},
{
"role": "user",
"content": "={{ JSON.stringify($json.rows) }}"
}
]
},
"options": {
"temperature": 0
}
},
"id": "openai-normalize",
"name": "AI Normalize + Deficit Calc",
"type": "n8n-nodes-base.openAi",
"typeVersion": 1.8,
"position": [
1020,
300
],
"credentials": {
"openAiApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "const raw = ($input.first().json.message?.content) || $input.first().json.text || $input.first().json.output || '[]';\nlet rows;\ntry { rows = JSON.parse(raw); } catch(e) {\n const m = raw.match(/\\[[\\s\\S]*\\]/);\n rows = m ? JSON.parse(m[0]) : [];\n}\nconst reorder = rows.filter(r => r.action_required === 'reorder');\nconst needs_input = rows.filter(r => r.action_required === 'manager_input_needed');\nreturn [{ json: { reorder, needs_input, count_reorder: reorder.length, count_needs_input: needs_input.length } }];"
},
"id": "code-split",
"name": "Split: Reorder vs Needs Input",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1240,
300
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"id": "has-reorder",
"leftValue": "={{ $json.count_reorder }}",
"rightValue": 0,
"operator": {
"type": "number",
"operation": "gt"
}
}
],
"combinator": "and"
}
},
"id": "if-reorder",
"name": "Anything to reorder?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
1460,
300
]
},
{
"parameters": {
"resource": "message",
"operation": "post",
"select": "channel",
"channelId": {
"__rl": true,
"value": "#qsr-reorder",
"mode": "name"
},
"text": "=:warning: *Low Stock Alert* \u2014 {{ $json.count_reorder }} items below par.\n\n```\n{{ $json.reorder.map(r => `${r.canonical_name}: on_hand=${r.on_hand_units}, par=${r.par_units}, deficit=${r.deficit}`).join('\\n') }}\n```\n\nApprove to add to today's vendor draft: <APPROVE_LINK> \u00b7 Decline: <DECLINE_LINK>\n\n_This message is a draft only. No order will be placed until a manager clicks Approve._",
"otherOptions": {}
},
"id": "slack-alert",
"name": "Slack Alert (Manager Approval)",
"type": "n8n-nodes-base.slack",
"typeVersion": 2.2,
"position": [
1680,
200
],
"credentials": {
"slackApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"fromEmail": "ops@example-qsr.com",
"toEmail": "manager@example-qsr.com",
"subject": "=QSR reorder draft \u2014 {{ $json.count_reorder }} items need approval",
"emailFormat": "text",
"text": "={{ $json.reorder.map(r => `- ${r.canonical_name}: on_hand=${r.on_hand_units}, par=${r.par_units}, reorder ${r.deficit}`).join('\\n') }}\n\nReply APPROVE to send to US Foods draft. Reply DECLINE to ignore.\nNo order is placed automatically.",
"options": {}
},
"id": "email-alert",
"name": "Email Alert (fallback)",
"type": "n8n-nodes-base.emailSend",
"typeVersion": 2.1,
"position": [
1680,
360
],
"credentials": {
"smtp": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "// HUMAN-IN-THE-LOOP CHECKPOINT.\n// This workflow intentionally STOPS here. It never writes to the POS, never confirms\n// a vendor order, never moves money. A manager must approve via the Slack button\n// or email reply. Approval triggers a separate workflow (out of scope for this demo)\n// that drafts the vendor order.\nreturn $input.all();"
},
"id": "code-stop",
"name": "Human Approval Required (Stop)",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1900,
280
]
}
],
"connections": {
"Run Once (manual)": {
"main": [
[
{
"node": "Read Daily POS Export",
"type": "main",
"index": 0
}
]
]
},
"Daily 09:00 Trigger": {
"main": [
[
{
"node": "Read Daily POS Export",
"type": "main",
"index": 0
}
]
]
},
"Read Daily POS Export": {
"main": [
[
{
"node": "Parse CSV Rows",
"type": "main",
"index": 0
}
]
]
},
"Parse CSV Rows": {
"main": [
[
{
"node": "Aggregate rows",
"type": "main",
"index": 0
}
]
]
},
"Aggregate rows": {
"main": [
[
{
"node": "AI Normalize + Deficit Calc",
"type": "main",
"index": 0
}
]
]
},
"AI Normalize + Deficit Calc": {
"main": [
[
{
"node": "Split: Reorder vs Needs Input",
"type": "main",
"index": 0
}
]
]
},
"Split: Reorder vs Needs Input": {
"main": [
[
{
"node": "Anything to reorder?",
"type": "main",
"index": 0
}
]
]
},
"Anything to reorder?": {
"main": [
[
{
"node": "Slack Alert (Manager Approval)",
"type": "main",
"index": 0
},
{
"node": "Email Alert (fallback)",
"type": "main",
"index": 0
}
],
[
{
"node": "Human Approval Required (Stop)",
"type": "main",
"index": 0
}
]
]
},
"Slack Alert (Manager Approval)": {
"main": [
[
{
"node": "Human Approval Required (Stop)",
"type": "main",
"index": 0
}
]
]
},
"Email Alert (fallback)": {
"main": [
[
{
"node": "Human Approval Required (Stop)",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1"
},
"tags": [
"qsr",
"inventory",
"human-in-the-loop",
"openclaw"
],
"meta": {
"n8n_min_version": "1.50.0",
"version_notes": "Demo workflow. Manual or daily-schedule trigger -> read CSV from disk -> AI SKU normalization -> deficit math -> manager-approval alert (Slack + email). No POS writes, no auto-orders. Replace REPLACE_WITH_YOUR_*_CRED_ID with your own n8n credential IDs."
}
}
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.
openAiApislackApismtp
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
QSR Inventory Reorder Alert (POS Export to Manager Approval). Uses readWriteFile, openAi, slack, emailSend. Event-driven trigger; 11 nodes.
Source: https://github.com/IgorGanapolsky/qsr-n8n-workflow-vault-site/blob/50ff02fd3f52f794f97d91889ee6bd3f6f5c45c5/demo/inventory_reorder_alert_n8n.json — 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.
Description This workflow automates a personalized pre-arrival guest experience for hotels by combining Google Sheets, OpenAI, Email, and Slack. It detects upcoming check-ins, maintains unified guest
Businesses using Jotform to collect customer feedback who want to automate sentiment analysis, email responses, and internal reporting — especially eCommerce, support, or CX teams looking to scale wit
Grain Real Estate Land Showcase v1. Uses formTrigger, httpRequest, openAi, emailSend. Event-driven trigger; 13 nodes.
Complete AI-powered sales system Automates lead capture, qualification, and follow-up from multiple channels. AI INTELLIGENCE:
Goal: This workflow demonstrates the full fluidX THE EYE integration — starting a live session, inviting both the customer (via SMS) and the service agent (via email), and then accessing the media (ph