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 →
{
"createdAt": "2025-10-08T15:15:18.621Z",
"updatedAt": "2025-10-25T08:30:15.000Z",
"id": "iawH7WdT0BTBaHsu",
"name": "Categories",
"active": true,
"isArchived": false,
"nodes": [
{
"parameters": {
"httpMethod": "POST",
"path": "api/components/categories",
"responseMode": "responseNode",
"options": {}
},
"type": "n8n-nodes-base.webhook",
"typeVersion": 2.1,
"position": [
-976,
656
],
"id": "cb4f9ba0-de9e-4c8c-84f0-fad61716307f",
"name": "api/components/categories [POST]"
},
{
"parameters": {
"jsCode": "// Get incoming data\nconst items = $input.all();\nconst body = (items[0] && items[0].json && items[0].json.body) ? items[0].json.body : {};\n\n// Extract category name\nconst categoryName = body.category_name || body.name || \"\";\n\n// Validate\nif (!categoryName.trim()) {\n throw new Error('Category name is required');\n}\n\nreturn [\n {\n json: {\n params: {\n categoryName: categoryName.trim()\n }\n }\n }\n];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-752,
656
],
"id": "11ff25d6-43b5-40c5-8251-4528c3032ab5",
"name": "Parse Create Params"
},
{
"parameters": {
"operation": "executeQuery",
"query": "INSERT INTO categories (category_name)\nVALUES ('{{ $json.params.categoryName }}')\nRETURNING category_id, category_name;",
"options": {}
},
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.6,
"position": [
-528,
656
],
"id": "5b8ac70f-0fa1-4a80-b946-de2092d4a26f",
"name": "Create Category",
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "const result = $input.first().json;\n\nreturn [\n {\n json: {\n success: true,\n message: 'Category created successfully',\n data: result\n }\n }\n];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-304,
656
],
"id": "1dcfbcb7-bc6d-43e9-9528-d72a7375e89d",
"name": "Format Response"
},
{
"parameters": {
"respondWith": "allIncomingItems",
"options": {}
},
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.4,
"position": [
-80,
656
],
"id": "23144832-6425-4701-aeaa-ffff2fce3d6d",
"name": "Respond to Webhook3"
},
{
"parameters": {
"httpMethod": "POST",
"path": "api/components/categories/delete",
"responseMode": "responseNode",
"options": {}
},
"type": "n8n-nodes-base.webhook",
"typeVersion": 2.1,
"position": [
-976,
1104
],
"id": "1862f4bc-5e71-4f4a-8a32-bb795f60ea28",
"name": "api/components/categories/delete [POST]"
},
{
"parameters": {
"jsCode": "// Get incoming data from POST body\nconst items = $input.all();\nconst body = (items[0] && items[0].json && items[0].json.body) ? items[0].json.body : {};\n\n// Extract category ID from body\nconst categoryId = parseInt(body.category_id || body.categoryId || body.id) || 0;\n\n// Validate\nif (!categoryId) {\n throw new Error('Category ID is required in request body');\n}\n\nreturn [\n {\n json: {\n params: {\n categoryId\n }\n }\n }\n];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-752,
1104
],
"id": "ee084609-5a54-44c8-80b8-da619ac8ce35",
"name": "Parse Delete Params"
},
{
"parameters": {
"operation": "executeQuery",
"query": "DELETE FROM categories\nWHERE category_id = {{ $json.params.categoryId }}\n AND NOT EXISTS (\n SELECT 1 FROM components WHERE category_id = {{ $json.params.categoryId }}\n )\nRETURNING category_id;",
"options": {}
},
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.6,
"position": [
-528,
1104
],
"id": "fd36110f-7d68-4a76-a922-789c1946dfbd",
"name": "Delete Category",
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"respondWith": "allIncomingItems",
"options": {}
},
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.4,
"position": [
-80,
1104
],
"id": "40789883-5ba9-4d04-874e-c1d164ad7fff",
"name": "Respond to Webhook"
},
{
"parameters": {
"jsCode": "const items = $input.all();\nconst result = items[0] && items[0].json ? items[0].json : null;\n\nif (result && result.category_id) {\n return [\n {\n json: {\n success: true,\n message: 'Category deleted successfully'\n }\n }\n ];\n} else {\n return [\n {\n json: {\n success: false,\n message: 'Cannot delete category. Either it does not exist or has components assigned to it.'\n }\n }\n ];\n}"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-304,
1104
],
"id": "b67a8b72-2c51-4b8b-bde9-06b0e2b0fc12",
"name": "Format Response2"
},
{
"parameters": {
"httpMethod": "POST",
"path": "api/components/categories/update",
"responseMode": "responseNode",
"options": {}
},
"type": "n8n-nodes-base.webhook",
"typeVersion": 2.1,
"position": [
-976,
1328
],
"id": "8b154df7-0959-49cd-9ecd-d7b58a574f81",
"name": "api/components/categories/update [POST]"
},
{
"parameters": {
"jsCode": "// Get incoming data from POST body\nconst items = $input.all();\nconst body = (items[0] && items[0].json && items[0].json.body) ? items[0].json.body : {};\n\n// Extract data from body\nconst categoryId = parseInt(body.category_id || body.categoryId || body.id) || 0;\nconst categoryName = body.category_name || body.categoryName || body.name || \"\";\n\n// Validate\nif (!categoryId) {\n throw new Error('Category ID is required in request body');\n}\nif (!categoryName.trim()) {\n throw new Error('Category name is required');\n}\n\nreturn [\n {\n json: {\n params: {\n categoryId,\n categoryName: categoryName.trim()\n }\n }\n }\n];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-752,
1328
],
"id": "87575854-96a6-4a29-8d57-8a2c6b0f24da",
"name": "Parse Update Params"
},
{
"parameters": {
"operation": "executeQuery",
"query": "UPDATE categories\nSET category_name = '{{ $json.params.categoryName }}'\nWHERE category_id = {{ $json.params.categoryId }}\nRETURNING category_id, category_name;",
"options": {}
},
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.6,
"position": [
-528,
1328
],
"id": "26b58d88-720d-4f4f-b402-4e5ac06879c7",
"name": "Update Category",
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "const result = $input.first().json;\n\nreturn [\n {\n json: {\n success: true,\n message: 'Category updated successfully',\n data: result\n }\n }\n];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-304,
1328
],
"id": "2af4908a-33ad-46ca-83a0-f6ec84b73dcf",
"name": "Format Response1"
},
{
"parameters": {
"respondWith": "allIncomingItems",
"options": {}
},
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.4,
"position": [
-80,
1328
],
"id": "ae411c04-e1ed-4a19-a853-f0cf2fbdec51",
"name": "Respond to Webhook1"
},
{
"parameters": {
"path": "api/components/categories/meta",
"responseMode": "responseNode",
"options": {}
},
"type": "n8n-nodes-base.webhook",
"typeVersion": 2.1,
"position": [
-976,
1552
],
"id": "46e0702c-d607-417f-83ef-32d594cadc0f",
"name": "api/components/categories/meta"
},
{
"parameters": {
"jsCode": "const items = $input.all();\nconst q = (items[0] && items[0].json && items[0].json.query) ? items[0].json.query : {};\n\nconst search = q.search || \"\";\nconst sortBy = q.sortBy || \"category_name\";\nconst limit = Number.isInteger(parseInt(q.pageSize)) ? parseInt(q.pageSize) : null;\nconst pageNo = Number.isInteger(parseInt(q.pageNo)) ? parseInt(q.pageNo) : 1;\nconst offset = limit ? (pageNo - 1) * limit : 0;\n\nreturn [{ json: { params: { search, sortBy, limit, offset } } }];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-752,
1552
],
"id": "1e41c783-42ad-42b4-8ee4-7212d519e3b0",
"name": "Parse Params"
},
{
"parameters": {
"operation": "executeQuery",
"query": "SELECT\n COUNT(DISTINCT c.category_id) AS total_categories,\n COUNT(DISTINCT CASE WHEN comp.component_id IS NOT NULL THEN c.category_id END) AS active_categories,\n COUNT(DISTINCT CASE WHEN comp.component_id IS NULL THEN c.category_id END) AS empty_categories,\n COALESCE(SUM(comp_count.component_count), 0) AS total_components,\n COALESCE(SUM(new_items.new_count), 0) AS recent_additions,\n COALESCE(MAX(latest.latest_added_date), NOW()) AS latest_component_date,\n COALESCE(MIN(oldest.oldest_added_date), NOW()) AS oldest_component_date,\n ROUND(AVG(cat_sizes.category_size)::numeric, 2) AS avg_category_size,\n MAX(cat_sizes.category_size) AS largest_category_size,\n (SELECT category_name FROM categories c2 LEFT JOIN (SELECT category_id, COUNT(*) AS cat_size FROM components GROUP BY category_id) s ON c2.category_id = s.category_id WHERE s.cat_size = (SELECT MAX(cat_size) FROM (SELECT category_id, COUNT(*) AS cat_size FROM components GROUP BY category_id) x) LIMIT 1) AS largest_category_name\nFROM categories c\nLEFT JOIN components comp ON c.category_id = comp.category_id\nLEFT JOIN (\n SELECT category_id, COUNT(*) AS component_count\n FROM components\n GROUP BY category_id\n) comp_count ON comp_count.category_id = c.category_id\nLEFT JOIN (\n SELECT category_id, COUNT(*) AS new_count\n FROM components\n WHERE date_added >= CURRENT_DATE - INTERVAL '30 days'\n GROUP BY category_id\n) new_items ON new_items.category_id = c.category_id\nLEFT JOIN (\n SELECT MAX(date_added) AS latest_added_date\n FROM components\n) latest ON true\nLEFT JOIN (\n SELECT MIN(date_added) AS oldest_added_date\n FROM components\n) oldest ON true\nLEFT JOIN (\n SELECT category_id, COUNT(*) AS category_size\n FROM components\n GROUP BY category_id\n) cat_sizes ON cat_sizes.category_id = c.category_id;",
"options": {}
},
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.6,
"position": [
-528,
1552
],
"id": "c004a56b-ca3c-41a8-a4f0-2f2511c93c16",
"name": "Get Categories Metadata",
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "const data = $input.item.json;\n\nreturn [\n {\n json: {\n summary: {\n totalCategories: parseInt(data.total_categories) || 0,\n activeCategories: parseInt(data.active_categories) || 0,\n emptyCategories: parseInt(data.empty_categories) || 0,\n totalComponents: parseInt(data.total_components) || 0,\n recentAdditions: parseInt(data.recent_additions) || 0\n },\n statistics: {\n averageCategorySize: parseFloat(data.avg_category_size) || 0,\n largestCategorySize: parseInt(data.largest_category_size) || 0,\n largestCategoryName: data.largest_category_name || 'N/A',\n categoryCompleteness: data.total_categories > 0 ? Math.round((data.active_categories / data.total_categories) * 100) : 0\n },\n timeline: {\n latestComponentDate: data.latest_component_date,\n oldestComponentDate: data.oldest_component_date\n },\n insights: {\n hasEmptyCategories: parseInt(data.empty_categories) > 0,\n emptyPercentage: data.total_categories > 0 ? Math.round((data.empty_categories / data.total_categories) * 100) : 0,\n recentActivityRate: data.total_components > 0 ? Math.round((data.recent_additions / data.total_components) * 100) : 0,\n status: data.active_categories === data.total_categories ? 'All categories in use' : data.empty_categories > 0 ? 'Some unused categories' : 'Optimal'\n }\n }\n }\n];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-304,
1552
],
"id": "82a3bcd0-083b-44d0-95e7-8ef38062823e",
"name": "Format Metadata Response"
},
{
"parameters": {
"respondWith": "allIncomingItems",
"options": {}
},
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.4,
"position": [
-80,
1552
],
"id": "ed7df4b9-be86-46ac-b620-3935f28ddddc",
"name": "Respond to Webhook4"
},
{
"parameters": {
"respondWith": "allIncomingItems",
"options": {}
},
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.4,
"position": [
-304,
880
],
"id": "a8304e5b-9e4a-4faf-bd54-b8ea7cdd4953",
"name": "Respond to Webhook2"
},
{
"parameters": {
"jsCode": "// Get all incoming items\nconst items = $input.all();\n\n// Extract query from the first item, or use an empty object\nconst q = (items[0] && items[0].json && items[0].json.query) ? items[0].json.query : {};\n\n// Safely parse and default parameters\nconst search = q.search || \"\";\nconst sortBy = q.sortBy || \"category_name\";\nconst limit = Number.isInteger(parseInt(q.pageSize)) ? parseInt(q.pageSize) : null;\nconst pageNo = Number.isInteger(parseInt(q.page)) ? parseInt(q.page) : 1;\nconst offset = limit ? (pageNo - 1) * limit : 0;\n\n// Return a single item with params for the next node\nreturn [\n {\n json: {\n params: {\n search,\n sortBy,\n limit,\n offset\n }\n }\n }\n];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-752,
880
],
"id": "20c3983f-a366-4da8-9cd6-0f3de8490d2d",
"name": "Parse Params1"
},
{
"parameters": {
"operation": "executeQuery",
"query": "WITH filtered_categories AS (\n SELECT \n c.category_id,\n c.category_name,\n COUNT(comp.component_id) as component_count,\n COUNT(CASE WHEN comp.date_added >= CURRENT_DATE - INTERVAL '30 days' THEN 1 END) as recent_additions,\n MIN(comp.date_added) as first_component_date,\n MAX(comp.date_added) as last_component_date\n FROM categories c\n LEFT JOIN components comp ON c.category_id = comp.category_id\n WHERE \n CASE \n WHEN '{{ $json.params.search }}' != '' \n THEN c.category_name ILIKE '%{{ $json.params.search }}%'\n ELSE TRUE\n END\n GROUP BY c.category_id, c.category_name\n),\npaginated_data AS (\n SELECT *\n FROM filtered_categories\n ORDER BY \n CASE WHEN '{{ $json.params.sortBy }}' = 'category_name' THEN category_name END ASC,\n CASE WHEN '{{ $json.params.sortBy }}' = 'component_count' THEN component_count END DESC,\n category_name ASC\n {{ $json.params.limit ? 'LIMIT ' + $json.params.limit : '' }}\n {{ $json.params.offset ? 'OFFSET ' + $json.params.offset : '' }}\n)\nSELECT \n pd.*,\n (SELECT COUNT(*) FROM filtered_categories) as total_records\nFROM paginated_data pd;\n",
"options": {}
},
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.6,
"position": [
-528,
880
],
"id": "4ac0ca48-aa96-417c-998e-aa1c40eae8cf",
"name": "Get Categories",
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"path": "api/components/categories",
"responseMode": "responseNode",
"options": {}
},
"type": "n8n-nodes-base.webhook",
"typeVersion": 2.1,
"position": [
-976,
880
],
"id": "a0a5b8c6-10c4-4599-9870-02c6708df7fe",
"name": "api/components/categories"
}
],
"connections": {
"api/components/categories [POST]": {
"main": [
[
{
"node": "Parse Create Params",
"type": "main",
"index": 0
}
]
]
},
"Parse Create Params": {
"main": [
[
{
"node": "Create Category",
"type": "main",
"index": 0
}
]
]
},
"Create Category": {
"main": [
[
{
"node": "Format Response",
"type": "main",
"index": 0
}
]
]
},
"Format Response": {
"main": [
[
{
"node": "Respond to Webhook3",
"type": "main",
"index": 0
}
]
]
},
"api/components/categories/delete [POST]": {
"main": [
[
{
"node": "Parse Delete Params",
"type": "main",
"index": 0
}
]
]
},
"Parse Delete Params": {
"main": [
[
{
"node": "Delete Category",
"type": "main",
"index": 0
}
]
]
},
"Delete Category": {
"main": [
[
{
"node": "Format Response2",
"type": "main",
"index": 0
}
]
]
},
"Format Response2": {
"main": [
[
{
"node": "Respond to Webhook",
"type": "main",
"index": 0
}
]
]
},
"api/components/categories/update [POST]": {
"main": [
[
{
"node": "Parse Update Params",
"type": "main",
"index": 0
}
]
]
},
"Parse Update Params": {
"main": [
[
{
"node": "Update Category",
"type": "main",
"index": 0
}
]
]
},
"Update Category": {
"main": [
[
{
"node": "Format Response1",
"type": "main",
"index": 0
}
]
]
},
"Format Response1": {
"main": [
[
{
"node": "Respond to Webhook1",
"type": "main",
"index": 0
}
]
]
},
"api/components/categories/meta": {
"main": [
[
{
"node": "Parse Params",
"type": "main",
"index": 0
}
]
]
},
"Parse Params": {
"main": [
[
{
"node": "Get Categories Metadata",
"type": "main",
"index": 0
}
]
]
},
"Get Categories Metadata": {
"main": [
[
{
"node": "Format Metadata Response",
"type": "main",
"index": 0
}
]
]
},
"Format Metadata Response": {
"main": [
[
{
"node": "Respond to Webhook4",
"type": "main",
"index": 0
}
]
]
},
"Parse Params1": {
"main": [
[
{
"node": "Get Categories",
"type": "main",
"index": 0
}
]
]
},
"Get Categories": {
"main": [
[
{
"node": "Respond to Webhook2",
"type": "main",
"index": 0
}
]
]
},
"api/components/categories": {
"main": [
[
{
"node": "Parse Params1",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1"
},
"staticData": null,
"meta": {
"templateCredsSetupCompleted": true
},
"versionId": "d63120e5-4949-4640-b94b-73748ec709ad",
"triggerCount": 5,
"tags": [],
"shared": [
{
"createdAt": "2025-10-08T15:15:18.632Z",
"updatedAt": "2025-10-08T15:15:18.632Z",
"role": "workflow:owner",
"workflowId": "iawH7WdT0BTBaHsu",
"projectId": "0tBJbgcFWwxEMKPn",
"project": {
"createdAt": "2025-10-05T16:55:31.619Z",
"updatedAt": "2025-10-05T16:55:58.616Z",
"id": "0tBJbgcFWwxEMKPn",
"name": "Bikash Panda <oksbwn@gmail.com>",
"type": "personal",
"icon": null,
"description": null
}
}
]
}
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.
postgres
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Categories. Uses postgres. Webhook trigger; 24 nodes.
Source: https://github.com/oksbwn/Inventory-Management-System/blob/7f26462a83a574049df178775808226e970cd4f8/n8n/iawH7WdT0BTBaHsu.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.
CMM. Uses httpRequest, postgres, redis. Webhook trigger; 90 nodes.
Scraping. Uses httpRequest, postgres, @apify/n8n-nodes-apify, respondToWebhook. Webhook trigger; 61 nodes.
Workflow B — AI Listing Engine. Uses httpRequest, postgres, errorTrigger. Webhook trigger; 47 nodes.