AutomationFlowsWeb Scraping › Vg Ops → Hub (categories + Products)

Vg Ops → Hub (categories + Products)

VG OPS → Hub (Categories + Products). Uses httpRequest, n8n-nodes-onprintshop. Event-driven trigger; 6 nodes.

Event trigger★★★★☆ complexity6 nodesHTTP RequestN8N Nodes Onprintshop
Web Scraping Trigger: Event Nodes: 6 Complexity: ★★★★☆ Added:

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 →

Download .json
{
  "id": "vg-ops-pull-001",
  "name": "VG OPS \u2192 Hub (Categories + Products)",
  "active": false,
  "nodes": [
    {
      "parameters": {},
      "id": "9a1b1111-0001-0000-0000-000000000001",
      "name": "When clicking 'Execute workflow'",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        240,
        320
      ]
    },
    {
      "parameters": {
        "url": "={{ $env.API_BASE_URL }}/api/suppliers",
        "options": {},
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "X-Ingest-Secret",
              "value": "={{ $env.INGEST_SHARED_SECRET }}"
            }
          ]
        }
      },
      "id": "9a1b1111-0002-0000-0000-000000000002",
      "name": "Get Suppliers",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        460,
        320
      ]
    },
    {
      "parameters": {
        "jsCode": "// n8n's HTTP Request node may return the array as a single item (json=array)\n// or split it into one item per element (each json=supplier). Handle both.\nconst items = $input.all();\nconst list = items.flatMap((i) => {\n  const j = i.json;\n  if (Array.isArray(j)) return j;\n  // Sometimes the response is wrapped: { data: [...] } or { body: [...] }\n  if (j && Array.isArray(j.data)) return j.data;\n  if (j && Array.isArray(j.body)) return j.body;\n  return j && typeof j === 'object' ? [j] : [];\n});\nconst vg = list.find((s) => s && s.slug === 'vg-ops');\nif (!vg) {\n  throw new Error(`VG OPS supplier not found. Saw ${list.length} suppliers: ${list.map(s => s?.slug).join(',')}`);\n}\nif (!vg.is_active) {\n  throw new Error(\"VG OPS supplier is_active=false. Flip it: UPDATE suppliers SET is_active=true WHERE slug='vg-ops';\");\n}\nreturn [{ json: { vg_sid: vg.id, vg_name: vg.name } }];"
      },
      "id": "9a1b1111-0003-0000-0000-000000000003",
      "name": "Resolve VG SID",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        680,
        320
      ]
    },
    {
      "parameters": {
        "resource": "product",
        "operation": "getManyDetailed",
        "returnAll": true,
        "limit": 100,
        "productFieldsManyDetailed": [
          "product_id",
          "product_name",
          "main_sku",
          "status",
          "default_category_id",
          "small_image",
          "large_image",
          "externalCatalogue"
        ],
        "productSizeFields": [
          "size_id",
          "size_title",
          "size_width",
          "size_height"
        ],
        "productAdditionalOptionsFields": [
          "product_additional_option_id",
          "master_option_id",
          "option_key",
          "title",
          "options_type",
          "required",
          "attributes"
        ],
        "additionalFields": {}
      },
      "id": "9a1b1111-0007-0000-0000-000000000007",
      "name": "OPS: Get Products Detailed",
      "type": "n8n-nodes-onprintshop.onPrintShop",
      "typeVersion": 1,
      "position": [
        900,
        420
      ],
      "credentials": {
        "onPrintShopApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// Categories disabled \u2014 empty catMap so category_name falls through to default_category_name.\nconst catMap = new Map();\n\n// Flatten any OPS response shape:\n//  A) { data: { products_details: { products: [...] } } }\n//  B) flat array of products\n//  C) single product object per item\nconst flatProducts = $input.all().flatMap((item) => {\n  const j = item.json ?? {};\n  const nested = j?.data?.products_details?.products;\n  if (Array.isArray(nested)) return nested;\n  if (Array.isArray(j)) return j;\n  if (j && typeof j === 'object' && (j.products_id || j.product_id || j.id)) return [j];\n  return [];\n});\n\nconst stripHtml = (s) => (typeof s === 'string'\n  ? s.replace(/<style[\\s\\S]*?<\\/style>/gi, '')\n     .replace(/<script[\\s\\S]*?<\\/script>/gi, '')\n     .replace(/<[^>]+>/g, '')\n     .replace(/&nbsp;/g, ' ')\n     .replace(/\\s+/g, ' ')\n     .trim() || null\n  : null);\n\n// \"Decals - General Performance (Arlon - 510MT)\" -> \"Arlon\"\nconst brandFromInternalName = (s) => {\n  if (typeof s !== 'string') return null;\n  const m = s.match(/\\(([^)]+)\\)\\s*$/);\n  if (!m) return null;\n  return (m[1].split(/\\s*-\\s*/)[0] || '').trim() || null;\n};\n\n// \"Through Cut_Knife|2\" -> \"Through Cut\", \"None_lamMaterial\" -> \"None\", \"Rectangle|1\" -> \"Rectangle\"\nconst titleFromAttributeKey = (k) => {\n  if (typeof k !== 'string' || !k) return 'Untitled';\n  const beforePipe = k.split('|')[0];\n  const beforeUnderscore = beforePipe.split('_')[0];\n  return beforeUnderscore.trim() || beforePipe.trim() || 'Untitled';\n};\n\nconst toNum = (v) => (v == null || v === '' ? null : Number(v));\n\nreturn flatProducts.map((p) => {\n  const productId = p.products_id ?? p.product_id ?? p.id;\n  const productIdStr = productId != null ? String(productId) : '';\n  const catExt = p.default_category_id != null ? String(p.default_category_id) : null;\n\n  const variants = (p.product_size ?? []).map((s) => {\n    const sizeId = s.product_size_id ?? s.size_id;\n    const sizeIdStr = sizeId != null ? String(sizeId) : '';\n    return {\n      part_id: sizeIdStr,\n      color: s.product_default_color ?? s.color ?? null,\n      size: s.product_size_label ?? s.size_label ?? s.size_title ?? null,\n      sku: s.product_size_sku ?? (sizeIdStr ? `${productIdStr}-${sizeIdStr}` : null),\n      base_price: null,\n      inventory: null,\n      warehouse: null,\n    };\n  });\n\n  const images = [];\n  if (p.small_image) images.push({ url: p.small_image, image_type: 'front', color: null, sort_order: 0 });\n  if (p.large_image && p.large_image !== p.small_image) {\n    images.push({ url: p.large_image, image_type: 'back', color: null, sort_order: 1 });\n  }\n\n  const options = (p.product_additional_options ?? [])\n    .map((o, i) => {\n      const key = o.option_key ?? o.optionKey ?? '';\n      const rawTitle = o.title ?? o.option_title ?? o.optionTitle ?? '';\n      const title = (typeof rawTitle === 'string' && rawTitle.trim()) || key || 'Option';\n      const firstProdAddOptId = Array.isArray(o.attributes) && o.attributes[0]?.prod_add_opt_id;\n      const opsOptionId = o.product_additional_option_id ?? (firstProdAddOptId != null ? firstProdAddOptId : null);\n      const reqRaw = o.required;\n      const required = !(reqRaw == null || reqRaw === '' || reqRaw === '0' || reqRaw === 0 || reqRaw === false);\n\n      // Remap each OPS attribute to backend OptionAttributeIngest shape.\n      // Backend requires `title: str` \u2014 derive from attribute_key.\n      const rawAttrs = Array.isArray(o.attributes) ? o.attributes : [];\n      const attributes = rawAttrs.map((a) => ({\n        title: titleFromAttributeKey(a.attribute_key),\n        sort_order: Number(a.sort_order ?? 0),\n        ops_attribute_id: a.attribute_id != null ? Number(a.attribute_id) : null,\n        master_attribute_id: a.master_attribute_id != null ? Number(a.master_attribute_id) : null,\n        attribute_key: typeof a.attribute_key === 'string' ? a.attribute_key : null,\n        price: null,\n        setup_cost: toNum(a.setup_cost),\n        multiplier: toNum(a.multiplier),\n      }));\n\n      return {\n        option_key: String(key),\n        title: String(title),\n        options_type: o.options_type ?? o.optionsType ?? null,\n        sort_order: Number(o.sort_order ?? o.sortOrder ?? i),\n        master_option_id: o.master_option_id != null ? Number(o.master_option_id) : null,\n        ops_option_id: null,\n        required,\n        attributes,\n      };\n    })\n    .filter((o) => o.option_key);\n\n  return {\n    json: {\n      supplier_sku: p.products_sku ?? p.main_sku ?? productIdStr,\n      product_name: p.products_title ?? p.product_name ?? 'Untitled',\n      brand: p.brand_name ?? brandFromInternalName(p.products_internal_name),\n      description:\n        stripHtml(p.short_description) ||\n        stripHtml(p.long_description) ||\n        stripHtml(p.long_description_two) ||\n        null,\n      product_type: p.predefined_product_type === '1' || p.predefined_product_type === 1 ? 'apparel' : 'standard',\n      image_url: p.small_image ?? p.large_image ?? null,\n      ops_product_id: productIdStr,\n      external_catalogue: 1,\n      category_external_id: catExt,\n      category_name: catExt ? (catMap.get(catExt) ?? p.default_category_name ?? null) : null,\n      variants,\n      images,\n      options,\n    },\n  };\n});\n"
      },
      "id": "9a1b1111-0008-0000-0000-000000000008",
      "name": "Shape Products",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1120,
        420
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "={{ $env.API_BASE_URL }}/api/ingest/{{ $('Resolve VG SID').item.json.vg_sid }}/products",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "X-Ingest-Secret",
              "value": "={{ $env.INGEST_SHARED_SECRET }}"
            },
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ $items().map(i => i.json) }}",
        "options": {}
      },
      "id": "9a1b1111-0009-0000-0000-000000000009",
      "name": "POST /ingest/products",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1340,
        420
      ]
    }
  ],
  "connections": {
    "When clicking 'Execute workflow'": {
      "main": [
        [
          {
            "node": "Get Suppliers",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Suppliers": {
      "main": [
        [
          {
            "node": "Resolve VG SID",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Resolve VG SID": {
      "main": [
        [
          {
            "node": "OPS: Get Products Detailed",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OPS: Get Products Detailed": {
      "main": [
        [
          {
            "node": "Shape Products",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Shape Products": {
      "main": [
        [
          {
            "node": "POST /ingest/products",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "vg-ops-pull-v1",
  "meta": {
    "templateCredsSetupCompleted": false
  },
  "tags": []
}

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.

Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

VG OPS → Hub (Categories + Products). Uses httpRequest, n8n-nodes-onprintshop. Event-driven trigger; 6 nodes.

Source: https://github.com/VisualGraphxLLC/API-HUB/blob/b97702eae79a70a862840d44ab9b5ec6c453cce0/n8n-workflows/vg-ops-pull.json — original creator credit. Request a take-down →

More Web Scraping workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

Web Scraping

This workflow listens for an “Approved” label on a Trello card, reads the AI draft bookkeeping JSON from card comments, and posts the corresponding transaction to Xero. It then adds a Xero deep link b

Trello Trigger, HTTP Request
Web Scraping

02_LLM_Pipeline v1.0. Uses executeWorkflowTrigger, httpRequest, seaTable. Event-driven trigger; 65 nodes.

Execute Workflow Trigger, HTTP Request, Sea Table
Web Scraping

This workflow allows you to import any workflow from a file or another n8n instance and map the credentials easily. A multi-form setup guides you through the entire process At the beginning you have t

Execute Command, Read Write File, HTTP Request +3
Web Scraping

[n8n] Advanced URL Parsing and Shortening Workflow - Switchy.io Integration. Uses splitInBatches, stickyNote, httpRequest, html. Event-driven trigger; 56 nodes.

HTTP Request, GitHub, Stop And Error +1
Web Scraping

[](https://youtu.be/c7yCZhmMjtI)

HTTP Request, GitHub, Stop And Error +1