AutomationFlowsAI & RAG › Automated Kurly Beauty Product Review Agent

Automated Kurly Beauty Product Review Agent

Original n8n title: Beauty Kurly Shopping Agent - Complete Workflow

Beauty Kurly Shopping Agent - Complete Workflow. Uses postgres, httpRequest, openAi, slack. Scheduled trigger; 26 nodes.

Cron / scheduled trigger★★★★☆ complexityAI-powered26 nodesPostgresHTTP RequestOpenAISlack
AI & RAG Trigger: Cron / scheduled Nodes: 26 Complexity: ★★★★☆ AI nodes: yes Added:

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 →

Download .json
{
  "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.

Pro

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 →

More AI & RAG workflows → · Browse all categories →

Related workflows

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

AI & RAG

Complete PostgreSQL-backed system: Keyword scoring → AI research → Multi-part content generation → fal.ai Nano Banana image generation → WordPress publishing

WordPress, OpenAI, Perplexity +8
AI & RAG

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.

HTTP Request, Google Sheets, XML +7
AI & RAG

This n8n template builds an automated daily news digest powered by Claude AI.

Postgres, HTTP Request, Chain Llm +3
AI & RAG

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

RSS Feed Read, Chain Llm, Google Gemini Chat +7
AI & RAG

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

HTTP Request, Agent, OpenAI Chat +6