{ "name": "Classify customer feedback with DeepSeek, store it in a free Cradler database and get AI reports", "nodes": [ { "parameters": { "content": "## Classify customer feedback and get AI reports, stored in a free database\n\nA feedback form for your users plus a report page for your team. Every message is classified by an LLM (category, sentiment, urgency, one-line summary, tags) and saved to a free [Cradler](https://cradler.ai) database, which creates the table and columns on the first write. The report page reads the last 7 or 30 days and has Claude write the themes, top issues and suggested actions. The models run through one key on [apimodels.app](https://apimodels.app).\n\n### How it works\n1. **Feedback form** collects the message, product area and an optional email.\n2. DeepSeek V4.1 Flash classifies it and returns JSON; the row is saved to the `feedback` table.\n3. **Report form** picks a period; the workflow loads that period's rows, counts them by category, sentiment, urgency and area, and Claude Sonnet 5.5 writes the report.\n\n### Setup\n1. Create an API key in the [apimodels console](https://apimodels.app/console/api-keys) and add it as a **Bearer Auth** credential named \"apimodels API key\".\n2. Create a free project at [cradler.ai](https://cradler.ai) and add its **service** key as a second Bearer Auth credential named \"Cradler service key\".\n3. In **Settings**, replace `YOUR_PROJECT_ID` in `cradler_api` with your Cradler project id.\n4. Activate the workflow. Share the feedback form; keep the report form for your team (turn on Basic Auth there).\nStep-by-step guide: [apimodels.app/docs/n8n](https://apimodels.app/docs/n8n)\n\n### Customize\n- Edit categories in **Classify feedback** and the report sections in **Write report**.\n- Add a Slack or email node after **Save to Cradler** to alert on high urgency.", "height": 940, "width": 560, "color": 1 }, "id": "9dbf5a29-e4f5-5981-8ae8-0579288d99d9", "name": "Overview", "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [ -700, -380 ] }, { "parameters": { "content": "## 1. Classify and store\nEach message is classified as JSON and saved as one row in Cradler.", "height": 340, "width": 1120, "color": 7 }, "id": "b946e899-f717-56b7-aac2-43fac675044f", "name": "Section: intake", "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [ 380, -160 ] }, { "parameters": { "content": "## 2. Report on demand\nLoads the period from Cradler, counts it, and Claude writes the report.", "height": 340, "width": 1340, "color": 7 }, "id": "d0f13ed1-0bef-5bfd-a986-c04319c3018d", "name": "Section: report", "type": "n8n-nodes-base.stickyNote", "typeVersion": 1, "position": [ 380, 260 ] }, { "parameters": { "formTitle": "Send us feedback", "formDescription": "Tell us what works, what is broken or what you wish we had. We read every message.", "formFields": { "values": [ { "fieldLabel": "Your feedback", "fieldType": "textarea", "requiredField": true }, { "fieldLabel": "Area", "fieldType": "dropdown", "requiredField": true, "fieldOptions": { "values": [ { "option": "General" }, { "option": "Onboarding" }, { "option": "Billing" }, { "option": "Performance" }, { "option": "Integrations" } ] } }, { "fieldLabel": "Email", "fieldType": "email", "placeholder": "Optional, if you want a reply" } ] }, "options": { "path": "feedback", "buttonLabel": "Send" } }, "id": "13341c77-ae64-58af-a401-306a02c006a3", "name": "Feedback form", "type": "n8n-nodes-base.formTrigger", "typeVersion": 2.2, "position": [ 0, 0 ], "webhookId": "aacc4470-920c-55cd-a0f8-5eac01935c61" }, { "parameters": { "formTitle": "Feedback report", "formDescription": "Summarise recent feedback with AI.", "formFields": { "values": [ { "fieldLabel": "Period", "fieldType": "dropdown", "requiredField": true, "fieldOptions": { "values": [ { "option": "Last 7 days" }, { "option": "Last 30 days" } ] } } ] }, "options": { "path": "feedback-report", "buttonLabel": "Build report" } }, "id": "97afb435-99dd-5da2-929d-ba157fad3ff1", "name": "Report form", "type": "n8n-nodes-base.formTrigger", "typeVersion": 2.2, "position": [ 0, 420 ], "webhookId": "b840fb1f-be87-5a1e-9b69-4d4a32f8b6e3" }, { "parameters": { "assignments": { "assignments": [ { "id": "50f332ac-afb5-593d-9b75-90ef3bfdc2b0", "name": "api_base", "value": "https://api.apimodels.app/v1", "type": "string" }, { "id": "618dff81-f3f9-5461-91d7-a01b1615c8c2", "name": "cradler_api", "value": "https://gateway.cradler.ai/v1/YOUR_PROJECT_ID", "type": "string" }, { "id": "acc6b53d-0289-5653-b7c0-6bd2d0e96426", "name": "table", "value": "feedback", "type": "string" }, { "id": "59a5630e-f880-5960-b473-9b24e59541be", "name": "classify_model", "value": "deepseek-v4.1-flash", "type": "string" }, { "id": "2a459666-0b02-54f6-829e-e4394f9f5783", "name": "report_model", "value": "claude-sonnet-5-5", "type": "string" } ] }, "includeOtherFields": true, "options": {} }, "id": "f4e45856-98d3-5aab-b4f8-abf261d8bdda", "name": "Settings", "type": "n8n-nodes-base.set", "typeVersion": 3.4, "position": [ 220, 200 ] }, { "parameters": { "conditions": { "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict", "version": 2 }, "conditions": [ { "id": "754357df-e8c0-5e0d-a27c-4cfdc9e95b39", "leftValue": "={{ $json['Period'] !== undefined }}", "operator": { "type": "boolean", "operation": "true", "singleValue": true }, "rightValue": "" } ], "combinator": "and" }, "options": {} }, "id": "188e5b90-4559-539a-b415-7eaaead65f35", "name": "Report request?", "type": "n8n-nodes-base.if", "typeVersion": 2.2, "position": [ 440, 200 ] }, { "parameters": { "method": "POST", "url": "={{ $json.api_base }}/chat/completions", "authentication": "genericCredentialType", "genericAuthType": "httpBearerAuth", "sendBody": true, "specifyBody": "json", "jsonBody": "={{ JSON.stringify({\n model: $json.classify_model,\n max_tokens: 600,\n messages: [\n { role: 'system', content: 'You triage customer feedback for a software product. Reply with one JSON object only, keys: category (one of bug, feature_request, complaint, question, praise, other), sentiment (positive, neutral or negative), urgency (low, medium or high; high means data loss, payment problems, security or the product unusable), summary (one English sentence under 20 words), tags (array of 1 to 4 short lowercase English tags), language (ISO 639-1 code of the message).' },\n { role: 'user', content: `Area: ${$json['Area']}\\n\\nFeedback:\\n${$json['Your feedback']}` }\n ]\n}) }}", "options": { "response": { "response": { "fullResponse": true } }, "timeout": 60000 } }, "id": "7918a7c7-0087-5ee9-ae48-5e887958bcaf", "name": "Classify feedback", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ 660, 0 ], "credentials": { "httpBearerAuth": { "id": "REPLACE_WITH_YOUR_CREDENTIAL", "name": "apimodels API key" } }, "onError": "continueErrorOutput" }, { "parameters": { "jsCode": "const res = $input.first().json;\nconst form = $('Settings').first().json;\nconst text = (res.body && res.body.choices && res.body.choices[0].message.content) || '';\nconst m = text.match(/\\{[\\s\\S]*\\}/);\nlet c = {};\ntry { c = JSON.parse(m ? m[0] : text); } catch (e) { c = { category: 'other', sentiment: 'neutral', urgency: 'medium', summary: '', tags: [] }; }\nconst pick = (v, allowed, dflt) => (allowed.includes(v) ? v : dflt);\n\n// Cradler column names must be lowercase snake_case.\nreturn [{ json: { row: {\n message: form['Your feedback'],\n area: form['Area'],\n email: form['Email'] || null,\n category: pick(c.category, ['bug', 'feature_request', 'complaint', 'question', 'praise', 'other'], 'other'),\n sentiment: pick(c.sentiment, ['positive', 'neutral', 'negative'], 'neutral'),\n urgency: pick(c.urgency, ['low', 'medium', 'high'], 'medium'),\n summary: String(c.summary || '').slice(0, 300),\n tags: Array.isArray(c.tags) ? c.tags.slice(0, 4).join(', ') : '',\n language: String(c.language || '').slice(0, 8),\n source: 'n8n form',\n classify_cost: Number(res.headers['x-apimodels-cost'] || 0),\n} } }];" }, "id": "c5e5a19b-cfa5-5344-8ee6-1e32d7800f9f", "name": "Build row", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 880, 0 ] }, { "parameters": { "method": "POST", "url": "={{ $('Settings').first().json.cradler_api }}/{{ $('Settings').first().json.table }}", "authentication": "genericCredentialType", "genericAuthType": "httpBearerAuth", "sendBody": true, "specifyBody": "json", "jsonBody": "={{ JSON.stringify([$json.row]) }}", "options": { "timeout": 30000 } }, "id": "8fac41c5-6579-53c4-83ed-dc34cbcdda93", "name": "Save to Cradler", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ 1100, 0 ], "credentials": { "httpBearerAuth": { "id": "REPLACE_WITH_YOUR_CREDENTIAL", "name": "Cradler service key" } }, "onError": "continueErrorOutput" }, { "parameters": { "operation": "completion", "respondWith": "showText", "responseText": "=
\nWe read every message{{ $('Settings').first().json['Email'] ? ' and will reply to ' + $('Settings').first().json['Email'] + ' if needed' : '' }}.
\nSend more\n${l}
` : '';\n}).join('\\n');\nconst chips = (obj) => Object.entries(obj).sort((a, b) => b[1] - a[1]).map(([k, v]) => `${esc(k)} ${v}`).join(' ');\n\nconst html = `\n\n{{ $json.error?.message || $json.error || 'Please try again.' }}
\n\nBack\n