This workflow follows the HTTP Request → 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": "Beauty Kurly Shopping Agent - Complete Workflow",
"description": "\ubdf0\ud2f0\uceec\ub9ac \uc1fc\ud551 \uc5d0\uc774\uc804\ud2b8 \uc790\ub3d9\ud654 \uc6cc\ud06c\ud50c\ub85c\uc6b0 - \ub9ac\ubdf0 \uc218\uc9d1, \ubd84\uc11d, \uc0ac\uc6a9\uc790 \uc9c8\uc758 \uc751\ub2f5",
"nodes": [
{
"id": "trigger_1",
"name": "Schedule Trigger",
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1,
"position": [
250,
300
],
"parameters": {
"rule": {
"interval": [
{
"field": "hours",
"hoursInterval": 24,
"triggerAtHour": 0
}
]
}
}
},
{
"id": "webhook_1",
"name": "User Query Webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 1,
"position": [
250,
600
],
"parameters": {
"path": "beauty-query",
"httpMethod": "POST",
"responseMode": "lastNode",
"options": {
"rawBody": false
}
}
},
{
"id": "postgres_1",
"name": "Get Product List",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.4,
"position": [
450,
300
],
"parameters": {
"operation": "executeQuery",
"query": "SELECT product_number, name, category FROM products WHERE category LIKE '%beauty%' ORDER BY updated_at DESC LIMIT 10;"
},
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"id": "split_1",
"name": "Split Products",
"type": "n8n-nodes-base.splitInBatches",
"typeVersion": 1,
"position": [
650,
300
],
"parameters": {
"batchSize": 5,
"options": {}
}
},
{
"id": "http_1",
"name": "Fetch Kurly Reviews",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 3,
"disabled": true,
"position": [
850,
300
],
"parameters": {
"method": "GET",
"url": "=https://api.kurly.com/v2/reviews/products/{{ $json.product_number }}",
"authentication": "none",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "User-Agent",
"value": "Mozilla/5.0"
},
{
"name": "Accept",
"value": "application/json"
}
]
},
"options": {
"timeout": 10000,
"redirect": {
"redirect": {
"followRedirects": true
}
}
}
}
},
{
"id": "code_1",
"name": "Extract Review Data",
"type": "n8n-nodes-base.code",
"typeVersion": 1,
"disabled": true,
"position": [
1050,
300
],
"parameters": {
"mode": "runOnceForAllItems",
"jsCode": "// \ub9ac\ubdf0 \ub370\uc774\ud130 \ucd94\ucd9c \ubc0f \uc815\uaddc\ud654\nconst extractedReviews = [];\n\nfor (const item of items) {\n const productNumber = item.json.product_number;\n const reviews = item.json.data?.reviews || [];\n \n for (const review of reviews) {\n extractedReviews.push({\n json: {\n product_number: productNumber,\n review_id: review.id,\n content: review.content || review.comment,\n rating: review.rating || review.score,\n created_at: review.created_at || review.registered_at,\n author: review.author?.name || 'Anonymous',\n like_count: review.like_count || 0,\n verified_purchase: review.verified_purchase || false,\n skin_type: review.attributes?.skin_type || null,\n age_group: review.attributes?.age_group || null\n }\n });\n }\n}\n\nreturn extractedReviews;"
}
},
{
"id": "postgres_2",
"name": "Check Existing Reviews",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.4,
"disabled": true,
"position": [
1250,
300
],
"parameters": {
"operation": "executeQuery",
"query": "=SELECT COUNT(*) as count FROM beauty_reviews WHERE review_id = '{{ $json.review_id }}' LIMIT 1;"
},
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"id": "if_1",
"name": "Filter New Reviews",
"type": "n8n-nodes-base.if",
"typeVersion": 1,
"disabled": true,
"position": [
1450,
300
],
"parameters": {
"conditions": {
"number": [
{
"value1": "={{ $json.count }}",
"operation": "equal",
"value2": 0
}
]
}
}
},
{
"id": "openai_1",
"name": "Sentiment Analysis",
"type": "n8n-nodes-base.openAi",
"typeVersion": 1,
"position": [
1650,
200
],
"parameters": {
"resource": "chat",
"model": "solar-pro",
"options": {
"temperature": 0.3,
"responseFormat": "json_object",
"baseURL": "https://api.upstage.ai/v1/solar"
},
"messages": {
"values": [
{
"role": "system",
"content": "\ub2f9\uc2e0\uc740 \ud654\uc7a5\ud488 \ub9ac\ubdf0\uc758 \uac10\uc131\uc744 \ubd84\uc11d\ud558\ub294 \uc804\ubb38\uac00\uc785\ub2c8\ub2e4.\n\n## \ud3c9\uac00 \uae30\uc900:\n- **5\uc810 (Very satisfied)**: \ub9e4\uc6b0 \ub9cc\uc871, \uadf9\ucc2c, \uc7ac\uad6c\ub9e4 \uc758\uc0ac \uac15\ud568, \uac15\ub825 \ucd94\ucc9c\n- **4\uc810 (Satisfied)**: \ub9cc\uc871, \uc88b\uc74c, \uae0d\uc815\uc801 \ud3c9\uac00\uac00 \ub9ce\uc74c\n- **3\uc810 (Neutral)**: \ubcf4\ud1b5, \uc911\ub9bd\uc801, \uc7a5\ub2e8\uc810 \ud63c\uc7ac, \ud310\ub2e8 \ubcf4\ub958\n- **2\uc810 (Dissatisfied)**: \ubd88\ub9cc\uc871, \uae30\ub300 \uc774\ud558, \ubd80\uc815\uc801 \ud3c9\uac00\n- **1\uc810 (Very dissatisfied)**: \ub9e4\uc6b0 \ubd88\ub9cc\uc871, \uac15\ud55c \ubd88\ub9cc, \ud53c\ubd80 \ud2b8\ub7ec\ube14, \uc0ac\uc6a9 \uc911\ub2e8\n\n## Few-Shot Examples:\n5\uc810: \"\uc7ac\uc7ac\uc7ac\uc7ac\uad6c\ub9e4 \ub108\ubb34 \uc88b\uc544\uc694\", \"\ub2ec\ubc14\ub294 \uc9c4\ub9ac\uc785\ub2c8\ub2e4\", \"\uba87\ub144\uc9f8 \uc4f0\ub294 \uafc0\ud15c\uc774\uc5d0\uc694\"\n4\uc810: \"\uad1c\ucc2e\ub124\uc694\", \"\ud5a5\ub3c4 \uc88b\uace0 \ucd09\ucd09\ud558\ub2c8 \uc88b\uc544\uc694\", \"\ub2e4\uc4f0\uba74 \ub610 \uad6c\ub9e4\uc758\ud5a5 \uc788\uc5b4\uc694\"\n3\uc810: \"\ub354 \uc368\ubd10\uc57c\uc54c\uac70 \uac19\uc544\uc694\", \"\uc0dd\uac01\ubcf4\ub2e4 \uadf8\ub0e5 \uc3d8\uc3d8\uc5d0\uc694\"\n2\uc810: \"\ud5a5\uc774 \uac15\ud574\uc11c \uc800\ub294 \ubcc4\ub85c\", \"\uae30\ub300\ub9cc\ud07c\uc740 \uc544\ub2c8\uc5d0\uc694\"\n1\uc810: \"\ud53c\ubd80 \ubd89\uc5b4\uc9c0\uace0 \uc54c\ub7ec\uc9c0 \ubc18\uc751\", \"\ub208\uc2dc\ub9bc\uc774 \uc2ec\ud558\ub124\uc694\", \"\ubabb\uc4f0\uac8c\ub418\uc11c \uc18d\uc0c1\"\n\n\ub9ac\ubdf0\ub97c \ubd84\uc11d\ud558\uc5ec JSON \ud615\uc2dd\uc73c\ub85c \ucd9c\ub825\ud558\uc138\uc694:\n{\n \"sentiment_score\": 1-5,\n \"sentiment_label\": \"Very satisfied\" | \"Satisfied\" | \"Neutral\" | \"Dissatisfied\" | \"Very dissatisfied\",\n \"emotions\": [\"\uae30\uc068\", \"\uc2e4\ub9dd\"] \ub4f1 \uac10\uc9c0\ub41c \uac10\uc815 \ub9ac\uc2a4\ud2b8,\n \"key_points\": [\"\ubcf4\uc2b5\ub825 \uc88b\uc74c\", \"\ub048\uc801\uc784\"] \ub4f1 \ud575\uc2ec \ud3ec\uc778\ud2b8\n}"
},
{
"role": "user",
"content": "=\ub9ac\ubdf0 \ud3c9\uc810: {{ $json.rating }}/5\n\ub9ac\ubdf0 \ub0b4\uc6a9: {{ $json.content }}"
}
]
}
},
"credentials": {
"openAiApi": {
"name": "<your credential>"
}
}
},
{
"id": "code_sentiment",
"name": "Parse Sentiment Result",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1850,
200
],
"parameters": {
"jsCode": "// Sentiment Analysis \uacb0\uacfc \ud30c\uc2f1 \ubc0f \uc6d0\ubcf8 \ub370\uc774\ud130\uc640 \ubcd1\ud569\nconst item = items[0].json;\n\n// AI \ubd84\uc11d \uacb0\uacfc \ud30c\uc2f1\nlet sentimentResult = {};\ntry {\n sentimentResult = JSON.parse(item.message?.content || '{}');\n} catch (e) {\n console.error('Sentiment parsing error:', e);\n sentimentResult = {\n sentiment_score: 3,\n sentiment_label: '\uc911\ub9bd',\n emotions: [],\n key_points: []\n };\n}\n\n// \uc6d0\ubcf8 \ub9ac\ubdf0 \ub370\uc774\ud130\uc640 \ubcd1\ud569\nreturn [{\n json: {\n // \uc6d0\ubcf8 \ub370\uc774\ud130 \uc720\uc9c0\n review_id: $('Filter New Reviews').item.json.review_id,\n product_number: $('Filter New Reviews').item.json.product_number,\n content: $('Filter New Reviews').item.json.content,\n rating: $('Filter New Reviews').item.json.rating,\n created_at: $('Filter New Reviews').item.json.created_at,\n author: $('Filter New Reviews').item.json.author,\n like_count: $('Filter New Reviews').item.json.like_count,\n verified_purchase: $('Filter New Reviews').item.json.verified_purchase,\n skin_type: $('Filter New Reviews').item.json.skin_type,\n age_group: $('Filter New Reviews').item.json.age_group,\n \n // Sentiment \uacb0\uacfc \ucd94\uac00\n sentiment_score: sentimentResult.sentiment_score,\n sentiment_label: sentimentResult.sentiment_label,\n emotions: JSON.stringify(sentimentResult.emotions || []),\n key_points: JSON.stringify(sentimentResult.key_points || [])\n }\n}];"
}
},
{
"id": "openai_2",
"name": "ABSA Analysis",
"type": "n8n-nodes-base.openAi",
"typeVersion": 1,
"position": [
2050,
200
],
"parameters": {
"resource": "chat",
"model": "solar-pro",
"options": {
"temperature": 0.2,
"responseFormat": "json_object",
"baseURL": "https://api.upstage.ai/v1/solar"
},
"messages": {
"values": [
{
"role": "system",
"content": "Aspect-Based Sentiment Analysis (ABSA) \uc804\ubb38\uac00\uc785\ub2c8\ub2e4.\n\n\ud654\uc7a5\ud488 \ub9ac\ubdf0\uc5d0\uc11c \ub2e4\uc74c \uc18d\uc131\ubcc4 \uac10\uc815\uc744 \ucd94\ucd9c\ud558\uc138\uc694:\n- \ubcf4\uc2b5\ub825 (hydration)\n- \ud761\uc218\ub825 (absorption)\n- \ub048\uc801\uc784 (stickiness)\n- \ud5a5 (fragrance)\n- \uc790\uadf9\uc131 (irritation)\n- \uac00\uc131\ube44 (value)\n- \ud6a8\uacfc (effectiveness)\n\nJSON \ud615\uc2dd:\n{\n \"aspects\": [\n {\n \"name\": \"\ubcf4\uc2b5\ub825\",\n \"sentiment\": \"\uae0d\uc815\" | \"\uc911\ub9bd\" | \"\ubd80\uc815\",\n \"score\": 1-5,\n \"mentioned\": true,\n \"quote\": \"\uad00\ub828 \ub9ac\ubdf0 \uc778\uc6a9\ubb38\"\n }\n ]\n}"
},
{
"role": "user",
"content": "={{ $json.content }}"
}
]
}
},
"credentials": {
"openAiApi": {
"name": "<your credential>"
}
}
},
{
"id": "code_absa",
"name": "Parse ABSA Result",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2250,
200
],
"parameters": {
"jsCode": "// ABSA \uacb0\uacfc \ud30c\uc2f1 \ubc0f \ubcd1\ud569\nconst item = items[0].json;\n\n// ABSA \uacb0\uacfc \ud30c\uc2f1\nlet absaResult = {};\ntry {\n absaResult = JSON.parse(item.message?.content || '{}');\n} catch (e) {\n console.error('ABSA parsing error:', e);\n absaResult = { aspects: [] };\n}\n\n// \uc774\uc804 Parse Sentiment Result\uc758 \ubaa8\ub4e0 \ub370\uc774\ud130 + ABSA \uacb0\uacfc\nreturn [{\n json: {\n // Parse Sentiment Result\uc5d0\uc11c \uc628 \ub370\uc774\ud130\n review_id: $('Parse Sentiment Result').item.json.review_id,\n product_number: $('Parse Sentiment Result').item.json.product_number,\n content: $('Parse Sentiment Result').item.json.content,\n rating: $('Parse Sentiment Result').item.json.rating,\n created_at: $('Parse Sentiment Result').item.json.created_at,\n author: $('Parse Sentiment Result').item.json.author,\n like_count: $('Parse Sentiment Result').item.json.like_count,\n verified_purchase: $('Parse Sentiment Result').item.json.verified_purchase,\n skin_type: $('Parse Sentiment Result').item.json.skin_type,\n age_group: $('Parse Sentiment Result').item.json.age_group,\n sentiment_score: $('Parse Sentiment Result').item.json.sentiment_score,\n sentiment_label: $('Parse Sentiment Result').item.json.sentiment_label,\n emotions: $('Parse Sentiment Result').item.json.emotions,\n key_points: $('Parse Sentiment Result').item.json.key_points,\n \n // ABSA \uacb0\uacfc \ucd94\uac00\n absa_aspects: JSON.stringify(absaResult.aspects || [])\n }\n}];"
}
},
{
"id": "openai_3",
"name": "Generate Embedding",
"type": "n8n-nodes-base.openAi",
"typeVersion": 1,
"position": [
2450,
200
],
"parameters": {
"resource": "embedding",
"model": "text-embedding-3-small",
"text": "={{ $json.content }}"
},
"credentials": {
"openAiApi": {
"name": "<your credential>"
}
}
},
{
"id": "code_2",
"name": "Prepare Review Data",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2650,
200
],
"parameters": {
"jsCode": "// \ucd5c\uc885 \ub9ac\ubdf0 \ub370\uc774\ud130 \uc900\ube44 (Parse ABSA Result + Embedding)\nconst absaData = $('Parse ABSA Result').item.json;\nconst embeddingData = items[0].json;\n\n// \uc784\ubca0\ub529 \ubca1\ud130 \ucd94\ucd9c\nconst embedding = embeddingData.data?.[0]?.embedding || [];\n\nreturn [{\n json: {\n // Parse ABSA Result\uc758 \ubaa8\ub4e0 \ub370\uc774\ud130\n review_id: absaData.review_id,\n product_number: absaData.product_number,\n content: absaData.content,\n rating: absaData.rating,\n created_at: absaData.created_at,\n author: absaData.author,\n like_count: absaData.like_count,\n verified_purchase: absaData.verified_purchase,\n skin_type: absaData.skin_type,\n age_group: absaData.age_group,\n sentiment_score: absaData.sentiment_score,\n sentiment_label: absaData.sentiment_label,\n emotions: absaData.emotions,\n key_points: absaData.key_points,\n absa_aspects: absaData.absa_aspects,\n \n // \uc784\ubca0\ub529 \ucd94\uac00\n embedding: JSON.stringify(embedding),\n \n processed_at: new Date().toISOString()\n }\n}];"
}
},
{
"id": "postgres_3",
"name": "Save to PostgreSQL",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.4,
"position": [
2850,
200
],
"parameters": {
"operation": "insert",
"schema": {
"__rl": true,
"value": "public",
"mode": "list"
},
"table": {
"__rl": true,
"value": "beauty_reviews",
"mode": "list"
},
"columns": {
"mappingMode": "defineBelow",
"value": {
"review_id": "={{ $json.review_id }}",
"product_number": "={{ $json.product_number }}",
"content": "={{ $json.content }}",
"rating": "={{ $json.rating }}",
"created_at": "={{ $json.created_at }}",
"author": "={{ $json.author }}",
"like_count": "={{ $json.like_count }}",
"verified_purchase": "={{ $json.verified_purchase }}",
"skin_type": "={{ $json.skin_type }}",
"age_group": "={{ $json.age_group }}",
"sentiment_score": "={{ $json.sentiment_score }}",
"sentiment_label": "={{ $json.sentiment_label }}",
"emotions": "={{ $json.emotions }}",
"key_points": "={{ $json.key_points }}",
"absa_aspects": "={{ $json.absa_aspects }}",
"processed_at": "={{ $json.processed_at }}"
},
"matchingColumns": [],
"schema": [
{
"id": "review_id",
"displayName": "review_id",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "product_number",
"displayName": "product_number",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "content",
"displayName": "content",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "rating",
"displayName": "rating",
"required": false,
"defaultMatch": false,
"display": true,
"type": "number",
"canBeUsedToMatch": true
},
{
"id": "created_at",
"displayName": "created_at",
"required": false,
"defaultMatch": false,
"display": true,
"type": "dateTime",
"canBeUsedToMatch": true
},
{
"id": "author",
"displayName": "author",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "like_count",
"displayName": "like_count",
"required": false,
"defaultMatch": false,
"display": true,
"type": "number",
"canBeUsedToMatch": true
},
{
"id": "verified_purchase",
"displayName": "verified_purchase",
"required": false,
"defaultMatch": false,
"display": true,
"type": "boolean",
"canBeUsedToMatch": true
},
{
"id": "skin_type",
"displayName": "skin_type",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "age_group",
"displayName": "age_group",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "sentiment_score",
"displayName": "sentiment_score",
"required": false,
"defaultMatch": false,
"display": true,
"type": "number",
"canBeUsedToMatch": true
},
{
"id": "sentiment_label",
"displayName": "sentiment_label",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "emotions",
"displayName": "emotions",
"required": false,
"defaultMatch": false,
"display": true,
"type": "json",
"canBeUsedToMatch": true
},
{
"id": "key_points",
"displayName": "key_points",
"required": false,
"defaultMatch": false,
"display": true,
"type": "json",
"canBeUsedToMatch": true
},
{
"id": "absa_aspects",
"displayName": "absa_aspects",
"required": false,
"defaultMatch": false,
"display": true,
"type": "json",
"canBeUsedToMatch": true
},
{
"id": "processed_at",
"displayName": "processed_at",
"required": false,
"defaultMatch": false,
"display": true,
"type": "dateTime",
"canBeUsedToMatch": true
}
]
},
"options": {
"outputColumns": [
"id"
]
}
},
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"id": "qdrant_1",
"name": "Save to Qdrant",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 3,
"position": [
3050,
200
],
"parameters": {
"method": "PUT",
"url": "http://qdrant:6333/collections/beauty_reviews/points",
"authentication": "none",
"sendBody": true,
"bodyParameters": {
"parameters": [
{
"name": "points",
"value": "=[{\n \"id\": {{ $json.id }},\n \"vector\": {{ $json.embedding }},\n \"payload\": {\n \"review_id\": \"{{ $json.review_id }}\",\n \"product_number\": \"{{ $json.product_number }}\",\n \"content\": \"{{ $json.content }}\",\n \"rating\": {{ $json.rating }},\n \"sentiment_score\": {{ $json.sentiment_score }},\n \"sentiment_label\": \"{{ $json.sentiment_label }}\",\n \"created_at\": \"{{ $json.created_at }}\"\n }\n}]"
}
]
},
"options": {
"bodyContentType": "json"
}
}
},
{
"id": "webhook_response_1",
"name": "Parse User Query",
"type": "n8n-nodes-base.openAi",
"typeVersion": 1,
"position": [
450,
600
],
"parameters": {
"resource": "chat",
"model": "solar-pro",
"options": {
"temperature": 0.2,
"responseFormat": "json_object",
"baseURL": "https://api.upstage.ai/v1/solar"
},
"messages": {
"values": [
{
"role": "system",
"content": "\uc0ac\uc6a9\uc790 \ucffc\ub9ac \ubd84\uc11d \uc804\ubb38\uac00\uc785\ub2c8\ub2e4.\n\n\uc785\ub825\ub41c \uc9c8\ubb38\uc744 \ubd84\uc11d\ud558\uc5ec JSON \ud615\uc2dd\uc73c\ub85c \ucd9c\ub825:\n{\n \"intent\": \"\ucd94\ucc9c\" | \"\ube44\uad50\" | \"\ub9ac\ubdf0\uc694\uc57d\" | \"\uc815\ubcf4\uc870\ud68c\",\n \"keywords\": [\"\ud575\uc2ec\ud0a4\uc6cc\ub4dc1\", \"\ud575\uc2ec\ud0a4\uc6cc\ub4dc2\"],\n \"filters\": {\n \"skin_type\": \"\uac74\uc131\" | \"\uc9c0\uc131\" | \"\ubcf5\ud569\uc131\" | null,\n \"price_range\": \"\uc800\uac00\" | \"\uc911\uac00\" | \"\uace0\uac00\" | null,\n \"concern\": \"\ubaa8\uacf5\" | \"\uc8fc\ub984\" | \"\ubbf8\ubc31\" | null\n },\n \"product_category\": \"\ud1a0\ub108\" | \"\uc5d0\uc13c\uc2a4\" | \"\ud06c\ub9bc\" | null\n}"
},
{
"role": "user",
"content": "={{ $json.body.query || $json.query }}"
}
]
}
},
"credentials": {
"openAiApi": {
"name": "<your credential>"
}
}
},
{
"id": "embedding_query_1",
"name": "Generate Query Embedding",
"type": "n8n-nodes-base.openAi",
"typeVersion": 1,
"position": [
650,
600
],
"parameters": {
"resource": "embedding",
"model": "text-embedding-3-small",
"text": "={{ $json.body.query || $json.query }}"
},
"credentials": {
"openAiApi": {
"name": "<your credential>"
}
}
},
{
"id": "qdrant_search_1",
"name": "Vector Search - Qdrant",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 3,
"position": [
850,
500
],
"parameters": {
"method": "POST",
"url": "http://qdrant:6333/collections/beauty_reviews/points/search",
"authentication": "none",
"sendBody": true,
"contentType": "application/json",
"body": "={\n \"vector\": {{ $json.data[0].embedding }},\n \"limit\": 20,\n \"with_payload\": true,\n \"score_threshold\": 0.7,\n \"filter\": {\n \"must\": [\n {\n \"key\": \"sentiment_score\",\n \"range\": {\n \"gte\": 3\n }\n }\n ]\n }\n}"
}
},
{
"id": "postgres_search_1",
"name": "Keyword Search - PostgreSQL",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.4,
"position": [
850,
700
],
"parameters": {
"operation": "executeQuery",
"query": "=SELECT \n br.*,\n p.name as product_name,\n p.price,\n p.brand\nFROM beauty_reviews br\nJOIN products p ON br.product_number = p.product_number\nWHERE \n br.sentiment_score >= 4\n AND (br.content ILIKE '%{{ $json.message.content ? JSON.parse($json.message.content).keywords[0] : '' }}%' \n OR br.content ILIKE '%{{ $json.message.content ? JSON.parse($json.message.content).keywords[1] : '' }}%')\nORDER BY br.like_count DESC, br.created_at DESC\nLIMIT 20;"
},
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"id": "merge_results_1",
"name": "Merge Search Results",
"type": "n8n-nodes-base.code",
"typeVersion": 1,
"position": [
1050,
600
],
"parameters": {
"mode": "runOnceForAllItems",
"jsCode": "// Vector Search\uc640 Keyword Search \uacb0\uacfc \ubcd1\ud569\nconst vectorResults = items.find(i => i.json.result)?.json.result || [];\nconst keywordResults = items.filter(i => i.json.content && !i.json.result) || [];\n\nconst mergedResults = new Map();\n\n// Vector Search \uacb0\uacfc (\uac00\uc911\uce58 0.6)\nvectorResults.forEach(item => {\n const id = item.id || item.payload.review_id;\n mergedResults.set(id, {\n ...item.payload,\n vector_score: item.score * 0.6,\n keyword_score: 0,\n total_score: item.score * 0.6,\n source: 'vector'\n });\n});\n\n// Keyword Search \uacb0\uacfc (\uac00\uc911\uce58 0.4)\nkeywordResults.forEach((item, idx) => {\n const id = item.json.review_id;\n const keywordScore = Math.max(0, (20 - idx) / 20) * 0.4;\n \n if (mergedResults.has(id)) {\n const existing = mergedResults.get(id);\n existing.keyword_score = keywordScore;\n existing.total_score += keywordScore;\n existing.source = 'both';\n } else {\n mergedResults.set(id, {\n ...item.json,\n vector_score: 0,\n keyword_score: keywordScore,\n total_score: keywordScore,\n source: 'keyword'\n });\n }\n});\n\n// \uc810\uc218 \uc21c \uc815\ub82c \ud6c4 Top 15\nconst sortedResults = Array.from(mergedResults.values())\n .sort((a, b) => b.total_score - a.total_score)\n .slice(0, 15);\n\nreturn [{\n json: {\n reviews: sortedResults,\n review_count: sortedResults.length,\n avg_rating: sortedResults.reduce((sum, r) => sum + (r.rating || 0), 0) / sortedResults.length,\n avg_sentiment: sortedResults.reduce((sum, r) => sum + (r.sentiment_score || 0), 0) / sortedResults.length\n }\n}];"
}
},
{
"id": "absa_aggregation_1",
"name": "Aggregate ABSA Data",
"type": "n8n-nodes-base.code",
"typeVersion": 1,
"position": [
1250,
600
],
"parameters": {
"jsCode": "// ABSA \uc18d\uc131\ubcc4 \uc9d1\uacc4\nconst reviews = items[0].json.reviews || [];\nconst aspectMap = new Map();\n\nreviews.forEach(review => {\n try {\n const aspects = JSON.parse(review.absa_aspects || '[]');\n \n aspects.forEach(aspect => {\n if (!aspect.mentioned) return;\n \n if (!aspectMap.has(aspect.name)) {\n aspectMap.set(aspect.name, {\n name: aspect.name,\n positive: 0,\n neutral: 0,\n negative: 0,\n total_score: 0,\n count: 0,\n examples: []\n });\n }\n \n const data = aspectMap.get(aspect.name);\n data[aspect.sentiment] = (data[aspect.sentiment] || 0) + 1;\n data.total_score += aspect.score;\n data.count += 1;\n \n if (data.examples.length < 3 && aspect.quote) {\n data.examples.push(aspect.quote);\n }\n });\n } catch (e) {\n // Skip invalid JSON\n }\n});\n\nconst aggregatedAspects = Array.from(aspectMap.values()).map(aspect => ({\n ...aspect,\n avg_score: aspect.total_score / aspect.count,\n sentiment_ratio: {\n positive: aspect.positive / aspect.count,\n neutral: aspect.neutral / aspect.count,\n negative: aspect.negative / aspect.count\n }\n}));\n\nreturn [{\n json: {\n ...items[0].json,\n absa_summary: aggregatedAspects\n }\n}];"
}
},
{
"id": "generate_answer_1",
"name": "Generate Final Answer",
"type": "n8n-nodes-base.openAi",
"typeVersion": 1,
"position": [
1450,
600
],
"parameters": {
"resource": "chat",
"model": "solar-pro",
"options": {
"temperature": 0.7,
"maxTokens": 1500,
"baseURL": "https://api.upstage.ai/v1/solar"
},
"messages": {
"values": [
{
"role": "system",
"content": "\ub2f9\uc2e0\uc740 10\ub144 \uacbd\ub825\uc758 \ubdf0\ud2f0 MD \ucd9c\uc2e0 \uc1fc\ud551 \uc5b4\ub4dc\ubc14\uc774\uc800\uc785\ub2c8\ub2e4.\n\n\uc218\uc9d1\ub41c \uc2e4\uc81c \ub9ac\ubdf0 \ub370\uc774\ud130\ub97c \uae30\ubc18\uc73c\ub85c \uc194\uc9c1\ud558\uace0 \uadfc\uac70 \uc788\ub294 \uc870\uc5b8\uc744 \uc81c\uacf5\ud558\uc138\uc694.\n\n# \ub2f5\ubcc0 \uad6c\uc870\n\n## \ud83c\udfaf 3\uc904 \uc694\uc57d\n\ud575\uc2ec \ub0b4\uc6a9\uc744 3\ubb38\uc7a5\uc73c\ub85c \uc694\uc57d\n\n## \ud83d\udcca \ub370\uc774\ud130 \uae30\ubc18 \ubd84\uc11d\n- \ubd84\uc11d\ud55c \ub9ac\ubdf0 \uc218: X\uac1c\n- \ud3c9\uade0 \ud3c9\uc810: X.X/5\n- \ud3c9\uade0 \uac10\uc815 \uc810\uc218: X.X/5\n\n## \ud83d\udd0d \uc18d\uc131\ubcc4 \ud3c9\uac00\n\uac01 \uc18d\uc131(\ubcf4\uc2b5\ub825, \ud761\uc218\ub825, \ud5a5 \ub4f1)\uc5d0 \ub300\ud574:\n- \uc810\uc218\uc640 \uae0d\uc815/\ubd80\uc815 \ube44\uc728\n- \uc2e4\uc81c \ub9ac\ubdf0 \uc778\uc6a9\n\n## \u2696\ufe0f \uc7a5\ub2e8\uc810\n### \u2705 \uc7a5\uc810\n- \uad6c\uccb4\uc801 \uc7a5\uc810 (\ub9ac\ubdf0 \uc778\uc6a9 \ud3ec\ud568)\n\n### \u274c \ub2e8\uc810\n- \uad6c\uccb4\uc801 \ub2e8\uc810 (\ub9ac\ubdf0 \uc778\uc6a9 \ud3ec\ud568)\n\n## \u26a0\ufe0f \uc8fc\uc758\uc0ac\ud56d\n\ud2b9\uc815 \ud53c\ubd80 \ud0c0\uc785\uc774\ub098 \uc0c1\ud669\uc5d0\uc11c\uc758 \uc8fc\uc758\uc810\n\n## \ud83d\udca1 \ucd5c\uc885 \ud310\ub2e8\n\ucd94\ucc9c \uc5ec\ubd80\uc640 \uadf8 \uc774\uc720\n\n---\n\ubc18\ub4dc\uc2dc \ud1b5\uacc4\uc640 \uc2e4\uc81c \ub9ac\ubdf0\ub97c \uc778\uc6a9\ud558\uc5ec \uadfc\uac70\ub97c \uc81c\uc2dc\ud558\uc138\uc694."
},
{
"role": "user",
"content": "=# \uc0ac\uc6a9\uc790 \uc9c8\ubb38\n{{ $json.body.query || $json.query }}\n\n# \ubd84\uc11d \ub370\uc774\ud130\n\ub9ac\ubdf0 \uc218: {{ $json.review_count }}\uac1c\n\ud3c9\uade0 \ud3c9\uc810: {{ $json.avg_rating }}/5\n\ud3c9\uade0 \uac10\uc815 \uc810\uc218: {{ $json.avg_sentiment }}/5\n\n# ABSA \uc18d\uc131\ubcc4 \uc9d1\uacc4\n{{ JSON.stringify($json.absa_summary, null, 2) }}\n\n# \uc8fc\uc694 \ub9ac\ubdf0 \uc0d8\ud50c (Top 5)\n{{ $json.reviews.slice(0, 5).map((r, i) => `${i+1}. [${r.rating}/5] ${r.content} (\uc88b\uc544\uc694: ${r.like_count})`).join('\\n') }}"
}
]
}
},
"credentials": {
"openAiApi": {
"name": "<your credential>"
}
}
},
{
"id": "postgres_log_1",
"name": "Log Query",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.4,
"position": [
1650,
600
],
"parameters": {
"operation": "insert",
"schema": {
"__rl": true,
"value": "public",
"mode": "list"
},
"table": {
"__rl": true,
"value": "query_logs",
"mode": "list"
},
"columns": {
"mappingMode": "defineBelow",
"value": {
"user_id": "={{ $('User Query Webhook').item.json.body?.user_id || $('User Query Webhook').item.json.user_id }}",
"query": "={{ $('User Query Webhook').item.json.body?.query || $('User Query Webhook').item.json.query }}",
"answer": "={{ $json.message?.content }}",
"reviews_analyzed": "={{ $json.review_count }}",
"avg_rating": "={{ $json.avg_rating }}",
"avg_sentiment": "={{ $json.avg_sentiment }}",
"response_time_ms": "={{ Math.round((Date.now() - new Date($workflow.startTime).getTime())) }}"
}
},
"options": {
"outputColumns": [
"id"
]
}
},
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"id": "respond_1",
"name": "Respond to Webhook",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1,
"position": [
1850,
600
],
"parameters": {
"options": {
"responseCode": 200,
"responseHeaders": {
"entries": [
{
"name": "Content-Type",
"value": "application/json"
}
]
}
},
"respondWith": "json",
"responseBody": "={\n \"success\": true,\n \"answer\": {{ JSON.stringify($json.message.content) }},\n \"metadata\": {\n \"reviews_analyzed\": {{ $json.review_count }},\n \"avg_rating\": {{ $json.avg_rating }},\n \"avg_sentiment\": {{ $json.avg_sentiment }},\n \"absa_aspects\": {{ $json.absa_summary.length }},\n \"generated_at\": \"{{ $now }}\"\n }\n}"
}
},
{
"id": "slack_1",
"name": "Slack Notification",
"type": "n8n-nodes-base.slack",
"typeVersion": 1,
"position": [
3250,
200
],
"parameters": {
"operation": "post",
"channelId": "#beauty-agent-logs",
"text": "=\u2705 \ud06c\ub864\ub9c1 \uc644\ub8cc!\n\n\ud83d\udcca \ucc98\ub9ac \uacb0\uacfc:\n- \uc218\uc9d1\ub41c \ub9ac\ubdf0: {{ $json.total_reviews || 0 }}\uac1c\n- \uc0c8\ub85c\uc6b4 \ub9ac\ubdf0: {{ $json.new_reviews || 0 }}\uac1c\n- \ubca1\ud130 \uc800\uc7a5: {{ $json.vectors_saved || 0 }}\uac1c\n\n\u23f0 \uc2e4\ud589 \uc2dc\uac04: {{ $workflow.startTime }}\n\u23ed\ufe0f \ub2e4\uc74c \uc2e4\ud589: \ub0b4\uc77c \uc790\uc815",
"otherOptions": {
"includeLinkToWorkflow": true
}
},
"credentials": {
"slackApi": {
"name": "<your credential>"
}
}
}
],
"connections": {
"trigger_1": {
"main": [
[
{
"node": "postgres_1",
"type": "main",
"index": 0
}
]
]
},
"postgres_1": {
"main": [
[
{
"node": "split_1",
"type": "main",
"index": 0
}
]
]
},
"split_1": {
"main": [
[
{
"node": "http_1",
"type": "main",
"index": 0
}
]
]
},
"http_1": {
"main": [
[
{
"node": "code_1",
"type": "main",
"index": 0
}
]
]
},
"code_1": {
"main": [
[
{
"node": "postgres_2",
"type": "main",
"index": 0
}
]
]
},
"postgres_2": {
"main": [
[
{
"node": "if_1",
"type": "main",
"index": 0
}
]
]
},
"if_1": {
"main": [
[
{
"node": "openai_1",
"type": "main",
"index": 0
}
]
]
},
"openai_1": {
"main": [
[
{
"node": "code_sentiment",
"type": "main",
"index": 0
}
]
]
},
"code_sentiment": {
"main": [
[
{
"node": "openai_2",
"type": "main",
"index": 0
}
]
]
},
"openai_2": {
"main": [
[
{
"node": "code_absa",
"type": "main",
"index": 0
}
]
]
},
"code_absa": {
"main": [
[
{
"node": "openai_3",
"type": "main",
"index": 0
}
]
]
},
"openai_3": {
"main": [
[
{
"node": "code_2",
"type": "main",
"index": 0
}
]
]
},
"code_2": {
"main": [
[
{
"node": "postgres_3",
"type": "main",
"index": 0
}
]
]
},
"postgres_3": {
"main": [
[
{
"node": "qdrant_1",
"type": "main",
"index": 0
}
]
]
},
"qdrant_1": {
"main": [
[
{
"node": "slack_1",
"type": "main",
"index": 0
}
]
]
},
"webhook_1": {
"main": [
[
{
"node": "webhook_response_1",
"type": "main",
"index": 0
}
]
]
},
"webhook_response_1": {
"main": [
[
{
"node": "embedding_query_1",
"type": "main",
"index": 0
},
{
"node": "postgres_search_1",
"type": "main",
"index": 0
}
]
]
},
"embedding_query_1": {
"main": [
[
{
"node": "qdrant_search_1",
"type": "main",
"index": 0
}
]
]
},
"qdrant_search_1": {
"main": [
[
{
"node": "merge_results_1",
"type": "main",
"index": 0
}
]
]
},
"postgres_search_1": {
"main": [
[
{
"node": "merge_results_1",
"type": "main",
"index": 0
}
]
]
},
"merge_results_1": {
"main": [
[
{
"node": "absa_aggregation_1",
"type": "main",
"index": 0
}
]
]
},
"absa_aggregation_1": {
"main": [
[
{
"node": "generate_answer_1",
"type": "main",
"index": 0
}
]
]
},
"generate_answer_1": {
"main": [
[
{
"node": "postgres_log_1",
"type": "main",
"index": 0
}
]
]
},
"postgres_log_1": {
"main": [
[
{
"node": "respond_1",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1"
},
"staticData": null,
"tags": [],
"triggerCount": 2,
"updatedAt": "2026-02-04T00:00:00.000Z",
"versionId": "1"
}
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.
openAiApipostgresslackApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Beauty Kurly Shopping Agent - Complete Workflow. Uses postgres, httpRequest, openAi, slack. Scheduled trigger; 26 nodes.
Source: https://github.com/juny79/self-hosted-ai-starter-kit/blob/eac23d291a8c99832df222e081077ba1e2176864/n8n/demo-data/workflows/workflow-beauty-kurly-shopping-agent.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.
Complete PostgreSQL-backed system: Keyword scoring → AI research → Multi-part content generation → fal.ai Nano Banana image generation → WordPress publishing
Marketing, content, and enablement teams that need a quick, human-readable summary of every new video published by the YouTube channels they care about—without leaving Slack.
This n8n template builds an automated daily news digest powered by Claude AI.
This workflow is designed for Japanese-speaking professionals, and learners who want to efficiently stay up to date with practical productivity, lifehack, and efficiency-related insights from Japanese
Automates sales data analysis and strategic insight generation for sales managers and strategists needing actionable intelligence. Fetches multi-source data from sales, marketing, and financial system