{
  "id": "XUANZYDSgp5uAypH",
  "name": "ai_content_repurposing_machine",
  "tags": [],
  "nodes": [
    {
      "id": "574c00cd-7a41-421a-a2e8-094b5f295429",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        0,
        0
      ],
      "parameters": {
        "width": 480,
        "height": 896,
        "content": "## ai_content_repurposing_machine\n\n### How it works\n\nThis workflow turns a YouTube video transcript into repurposed LinkedIn content. It starts with a form submission, retrieves and processes the transcript, sends it to Gemini for content generation, parses the result, saves it in Google Sheets, and publishes it to LinkedIn. It also has a separate error logging path that records failures to a sheet.\n\n### Setup steps\n\n- Configure the form trigger with the fields required by the workflow, such as the YouTube video URL or video ID.\n- Add RapidAPI credentials or headers for the YouTube Transcript API request node.\n- Connect Google Gemini credentials for the Gemini chat model used by the LLM chain.\n- Connect Google Sheets credentials and set the spreadsheet, sheet names, and column mappings for content records, status updates, and error logs.\n- Connect LinkedIn credentials and verify the target profile or organization page for publishing.\n- Review the custom code nodes to ensure they match the transcript API response format and the expected AI response structure.\n\n### Customization\n\nYou can adjust the Gemini prompt to produce different content formats, change the Google Sheets columns used for tracking, or replace LinkedIn publishing with another social channel."
      },
      "typeVersion": 1
    },
    {
      "id": "38347ac7-8b4a-43c9-a610-9decc9ebe3d6",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        560,
        48
      ],
      "parameters": {
        "color": 7,
        "width": 672,
        "height": 320,
        "content": "## Collect video transcript\n\nStarts from a submitted form, fetches the YouTube transcript through the RapidAPI transcript endpoint, and runs custom code to clean or normalize the transcript for downstream AI processing."
      },
      "typeVersion": 1
    },
    {
      "id": "987fc2a8-fefd-45d2-b4f5-6b0eed1a1217",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1328,
        32
      ],
      "parameters": {
        "color": 7,
        "width": 480,
        "height": 560,
        "content": "## Generate and parse content\n\nUses a Gemini-powered LLM chain to repurpose the processed transcript into social content, then parses the AI output into structured fields for saving and publishing."
      },
      "typeVersion": 1
    },
    {
      "id": "9b539284-c84d-4991-bdbc-cc40b073b155",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1888,
        0
      ],
      "parameters": {
        "color": 7,
        "width": 768,
        "height": 480,
        "content": "## Publish and update status\n\nAppends the generated content to Google Sheets, publishes it to LinkedIn, and updates the sheet with either posted or failed status based on the publishing result."
      },
      "typeVersion": 1
    },
    {
      "id": "a381ac29-5ecc-49c0-8bfd-087b4d9908ca",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2208,
        576
      ],
      "parameters": {
        "color": 7,
        "height": 432,
        "content": "## Handle workflow errors\n\nA separate lower canvas cluster that receives errors from transcript fetching, transcript processing, AI generation, parsing, and sheet appending, formats the error details, and logs them to Google Sheets."
      },
      "typeVersion": 1
    },
    {
      "id": "d2923783-b8f4-4382-a3bf-1d8100be705f",
      "name": "When Form Submitted",
      "type": "n8n-nodes-base.formTrigger",
      "position": [
        608,
        208
      ],
      "parameters": {
        "options": {},
        "formTitle": "AI Content Repurposing Machine",
        "formFields": {
          "values": [
            {
              "fieldLabel": "YouTube Video URL",
              "placeholder": "https://www.youtube.com/watch?v=...",
              "requiredField": true
            }
          ]
        },
        "formDescription": "Enter a YouTube video URL to automatically generate multiple content assets."
      },
      "typeVersion": 2.2
    },
    {
      "id": "e7dd8a84-0422-422b-95ae-9d8193f22829",
      "name": "Fetch Transcript from YouTube",
      "type": "n8n-nodes-base.httpRequest",
      "notes": "Calls RapidAPI transcript API and pulls transcript text from the submitted YouTube URL.",
      "onError": "continueErrorOutput",
      "position": [
        848,
        208
      ],
      "parameters": {
        "url": "https://youtube-transcript3.p.rapidapi.com/api/transcript-with-url",
        "options": {},
        "sendQuery": true,
        "sendHeaders": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "url",
              "value": "={{ $json['YouTube Video URL'] }}"
            },
            {
              "name": "flat_text",
              "value": "true"
            }
          ]
        },
        "headerParameters": {
          "parameters": [
            {
              "name": "x-rapidapi-host",
              "value": "youtube-transcript3.p.rapidapi.com"
            },
            {
              "name": "x-rapidapi-key",
              "value": "YOUR_RAPIDAPI_KEY_HERE"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "ae3b25ed-7fcc-4e73-9d62-af5735be2812",
      "name": "Process YouTube Transcript",
      "type": "n8n-nodes-base.code",
      "notes": "Normalizes transcript API output into a clean single string.",
      "onError": "continueErrorOutput",
      "position": [
        1088,
        208
      ],
      "parameters": {
        "jsCode": "const input = $input.first().json;\nlet transcript = '';\nif (typeof input === 'string') { transcript = input; }\nelse if (typeof input.transcript === 'string') { transcript = input.transcript; }\nelse if (typeof input.text === 'string') { transcript = input.text; }\nelse if (Array.isArray(input)) { transcript = input.map(i => i.text || i.transcript || '').join(' '); }\nelse if (Array.isArray(input.transcripts)) { transcript = input.transcripts.map(i => i.text || '').join(' '); }\nelse { const raw = JSON.stringify(input); const matches = [...raw.matchAll(/\\\"text\\\":\\\"(.*?)\\\"/g)]; transcript = matches.map(m => m[1]).join(' '); }\ntranscript = transcript.replace(/\\[\\d{2}:\\d{2}(?::\\d{2})?\\]/g, ' ').replace(/\\(\\d{2}:\\d{2}(?::\\d{2})?\\)/g, ' ').replace(/\\s+/g, ' ').trim();\nif (!transcript || transcript.length < 50) throw new Error('Transcript is empty or too short.');\nreturn [{ json: { youtubeUrl: $('When Form Submitted').first().json['YouTube Video URL'], transcript, transcriptLength: transcript.length } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "5a3b41ac-e6cf-4fe7-902a-9fd48464ce0d",
      "name": "Generate Content with LLM",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "notes": "Generates all requested content assets from the cleaned transcript using Gemini.",
      "onError": "continueErrorOutput",
      "position": [
        1376,
        208
      ],
      "parameters": {
        "text": "=You are an expert content repurposing strategist. Analyze the following YouTube transcript and return ONLY valid JSON with these exact keys: title, summary, blog, linkedin, twitter_thread, newsletter, content_ideas, viral_hooks, cta_suggestions.\\n\\nRules:\\n- title: concise and SEO-friendly\\n- summary: 150-200 words\\n- blog: minimum 1500 words in markdown with headings\\n- linkedin: highly engaging post for LinkedIn\\n- twitter_thread: exactly 10 tweets, numbered 1/10 to 10/10\\n- newsletter: 400-600 words with subject line\\n- content_ideas: 5 ideas as numbered list\\n- viral_hooks: 10 hooks as numbered list\\n- cta_suggestions: 5 CTA suggestions as numbered list\\n- Return raw JSON only, no code fences, no commentary\\n\\nYouTube URL: {{ $json.youtubeUrl }}\\n\\nTranscript:\\n{{ $json.transcript }}",
        "promptType": "define"
      },
      "typeVersion": 1.5
    },
    {
      "id": "764b54ba-28db-407a-a1ab-a35e70ad4ab2",
      "name": "AI Chat Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini",
      "notes": "Gemini chat model connected to the Basic LLM Chain node.",
      "position": [
        1376,
        448
      ],
      "parameters": {
        "options": {
          "temperature": 0.7,
          "maxOutputTokens": 8192
        }
      },
      "credentials": {
        "googlePalmApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "0a6cb248-d9b5-486a-be99-3a698c009886",
      "name": "Parse Content Response",
      "type": "n8n-nodes-base.code",
      "notes": "Parses the Gemini JSON response and prepares a normalized object for downstream steps.",
      "onError": "continueErrorOutput",
      "position": [
        1664,
        208
      ],
      "parameters": {
        "jsCode": "const ai = $input.first().json;\nlet text = ai.text || ai.content || ai.response || ai.message || JSON.stringify(ai);\ntext = text.replace(/```json\\n?/g, '').replace(/```\\n?/g, '').trim();\nconst start = text.indexOf('{');\nconst end = text.lastIndexOf('}');\nif (start === -1 || end === -1) throw new Error('AI did not return valid JSON.');\nconst raw = text.slice(start, end + 1);\nlet parsed;\ntry { parsed = JSON.parse(raw); } catch (e) { throw new Error('Failed to parse AI JSON: ' + e.message); }\nconst required = ['title','summary','blog','linkedin','twitter_thread','newsletter','content_ideas','viral_hooks','cta_suggestions'];\nfor (const key of required) { if (!parsed[key]) parsed[key] = ''; }\nconst transcriptData = $('Process YouTube Transcript').first().json;\nreturn [{ json: { timestamp: new Date().toISOString(), youtubeUrl: transcriptData.youtubeUrl, transcript: transcriptData.transcript, title: parsed.title, summary: parsed.summary, blog: parsed.blog, linkedin: parsed.linkedin, twitter_thread: parsed.twitter_thread, newsletter: parsed.newsletter, content_ideas: parsed.content_ideas, viral_hooks: parsed.viral_hooks, cta_suggestions: parsed.cta_suggestions, linkedin_status: 'Pending', created_date: new Date().toISOString().split('T')[0] } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "222a9db9-f442-4d6b-b196-902d77c98e5b",
      "name": "Append Output to Sheets",
      "type": "n8n-nodes-base.googleSheets",
      "notes": "Appends all generated content into Sheet1. Create the exact header names in advance.",
      "onError": "continueErrorOutput",
      "position": [
        1936,
        208
      ],
      "parameters": {
        "columns": {
          "value": {
            "Summary": "={{ $json.summary }}",
            "Timestamp": "={{ $json.timestamp }}",
            "Newsletter": "={{ $json.newsletter }}",
            "Transcript": "={{ $json.transcript }}",
            "Video Title": "={{ $json.title }}",
            "Viral Hooks": "={{ $json.viral_hooks }}",
            "YouTube URL": "={{ $json.youtubeUrl }}",
            "Blog Article": "={{ $json.blog }}",
            "Created Date": "={{ $json.created_date }}",
            "Content Ideas": "={{ $json.content_ideas }}",
            "LinkedIn Post": "={{ $json.linkedin }}",
            "Twitter Thread": "={{ $json.twitter_thread }}",
            "CTA Suggestions": "={{ $json.cta_suggestions }}",
            "LinkedIn Status": "={{ $json.linkedin_status }}"
          },
          "mappingMode": "defineBelow"
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "Sheet1"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_GOOGLE_SHEET_ID_HERE"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "54ab2b1a-62c1-44ea-abff-3826df0f3550",
      "name": "Post Content on LinkedIn",
      "type": "n8n-nodes-base.linkedIn",
      "notes": "Publishes the AI-generated LinkedIn post to your LinkedIn profile. Uses OAuth2 for authentication. Ensure your LinkedIn OAuth2 credentials are configured in n8n credentials.",
      "onError": "continueErrorOutput",
      "position": [
        2240,
        192
      ],
      "parameters": {
        "text": "={{ $('Parse Content Response').first().json.linkedin }}",
        "person": "urn:li:person:me",
        "additionalFields": {}
      },
      "retryOnFail": false,
      "typeVersion": 1
    },
    {
      "id": "8ad470a3-01dc-43ae-b5ca-634e05884f5e",
      "name": "Update Posted Status in Sheets",
      "type": "n8n-nodes-base.googleSheets",
      "notes": "Marks the matching row as Posted after successful LinkedIn publish.",
      "position": [
        2512,
        128
      ],
      "parameters": {
        "columns": {
          "value": {
            "LinkedIn Status": "Posted"
          },
          "mappingMode": "defineBelow"
        },
        "options": {},
        "operation": "update",
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "Sheet1"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_GOOGLE_SHEET_ID_HERE"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "47c756d7-cdc9-445d-93ea-d03309001d00",
      "name": "Update Failure Status in Sheets",
      "type": "n8n-nodes-base.googleSheets",
      "notes": "Marks the matching row as Failed with the LinkedIn error message when publishing fails.",
      "position": [
        2512,
        304
      ],
      "parameters": {
        "columns": {
          "value": {
            "LinkedIn Status": "={{ 'Failed - ' + ($json.message || $json.error?.message || 'Unknown LinkedIn error') }}"
          },
          "mappingMode": "defineBelow"
        },
        "options": {},
        "operation": "update",
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "Sheet1"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_GOOGLE_SHEET_ID_HERE"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "c8e40a99-f187-4b5c-ab39-70b6472ddf6c",
      "name": "Log Errors to Sheets",
      "type": "n8n-nodes-base.googleSheets",
      "notes": "Appends transcript, Gemini, parse, or sheets errors to the Errors tab.",
      "position": [
        2480,
        848
      ],
      "parameters": {
        "columns": {
          "value": {
            "Timestamp": "={{ $json['Timestamp'] }}",
            "Error Node": "={{ $json['Error Node'] }}",
            "YouTube URL": "={{ $json['YouTube URL'] }}",
            "Error Message": "={{ $json['Error Message'] }}"
          },
          "mappingMode": "defineBelow"
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "Errors"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_GOOGLE_SHEET_ID_HERE"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "a7aa6cbe-84c1-4088-830b-4a4f7d9cdd6d",
      "name": "Handle Errors in Workflow",
      "type": "n8n-nodes-base.code",
      "notes": "Normalizes upstream errors into a simple row format for the Errors sheet.",
      "position": [
        2256,
        848
      ],
      "parameters": {
        "jsCode": "const err = $input.first().json;\nconst msg = err.error?.message || err.message || JSON.stringify(err);\nconst node = err.error?.node?.name || 'Unknown Node';\nreturn [{ json: { Timestamp: new Date().toISOString(), 'YouTube URL': (() => { try { return $('When Form Submitted').first().json['YouTube Video URL']; } catch (e) { return ''; } })(), 'Error Message': msg, 'Error Node': node } }];"
      },
      "typeVersion": 2
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "executionOrder": "v1"
  },
  "versionId": "c22aaa7a-dc50-4a38-9608-0dd6be20aa1d",
  "connections": {
    "AI Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "Generate Content with LLM",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "When Form Submitted": {
      "main": [
        [
          {
            "node": "Fetch Transcript from YouTube",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Content Response": {
      "main": [
        [
          {
            "node": "Append Output to Sheets",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Handle Errors in Workflow",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Append Output to Sheets": {
      "main": [
        [
          {
            "node": "Post Content on LinkedIn",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Handle Errors in Workflow",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Post Content on LinkedIn": {
      "main": [
        [
          {
            "node": "Update Posted Status in Sheets",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Update Failure Status in Sheets",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate Content with LLM": {
      "main": [
        [
          {
            "node": "Parse Content Response",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Handle Errors in Workflow",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Handle Errors in Workflow": {
      "main": [
        [
          {
            "node": "Log Errors to Sheets",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Process YouTube Transcript": {
      "main": [
        [
          {
            "node": "Generate Content with LLM",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Handle Errors in Workflow",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Transcript from YouTube": {
      "main": [
        [
          {
            "node": "Process YouTube Transcript",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Handle Errors in Workflow",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}