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 →
{
"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(/ /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.
onPrintShopApi
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 →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
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
02_LLM_Pipeline v1.0. Uses executeWorkflowTrigger, httpRequest, seaTable. Event-driven trigger; 65 nodes.
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
[n8n] Advanced URL Parsing and Shortening Workflow - Switchy.io Integration. Uses splitInBatches, stickyNote, httpRequest, html. Event-driven trigger; 56 nodes.
[](https://youtu.be/c7yCZhmMjtI)