{
  "name": "Look up Shopify order status from a website chatbot using OpenAI",
  "nodes": [
    {
      "parameters": {
        "content": "## Look up Shopify order status from a website chatbot using OpenAI\n\nFor store owners who want to answer where is my order questions automatically, without exposing other customers' orders.\n\n### How it works\n1. A webhook receives the widget message and checks a shared secret.\n2. An OpenAI agent answers shipping and product questions from your text and asks for the order number and email.\n3. The look up order tool queries the Shopify GraphQL Admin API and only replies if the email matches the order.\n4. It returns status, payment status, items and tracking. It is read-only and cannot change orders.\n\n### Setup\n- [ ] Edit **Chatbot settings**: company name, your knowledge text and a random `webhook_secret`.\n- [ ] Add your OpenAI credential to **GPT-4.1 mini model**.\n- [ ] Copy each tool section below the main flow into its own new workflow, save it, then select it in the matching tool node.\n- [ ] In each tool workflow, add your Shopify credential and fill in **Tool settings**.\n- [ ] Test with a POST to the webhook Test URL (the n8n chat panel needs a Chat Trigger). Then activate and add the production URL to your widget, with the same secret as `metadata.webhook_secret`.\n\n### Requirements\nn8n, an OpenAI API key and a Shopify custom app token with read orders access. Orders older than 60 days need extra approval.\n\n### Customization tips\nChange the fields returned, the API version or the prompt.",
        "width": 480,
        "height": 1104
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -560,
        0
      ],
      "id": "6bd4edd3-4389-46ab-a7e5-2ebf3ec1215c",
      "name": "Template overview"
    },
    {
      "parameters": {
        "content": "## 1. Receive and verify the message\nThe webhook takes the widget message. Requests without your secret are rejected.",
        "width": 944,
        "height": 432,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        0,
        0
      ],
      "id": "6116aa37-e932-4f91-b1f4-9a191c0c15f1",
      "name": "Section 1: Receive and verify the message"
    },
    {
      "parameters": {
        "content": "## 2. Answer with the AI agent\nThe agent answers from your settings, remembers the session and calls its tools when needed.",
        "width": 704,
        "height": 512,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        0,
        528
      ],
      "id": "51debb6b-360f-4261-8cf6-dfb8f163e672",
      "name": "Section 2: Answer with the AI agent"
    },
    {
      "parameters": {
        "content": "## 3. Tool: look_up_order (copy to its own workflow)\nCopy these nodes into a new workflow and save it. Then select that workflow in the look_up_order tool node.",
        "width": 1104,
        "height": 272,
        "color": 7
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        0,
        1136
      ],
      "id": "35fb8420-d5f4-478b-ac1f-a2264aeb1251",
      "name": "Section 3: Tool: look_up_order (copy to its own workflow)"
    },
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "shopify-order-lookup",
        "responseMode": "responseNode",
        "options": {}
      },
      "id": "webhook",
      "name": "Chat webhook",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        64,
        112
      ],
      "webhookId": "shopify-order-lookup-webhook"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "c1",
              "name": "company_name",
              "value": "Your Store",
              "type": "string"
            },
            {
              "id": "c2",
              "name": "knowledge",
              "value": "PASTE YOUR PRODUCT / SHIPPING / RETURNS FAQ COPY HERE.\n\nThe bot answers only from this text for general questions. Order-specific questions go through the order lookup tool instead.",
              "type": "string"
            },
            {
              "id": "c3",
              "name": "webhook_secret",
              "value": "CHANGE_ME_TO_A_RANDOM_STRING",
              "type": "string"
            },
            {
              "id": "c4",
              "name": "message",
              "value": "={{ $json.body.chatInput || $json.body.message || '' }}",
              "type": "string"
            },
            {
              "id": "c5",
              "name": "session_id",
              "value": "={{ $json.body.sessionId || 'no-session' }}",
              "type": "string"
            },
            {
              "id": "c6",
              "name": "sent_secret",
              "value": "={{ $json.body.metadata?.webhook_secret || (typeof $json.body.metadata === 'string' ? ($json.body.metadata.match(/\"webhook_secret\"\\s*:\\s*\"([^\"]*)\"/) || [])[1] : '') || '' }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "config",
      "name": "Chatbot settings",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        304,
        112
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "sec1",
              "leftValue": "={{ $json.sent_secret }}",
              "rightValue": "={{ $json.webhook_secret }}",
              "operator": {
                "type": "string",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "check-secret",
      "name": "Request from your widget?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        544,
        112
      ]
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify({ output: 'Sorry, something went wrong. Please try again later.' }) }}",
        "options": {
          "responseCode": 401
        }
      },
      "id": "reject",
      "name": "Reject unknown requests",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        784,
        272
      ]
    },
    {
      "parameters": {
        "promptType": "define",
        "text": "={{ $json.message }}",
        "options": {
          "systemMessage": "=You are the assistant on {{ $json.company_name }}'s online store. You answer product and shipping questions, and look up order status - nothing else.\n\nWHAT YOU KNOW\n{{ $json.knowledge }}\n\nGENERAL QUESTIONS\n- Answer only from the information above. If it is not there, say you are not sure.\n- Never invent pricing, stock levels, or shipping times.\n\nORDER LOOKUPS\n- To look up an order you need BOTH the order number and the email address used on the order. Ask for whichever you don't have, one at a time.\n- Call look_up_order with both values once you have them.\n- If the tool says the order wasn't found or the email didn't match, tell the visitor plainly and ask them to double check the order number and email - do not guess or make up order details.\n- Never invent tracking numbers, statuses, or delivery dates. Only state what the tool returns.\n\nWHAT YOU CANNOT DO\n- You cannot change, cancel, or refund an order. If a visitor asks for this, tell them you can look up their order status but changes need a human, and offer to note it down.\n\nTONE\n- Short. Plain. No exclamation marks."
        }
      },
      "id": "agent",
      "name": "Answer questions and look up orders",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "typeVersion": 1.7,
      "position": [
        64,
        640
      ]
    },
    {
      "parameters": {
        "model": "gpt-4.1-mini",
        "options": {}
      },
      "id": "model",
      "name": "GPT-4.1 mini model",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "typeVersion": 1.2,
      "position": [
        64,
        880
      ]
    },
    {
      "parameters": {
        "sessionIdType": "customKey",
        "sessionKey": "={{ $('Chatbot settings').item.json.session_id }}",
        "contextWindowLength": 20
      },
      "id": "memory",
      "name": "Conversation memory",
      "type": "@n8n/n8n-nodes-langchain.memoryBufferWindow",
      "typeVersion": 1.3,
      "position": [
        304,
        880
      ]
    },
    {
      "parameters": {
        "name": "look_up_order",
        "description": "Look up a Shopify order's status by order number and email. Read-only - cannot change, cancel, or refund anything. Requires both order_number and email; returns a not-found message if either doesn't match.",
        "workflowId": {
          "__rl": true,
          "mode": "list",
          "value": "PASTE_THE_LOOK_UP_ORDER_WORKFLOW_ID_HERE",
          "cachedResultName": "look_up_order (Shopify)"
        },
        "specifyInputSchema": true,
        "jsonSchemaExample": "{\n  \"order_number\": \"#1001\",\n  \"email\": \"sam@example.com\"\n}"
      },
      "id": "tool-look-up-order",
      "name": "look_up_order",
      "type": "@n8n/n8n-nodes-langchain.toolWorkflow",
      "typeVersion": 1.2,
      "position": [
        544,
        880
      ]
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify({ output: $json.output }) }}",
        "options": {}
      },
      "id": "respond",
      "name": "Respond to widget",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        544,
        640
      ]
    },
    {
      "parameters": {
        "inputSource": "passthrough"
      },
      "id": "sub-trigger",
      "name": "When called by the agent",
      "type": "n8n-nodes-base.executeWorkflowTrigger",
      "typeVersion": 1.1,
      "position": [
        64,
        1248
      ]
    },
    {
      "parameters": {
        "jsCode": "const q = $input.first().json.query ?? $input.first().json;\nconst d = typeof q === 'string' ? JSON.parse(q) : q;\n\n// Normalise the order number - visitors may or may not include the '#'.\nlet orderNumber = String(d.order_number || '').trim();\nif (orderNumber && !orderNumber.startsWith('#')) orderNumber = '#' + orderNumber;\n\nreturn [{ json: {\n  order_number: orderNumber,\n  email: String(d.email || '').trim().toLowerCase()\n} }];"
      },
      "id": "parse-input",
      "name": "Parse input",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        284,
        1248
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "s1",
              "name": "shop_domain",
              "value": "your-store.myshopify.com",
              "type": "string"
            },
            {
              "id": "s2",
              "name": "api_version",
              "value": "2026-07",
              "type": "string"
            }
          ]
        },
        "includeOtherFields": true,
        "options": {}
      },
      "id": "sub-config",
      "name": "Tool settings",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        504,
        1248
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{ 'https://' + $json.shop_domain + '/admin/api/' + $json.api_version + '/graphql.json' }}",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ query: \"query($q: String!) { orders(first: 1, query: $q) { edges { node { id name createdAt displayFinancialStatus displayFulfillmentStatus email totalPriceSet { shopMoney { amount currencyCode } } lineItems(first: 10) { edges { node { name quantity } } } fulfillments(first: 3) { trackingInfo { number url company } } } } } }\", variables: { q: 'name:' + $json.order_number } }) }}",
        "options": {}
      },
      "id": "call-shopify-graphql",
      "name": "Call Shopify GraphQL",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        724,
        1248
      ]
    },
    {
      "parameters": {
        "jsCode": "// [TBC] - fulfillments/trackingInfo field names weren't fully confirmed against\n// live docs at build time; verify against a real order with tracking before\n// relying on this. Core order fields (name, email, displayFulfillmentStatus,\n// totalPriceSet, lineItems) ARE confirmed against Shopify's GraphQL Admin docs.\nconst cfg = $('Parse input').first().json;\nconst body = $input.first().json;\nconst edges = body?.data?.orders?.edges || [];\n\nif (edges.length === 0) {\n  return [{ json: { response: \"I couldn't find an order with that number. Please double check the order number and try again.\" } }];\n}\n\nconst order = edges[0].node;\nconst orderEmail = String(order.email || '').trim().toLowerCase();\n\n// Security check: the email provided must match the order's own email.\n// Without this, anyone who guesses a sequential order number could pull up\n// someone else's order details.\nif (orderEmail !== cfg.email) {\n  return [{ json: { response: \"I couldn't find an order matching that number and email together. Please double check both.\" } }];\n}\n\nconst items = (order.lineItems?.edges || []).map(e => e.node.quantity + 'x ' + e.node.name).join(', ');\nconst tracking = (order.fulfillments || []).flatMap(f => f.trackingInfo || []);\nconst trackingText = tracking.length\n  ? tracking.map(t => t.company + ': ' + t.number + (t.url ? ' (' + t.url + ')' : '')).join('; ')\n  : 'no tracking number yet';\n\nreturn [{ json: { response:\n  'Order ' + order.name + ' - status: ' + order.displayFulfillmentStatus + ', payment: ' + order.displayFinancialStatus + '. ' +\n  'Items: ' + items + '. Tracking: ' + trackingText + '.'\n} }];"
      },
      "id": "verify-and-format",
      "name": "Verify email match and format",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        944,
        1248
      ]
    }
  ],
  "pinData": {},
  "connections": {
    "Chat webhook": {
      "main": [
        [
          {
            "node": "Chatbot settings",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Chatbot settings": {
      "main": [
        [
          {
            "node": "Request from your widget?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Request from your widget?": {
      "main": [
        [
          {
            "node": "Answer questions and look up orders",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Reject unknown requests",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Answer questions and look up orders": {
      "main": [
        [
          {
            "node": "Respond to widget",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "GPT-4.1 mini model": {
      "ai_languageModel": [
        [
          {
            "node": "Answer questions and look up orders",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Conversation memory": {
      "ai_memory": [
        [
          {
            "node": "Answer questions and look up orders",
            "type": "ai_memory",
            "index": 0
          }
        ]
      ]
    },
    "look_up_order": {
      "ai_tool": [
        [
          {
            "node": "Answer questions and look up orders",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "When called by the agent": {
      "main": [
        [
          {
            "node": "Parse input",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse input": {
      "main": [
        [
          {
            "node": "Tool settings",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Tool settings": {
      "main": [
        [
          {
            "node": "Call Shopify GraphQL",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Call Shopify GraphQL": {
      "main": [
        [
          {
            "node": "Verify email match and format",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "tags": []
}