This workflow follows the Executecommand → HTTP Request 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": "PsyCardv2",
"nodes": [
{
"parameters": {
"httpMethod": "POST",
"path": "psy-card",
"options": {}
},
"id": "ca220db5-d665-4291-be92-ed75a1a23707",
"name": "Webhook Trigger",
"type": "n8n-nodes-base.webhook",
"typeVersion": 1,
"position": [
-352,
80
]
},
{
"parameters": {
"jsCode": "// Accept input either from Webhook (body) or from direct Cron-generated JSON\nconst body = $json.body ?? $json;\nif (!body || !body.cards) {\n throw new Error('Missing cards in input. Expected {cards, series_metadata}');\n}\n\nconst metadata = body.series_metadata || {};\nlet cards = Array.isArray(body.cards) ? body.cards : [];\n\nconst title = (metadata.title || metadata.series_title || 'Psychological Insight').trim();\n\nfunction today_ddmmyyyy() {\n const d = new Date();\n const dd = String(d.getDate()).padStart(2, '0');\n const mm = String(d.getMonth() + 1).padStart(2, '0');\n const yyyy = String(d.getFullYear());\n return `${dd}-${mm}-${yyyy}`;\n}\n\nfunction nonEmpty(s) {\n return typeof s === 'string' && s.trim().length > 0;\n}\n\nfunction defaultPrompt(text) {\n const t = String(text || title).replace(/\\s+/g, ' ').trim();\n // English only-ish prompt; no text/logo/watermark\n return `Cinematic portrait 9:16, symbolic psychological concept, ${t}, moody lighting, realistic, shallow depth of field, no text, no logo, no watermark`;\n}\n\n// Ensure there is a proper cover at index 0\nconst hasCover0 = cards.some(c => (c?.card_index === 0) && (String(c?.type || '').toLowerCase() === 'cover'));\n\nif (!hasCover0) {\n const first = cards[0] || {};\n const cover = {\n type: 'cover',\n card_index: 0,\n headline: title,\n content: (title || 'M\u1ed9t g\u00f3c nh\u00ecn t\u00e2m l\u00fd gi\u00fap b\u1ea1n hi\u1ec3u m\u00ecnh r\u00f5 h\u01a1n m\u1ed7i ng\u00e0y.'),\n image_prompt: nonEmpty(first.image_prompt) ? first.image_prompt : defaultPrompt(title),\n };\n\n // Shift existing cards to start from 1\n const shifted = cards.map((c, i) => {\n const idx = (typeof c.card_index === 'number' && Number.isFinite(c.card_index)) ? c.card_index : i;\n return {\n ...c,\n type: 'content',\n card_index: idx + 1,\n };\n });\n\n cards = [cover, ...shifted];\n}\n\n// Final normalize: sequential card_index, cover first, and ensure image_prompt exists\ncards = cards\n .slice()\n .sort((a, b) => (a.card_index ?? 0) - (b.card_index ?? 0))\n .map((c, i) => {\n const isCover = i === 0;\n const headline = (isCover ? title : (c.headline || title));\n const image_prompt = nonEmpty(c.image_prompt) ? c.image_prompt : defaultPrompt(headline);\n\n return {\n ...c,\n type: isCover ? 'cover' : 'content',\n card_index: i,\n headline: headline,\n content: (c.content ?? ''),\n image_prompt,\n };\n });\n\nreturn {\n cards_list: cards,\n total_count: cards.length,\n series_date: nonEmpty(metadata.date) ? metadata.date : today_ddmmyyyy(),\n series_title: title,\n};\n"
},
"id": "448342ea-f218-45c4-b5e8-2005e4113a38",
"name": "Parse Psy",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-128,
-128
]
},
{
"parameters": {
"jsCode": "const data = $node['Build Series Context'].json;\n\nfunction cleanStepHeadline(headline) {\n let s = (headline || '').trim();\n // Remove common step prefixes: 'B\u01b0\u1edbc 1', 'B\u01b0\u1edbc 1:', 'Step 1', '1.', '1)', '1 -'\n s = s.replace(/^(?:b\u01b0\u1edbc|step)\\s*\\d+\\s*[:\\-\\.\\)]*\\s*/i, '');\n s = s.replace(/^\\d+\\s*[:\\-\\.\\)]\\s*/i, '');\n // Also remove 'B\u01b0\u1edbc:' without number\n s = s.replace(/^b\u01b0\u1edbc\\s*[:\\-\\.\\)]\\s*/i, '');\n return s.trim();\n}\n\nfunction toFolderDate(dateStr) {\n const s = String(dateStr || '').trim();\n // Supports: dd-mm-YYYY, dd/mm/YYYY, YYYY-mm-dd, YYYY/mm/dd\n let dd, mm, yyyy;\n\n let m = s.match(/^(\\d{2})[\\/-](\\d{2})[\\/-](\\d{4})$/);\n if (m) {\n dd=m[1]; mm=m[2]; yyyy=m[3];\n return `${yyyy}-${mm}-${dd}`;\n }\n\n m = s.match(/^(\\d{4})[\\/-](\\d{2})[\\/-](\\d{2})$/);\n if (m) {\n yyyy=m[1]; mm=m[2]; dd=m[3];\n return `${yyyy}-${mm}-${dd}`;\n }\n\n // Fallback to today\n const d = new Date();\n dd = String(d.getDate()).padStart(2,'0');\n mm = String(d.getMonth()+1).padStart(2,'0');\n yyyy = String(d.getFullYear());\n return `${yyyy}-${mm}-${dd}`;\n}\n\nconst global_date_folder = toFolderDate(data.series_date);\n\nreturn data.cards_list.map((card, idx) => {\n const isCover = idx === 0 || card.type === 'cover' || card.card_index === 0;\n const cleaned = (!isCover) ? cleanStepHeadline(card.headline) : (card.headline || data.series_title);\n return {\n json: {\n ...card,\n headline: cleaned,\n global_date: data.series_date,\n global_date_folder,\n global_title: data.series_title,\n total_count: data.total_count,\n folderId: data.folderId,\n }\n };\n});\n"
},
"id": "03f96ffd-e851-4e26-94b4-08d9b20be4a8",
"name": "Flatten Items",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1888,
-128
]
},
{
"parameters": {
"batchSize": 1,
"options": {}
},
"id": "33b1c100-5ffa-4efa-9a62-83767edb185f",
"name": "Split In Batches",
"type": "n8n-nodes-base.splitInBatches",
"typeVersion": 1,
"position": [
2112,
-128
]
},
{
"parameters": {
"command": "=mkdir -p /tmp/psy_{{ $execution.id }} && python3 /home/vietnx/WorkSpace/PsychologicalCard/scripts/generate_psy_image.py /tmp/psy_{{ $execution.id }}/raw_{{ $node[\"Split In Batches\"].json.card_index }}.png \"{{ $node[\"Split In Batches\"].json.image_prompt.replace(/\\\"/g, '\\\\\\\"') }}\""
},
"id": "6e7df629-cff2-4fe3-9ed3-b9812e0438eb",
"name": "Gen AI Image",
"type": "n8n-nodes-base.executeCommand",
"typeVersion": 1,
"position": [
2336,
-288
]
},
{
"parameters": {
"command": "=mkdir -p /tmp/psy_{{ $execution.id }} && cat <<EOF > /tmp/psy_{{ $execution.id }}/card_{{ $node[\"Split In Batches\"].json.card_index }}.json\n{{ JSON.stringify($node[\"Split In Batches\"].json) }}\nEOF"
},
"id": "d152fc81-e18c-4a3b-b99a-7523350bc9db",
"name": "Write Temp JSON",
"type": "n8n-nodes-base.executeCommand",
"typeVersion": 1,
"position": [
2560,
-288
]
},
{
"parameters": {
"command": "=mkdir -p /home/vietnx/WorkSpace/PsychologicalCard/data/{{ $node[\"Split In Batches\"].json.global_date_folder }} && mkdir -p /home/vietnx/WorkSpace/PsychologicalCard/data/{{ $node[\"Split In Batches\"].json.global_date_folder }}/binary-{{ $execution.id }} && mkdir -p /home/vietnx/.n8n-files/PsychologicalCard/data/{{ $node[\"Split In Batches\"].json.global_date_folder }} && mkdir -p /tmp/psy_{{ $execution.id }} && python3 /home/vietnx/WorkSpace/PsychologicalCard/scripts/generate_card_v2.py --template /home/vietnx/WorkSpace/PsychologicalCard/templates/psychological_template.json --image /tmp/psy_{{ $execution.id }}/raw_{{ $node[\"Split In Batches\"].json.card_index }}.png --output /home/vietnx/WorkSpace/PsychologicalCard/data/{{ $node[\"Split In Batches\"].json.global_date_folder }}/card_{{ $node[\"Split In Batches\"].json.card_index }}.png --json_input /tmp/psy_{{ $execution.id }}/card_{{ $node[\"Split In Batches\"].json.card_index }}.json && cp -f /home/vietnx/WorkSpace/PsychologicalCard/data/{{ $node[\"Split In Batches\"].json.global_date_folder }}/card_{{ $node[\"Split In Batches\"].json.card_index }}.png /home/vietnx/WorkSpace/PsychologicalCard/data/{{ $node[\"Split In Batches\"].json.global_date_folder }}/binary-{{ $execution.id }}/card_{{ $node[\"Split In Batches\"].json.card_index }}.png && cp -f /home/vietnx/WorkSpace/PsychologicalCard/data/{{ $node[\"Split In Batches\"].json.global_date_folder }}/card_{{ $node[\"Split In Batches\"].json.card_index }}.png /home/vietnx/.n8n-files/PsychologicalCard/data/{{ $node[\"Split In Batches\"].json.global_date_folder }}/card_{{ $node[\"Split In Batches\"].json.card_index }}.png"
},
"id": "4571a16d-7813-4833-aab7-92deda53846a",
"name": "Render Final Card",
"type": "n8n-nodes-base.executeCommand",
"typeVersion": 1,
"position": [
2784,
-288
]
},
{
"parameters": {
"jsCode": "const totalRequired = $node[\"Parse Psy\"].json.total_count;\nconst doneCount = $node[\"Split In Batches\"].context.currentRunIndex + 1;\nconst desc = $node[\"Parse Psy\"].json.cards_list[0].content;\n\nreturn {\n isFinished: doneCount >= totalRequired,\n doneCount,\n totalRequired,\n series_title: $node[\"Parse Psy\"].json.series_title,\n desc,\n series_date: $node[\"Parse Psy\"].json.series_date\n};"
},
"id": "fa782e14-fab4-470f-89e9-a133c284a9e0",
"name": "Check Completion",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
3680,
-192
]
},
{
"parameters": {
"conditions": {
"boolean": [
{
"value1": "={{ $json.isFinished }}",
"value2": true
}
]
}
},
"id": "c7dc32bc-af19-4c62-88ca-18a0efc3e267",
"name": "If Finished",
"type": "n8n-nodes-base.if",
"typeVersion": 1,
"position": [
3904,
-128
]
},
{
"parameters": {
"chatId": "1230031341",
"text": "=\u2705 [Psychological Card] HO\u00c0N T\u1ea4T B\u1ed8 \u1ea2NH!\n\ud83d\udccc Series: `{{ $node[\"Build Series Context\"].json.series_title }}`\n\ud83d\udccb Desc: `{{ $node[\"Build Series Context\"].json.cards_list[0].content }}\n#psychology #better #daily #selfimprovement #trending`\n\ud83d\udcc5 Ng\u00e0y: {{ $node[\"Build Series Context\"].json.series_date }}\n\ud83d\udcf8 T\u1ed5ng s\u1ed1: {{ $node[\"Build Series Context\"].json.total_count }} cards\n\ud83d\ude80 Google Drive: https://drive.google.com/drive/u/0/folders/{{ $node[\"Pick FolderId\"].json.folderId }}",
"additionalFields": {}
},
"id": "284471ba-f0a0-4b74-9f82-c1af4ef2a593",
"name": "Telegram Notify Final",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.1,
"position": [
4352,
-128
],
"credentials": {
"telegramApi": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"chatId": "1230031341",
"text": "=\ud83d\ude80 [Psychological Card] B\u1eaeT \u0110\u1ea6U QUY TR\u00ccNH\n\ud83d\udccc Ch\u1ee7 \u0111\u1ec1: `{{ $json.series_title }}`\n\ud83d\udccb M\u00f4 t\u1ea3: `{{ $json.cards_list[0].content }}\n#psychology #better #daily #selfimprovement #trending`\n\ud83d\udcf8 D\u1ef1 ki\u1ebfn: {{ $json.total_count }} cards\n\u23f3 H\u1ec7 th\u1ed1ng \u0111ang ti\u1ebfn h\u00e0nh render, vui l\u00f2ng ch\u1edd...",
"additionalFields": {}
},
"id": "a1528da3-e0a6-4432-ac90-1361c5fe9ab8",
"name": "Telegram Notify Start",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1,
"position": [
1664,
-128
],
"credentials": {
"telegramApi": {
"name": "<your credential>"
}
},
"onError": "continueErrorOutput"
},
{
"parameters": {
"filePath": "=/home/vietnx/.n8n-files/PsychologicalCard/data/{{ $node[\"Split In Batches\"].json.global_date_folder }}/card_{{ $node[\"Split In Batches\"].json.card_index }}.png"
},
"id": "0a87eaa8-c743-428e-8967-e031aed5ec7c",
"name": "Read Card Image",
"type": "n8n-nodes-base.readBinaryFile",
"typeVersion": 1,
"position": [
3008,
-288
]
},
{
"parameters": {
"resource": "fileFolder",
"queryString": "={{ $node['Parse Psy'].json.series_date }}",
"limit": 5,
"filter": {
"driveId": {
"mode": "list",
"value": "My Drive"
},
"folderId": {
"mode": "id",
"value": "1fXWSPyFjoUEIccdZCkIfS-tDTp4eHVcU"
},
"whatToSearch": "folders",
"includeTrashed": false
},
"options": {}
},
"id": "e2d92193-8a6e-427a-a903-318f750aea4d",
"name": "Find Date Folder",
"type": "n8n-nodes-base.googleDrive",
"typeVersion": 3,
"position": [
96,
-128
],
"alwaysOutputData": true,
"credentials": {
"googleDriveOAuth2Api": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"conditions": {
"boolean": [
{
"value1": "={{ $json.hasFolder }}",
"value2": true
}
]
}
},
"id": "a5fa951c-3458-4f40-bc76-2048fe47ad1f",
"name": "Has Date Folder?",
"type": "n8n-nodes-base.if",
"typeVersion": 1,
"position": [
544,
-128
]
},
{
"parameters": {
"resource": "folder",
"name": "={{ $node[\"Parse Psy\"].json.series_date }}",
"driveId": {
"__rl": true,
"mode": "list",
"value": "My Drive"
},
"folderId": {
"mode": "id",
"value": "1fXWSPyFjoUEIccdZCkIfS-tDTp4eHVcU"
},
"options": {
"simplifyOutput": true
}
},
"id": "21a6a524-2c67-4661-9917-cb013575154c",
"name": "Create Date Folder",
"type": "n8n-nodes-base.googleDrive",
"typeVersion": 3,
"position": [
768,
-80
],
"credentials": {
"googleDriveOAuth2Api": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "// Folder created by Google Drive create\nreturn [{ json: { folderId: $json.id } }];\n"
},
"id": "6e82ee57-17de-49a3-9943-97f269151fd3",
"name": "FolderId From Create",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
992,
-80
]
},
{
"parameters": {
"mode": "mergeByIndex",
"join": "enrichInput1"
},
"id": "bc068be9-3711-49ab-9537-d584e63647f3",
"name": "Attach FolderId",
"type": "n8n-nodes-base.merge",
"typeVersion": 1,
"position": [
3232,
-192
],
"alwaysOutputData": true
},
{
"parameters": {
"name": "={{ 'card_' + $node[\"Split In Batches\"].json.card_index + '.png' }}",
"driveId": {
"__rl": true,
"mode": "list",
"value": "My Drive"
},
"folderId": {
"mode": "id",
"value": "={{ $json.folderId }}"
},
"options": {}
},
"id": "4a97b80b-8e9a-4941-b01c-75b621a8d794",
"name": "Upload to Drive",
"type": "n8n-nodes-base.googleDrive",
"typeVersion": 3,
"position": [
3456,
-192
],
"credentials": {
"googleDriveOAuth2Api": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"triggerTimes": {
"item": [
{
"mode": "custom",
"cronExpression": "30 7 * * *"
}
]
}
},
"name": "Cron Trigger (Theory)",
"type": "n8n-nodes-base.cron",
"typeVersion": 1,
"position": [
-2144,
-128
],
"id": "f8e87875-8b48-43de-8f7b-80289be056b4"
},
{
"parameters": {
"command": "cat /home/vietnx/WorkSpace/PsychologicalCard/topics_history_theory.md 2>/dev/null || true"
},
"name": "Read Topic History (Theory)",
"type": "n8n-nodes-base.executeCommand",
"typeVersion": 1,
"position": [
-1920,
-128
],
"id": "11d187c7-ea9c-44f8-8567-9f1d7499bc46"
},
{
"parameters": {
"method": "POST",
"url": "https://ai-proxy.vietnx.io.vn/v1/chat/completions",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "Bearer nguyenviet02"
},
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ ({\n model: 'gpt-5.2',\n temperature: 0.7,\n max_tokens: 2600,\n messages: [\n { role: 'system', content: 'You generate Psychological Card THEORY series in natural, high-quality Vietnamese.\\n\\nN\u1ed9i dung: Gi\u1ea3i th\u00edch kh\u00e1i ni\u1ec7m, hi\u1ec7u \u1ee9ng ho\u1eb7c hi\u1ec7n t\u01b0\u1ee3ng t\u00e2m l\u00fd h\u1ecdc chuy\u00ean s\u00e2u.\\n\\nOutput MUST be valid JSON only (no markdown), matching schema:\\n{series_metadata:{title,date,source_url}, cards:[{type,card_index,headline,content,image_prompt}]}\\n\\nStrict rules:\\n- cards[0] MUST be {type: \"cover\", card_index:0, headline:series_metadata.title, content:10-15 word hook}.\\n- Cards 1..N MUST be {type: \"content\", card_index:1..N} explaining the psychological theory/concept.\\n- Each content card: 25-35 Vietnamese words.\\n- Plain text only: no ** __ # ` [ ] ( ) and no bullets - or \u2022. No emojis.\\n- 5-7 cards total.\\n- image_prompt: English cinematic, portrait 9:16, no text/logo/watermark.\\n- Topic must be NEW vs provided history (case-insensitive; ignore (Duplicate) suffix).\\n- Return ONLY JSON.\\n\\nVietnamese phrasing quality (VERY IMPORTANT):\\n- Write like a seasoned Vietnamese psychologist or academic: sophisticated, clear, and engaging.\\n- Cover hook: Must be a thought-provoking question or a powerful statement that makes people want to swipe.\\n- Content card headlines: Short, impactful phrases (2-6 words) that summarize the core point of the card.\\n- Avoid academic jargon without explanation; make complex concepts accessible but remain authoritative.\\n- Do NOT use overly formal Sino-Vietnamese chains where simple, elegant Vietnamese works better.' },\n { role: 'user', content: 'Topic history table (markdown):\\n\\n' + ($node['Read Topic History (Theory)'].json.stdout || '') + '\\n\\nAttempt: ' + String($node['Attempt Counter (Theory)'].json.attempt) + '\\nGenerate one THEORY series for today date in dd-mm-YYYY format. source_url can be empty string. Return JSON only.' }\n ]\n}) }}",
"options": {}
},
"name": "LLM Generate Psy JSON (Theory)",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4,
"position": [
-1472,
-224
],
"id": "58adb3f0-f57b-4368-8575-ce99d489df08"
},
{
"parameters": {
"jsCode": "// OpenAI compat response: {choices:[{message:{content:'...'}}]}\nconst content = $json.choices?.[0]?.message?.content;\nif (!content) throw new Error('LLM response missing choices[0].message.content');\n\nlet text = content.trim();\n// Strip common code fences safely (no regex with literal newlines)\nif (text.startsWith('```')) {\n text = text.replace(/^```json\\s*/i, '').replace(/^```\\s*/i, '');\n if (text.endsWith('```')) text = text.slice(0, -3);\n text = text.trim();\n}\n\ntry {\n return JSON.parse(text);\n} catch (e) {\n throw new Error('LLM returned non-JSON content. First 200 chars: ' + text.slice(0, 200));\n}\n"
},
"name": "Extract Body JSON",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-1248,
-224
],
"id": "391d64e0-ce3e-4769-a6dd-313a673d76ef"
},
{
"parameters": {
"command": "=python3 /home/vietnx/WorkSpace/PsychologicalCard/scripts/update_topics_history.py --type Theory --topic \"{{ $json.series_metadata.title.replace(/\"/g,'\\\\\"') }}\""
},
"name": "Update Topic History (Theory)",
"type": "n8n-nodes-base.executeCommand",
"typeVersion": 1,
"position": [
-576,
-192
],
"id": "d225b3c3-9908-4b2a-8f2a-142463bef614"
},
{
"parameters": {
"jsCode": "const target = $node[\"Parse Psy\"].json.series_date;\nconst items = $input.all().map(i => i.json);\n\nconst exact = items.find(x => x?.name === target) || null;\nreturn [{ json: { hasFolder: !!exact, folderId: exact?.id ?? null, folderName: exact?.name ?? null } }];"
},
"name": "Found Folder Info",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
320,
-128
],
"id": "f0365553-aea6-4acf-8739-8aaa31b80f30"
},
{
"parameters": {
"jsCode": "return [{ json: { folderId: $json.folderId } }];"
},
"name": "FolderId From Found",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
992,
-272
],
"id": "80d056fd-c75e-4653-9380-e60c14918138"
},
{
"parameters": {
"mode": "passThrough"
},
"name": "Pick FolderId",
"type": "n8n-nodes-base.merge",
"typeVersion": 1,
"position": [
1216,
-128
],
"id": "ac4da03e-ce29-4013-b032-ca90572e5aed",
"alwaysOutputData": true
},
{
"parameters": {
"jsCode": "// Input: { folderId }\nconst folderId = $json.folderId;\nif (!folderId) throw new Error('Missing folderId');\nconst series = $node['Parse Psy'].json;\nreturn {\n ...series,\n folderId,\n};\n"
},
"name": "Build Series Context",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1440,
-128
],
"id": "532db6c3-3c25-4cd7-8e03-78510c4e2e7c"
},
{
"parameters": {
"mode": "passThrough"
},
"name": "Gate After History",
"type": "n8n-nodes-base.merge",
"typeVersion": 1,
"position": [
-352,
-128
],
"id": "16b05b39-6020-45c4-b5f8-79dce2204042"
},
{
"parameters": {
"triggerTimes": {
"item": [
{
"mode": "custom",
"cronExpression": "0 7 * * *"
}
]
}
},
"name": "Cron Trigger (Actionable)",
"type": "n8n-nodes-base.cron",
"typeVersion": 1,
"position": [
-2144,
-464
],
"id": "5e48be6e-20e6-4d2d-8105-ad96e1b6bfe5"
},
{
"parameters": {
"command": "cat /home/vietnx/WorkSpace/PsychologicalCard/topics_history_actionable.md 2>/dev/null || true"
},
"name": "Read Topic History (Actionable)",
"type": "n8n-nodes-base.executeCommand",
"typeVersion": 1,
"position": [
-1920,
-464
],
"id": "ab3300c1-4fd3-414a-9d13-cbee86d1a91a"
},
{
"parameters": {
"method": "POST",
"url": "https://ai-proxy.vietnx.io.vn/v1/chat/completions",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "Bearer nguyenviet02"
},
{
"name": "Content-Type",
"value": "application/json"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ ({\n model: 'gpt-5.2',\n temperature: 0.7,\n max_tokens: 2600,\n messages: [\n { role: 'system', content: 'You generate Psychological Card ACTIONABLE series in natural, high-quality Vietnamese.\\n\\nOutput MUST be valid JSON only (no markdown), matching schema:\\n{series_metadata:{title,date,source_url}, cards:[{type,card_index,headline,content,image_prompt}]}\\n\\nStrict rules:\\n- cards[0] MUST be {type:\\\"cover\\\", card_index:0, headline:series_metadata.title, content:10-15 word hook}\\n- Cards 1..N MUST be {type:\\\"content\\\", card_index:1..N} with practical step-by-step guidance\\n- Each content card: 25-35 Vietnamese words\\n- Plain text only: no ** __ # ` [ ] ( ) and no bullets - or \u2022. No emojis.\\n- 5-7 cards total\\n- image_prompt: English cinematic, portrait 9:16, no text/logo/watermark\\n- Topic must be NEW vs provided history (case-insensitive; ignore (Duplicate) suffix)\\n- Return ONLY JSON\\n\\nVietnamese phrasing quality (VERY IMPORTANT):\\n- Write like a seasoned Vietnamese psychologist/coach: smooth, idiomatic, not literal/robotic.\\n- Avoid awkward noun stacks like: \\\"6 Th\u1ef1c H\u00e0nh Ph\u1ee5c H\u1ed3i...\\\". Use natural templates:\\n \\\"6 b\u01b0\u1edbc ph\u1ee5c h\u1ed3i khi ...\\\", \\\"6 c\u00e1ch ...\\\", \\\"6 vi\u1ec7c b\u1ea1n c\u00f3 th\u1ec3 l\u00e0m khi ...\\\", \\\"6 chi\u1ebfn l\u01b0\u1ee3c ...\\\".\\n- Cover title MUST start with a number + classifier when it is a list (e.g., \\\"6 b\u01b0\u1edbc...\\\", \\\"5 c\u00e1ch...\\\").\\n- For content card headlines: short verb-led phrases (2-6 words), natural Vietnamese, no awkward calques.\\n Examples: \\\"H\u1ea1 nh\u1ecbp c\u01a1 th\u1ec3\\\", \\\"G\u1ecdi t\u00ean c\u1ea3m x\u00fac\\\", \\\"Gi\u1ea3m k\u1ef3 v\u1ecdng\\\", \\\"L\u00e0m nh\u1ecf vi\u1ec7c\\\".\\n- Do NOT include step numbering or the word \\\"B\u01b0\u1edbc\\\"/\\\"Step\\\" in content card headlines (template will auto-number).\\n- Do NOT use overly formal Sino-Vietnamese chains; prefer simple Vietnamese.\\n' },\n { role: 'user', content: 'Topic history table (markdown):\\n\\n' + ($node['Read Topic History (Actionable)'].json.stdout || '') + '\\n\\nAttempt: ' + String($node['Attempt Counter (Actionable)'].json.attempt) + '\\nGenerate one ACTIONABLE series for today date in dd-mm-YYYY format. source_url can be empty string. Return JSON only.' }\n ]\n}) }}",
"options": {}
},
"name": "LLM Generate Psy JSON (Actionable)",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4,
"position": [
-1472,
-528
],
"id": "0ead250a-b0da-4a18-bc5f-9cee0b8360f9"
},
{
"parameters": {
"jsCode": "// same as Extract Body JSON\nconst content = $json.choices?.[0]?.message?.content;\nif (!content) throw new Error('LLM response missing choices[0].message.content');\n\nlet text = content.trim();\nif (text.startsWith('```')) {\n text = text.replace(/^```json\\s*/i, '').replace(/^```\\s*/i, '');\n if (text.endsWith('```')) text = text.slice(0, -3);\n text = text.trim();\n}\n\ntry {\n return JSON.parse(text);\n} catch (e) {\n throw new Error('LLM returned non-JSON content. First 200 chars: ' + text.slice(0, 200));\n}\n"
},
"name": "Extract Body JSON (Actionable)",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-1248,
-528
],
"id": "1e6f2a47-5709-4f4d-ac34-5a1d3831f895"
},
{
"parameters": {
"command": "=python3 /home/vietnx/WorkSpace/PsychologicalCard/scripts/update_topics_history.py --type Actionable --topic \"{{ $json.series_metadata.title.replace(/\"/g,'\\\\\"') }}\""
},
"name": "Update Topic History (Actionable)",
"type": "n8n-nodes-base.executeCommand",
"typeVersion": 1,
"position": [
-576,
-384
],
"id": "7c8c551e-74ee-4b89-bd83-e7fa73ecf905"
},
{
"parameters": {
"mode": "passThrough"
},
"name": "Gate After History (Actionable)",
"type": "n8n-nodes-base.merge",
"typeVersion": 1,
"position": [
-352,
-464
],
"id": "c70307ff-e049-4711-8040-bf9701bd89fc"
},
{
"parameters": {
"jsCode": "const prev = $json.attempt || 0;\nreturn { attempt: prev + 1 };"
},
"name": "Attempt Counter (Theory)",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-1696,
-128
],
"id": "66aac715-5c85-4dd9-8a6b-24d0bec46c01"
},
{
"parameters": {
"jsCode": "const prev = $json.attempt || 0;\nreturn { attempt: prev + 1 };"
},
"name": "Attempt Counter (Actionable)",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-1696,
-464
],
"id": "8769e127-ffd5-40c7-8e50-d0a2a3def340"
},
{
"parameters": {
"jsCode": "// Validate uniqueness by SERIES TITLE only (not per-card)\nconst attempt = $node['Attempt Counter (Theory)'].json.attempt;\nconst payload = $json;\n\nfunction normalizeTopic(topic) {\n return String(topic || '')\n .trim()\n .toLowerCase()\n .replace(/\\s*\\(duplicate\\)\\s*$/i, '')\n .replace(/\\s*\\[duplicate\\]\\s*$/i, '')\n .replace(/\\s*-\\s*duplicate\\s*$/i, '')\n .replace(/\\s+/g, ' ');\n}\n\nfunction extractTopicsFromMarkdown(md) {\n const topics = new Set();\n const lines = String(md || '').split('\\n');\n for (let line of lines) {\n if (line.endsWith('\\r')) line = line.slice(0, -1);\n const s = line.trim();\n if (!s.startsWith('|')) continue;\n if (s.startsWith('| Date ') || s.startsWith('|------')) continue;\n const parts = s.replace(/^\\|/,'').replace(/\\|$/,'').split('|').map(p => p.trim());\n if (parts.length < 3) continue;\n const topic = parts[2];\n const n = normalizeTopic(topic);\n if (n) topics.add(n);\n }\n return topics;\n}\n\nconst title = payload?.series_metadata?.title || payload?.series_metadata?.series_title || '';\nconst titleN = normalizeTopic(title);\nconst historyMd = $node['Read Topic History (Theory)'].json.stdout || '';\nconst existing = extractTopicsFromMarkdown(historyMd);\n\nconst isDuplicate = titleN && existing.has(titleN);\nif (isDuplicate) {\n if (attempt >= 4) {\n throw new Error(`Duplicate TITLE detected (Theory) after ${attempt} attempts: ${title}`);\n }\n return { __duplicate: true, attempt, title };\n}\n\nreturn { ...payload, __duplicate: false, attempt };\n"
},
"name": "Validate Uniqueness (Theory)",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-1024,
-224
],
"id": "acb77352-efdd-42da-a23b-80a4daa66061"
},
{
"parameters": {
"jsCode": "// Validate uniqueness by SERIES TITLE only (not per-card)\nconst attempt = $node['Attempt Counter (Actionable)'].json.attempt;\nconst payload = $json;\n\nfunction normalizeTopic(topic) {\n return String(topic || '')\n .trim()\n .toLowerCase()\n .replace(/\\s*\\(duplicate\\)\\s*$/i, '')\n .replace(/\\s*\\[duplicate\\]\\s*$/i, '')\n .replace(/\\s*-\\s*duplicate\\s*$/i, '')\n .replace(/\\s+/g, ' ');\n}\n\nfunction extractTopicsFromMarkdown(md) {\n const topics = new Set();\n const lines = String(md || '').split('\\n');\n for (let line of lines) {\n if (line.endsWith('\\r')) line = line.slice(0, -1);\n const s = line.trim();\n if (!s.startsWith('|')) continue;\n if (s.startsWith('| Date ') || s.startsWith('|------')) continue;\n const parts = s.replace(/^\\|/,'').replace(/\\|$/,'').split('|').map(p => p.trim());\n if (parts.length < 3) continue;\n const topic = parts[2];\n const n = normalizeTopic(topic);\n if (n) topics.add(n);\n }\n return topics;\n}\n\nconst title = payload?.series_metadata?.title || payload?.series_metadata?.series_title || '';\nconst titleN = normalizeTopic(title);\nconst historyMd = $node['Read Topic History (Actionable)'].json.stdout || '';\nconst existing = extractTopicsFromMarkdown(historyMd);\n\nconst isDuplicate = titleN && existing.has(titleN);\nif (isDuplicate) {\n if (attempt >= 4) {\n throw new Error(`Duplicate TITLE detected (Actionable) after ${attempt} attempts: ${title}`);\n }\n return { __duplicate: true, attempt, title };\n}\n\nreturn { ...payload, __duplicate: false, attempt };\n"
},
"name": "Validate Uniqueness (Actionable)",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-1024,
-528
],
"id": "07132542-01f1-477b-8af6-cdf9194b7f83"
},
{
"parameters": {
"conditions": {
"boolean": [
{
"value1": "={{ $json.__duplicate }}",
"value2": true
}
]
}
},
"name": "If Duplicate? (Theory)",
"type": "n8n-nodes-base.if",
"typeVersion": 1,
"position": [
-800,
-128
],
"id": "f015e3de-17d7-4579-b159-10993da693e8"
},
{
"parameters": {
"conditions": {
"boolean": [
{
"value1": "={{ $json.__duplicate }}",
"value2": true
}
]
}
},
"name": "If Duplicate? (Actionable)",
"type": "n8n-nodes-base.if",
"typeVersion": 1,
"position": [
-800,
-464
],
"id": "260f29a4-8a11-470f-bc2e-659e3b1e67a0"
},
{
"parameters": {
"command": "=rm -rf -- /tmp/psy_{{ $execution.id }} || true"
},
"id": "cleanup-temp-psy-q9oDF42tvdopxJT5",
"name": "Cleanup Temp",
"type": "n8n-nodes-base.executeCommand",
"typeVersion": 1,
"position": [
4128,
-128
],
"continueOnFail": true
}
],
"connections": {
"Webhook Trigger": {
"main": [
[
{
"node": "Parse Psy",
"type": "main",
"index": 0
}
]
]
},
"Parse Psy": {
"main": [
[
{
"node": "Find Date Folder",
"type": "main",
"index": 0
}
]
]
},
"Flatten Items": {
"main": [
[
{
"node": "Split In Batches",
"type": "main",
"index": 0
}
]
]
},
"Split In Batches": {
"main": [
[
{
"node": "Gen AI Image",
"type": "main",
"index": 0
},
{
"node": "Attach FolderId",
"type": "main",
"index": 1
}
]
]
},
"Gen AI Image": {
"main": [
[
{
"node": "Write Temp JSON",
"type": "main",
"index": 0
}
]
]
},
"Write Temp JSON": {
"main": [
[
{
"node": "Render Final Card",
"type": "main",
"index": 0
}
]
]
},
"Render Final Card": {
"main": [
[
{
"node": "Read Card Image",
"type": "main",
"index": 0
}
]
]
},
"Check Completion": {
"main": [
[
{
"node": "If Finished",
"type": "main",
"index": 0
}
]
]
},
"If Finished": {
"main": [
[
{
"node": "Cleanup Temp",
"type": "main",
"index": 0
}
],
[
{
"node": "Split In Batches",
"type": "main",
"index": 1
}
]
]
},
"Telegram Notify Start": {
"main": [
[
{
"node": "Flatten Items",
"type": "main",
"index": 0
}
]
]
},
"Read Card Image": {
"main": [
[
{
"node": "Attach FolderId",
"type": "main",
"index": 0
}
]
]
},
"Find Date Folder": {
"main": [
[
{
"node": "Found Folder Info",
"type": "main",
"index": 0
}
]
]
},
"Has Date Folder?": {
"main": [
[
{
"node": "FolderId From Found",
"type": "main",
"index": 0
}
],
[
{
"node": "Create Date Folder",
"type": "main",
"index": 0
}
]
]
},
"Create Date Folder": {
"main": [
[
{
"node": "FolderId From Create",
"type": "main",
"index": 0
}
]
]
},
"Upload to Drive": {
"main": [
[
{
"node": "Check Completion",
"type": "main",
"index": 0
}
]
]
},
"Cron Trigger (Theory)": {
"main": [
[
{
"node": "Read Topic History (Theory)",
"type": "main",
"index": 0
}
]
]
},
"Read Topic History (Theory)": {
"main": [
[
{
"node": "Attempt Counter (Theory)",
"type": "main",
"index": 0
}
]
]
},
"LLM Generate Psy JSON (Theory)": {
"main": [
[
{
"node": "Extract Body JSON",
"type": "main",
"index": 0
}
]
]
},
"Extract Body JSON": {
"main": [
[
{
"node": "Validate Uniqueness (Theory)",
"type": "main",
"index": 0
}
]
]
},
"Update Topic History (Theory)": {
"main": [
[
{
"node": "Gate After History",
"type": "main",
"index": 1
}
]
]
},
"Ensure FolderId": {
"main": [
[
{
"node": "Upload to Drive",
"type": "main",
"index": 0
}
]
]
},
"Found Folder Info": {
"main": [
[
{
"node": "Has Date Folder?",
"type": "main",
"index": 0
}
]
]
},
"FolderId From Found": {
"main": [
[
{
"node": "Pick FolderId",
"type": "main",
"index": 0
}
]
]
},
"FolderId From Create": {
"main": [
[
{
"node": "Pick FolderId",
"type": "main",
"index": 0
}
]
]
},
"Pick FolderId": {
"main": [
[
{
"node": "Build Series Context",
"type": "main",
"index": 0
}
]
]
},
"Build Series Context": {
"main": [
[
{
"node": "Telegram Notify Start",
"type": "main",
"index": 0
}
]
]
},
"Attach FolderId": {
"main": [
[
{
"node": "Upload to Drive",
"type": "main",
"index": 0
}
]
]
},
"Gate After History": {
"main": [
[
{
"node": "Parse Psy",
"type": "main",
"index": 0
}
]
]
},
"Cron Trigger (Actionable)": {
"main": [
[
{
"node": "Read Topic History (Actionable)",
"type": "main",
"index": 0
}
]
]
},
"Read Topic History (Actionable)": {
"main": [
[
{
"node": "Attempt Counter (Actionable)",
"type": "main",
"index": 0
}
]
]
},
"LLM Generate Psy JSON (Actionable)": {
"main": [
[
{
"node": "Extract Body JSON (Actionable)",
"type": "main",
"index": 0
}
]
]
},
"Extract Body JSON (Actionable)": {
"main": [
[
{
"node": "Validate Uniqueness (Actionable)",
"type": "main",
"index": 0
}
]
]
},
"Update Topic History (Actionable)": {
"main": [
[
{
"node": "Gate After History (Actionable)",
"type": "main",
"index": 1
}
]
]
},
"Gate After History (Actionable)": {
"main": [
[
{
"node": "Parse Psy",
"type": "main",
"index": 0
}
]
]
},
"Attempt Counter (Theory)": {
"main": [
[
{
"node": "LLM Generate Psy JSON (Theory)",
"type": "main",
"index": 0
}
]
]
},
"Validate Uniqueness (Theory)": {
"main": [
[
{
"node": "If Duplicate? (Theory)",
"type": "main",
"index": 0
}
]
]
},
"If Duplicate? (Theory)": {
"main": [
[
{
"node": "Attempt Counter (Theory)",
"type": "main",
"index": 0
}
],
[
{
"node": "Update Topic History (Theory)",
"type": "main",
"index": 0
},
{
"node": "Gate After History",
"type": "main",
"index": 0
}
]
]
},
"Attempt Counter (Actionable)": {
"main": [
[
{
"node": "LLM Generate Psy JSON (Actionable)",
"type": "main",
"index": 0
}
]
]
},
"Validate Uniqueness (Actionable)": {
"main": [
[
{
"node": "If Duplicate? (Actionable)",
"type": "main",
"index": 0
}
]
]
},
"If Duplicate? (Actionable)": {
"main": [
[
{
"node": "Attempt Counter (Actionable)",
"type": "main",
"index": 0
}
],
[
{
"node": "Update Topic History (Actionable)",
"type": "main",
"index": 0
},
{
"node": "Gate After History (Actionable)",
"type": "main",
"index": 0
}
]
]
},
"Cleanup Temp": {
"main": [
[
{
"node": "Telegram Notify Final",
"type": "main",
"index": 0
}
]
]
}
},
"active": true,
"settings": {
"executionOrder": "v1",
"binaryMode": "separate",
"availableInMCP": false,
"callerPolicy": "workflowsFromSameOwner"
},
"versionId": "f8f01c68-e340-4b01-abfe-9bd7dbf47519",
"meta": {
"templateCredsSetupCompleted": true
},
"id": "q9oDF42tvdopxJT5",
"tags": []
}
Credentials you'll need
Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.
googleDriveOAuth2ApitelegramApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
PsyCardv2. Uses executeCommand, telegram, readBinaryFile, googleDrive. Webhook trigger; 41 nodes.
Source: https://github.com/nguyenviet02/PsychologicalCard/blob/2ded90e4b21e9145279a6e7acb3be8e1886bc925/PsyCardv2.json — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
This workflow is an AI-assisted clean plate and object removal pipeline built for modern VFX production environments. It transforms a single plate image and removal brief into multiple high-quality cl
Extend N8N With Additional Tools. Uses telegram, telegramTrigger, httpRequest, spreadsheetFile. Event-driven trigger; 21 nodes.
This workflow extends n8n and uses R language graphic capabilities. This is a Telegram bot which fetches weather data via the openweathermap.org API, plots an image using ggoplot2 package from R and s
google drive to instagram, tiktok and youtube. Uses googleDriveTrigger, googleDrive, errorTrigger, telegram. Event-driven trigger; 15 nodes.
qualiopi. Uses airtable, telegram, emailSend, httpRequest. Webhook trigger; 51 nodes.