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": "content-orchestrator-v4-anime-shortthreads",
"nodes": [
{
"parameters": {
"rule": {
"interval": [
{
"field": "cronExpression",
"expression": "0 21 * * *"
}
]
}
},
"id": "1a000000-0000-0000-0000-000000000001",
"name": "Schedule 21:00",
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1.2,
"position": [
240,
300
]
},
{
"parameters": {
"jsCode": "// Fetch latest fb-post-* Gist (Code node \u2014 bypasses HTTP node sendHeaders schema bug)\nconst response = await this.helpers.httpRequest({\n method: 'GET',\n url: 'https://api.github.com/gists?per_page=10',\n headers: {\n 'Authorization': 'token ' + $env.GIST_PAT,\n 'Accept': 'application/vnd.github+json',\n 'User-Agent': 'n8n-content-orchestrator'\n },\n json: true\n});\n\nconst arr = Array.isArray(response) ? response : [];\nconst fbGist = arr.find(g => (g.description || '').startsWith('fb-post-'));\nif (!fbGist) {\n throw new Error('No fb-post-* Gist found in latest 10. Cloud routine \u6c92\u5beb Gist? Available: ' + arr.slice(0, 3).map(g => g.description || '(no desc)').join(' | '));\n}\nconst fileEntry = Object.values(fbGist.files)[0];\nreturn [{\n json: {\n gist_id: fbGist.id,\n gist_html_url: fbGist.html_url,\n raw_url: fileEntry.raw_url,\n filename: fileEntry.filename,\n description: fbGist.description,\n created_at: fbGist.created_at\n }\n}];"
},
"id": "2a000000-0000-0000-0000-000000000002",
"name": "Pick Latest fb-post Gist",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
460,
300
]
},
{
"parameters": {
"url": "={{ $json.raw_url }}",
"options": {
"response": {
"response": {
"responseFormat": "text"
}
}
}
},
"id": "3a000000-0000-0000-0000-000000000003",
"name": "Fetch Raw Markdown",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
680,
300
]
},
{
"parameters": {
"jsCode": "// Parse YAML frontmatter + extract draft + optional Threads short version\nconst raw = $input.first().json.data || $input.first().json;\nconst rawText = typeof raw === 'string' ? raw : JSON.stringify(raw);\n\nconst fmMatch = rawText.match(/^---\\s*\\n([\\s\\S]*?)\\n---\\s*\\n([\\s\\S]*)$/);\nif (!fmMatch) throw new Error('No YAML frontmatter found in Gist content');\n\nconst fmText = fmMatch[1];\nconst body = fmMatch[2];\n\n// Simple YAML parser (flat keys + arrays)\nconst meta = {};\nfmText.split('\\n').forEach(line => {\n const m = line.match(/^([a-z_]+):\\s*(.*)$/);\n if (m) {\n let val = m[2].trim();\n if (val.startsWith('[') && val.endsWith(']')) {\n val = val.slice(1, -1).split(',').map(s => s.trim().replace(/^[\"']|[\"']$/g, '')).filter(s => s);\n } else if (val.startsWith('\"') && val.endsWith('\"')) {\n val = val.slice(1, -1);\n } else if (!isNaN(val) && val !== '') {\n val = Number(val);\n }\n meta[m[1]] = val;\n }\n});\n\n// \u984d\u5916\u6293 post_text_threads \u77ed\u7248\uff08cloud routine \u7528 YAML | \u591a\u884c\u5b57\u4e32\u5beb\u5728 frontmatter\uff09\nlet postTextThreads = null;\nconst threadsMatch = fmText.match(/post_text_threads:\\s*\\|\\s*\\n([\\s\\S]+?)(?=\\n[a-z_]+:\\s|$(?![\\s\\S]))/);\nif (threadsMatch) {\n postTextThreads = threadsMatch[1].replace(/^ /gm, '').trim();\n}\n\nconst draftMatch = body.match(/##\\s*(?:\u8349\u7a3f|draft)\\s*\\n([\\s\\S]*?)(?=\\n##\\s|$)/i);\nlet draft = draftMatch ? draftMatch[1].trim() : body.trim();\n\n\n// Defensive: strip cloud-routine internal markers that should have been removed\n// e.g.: \"\u2500\u2500\u2500\u2500\u2500\u2500 \u4ee5\u4e0a 3 \u7bc0\u70ba Threads \u622a\u65b7\u908a\u754c \u2500\u2500\u2500\u2500\u2500\u2500\"\ndraft = draft.replace(/^.*\u4ee5\u4e0a\\s*\\d+\\s*\u7bc0\u70ba\\s*Threads\\s*\u622a\u65b7\u908a\u754c.*$/gm, '');\n// Collapse 3+ consecutive newlines back to 2\ndraft = draft.replace(/\\n{3,}/g, '\\n\\n').trim();\n\nreturn [{\n json: {\n metadata: meta,\n post_text: draft,\n post_text_chars: draft.length,\n post_text_threads: postTextThreads,\n post_text_threads_chars: postTextThreads ? postTextThreads.length : 0,\n format: meta.format || 'text',\n gist_id: $('Pick Latest fb-post Gist').first().json.gist_id,\n gist_url: $('Pick Latest fb-post Gist').first().json.gist_html_url\n }\n}];"
},
"id": "4a000000-0000-0000-0000-000000000004",
"name": "Parse + Extract Draft",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
900,
300
]
},
{
"parameters": {
"jsCode": "// Resolve image: Drive folder scan \u2192 ComfyUI local SDXL anime mascot\n// 2026-06-08: \u63db ComfyUI \u53d6\u4ee3 Pollinations\uff08\u8b8a\u4ed8\u8cbb + \u4e0d\u7a69\uff09\nconst meta = $input.first().json.metadata;\nconst upstream = $input.first().json;\nconst topic = meta.image_topic_for_gen || meta.topic;\n\nlet image_base64 = null;\nlet image_source = 'none';\nlet image_error = null;\nlet drive_file_used = null;\n\nconst wfData = $getWorkflowStaticData('global');\nwfData.processed_drive_ids = wfData.processed_drive_ids || [];\n\nconst COMFY_BASE = $env.COMFYUI_BASE_URL || 'http://host.docker.internal:8188';\nconst COMFY_CKPT = $env.COMFYUI_CHECKPOINT || 'animagine-xl-4.0.safetensors';\n\nfunction buildComfyWorkflow(topicText, seed) {\n // Hybrid seed: MASCOT_SEED anchors character; day-of-year shifts background/light for daily variation\n const MASCOT_SEED = 42;\n const today = new Date();\n const dayOfYear = Math.floor((today - new Date(today.getFullYear(), 0, 0)) / 86400000);\n const finalSeed = MASCOT_SEED + dayOfYear;\n\n // Cinematography-vocabulary prompt \u2014 borrowed from ai-media-generator skill (5 concrete > 20 generic)\n const POSITIVE = \"Asian woman in her late 20s, chin-length to waist-length wavy chestnut brown hair, soft side bangs, \" +\n \"((light brown cat ears:1.4)), ((kemonomimi)), fluffy ear fur detail, \" +\n \"warm brown eyes, sharp catchlights, long eyelashes, light freckles across nose bridge, gentle pink lips, calm slight smile, looking at viewer, \" +\n \"upper body portrait, \" +\n \"navy blue collared formal shirt, \" +\n \"black leather choker with circular obsidian tech pendant, \" +\n \"((large matte black over-ear headphones:1.3)), glowing circular sensor on ear cup, subtle cybernetic trim, \" +\n \"small data-port chain earrings, \" +\n \"85mm portrait, f/2 shallow depth of field, \" +\n \"((Rembrandt lighting 4:1 contrast)), soft window light from camera left, gentle rim light, \" +\n \"((golden hour warmth)), \" +\n \"((teal and amber color grade)), Kodak Vision3 500T grain, \" +\n \"Lubezki cinematography, \u738b\u5bb6\u885b mood, \" +\n \"((thin cyan fiber optic strands in background bokeh)), \" +\n \"tiny holographic UI fragments floating softly out of focus, \" +\n \"((miniature companion robot drone hovering at shoulder height:1.2)), single glowing lens eye, \" +\n \"AI semiconductor industry topic, \" + topicText + \", \" +\n \"masterpiece, best quality, photorealistic, detailed skin texture\";\n\n const NEGATIVE = \"worst quality, low quality, bad anatomy, bad hands, extra fingers, blurry, \" +\n \"watermark, signature, text, nsfw, 2girls, \" +\n \"((short hair)), blonde, black hair, blue hair, pink hair, \" +\n \"((flat shading)), ((cel shading)), ((2d anime drawing)), ((manga sketch)), chibi, \" +\n \"((heavy makeup)), plastic skin, glossy skin, \" +\n \"((cyberpunk city)), ((neon overload)), blade runner cliche, harsh neon, \" +\n \"full robot face, mecha helmet, mask covering face, \" +\n \"lens flare overload, glitch, pixelation\";\n\n return {\n \"3\": {\"inputs\":{\"seed\":finalSeed,\"steps\":20,\"cfg\":4.5,\"sampler_name\":\"dpmpp_sde\",\"scheduler\":\"karras\",\"denoise\":1,\"model\":[\"4\",0],\"positive\":[\"6\",0],\"negative\":[\"7\",0],\"latent_image\":[\"5\",0]},\"class_type\":\"KSampler\"},\n \"4\": {\"inputs\":{\"ckpt_name\":COMFY_CKPT},\"class_type\":\"CheckpointLoaderSimple\"},\n \"5\": {\"inputs\":{\"width\":1024,\"height\":1024,\"batch_size\":1},\"class_type\":\"EmptyLatentImage\"},\n \"6\": {\"inputs\":{\"text\":POSITIVE,\"clip\":[\"4\",1]},\"class_type\":\"CLIPTextEncode\"},\n \"7\": {\"inputs\":{\"text\":NEGATIVE,\"clip\":[\"4\",1]},\"class_type\":\"CLIPTextEncode\"},\n \"8\": {\"inputs\":{\"samples\":[\"3\",0],\"vae\":[\"4\",2]},\"class_type\":\"VAEDecode\"},\n \"9\": {\"inputs\":{\"filename_prefix\":\"n8n_orchestrator\",\"images\":[\"8\",0]},\"class_type\":\"SaveImage\"}\n };\n}\n\n\n\ntry {\n // ---- Step 1: Drive folder scan (priority) ----\n if ($env.DRIVE_API_KEY && $env.DRIVE_INBOX_FOLDER_ID) {\n const listResp = await this.helpers.httpRequest({\n method: 'GET',\n url: 'https://www.googleapis.com/drive/v3/files',\n qs: {\n q: `'${$env.DRIVE_INBOX_FOLDER_ID}' in parents and trashed=false and mimeType contains 'image/'`,\n key: $env.DRIVE_API_KEY,\n fields: 'files(id,name,mimeType,createdTime)',\n orderBy: 'createdTime'\n },\n json: true\n });\n const files = (listResp.files || []).filter(f => f.mimeType && f.mimeType.startsWith('image/'));\n if (files.length > 0) {\n const fresh = files.find(f => !wfData.processed_drive_ids.includes(f.id));\n if (fresh) {\n const buf = await this.helpers.httpRequest({\n method: 'GET',\n url: `https://www.googleapis.com/drive/v3/files/${fresh.id}?alt=media&key=${$env.DRIVE_API_KEY}`,\n encoding: 'arraybuffer',\n json: false,\n returnFullResponse: false\n });\n image_base64 = Buffer.from(buf).toString('base64');\n image_source = `drive:${fresh.id}`;\n drive_file_used = { id: fresh.id, name: fresh.name };\n wfData.processed_drive_ids.push(fresh.id);\n }\n }\n }\n\n // ---- Step 2: ComfyUI local SDXL anime generation ----\n if (!image_base64 && topic) {\n let positivePrompt = typeof topic === 'string' ? topic : String(topic);\n const seed = Math.floor(Math.random() * 1e15);\n const workflow = buildComfyWorkflow(positivePrompt, seed);\n const clientId = 'n8n-orchestrator';\n\n // 1. POST /prompt\n const submitResp = await this.helpers.httpRequest({\n method: 'POST',\n url: `${COMFY_BASE}/prompt`,\n body: { prompt: workflow, client_id: clientId },\n json: true,\n timeout: 30000\n });\n const promptId = submitResp.prompt_id;\n if (!promptId) throw new Error('ComfyUI no prompt_id: ' + JSON.stringify(submitResp).slice(0, 300));\n\n // 2. Poll /history (SDXL 28 steps on 3060 \u2248 25-40s warm, +140s cold start)\n let history = null;\n const maxPollSec = 240;\n for (let i = 0; i < maxPollSec / 2; i++) {\n await new Promise(r => setTimeout(r, 2000));\n const hr = await this.helpers.httpRequest({\n method: 'GET',\n url: `${COMFY_BASE}/history/${promptId}`,\n json: true,\n timeout: 10000\n });\n if (hr[promptId] && hr[promptId].outputs && Object.keys(hr[promptId].outputs).length > 0) {\n history = hr[promptId];\n break;\n }\n }\n if (!history) throw new Error('ComfyUI poll timeout (>240s)');\n\n // 3. Find output image\n let imgFile = null;\n for (const nodeId of Object.keys(history.outputs)) {\n const out = history.outputs[nodeId];\n if (out.images && out.images.length > 0) {\n imgFile = out.images[0];\n break;\n }\n }\n if (!imgFile) throw new Error('ComfyUI no images in history');\n\n // 4. GET /view \u2192 image bytes\n const buf = await this.helpers.httpRequest({\n method: 'GET',\n url: `${COMFY_BASE}/view`,\n qs: { filename: imgFile.filename, subfolder: imgFile.subfolder || '', type: 'output' },\n encoding: 'arraybuffer',\n json: false,\n returnFullResponse: false,\n timeout: 30000\n });\n image_base64 = Buffer.from(buf).toString('base64');\n image_source = 'comfyui:dreamshaper-realistic';\n image_error = `prompt_seed: ${seed}, prompt: ${positivePrompt.slice(0, 100)}`;\n }\n\n if (!image_base64) {\n image_error = image_error || 'No new Drive image and ComfyUI not invoked';\n }\n} catch (e) {\n image_error = `comfyui: ${e.message}`;\n}\n\nreturn [{\n json: {\n ...upstream,\n image_base64,\n image_source,\n image_error,\n has_image: !!image_base64,\n drive_file_used,\n processed_count: wfData.processed_drive_ids.length\n }\n}];"
},
"id": "5a000000-0000-0000-0000-000000000005",
"name": "Resolve Image",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1120,
300
]
},
{
"parameters": {
"jsCode": "// Upload image to ImgBB (anonymous) \u2192 public URL for IG/Threads to fetch\nconst upstream = $input.first().json;\nconst { image_base64, has_image } = upstream;\n\nlet image_url = null;\nlet imgur_id = null;\nlet imgur_error = null;\n\nif (has_image && $env.IMGUR_CLIENT_ID) {\n try {\n const resp = await this.helpers.httpRequest({\n method: 'POST',\n url: `https://api.imgbb.com/1/upload?key=${$env.IMGUR_CLIENT_ID}`,\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body: { image: image_base64 },\n json: true\n });\n if (resp && resp.success && resp.data) {\n image_url = resp.data.url || (resp.data.image && resp.data.image.url);\n imgur_id = resp.data.id;\n } else {\n imgur_error = 'ImgBB returned non-success: ' + JSON.stringify(resp).slice(0, 500);\n }\n } catch (e) {\n imgur_error = e.message;\n }\n} else if (!has_image) {\n imgur_error = 'no image to upload';\n} else {\n imgur_error = 'IMGUR_CLIENT_ID (ImgBB) not set';\n}\n\n// Strip image_base64 from downstream output to keep payload small\nconst { image_base64: _strip, ...rest } = upstream;\nreturn [{\n json: {\n ...rest,\n image_url,\n imgur_id,\n imgur_error\n }\n}];"
},
"id": "6a000000-0000-0000-0000-000000000006",
"name": "Upload to Imgur",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1340,
300
]
},
{
"parameters": {
"jsCode": "// Publish to FB Page: photo if image_url, else text-only. AI \u751f\u5716\u6642\u81ea\u52d5\u52a0\u6a19\u8a3b.\nconst upstream = $input.first().json;\nconst { post_text, image_url } = upstream;\nconst V = $env.FB_GRAPH_VERSION || 'v24.0';\nconst PAGE_ID = $env.FB_PAGE_ID;\nconst SYS_TOKEN = $env.FB_SYSTEM_USER_TOKEN;\n\nconst imageSource = upstream.image_source || '';\nconst aiNote = (imageSource.startsWith('comfyui') || imageSource.startsWith('pollinations.ai')) ? '\\n\\n\uff08\u5716\u7247 AI \u751f\u6210\uff09' : '';\nconst fullText = (post_text || '') + aiNote;\n\nlet fb_result = { success: false };\n\ntry {\n const accounts = await this.helpers.httpRequest({\n method: 'GET',\n url: `https://graph.facebook.com/${V}/me/accounts`,\n qs: { fields: 'id,name,access_token', limit: 100, access_token: SYS_TOKEN },\n json: true\n });\n const page = (accounts.data || []).find(p => String(p.id) === String(PAGE_ID));\n if (!page) throw new Error(`Page ${PAGE_ID} not in /me/accounts`);\n const pageToken = page.access_token;\n\n let post;\n if (image_url) {\n post = await this.helpers.httpRequest({\n method: 'POST',\n url: `https://graph.facebook.com/${V}/${PAGE_ID}/photos`,\n body: { url: image_url, caption: fullText, access_token: pageToken },\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n json: true\n });\n } else {\n post = await this.helpers.httpRequest({\n method: 'POST',\n url: `https://graph.facebook.com/${V}/${PAGE_ID}/feed`,\n body: { message: fullText, access_token: pageToken },\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n json: true\n });\n }\n\n const postId = post.post_id || post.id || '';\n const [pageNum, postPart] = (postId || '').split('_');\n fb_result = {\n success: true,\n post_id: postId,\n permalink: postPart ? `https://www.facebook.com/${pageNum}/posts/${postPart}` : `https://www.facebook.com/${PAGE_ID}`,\n type: image_url ? 'photo' : 'text',\n ai_disclosure: !!aiNote\n };\n} catch (e) {\n fb_result = { success: false, error: e.message };\n}\n\nreturn [{ json: { ...upstream, fb_result } }];"
},
"id": "7a000000-0000-0000-0000-000000000007",
"name": "Publish FB Page",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1560,
300
]
},
{
"parameters": {
"jsCode": "// Publish to Instagram. AI \u751f\u5716\u6642\u81ea\u52d5\u52a0\u6a19\u8a3b.\nconst upstream = $input.first().json;\nconst { post_text, image_url, metadata } = upstream;\nconst V = $env.FB_GRAPH_VERSION || 'v24.0';\nconst IG_ID = $env.IG_BUSINESS_ACCOUNT_ID;\nconst SYS_TOKEN = $env.FB_SYSTEM_USER_TOKEN;\n\nlet ig_result = { success: false, skipped: false };\n\nif (!image_url) {\n ig_result = { success: false, skipped: true, reason: 'IG requires image, none resolved' };\n} else if (!IG_ID || !SYS_TOKEN) {\n ig_result = { success: false, skipped: true, reason: 'IG env not configured' };\n} else {\n try {\n const imageSource = upstream.image_source || '';\n const aiNote = (imageSource.startsWith('comfyui') || imageSource.startsWith('pollinations.ai')) ? '\\n\\n\uff08\u5716\u7247 AI \u751f\u6210\uff09' : '';\n const tags = (metadata && Array.isArray(metadata.tags)) ? metadata.tags.map(t => '#' + String(t).replace(/^#/, '')).join(' ') : '';\n const caption = (post_text || '') + aiNote + (tags ? '\\n\\n' + tags : '');\n\n const container = await this.helpers.httpRequest({\n method: 'POST',\n url: `https://graph.facebook.com/${V}/${IG_ID}/media`,\n qs: {\n image_url: image_url,\n caption: caption,\n access_token: SYS_TOKEN\n },\n json: true\n });\n const creationId = container.id;\n if (!creationId) throw new Error('No creation_id from /media: ' + JSON.stringify(container).slice(0, 300));\n\n let status = 'IN_PROGRESS';\n for (let i = 0; i < 10; i++) {\n await new Promise(r => setTimeout(r, 3000));\n const statusResp = await this.helpers.httpRequest({\n method: 'GET',\n url: `https://graph.facebook.com/${V}/${creationId}`,\n qs: { fields: 'status_code', access_token: SYS_TOKEN },\n json: true\n });\n status = statusResp.status_code;\n if (status === 'FINISHED') break;\n if (status === 'ERROR') throw new Error('IG container error: ' + JSON.stringify(statusResp));\n }\n if (status !== 'FINISHED') throw new Error('IG container not ready after polling: ' + status);\n\n const publishResp = await this.helpers.httpRequest({\n method: 'POST',\n url: `https://graph.facebook.com/${V}/${IG_ID}/media_publish`,\n qs: { creation_id: creationId, access_token: SYS_TOKEN },\n json: true\n });\n\n ig_result = {\n success: true,\n media_id: publishResp.id,\n permalink: `https://www.instagram.com/p/${publishResp.id}/`,\n ai_disclosure: !!aiNote\n };\n } catch (e) {\n // Deep dump every property of the error for diagnosis\n const errProps = {};\n try {\n for (const k of Object.getOwnPropertyNames(e)) {\n try { errProps[k] = JSON.parse(JSON.stringify(e[k])); }\n catch { errProps[k] = String(e[k]).substring(0, 500); }\n }\n } catch {}\n // Also dig into nested response / cause\n const meta_error = e.response?.data || e.response?.body || e.body || e.cause?.error || e.cause || null;\n ig_result = {\n success: false,\n error: e.message,\n error_name: e.name,\n meta_error: meta_error,\n all_props: Object.keys(errProps),\n err_dump: JSON.stringify(errProps).substring(0, 3000)\n };\n }\n}\n\nreturn [{ json: { ...upstream, ig_result } }];"
},
"id": "8a000000-0000-0000-0000-000000000008",
"name": "Publish Instagram",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1780,
300
]
},
{
"parameters": {
"jsCode": "// Publish to Threads: prefer post_text_threads (cloud routine short version), else truncate long version. AI \u751f\u5716\u6642\u52a0\u6a19\u8a3b.\nconst upstream = $input.first().json;\nconst { post_text, image_url, post_text_threads } = upstream;\nconst T_TOKEN = $env.THREADS_LONG_LIVED_TOKEN;\nconst T_USER = $env.THREADS_USER_ID;\n\nlet threads_result = { success: false, skipped: false };\n\nfunction truncateThreads(text, max = 480) {\n if (!text) return '';\n if (text.length <= max) return text;\n let sectionMarkers = [];\n let idx = 0;\n while ((idx = text.indexOf('\u258b', idx)) !== -1) { sectionMarkers.push(idx); idx++; }\n if (sectionMarkers.length === 0) {\n const re = /\\n\\*\\*[^\\n*]{2,40}\\*\\*\\n/g;\n let m;\n while ((m = re.exec(text)) !== null) { sectionMarkers.push(m.index + 1); }\n }\n if (sectionMarkers.length >= 4) {\n const cut = text.slice(0, sectionMarkers[3]).trim();\n if (cut.length <= max && cut.length > max * 0.5) return cut;\n }\n if (sectionMarkers.length >= 3) {\n const cut = text.slice(0, sectionMarkers[2]).trim();\n if (cut.length <= max && cut.length > max * 0.5) return cut;\n }\n if (sectionMarkers.length >= 2) {\n const cut = text.slice(0, sectionMarkers[1]).trim();\n if (cut.length <= max && cut.length > max * 0.4) return cut;\n }\n const cut = text.slice(0, max);\n const boundaries = ['\u3002', '. ', '\uff01', '!', '\uff1f', '?', '\\n\\n', '\\n'];\n let lastIdx = -1;\n for (const b of boundaries) {\n const i = cut.lastIndexOf(b);\n if (i > lastIdx) lastIdx = i + b.length;\n }\n if (lastIdx > max * 0.6) return cut.slice(0, lastIdx).trim();\n return cut.slice(0, max - 3).trim() + '...';\n}\n\nif (!T_TOKEN || !T_USER) {\n threads_result = { success: false, skipped: true, reason: 'Threads env not configured' };\n} else {\n try {\n const imageSource = upstream.image_source || '';\n const aiNote = (imageSource.startsWith('comfyui') || imageSource.startsWith('pollinations.ai')) ? '\\n\uff08\u5716\u7247 AI \u751f\u6210\uff09' : '';\n const aiNoteLen = aiNote.length;\n const longVersion = post_text || '';\n const shortVersion = post_text_threads || null;\n const maxLen = 480 - aiNoteLen;\n\n let text;\n let usedShortVersion = false;\n let wasTruncated = false;\n\n if (shortVersion && shortVersion.length > 0 && shortVersion.length <= maxLen) {\n text = shortVersion + aiNote;\n usedShortVersion = true;\n } else {\n const truncated = truncateThreads(longVersion, maxLen);\n text = truncated + aiNote;\n wasTruncated = longVersion.length > truncated.length;\n }\n\n const mediaType = image_url ? 'IMAGE' : 'TEXT';\n\n const containerQs = {\n media_type: mediaType,\n text: text,\n access_token: T_TOKEN\n };\n if (image_url) containerQs.image_url = image_url;\n\n const container = await this.helpers.httpRequest({\n method: 'POST',\n url: `https://graph.threads.net/v1.0/${T_USER}/threads`,\n qs: containerQs,\n json: true\n });\n const containerId = container.id;\n if (!containerId) throw new Error('No creation_id from Threads /threads: ' + JSON.stringify(container).slice(0, 300));\n\n await new Promise(r => setTimeout(r, image_url ? 5000 : 2000));\n\n const publishResp = await this.helpers.httpRequest({\n method: 'POST',\n url: `https://graph.threads.net/v1.0/${T_USER}/threads_publish`,\n qs: { creation_id: containerId, access_token: T_TOKEN },\n json: true\n });\n\n threads_result = {\n success: true,\n thread_id: publishResp.id,\n type: mediaType,\n used_short_version: usedShortVersion,\n long_chars: longVersion.length,\n published_chars: text.length,\n truncated: wasTruncated,\n ai_disclosure: !!aiNote\n };\n } catch (e) {\n threads_result = { success: false, error: e.message };\n }\n}\n\nreturn [{ json: { ...upstream, threads_result } }];"
},
"id": "9a000000-0000-0000-0000-000000000009",
"name": "Publish Threads",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2000,
300
]
},
{
"parameters": {
"jsCode": "// Aggregate final log\nconst u = $input.first().json;\nreturn [{\n json: {\n timestamp: new Date().toISOString(),\n gist_id: u.gist_id,\n gist_url: u.gist_url,\n chars: u.post_text_chars,\n chars_threads_short: u.post_text_threads_chars,\n image: { source: u.image_source, has_image: u.has_image, error: u.image_error, url: u.image_url, imgur_error: u.imgur_error },\n fb: u.fb_result,\n ig: u.ig_result,\n threads: u.threads_result,\n summary: [\n u.fb_result && u.fb_result.success ? 'FB OK' : ('FB FAIL: ' + (u.fb_result && u.fb_result.error || 'unknown')),\n u.ig_result && u.ig_result.success ? 'IG OK' : (u.ig_result && u.ig_result.skipped ? 'IG SKIPPED' : ('IG FAIL: ' + (u.ig_result && u.ig_result.error || 'unknown'))),\n u.threads_result && u.threads_result.success ? 'Threads OK' : (u.threads_result && u.threads_result.skipped ? 'Threads SKIPPED' : ('Threads FAIL: ' + (u.threads_result && u.threads_result.error || 'unknown')))\n ].join(' | ')\n }\n}];"
},
"id": "10000000-0000-0000-0000-00000000000a",
"name": "Final Log",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2220,
300
]
}
],
"connections": {
"Schedule 21:00": {
"main": [
[
{
"node": "Pick Latest fb-post Gist",
"type": "main",
"index": 0
}
]
]
},
"Pick Latest fb-post Gist": {
"main": [
[
{
"node": "Fetch Raw Markdown",
"type": "main",
"index": 0
}
]
]
},
"Fetch Raw Markdown": {
"main": [
[
{
"node": "Parse + Extract Draft",
"type": "main",
"index": 0
}
]
]
},
"Parse + Extract Draft": {
"main": [
[
{
"node": "Resolve Image",
"type": "main",
"index": 0
}
]
]
},
"Resolve Image": {
"main": [
[
{
"node": "Upload to Imgur",
"type": "main",
"index": 0
}
]
]
},
"Upload to Imgur": {
"main": [
[
{
"node": "Publish FB Page",
"type": "main",
"index": 0
}
]
]
},
"Publish FB Page": {
"main": [
[
{
"node": "Publish Instagram",
"type": "main",
"index": 0
}
]
]
},
"Publish Instagram": {
"main": [
[
{
"node": "Publish Threads",
"type": "main",
"index": 0
}
]
]
},
"Publish Threads": {
"main": [
[
{
"node": "Final Log",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1",
"binaryMode": "separate"
},
"staticData": {
"node:Schedule 21:": {
"recurrenceRules": []
},
"node:Schedule 21:00": {
"recurrenceRules": []
},
"global": {
"processed_drive_ids": []
}
},
"tags": [],
"versionId": ""
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
content-orchestrator-v4-anime-shortthreads. Uses httpRequest. Scheduled trigger; 10 nodes.
Source: https://github.com/Lee-unhn/n8n-content-orchestrator/blob/main/workflow.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.
Birthday Automation - Production (Fixed). Uses stopAndError, httpRequest, emailSend, bannerbear. Scheduled trigger; 86 nodes.
This template runs two scheduled workflows to govern Microsoft Entra ID (Azure AD) guest accounts by detecting stale users via Microsoft Graph, staging deletions in SharePoint with a 72-hour window, n
Jira-Allure-Auto-Qa. Uses httpRequest, jira. Scheduled trigger; 68 nodes.
Spotify-Sync-Surrealdb-V1. Uses httpRequest, n8n-nodes-surrealdb, spotify. Scheduled trigger; 62 nodes.
As n8n instances scale, teams often lose track of sub-workflows—who uses them, where they are referenced, and whether they can be safely updated. This leads to inefficiencies like unnecessary copies o