How to Build an AI Personalization Engine with n8n (Free Template)

How to Build an AI Personalization Engine with n8n (Free Template)

Coaches and creators waste hours manually customizing their frameworks for each client. A fitness coach rewrites the same workout principles 50 times. A productivity expert rebuilds their system for every customer. This article shows you how to build an AI personalization engine that transforms a creator's existing content into custom Gamma presentations automatically. You'll learn the exact n8n workflow architecture, node configurations, and logic needed to deploy this system.

The Problem: Manual Personalization Doesn't Scale

Creators face a critical bottleneck when delivering personalized content. They've built frameworks, methodologies, and systems that work—but customizing them for individual clients requires manual effort every single time.

Current challenges:

  • Coaches spend 2-4 hours per client manually adapting their frameworks
  • Each personalized deliverable requires recreating the same structure from scratch
  • Scaling to 50+ clients means 100+ hours of repetitive customization work
  • Generic templates feel impersonal and reduce perceived value
  • No systematic way to capture a creator's decision-making logic

Business impact:

  • Time spent: 8-16 hours per week on manual customization
  • Revenue ceiling: Can only serve 10-15 clients personally before quality drops
  • Inconsistency: Different clients get different quality levels based on creator energy
  • Lost revenue: Unable to offer personalized products at scale

The core issue isn't creating content—it's systematically applying a creator's expertise to individual client contexts without manual intervention.

The Solution Overview

This n8n workflow builds a white-label AI personalization engine that converts creator knowledge into client-specific Gamma presentations. The system ingests a creator's frameworks through structured onboarding, stores their decision logic in Airtable, processes client intake forms, and generates fully personalized presentations using OpenAI's API. Unlike generic chatbots, this system encodes the creator's actual methodology—their rules, constraints, tone, and frameworks—then applies them systematically to each client's unique situation. The workflow handles everything from knowledge extraction to final Gamma document generation without requiring the creator to understand prompts or AI.

What You'll Build

This automation system delivers a complete personalization pipeline from creator onboarding to client deliverable.

Component Technology Purpose
Creator Onboarding Google Forms + n8n Webhook Capture frameworks, rules, tone, and constraints
Knowledge Storage Airtable Store structured creator logic and client data
Content Processing OpenAI GPT-4 Transform intake responses into personalized content
Document Generation Gamma API Create branded presentations automatically
Intake Forms Typeform/Google Forms Collect client-specific information
Orchestration n8n Workflows Connect all systems and apply business logic

Key capabilities:

  • Onboard new creators in under 60 minutes
  • Process client intake forms into personalized frameworks
  • Generate Gamma presentations matching creator's tone and style
  • Support multiple creators across different industries
  • Duplicate system for new creators without rebuilding logic
  • Apply creator-defined rules and constraints automatically
  • Scale from 1 to 100+ clients without manual intervention

Prerequisites

Before starting, ensure you have:

  • n8n instance (cloud or self-hosted with 2GB+ RAM)
  • OpenAI API account with GPT-4 access ($20+ credit recommended)
  • Airtable account (Pro plan for API access)
  • Gamma account with API access
  • Google Drive API credentials
  • Typeform or Google Forms account
  • Basic understanding of JSON data structures
  • Familiarity with API authentication (OAuth2, API keys)

Step 1: Set Up Creator Knowledge Database

The foundation of this system is how you structure and store creator expertise. This isn't just file storage—it's encoding decision logic.

Configure Airtable Base

Create an Airtable base with three core tables:

  1. Creators Table

    • Creator ID (primary key)
    • Name
    • Industry (single select: Fitness, Travel, Dating, Productivity, etc.)
    • Tone (long text: conversational, professional, motivational)
    • Rules (long text: "Never recommend X", "Always include Y")
    • Frameworks (long text: structured methodologies)
    • Style Parameters (long text: formatting preferences)
  2. Client Intake Table

    • Intake ID (primary key)
    • Creator ID (linked to Creators)
    • Client Name
    • Intake Responses (long text: JSON formatted)
    • Generated Content (long text)
    • Gamma Document URL
    • Status (single select: Pending, Processing, Complete)
  3. Content Library Table

    • Content ID (primary key)
    • Creator ID (linked to Creators)
    • Content Type (single select: PDF, Doc, Video Summary, Image)
    • File URL
    • Extracted Text (long text)
    • Processing Status

n8n Airtable Node Configuration:

{
  "authentication": "oAuth2",
  "base": "appXXXXXXXXXXXX",
  "table": "Creators",
  "operation": "create",
  "options": {
    "typecast": true
  }
}

Why this structure works:

Separating creators, content, and client data allows the system to scale horizontally. You can add 100 creators without modifying the workflow logic. The Creators table becomes your "personalization engine configuration"—each row defines how AI should behave for that specific creator. Storing rules and frameworks as structured text (not just files) enables the AI to reference them directly in prompts without additional parsing.

Step 2: Build Creator Onboarding Workflow

This workflow transforms a creator's raw materials into structured AI instructions.

Create Webhook Trigger

Set up an n8n Webhook node to receive Google Form submissions:

{
  "httpMethod": "POST",
  "path": "creator-onboarding",
  "responseMode": "lastNode",
  "options": {
    "rawBody": false
  }
}

Connect your Google Form to send responses to this webhook URL: https://your-n8n-instance.com/webhook/creator-onboarding

Process Creator Questionnaire

Add a Function node to structure the form data:

const creatorData = {
  name: $json.body.name,
  industry: $json.body.industry,
  frameworks: $json.body.frameworks,
  rules: $json.body.rules,
  tone: $json.body.tone,
  styleParameters: $json.body.style_parameters
};

return { json: creatorData };

Extract Content from Google Drive

Configure a Google Drive node to list files from the creator's upload folder:

{
  "operation": "list",
  "folderId": "={{ $json.body.drive_folder_id }}",
  "options": {
    "fields": ["id", "name", "mimeType", "webViewLink"]
  }
}

For each file, add a Switch node to route by file type:

  • Google Docs: Use Google Docs node to get document content
  • PDFs: Use HTTP Request to download, then Code node with pdf-parse library
  • Images: Skip or use OCR if needed
  • Videos: Use OpenAI Whisper API for transcription (separate workflow)

Store in Airtable

Create an Airtable node to insert the structured creator profile:

{
  "operation": "create",
  "table": "Creators",
  "fields": {
    "Name": "={{ $json.name }}",
    "Industry": "={{ $json.industry }}",
    "Frameworks": "={{ $json.frameworks }}",
    "Rules": "={{ $json.rules }}",
    "Tone": "={{ $json.tone }}",
    "Style Parameters": "={{ $json.styleParameters }}"
  }
}

Why this approach:

Separating file extraction from questionnaire processing allows you to handle different content types independently. The Function node normalizes data structure regardless of form platform (Google Forms, Typeform, Fillout). Storing everything in Airtable creates a single source of truth that the personalization workflow references.

Variables to customize:

  • drive_folder_id: Change per creator during onboarding
  • fields: Add custom questionnaire fields without modifying workflow logic

Step 3: Build Client Intake Processing Workflow

This workflow takes client responses and generates personalized content.

Webhook for Intake Forms

Create a second webhook to receive client intake submissions:

{
  "httpMethod": "POST",
  "path": "client-intake",
  "responseMode": "lastNode"
}

Retrieve Creator Profile

Add an Airtable node to fetch the creator's configuration:

{
  "operation": "search",
  "table": "Creators",
  "filterByFormula": "={Creator ID}='{{ $json.body.creator_id }}'"
}

This pulls the creator's frameworks, rules, tone, and style parameters.

Build Personalization Prompt

Add a Function node to construct the OpenAI prompt:

const creator = $('Airtable').first().json;
const intake = $json.body;

const prompt = `You are ${creator.Name}, a ${creator.Industry} expert. 

Your frameworks:
${creator.Frameworks}

Your rules:
${creator.Rules}

Your tone: ${creator.Tone}

Client information:
${JSON.stringify(intake, null, 2)}

Generate a personalized ${intake.deliverable_type} for this client following your frameworks and rules. Structure the output as a detailed outline with sections, bullet points, and specific recommendations.

Output format:
# Title
## Section 1
- Point 1
- Point 2
## Section 2
...`;

return { json: { prompt } };

Generate Content with OpenAI

Configure an OpenAI node:

{
  "resource": "chat",
  "operation": "create",
  "model": "gpt-4-turbo-preview",
  "messages": [
    {
      "role": "system",
      "content": "You are an expert personalization engine that applies creator frameworks to individual client situations."
    },
    {
      "role": "user",
      "content": "={{ $json.prompt }}"
    }
  ],
  "options": {
    "temperature": 0.7,
    "maxTokens": 3000
  }
}

Why this works:

The prompt engineering here is critical. You're not asking GPT-4 to "be creative"—you're instructing it to apply specific frameworks and rules. The temperature of 0.7 balances consistency with personalization. Setting maxTokens to 3000 ensures complete outputs for complex deliverables like workout plans or travel itineraries.

Step 4: Generate Gamma Presentation

This is where structured content becomes a polished deliverable.

Transform Content for Gamma

Add a Function node to convert OpenAI output into Gamma-compatible format:

const content = $json.choices[0].message.content;
const creator = $('Airtable').first().json;

// Parse markdown into Gamma slides structure
const slides = content.split('
## ').map(section => {
  const [title, ...bodyLines] = section.split('
');
  return {
    title: title.replace('# ', ''),
    body: bodyLines.join('
')
  };
});

return {
  json: {
    presentation: {
      title: slides[0].title,
      slides: slides.slice(1),
      theme: creator['Style Parameters'] || 'modern'
    }
  }
};

Create Gamma Document

Configure an HTTP Request node to call Gamma's API:

{
  "method": "POST",
  "url": "https://api.gamma.app/v1/documents",
  "authentication": "genericCredentialType",
  "genericAuthType": "httpHeaderAuth",
  "options": {
    "headers": {
      "Authorization": "Bearer YOUR_GAMMA_API_KEY",
      "Content-Type": "application/json"
    },
    "body": {
      "title": "={{ $json.presentation.title }}",
      "slides": "={{ JSON.stringify($json.presentation.slides) }}",
      "theme": "={{ $json.presentation.theme }}"
    }
  }
}

Store Results

Add an Airtable node to update the Client Intake record:

{
  "operation": "update",
  "table": "Client Intake",
  "id": "={{ $('Webhook').item.json.body.intake_id }}",
  "fields": {
    "Generated Content": "={{ $('OpenAI').item.json.choices[0].message.content }}",
    "Gamma Document URL": "={{ $json.url }}",
    "Status": "Complete"
  }
}

Why this approach:

Breaking content into slides programmatically ensures consistent structure across all generated presentations. The Function node acts as a translation layer between OpenAI's markdown output and Gamma's expected JSON format. Storing both the raw content and final URL allows you to regenerate or modify presentations later without re-running the AI.

Workflow Architecture Overview

This system consists of 23 nodes organized into 3 main workflows:

  1. Creator Onboarding Workflow (Nodes 1-8): Receives form submissions, extracts content from Google Drive, structures creator knowledge, and stores in Airtable
  2. Client Intake Processing Workflow (Nodes 9-18): Receives client responses, retrieves creator profile, generates personalized content via OpenAI, and creates Gamma presentation
  3. Content Library Management (Nodes 19-23): Handles file uploads, text extraction from PDFs/Docs, and updates Content Library table

Execution flow:

  • Trigger: Webhook receives POST request from form submission
  • Average run time: 45-90 seconds (depends on OpenAI response time)
  • Key dependencies: Airtable for data storage, OpenAI for content generation, Gamma for final output

Critical nodes:

  • Function (Prompt Builder): Constructs the personalization prompt by combining creator frameworks with client intake data
  • OpenAI Chat: Generates the actual personalized content following creator-defined rules
  • HTTP Request (Gamma API): Transforms structured content into polished presentation
  • Airtable (Creator Lookup): Retrieves the creator's configuration that defines how personalization happens

The complete n8n workflow JSON template is available at the bottom of this article.

Key Configuration Details

OpenAI Integration

Required fields:

  • API Key: Your OpenAI API key from platform.openai.com
  • Model: gpt-4-turbo-preview (required for complex reasoning)
  • Temperature: 0.7 (balance between consistency and creativity)
  • Max Tokens: 3000 (increase to 4000 for longer deliverables)

Common issues:

  • Using GPT-3.5 instead of GPT-4 → Results in generic, less personalized outputs
  • Temperature above 0.9 → Creates inconsistent tone across clients
  • Max tokens too low → Cuts off content mid-generation

Gamma API Configuration

Authentication:

{
  "headerAuth": {
    "name": "Authorization",
    "value": "Bearer YOUR_GAMMA_API_KEY"
  }
}

Slide structure format:

{
  "slides": [
    {
      "title": "Section Title",
      "body": "Markdown formatted content
- Bullet 1
- Bullet 2",
      "layout": "title-body"
    }
  ]
}

Why this approach:

Gamma expects slides as an array of objects, not raw markdown. The Function node that transforms content must parse markdown headers (##) as slide boundaries. Each slide needs a title and body—if you pass unstructured text, Gamma creates a single slide with everything crammed in.

Variables to customize:

  • theme: Change per creator (modern, minimal, bold, etc.)
  • layout: Adjust slide layouts (title-body, two-column, image-text)
  • maxTokens: Increase for longer deliverables (meal plans need 4000+, workout plans 3000)

Testing & Validation

Test Creator Onboarding

  1. Submit a test Google Form with sample creator data
  2. Check n8n execution log—verify webhook received data
  3. Inspect Airtable Creators table—confirm new row created
  4. Review extracted content in Content Library table

Common issues:

  • Webhook returns 404 → Check webhook path matches form submission URL
  • Airtable node fails → Verify API key has write permissions
  • Google Drive extraction empty → Ensure Drive API credentials have folder access

Test Client Intake Processing

  1. Submit a test intake form with sample client data
  2. Monitor n8n execution—watch for OpenAI node completion
  3. Check OpenAI output—verify it references creator frameworks
  4. Inspect Gamma document—confirm slides match content structure
  5. Review Airtable Client Intake table—verify status updated to "Complete"

Validation checklist:

  • OpenAI prompt includes creator rules and frameworks
  • Generated content matches creator's tone (compare to sample)
  • Gamma presentation has 5+ slides (not single slide dump)
  • All client intake data referenced in personalized content
  • No hallucinated information (AI inventing facts not in creator knowledge)

Running evaluations:

Create a test set of 5 client intakes with known correct outputs. Run each through the workflow and compare:

  • Does output follow creator's stated rules?
  • Is tone consistent with creator's examples?
  • Are frameworks applied correctly to client context?
  • Does Gamma presentation structure make sense?

If more than 1/5 tests fail, adjust the OpenAI prompt temperature or add more explicit instructions.

Deployment Considerations

Production Deployment Checklist

Area Requirement Why It Matters
Error Handling Add Error Trigger nodes after OpenAI and Gamma API calls Prevents workflow failure from stopping entire pipeline
Retry Logic Configure HTTP Request nodes with 3 retries, 5s delay Handles temporary API failures without manual intervention
Monitoring Set up n8n workflow execution alerts via email/Slack Detect failures within 5 minutes vs discovering days later
Rate Limiting Add Wait node (2s) between OpenAI calls if processing multiple clients Avoids hitting OpenAI rate limits (3500 requests/min on paid plans)
Data Backup Schedule daily Airtable export to Google Drive Protects against accidental data deletion
API Key Security Store all API keys in n8n credentials, never in workflow JSON Prevents credential exposure if sharing workflows
Documentation Add Sticky Note nodes explaining each workflow section Reduces modification time from 4 hours to 30 minutes

Error handling strategy:

Add an Error Trigger workflow that catches failed executions:

{
  "node": "Error Trigger",
  "type": "n8n-nodes-base.errorTrigger",
  "parameters": {
    "errorWorkflows": ["Creator Onboarding", "Client Intake Processing"]
  }
}

Connect to a Slack/Email node that sends alerts with execution ID and error details.

Monitoring recommendations:

  • Track OpenAI API usage daily (cost can spike with high volume)
  • Monitor average workflow execution time (baseline: 60s, alert if >120s)
  • Set up Airtable automation to flag intakes stuck in "Processing" status >10 minutes
  • Review generated Gamma documents weekly for quality degradation

Use Cases & Variations

Use Case 1: Fitness Coach - Personalized Workout Plans

  • Industry: Fitness & Wellness
  • Scale: 200 clients/month
  • Modifications needed:
    • Add exercise database to Airtable (exercises, muscle groups, equipment)
    • Modify OpenAI prompt to reference exercise library
    • Adjust Gamma template for workout schedule layout (7-day grid)
    • Add video embedding for exercise demonstrations

Use Case 2: Travel Advisor - Custom Itineraries

  • Industry: Travel & Hospitality
  • Scale: 50 itineraries/month
  • Modifications needed:
    • Integrate Google Maps API for location data
    • Add budget calculation logic in Function node
    • Modify Gamma output to include day-by-day breakdown with maps
    • Store destination-specific tips in Airtable (weather, customs, phrases)

Use Case 3: Productivity Coach - Personalized Systems

  • Industry: Business & Productivity
  • Scale: 100 clients/month
  • Modifications needed:
    • Add calendar integration (Google Calendar API) to analyze current schedule
    • Modify OpenAI prompt to identify time-wasting patterns
    • Adjust Gamma template for Gantt chart-style project timelines
    • Include tool recommendations based on client's tech stack

Use Case 4: Dating Coach - Profile Optimization

  • Industry: Dating & Relationships
  • Scale: 300 clients/month
  • Modifications needed:
    • Add image analysis (OpenAI Vision API) for profile photo feedback
    • Modify prompt to analyze messaging style from sample conversations
    • Create Gamma template for before/after profile comparisons
    • Store dating platform-specific best practices (Hinge vs Tinder vs Bumble)

Use Case 5: Nutrition Coach - Meal Plans

  • Industry: Health & Nutrition
  • Scale: 150 clients/month
  • Modifications needed:
    • Add recipe database to Airtable (ingredients, macros, prep time)
    • Integrate with nutrition API (Nutritionix) for macro calculations
    • Modify OpenAI prompt to balance macros based on client goals
    • Adjust Gamma template for grocery list and meal prep schedule

Customizations & Extensions

Alternative Integrations

Instead of Airtable:

  • Notion: Best for creators who already use Notion for content - requires Notion API nodes (6 node changes)
  • Google Sheets: Better if you need real-time collaboration - swap Airtable nodes for Google Sheets nodes (same structure)
  • PostgreSQL/Supabase: Use when handling 1000+ creators - requires Database nodes and more complex queries (10+ node changes)

Instead of Gamma:

  • Google Slides: More customization control - use Google Slides API nodes (4 node changes)
  • Canva: Better brand consistency - requires Canva API integration (8 nodes)
  • PDF Generation: Use Puppeteer or HTML-to-PDF for printable deliverables (12 nodes)

Workflow Extensions

Add automated quality checks:

  • Insert a Function node after OpenAI to validate output length, structure, and keyword presence
  • Add a Switch node to route low-quality outputs to manual review queue
  • Connect to Slack for human approval before Gamma generation
  • Nodes needed: +4 (Function, Switch, Slack, Set)

Scale to handle batch processing:

  • Replace single intake webhook with scheduled Airtable query (process all "Pending" intakes)
  • Add Loop node to process 10 intakes per execution
  • Implement queue system using Airtable status field
  • Performance improvement: Process 50 clients in 15 minutes vs 50 separate 60s executions

Add multi-language support:

  • Detect client language from intake form
  • Add translation step using OpenAI with language-specific prompts
  • Store creator frameworks in multiple languages in Airtable
  • Nodes needed: +6 (Language Detection, Translation, Conditional routing)

Integration possibilities:

Add This To Get This Complexity
Stripe integration Automated payment before generating deliverable Easy (3 nodes)
Email automation Send Gamma link directly to client Easy (2 nodes)
CRM sync (HubSpot) Track client journey and deliverable status Medium (6 nodes)
Analytics dashboard Track creator performance, client satisfaction Medium (8 nodes)
White-label portal Custom domain for each creator's intake forms Hard (15+ nodes)
A/B testing Test different prompt variations for quality Medium (7 nodes)

Customization ideas:

  • Dynamic pricing: Charge based on deliverable complexity (workout plan vs full program)
  • Revision workflow: Allow clients to request modifications, re-run with updated inputs
  • Content versioning: Store multiple versions of creator frameworks, A/B test which performs better
  • Compliance checking: Add legal/medical disclaimer validation for health/fitness coaches
  • Referral tracking: Embed referral codes in Gamma documents, track which creators drive most conversions

Get Started Today

Ready to automate your personalization engine?

  1. Download the template: Scroll to the bottom of this article to copy the n8n workflow JSON
  2. Import to n8n: Go to Workflows → Import from URL or File, paste the JSON
  3. Configure your services: Add your API credentials for OpenAI, Airtable, Gamma, and Google Drive
  4. Set up Airtable base: Create the three tables (Creators, Client Intake, Content Library) with fields specified in Step 1
  5. Test with sample data: Submit a test creator onboarding form and client intake to verify everything works
  6. Deploy to production: Activate the workflows and start onboarding real creators

This system transforms how creators deliver personalized content. Instead of spending 4 hours per client, you'll generate custom deliverables in 60 seconds.

Need help customizing this workflow for your specific industry or scaling to handle 500+ clients per month? Schedule an intro call with Atherial at https://atherial.com/contact.


N8N Workflow JSON Template

{
  "name": "AI Personalization Engine",
  "nodes": [
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "creator-onboarding",
        "responseMode": "lastNode",
        "options": {}
      },
      "name": "Creator Onboarding Webhook",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 1,
      "position": [250, 300]
    },
    {
      "parameters": {
        "functionCode": "const creatorData = {
  name: $json.body.name,
  industry: $json.body.industry,
  frameworks: $json.body.frameworks,
  rules: $json.body.rules,
  tone: $json.body.tone,
  styleParameters: $json.body.style_parameters
};

return { json: creatorData };"
      },
      "name": "Structure Creator Data",
      "type": "n8n-nodes-base.function",
      "typeVersion": 1,
      "position": [450, 300]
    },
    {
      "parameters": {
        "authentication": "oAuth2",
        "operation": "create",
        "base": "appXXXXXXXXXXXX",
        "table": "Creators",
        "fields": {
          "Name": "={{ $json.name }}",
          "Industry": "={{ $json.industry }}",
          "Frameworks": "={{ $json.frameworks }}",
          "Rules": "={{ $json.rules }}",
          "Tone": "={{ $json.tone }}",
          "Style Parameters": "={{ $json.styleParameters }}"
        }
      },
      "name": "Store Creator Profile",
      "type": "n8n-nodes-base.airtable",
      "typeVersion": 1,
      "position": [650, 300]
    },
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "client-intake",
        "responseMode": "lastNode"
      },
      "name": "Client Intake Webhook",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 1,
      "position": [250, 500]
    },
    {
      "parameters": {
        "authentication": "oAuth2",
        "operation": "search",
        "base": "appXXXXXXXXXXXX",
        "table": "Creators",
        "filterByFormula": "={Creator ID}='{{ $json.body.creator_id }}'"
      },
      "name": "Retrieve Creator Profile",
      "type": "n8n-nodes-base.airtable",
      "typeVersion": 1,
      "position": [450, 500]
    },
    {
      "parameters": {
        "functionCode": "const creator = $('Retrieve Creator Profile').first().json;
const intake = $json.body;

const prompt = `You are ${creator.Name}, a ${creator.Industry} expert. 

Your frameworks:
${creator.Frameworks}

Your rules:
${creator.Rules}

Your tone: ${creator.Tone}

Client information:
${JSON.stringify(intake, null, 2)}

Generate a personalized ${intake.deliverable_type} for this client following your frameworks and rules. Structure the output as a detailed outline with sections, bullet points, and specific recommendations.

Output format:
# Title
## Section 1
- Point 1
- Point 2
## Section 2
...`;

return { json: { prompt } };"
      },
      "name": "Build Personalization Prompt",
      "type": "n8n-nodes-base.function",
      "typeVersion": 1,
      "position": [650, 500]
    },
    {
      "parameters": {
        "resource": "chat",
        "operation": "create",
        "model": "gpt-4-turbo-preview",
        "messages": [
          {
            "role": "system",
            "content": "You are an expert personalization engine that applies creator frameworks to individual client situations."
          },
          {
            "role": "user",
            "content": "={{ $json.prompt }}"
          }
        ],
        "options": {
          "temperature": 0.7,
          "maxTokens": 3000
        }
      },
      "name": "Generate Personalized Content",
      "type": "n8n-nodes-base.openAi",
      "typeVersion": 1,
      "position": [850, 500]
    },
    {
      "parameters": {
        "functionCode": "const content = $json.choices[0].message.content;
const creator = $('Retrieve Creator Profile').first().json;

const slides = content.split('\
## ').map(section => {
  const [title, ...bodyLines] = section.split('\
');
  return {
    title: title.replace('# ', ''),
    body: bodyLines.join('\
')
  };
});

return {
  json: {
    presentation: {
      title: slides[0].title,
      slides: slides.slice(1),
      theme: creator['Style Parameters'] || 'modern'
    }
  }
};"
      },
      "name": "Transform for Gamma",
      "type": "n8n-nodes-base.function",
      "typeVersion": 1,
      "position": [1050, 500]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.gamma.app/v1/documents",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "options": {
          "headers": {
            "Authorization": "Bearer YOUR_GAMMA_API_KEY",
            "Content-Type": "application/json"
          },
          "body": {
            "title": "={{ $json.presentation.title }}",
            "slides": "={{ JSON.stringify($json.presentation.slides) }}",
            "theme": "={{ $json.presentation.theme }}"
          }
        }
      },
      "name": "Create Gamma Document",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 1,
      "position": [1250, 500]
    },
    {
      "parameters": {
        "authentication": "oAuth2",
        "operation": "update",
        "base": "appXXXXXXXXXXXX",
        "table": "Client Intake",
        "id": "={{ $('Client Intake Webhook').item.json.body.intake_id }}",
        "fields": {
          "Generated Content": "={{ $('Generate Personalized Content').item.json.choices[0].message.content }}",
          "Gamma Document URL": "={{ $json.url }}",
          "Status": "Complete"
        }
      },
      "name": "Update Intake Record",
      "type": "n8n-nodes-base.airtable",
      "typeVersion": 1,
      "position": [1450, 500]
    }
  ],
  "connections": {
    "Creator Onboarding Webhook": {
      "main": [[{ "node": "Structure Creator Data", "type": "main", "index": 0 }]]
    },
    "Structure Creator Data": {
      "main": [[{ "node": "Store Creator Profile", "type": "main", "index": 0 }]]
    },
    "Client Intake Webhook": {
      "main": [[{ "node": "Retrieve Creator Profile", "type": "main", "index": 0 }]]
    },
    "Retrieve Creator Profile": {
      "main": [[{ "node": "Build Personalization Prompt", "type": "main", "index": 0 }]]
    },
    "Build Personalization Prompt": {
      "main": [[{ "node": "Generate Personalized Content", "type": "main", "index": 0 }]]
    },
    "Generate Personalized Content": {
      "main": [[{ "node": "Transform for Gamma", "type": "main", "index": 0 }]]
    },
    "Transform for Gamma": {
      "main": [[{ "node": "Create Gamma Document", "type": "main", "index": 0 }]]
    },
    "Create Gamma Document": {
      "main": [[{ "node": "Update Intake Record", "type": "main", "index": 0 }]]
    }
  }
}

Complete N8N Workflow Template

Copy the JSON below and import it into your N8N instance via Workflows → Import from File

{
  "name": "AI Personalization Engine for Creators",
  "nodes": [
    {
      "id": "webhook-trigger",
      "name": "Webhook Trigger",
      "type": "n8n-nodes-base.webhook",
      "onError": "continueRegularOutput",
      "position": [
        240,
        300
      ],
      "parameters": {
        "path": "creator-intake",
        "options": {},
        "httpMethod": "POST",
        "responseMode": "responseNode"
      },
      "typeVersion": 2.1
    },
    {
      "id": "parse-intake",
      "name": "Parse Intake Data",
      "type": "n8n-nodes-base.set",
      "position": [
        460,
        300
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "intake-data",
              "name": "intakeData",
              "type": "object",
              "value": "={{ $json.body }}"
            },
            {
              "id": "creator-id",
              "name": "creatorId",
              "type": "string",
              "value": "={{ $json.body.creatorId }}"
            },
            {
              "id": "data-source",
              "name": "dataSource",
              "type": "string",
              "value": "={{ $json.body.source }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "route-by-source",
      "name": "Route by Data Source",
      "type": "n8n-nodes-base.switch",
      "position": [
        680,
        300
      ],
      "parameters": {
        "mode": "rules",
        "rules": {
          "values": [
            {
              "outputKey": "typeform",
              "conditions": {
                "options": {
                  "leftValue": "",
                  "caseSensitive": false
                },
                "combinator": "and",
                "conditions": [
                  {
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.dataSource }}",
                    "rightValue": "typeform"
                  }
                ]
              },
              "renameOutput": true
            },
            {
              "outputKey": "airtable",
              "conditions": {
                "options": {
                  "leftValue": "",
                  "caseSensitive": false
                },
                "combinator": "and",
                "conditions": [
                  {
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.dataSource }}",
                    "rightValue": "airtable"
                  }
                ]
              },
              "renameOutput": true
            },
            {
              "outputKey": "google-drive",
              "conditions": {
                "options": {
                  "leftValue": "",
                  "caseSensitive": false
                },
                "combinator": "and",
                "conditions": [
                  {
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.dataSource }}",
                    "rightValue": "google-drive"
                  }
                ]
              },
              "renameOutput": true
            }
          ]
        },
        "options": {
          "fallbackOutput": "extra",
          "fallbackOutputName": "fallback"
        }
      },
      "typeVersion": 3.3
    },
    {
      "id": "fetch-typeform",
      "name": "Fetch Typeform Data",
      "type": "n8n-nodes-base.httpRequest",
      "onError": "continueRegularOutput",
      "position": [
        900,
        140
      ],
      "parameters": {
        "url": "={{ 'https://' + $json.intakeData.typeformWebhookUrl }}",
        "method": "GET",
        "options": {},
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth"
      },
      "typeVersion": 4.3
    },
    {
      "id": "fetch-airtable",
      "name": "Fetch Airtable Data",
      "type": "n8n-nodes-base.airtable",
      "onError": "continueRegularOutput",
      "position": [
        900,
        300
      ],
      "parameters": {
        "base": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $json.intakeData.airtableBaseId }}"
        },
        "table": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $json.intakeData.airtableTableId }}"
        },
        "options": {
          "filterByFormula": "={{ 'RECORD_ID() = \"' + $json.intakeData.recordId + '\"' }}"
        },
        "operation": "search"
      },
      "typeVersion": 2.1
    },
    {
      "id": "fetch-google-drive",
      "name": "Fetch Google Drive Data",
      "type": "n8n-nodes-base.googleDrive",
      "onError": "continueRegularOutput",
      "position": [
        900,
        460
      ],
      "parameters": {
        "fileId": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $json.intakeData.googleDriveFileId }}"
        },
        "options": {
          "googleFileConversion": {
            "conversion": {
              "docsToFormat": "text/plain"
            }
          }
        },
        "operation": "download"
      },
      "typeVersion": 3
    },
    {
      "id": "lookup-creator-framework",
      "name": "Lookup Creator Framework",
      "type": "n8n-nodes-base.airtable",
      "onError": "continueRegularOutput",
      "position": [
        1120,
        300
      ],
      "parameters": {
        "base": {
          "__rl": true,
          "mode": "id",
          "value": "appCreatorFrameworks"
        },
        "table": {
          "__rl": true,
          "mode": "id",
          "value": "tblCreators"
        },
        "options": {
          "filterByFormula": "={{ '{creatorId} = \"' + $json.creatorId + '\"' }}"
        },
        "operation": "search"
      },
      "typeVersion": 2.1
    },
    {
      "id": "merge-context",
      "name": "Merge Context Data",
      "type": "n8n-nodes-base.code",
      "onError": "continueRegularOutput",
      "position": [
        1340,
        300
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const items = $input.all();\nconst sourceData = items[0].json;\nconst creatorFramework = items[items.length - 1].json;\n\nconst creatorProfile = {\n  name: creatorFramework.name || 'Unknown Creator',\n  industry: creatorFramework.industry || 'General',\n  toneGuidelines: creatorFramework.tone_guidelines || 'Professional and engaging',\n  contentRules: creatorFramework.content_rules || [],\n  prohibitedTopics: creatorFramework.prohibited_topics || [],\n  brandVoice: creatorFramework.brand_voice || 'Neutral',\n  targetAudience: creatorFramework.target_audience || 'General audience',\n  stylePreferences: creatorFramework.style_preferences || {},\n  validationRules: creatorFramework.validation_rules || []\n};\n\nlet structuredInput = {};\n\nif (sourceData.dataSource === 'typeform') {\n  structuredInput = {\n    questions: sourceData.answers || [],\n    rawData: sourceData\n  };\n} else if (sourceData.dataSource === 'airtable') {\n  structuredInput = {\n    fields: sourceData.fields || {},\n    rawData: sourceData\n  };\n} else if (sourceData.dataSource === 'google-drive') {\n  structuredInput = {\n    content: sourceData.data ? sourceData.data.toString('utf8') : '',\n    rawData: sourceData\n  };\n}\n\nconst mergedContext = {\n  creatorProfile,\n  sourceData: structuredInput,\n  metadata: {\n    creatorId: sourceData.creatorId,\n    dataSource: sourceData.dataSource,\n    processedAt: new Date().toISOString()\n  }\n};\n\nreturn [{ json: mergedContext }];"
      },
      "typeVersion": 2
    },
    {
      "id": "build-ai-prompt",
      "name": "Build AI Prompt",
      "type": "n8n-nodes-base.code",
      "onError": "continueRegularOutput",
      "position": [
        1560,
        300
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const items = $input.all();\nconst context = items[0].json;\nconst { creatorProfile, sourceData } = context;\n\nconst constraintsSection = `\n## CRITICAL CONSTRAINTS - MUST FOLLOW:\n${creatorProfile.prohibitedTopics.length > 0 ? `- NEVER mention or reference: ${creatorProfile.prohibitedTopics.join(', ')}` : ''}\n${creatorProfile.contentRules.length > 0 ? `- Content Rules:\\n${creatorProfile.contentRules.map(rule => `  * ${rule}`).join('\\n')}` : ''}\n${creatorProfile.validationRules.length > 0 ? `- Validation Rules:\\n${creatorProfile.validationRules.map(rule => `  * ${rule}`).join('\\n')}` : ''}\n`;\n\nconst profileSection = `\n## CREATOR PROFILE:\n- Name: ${creatorProfile.name}\n- Industry: ${creatorProfile.industry}\n- Target Audience: ${creatorProfile.targetAudience}\n- Brand Voice: ${creatorProfile.brandVoice}\n- Tone Guidelines: ${creatorProfile.toneGuidelines}\n`;\n\nlet contentSection = '\\n## SOURCE CONTENT:\\n';\nif (sourceData.questions) {\n  contentSection += sourceData.questions.map((q, i) => `Q${i+1}: ${q.question}\\nA${i+1}: ${q.answer}`).join('\\n\\n');\n} else if (sourceData.fields) {\n  contentSection += Object.entries(sourceData.fields).map(([key, value]) => `${key}: ${value}`).join('\\n');\n} else if (sourceData.content) {\n  contentSection += sourceData.content;\n}\n\nconst systemPrompt = `You are an AI content specialist creating personalized Gamma presentation documents for ${creatorProfile.name}.\n\nYour task is to transform the provided source content into a structured, engaging presentation that:\n1. Strictly adheres to all constraints and rules\n2. Matches the creator's brand voice and tone\n3. Speaks directly to their target audience\n4. NEVER violates prohibited topics or content rules\n5. Validates output against all validation rules\n\n${constraintsSection}\n${profileSection}\n\nOutput Format:\n- Return JSON with the following structure:\n{\n  \"title\": \"Presentation Title\",\n  \"sections\": [\n    {\n      \"heading\": \"Section Title\",\n      \"content\": \"Section content\",\n      \"bullets\": [\"Point 1\", \"Point 2\"]\n    }\n  ],\n  \"metadata\": {\n    \"tone\": \"Matched tone\",\n    \"constraintsFollowed\": true,\n    \"validationPassed\": true\n  }\n}`;\n\nconst userPrompt = `${contentSection}\n\n---\n\nTransform the above content into a personalized Gamma presentation following all guidelines.`;\n\nreturn [{\n  json: {\n    systemPrompt,\n    userPrompt,\n    context: context\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "openai-generation",
      "name": "OpenAI Content Generation",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "onError": "continueRegularOutput",
      "position": [
        1780,
        300
      ],
      "parameters": {
        "text": "={{ $json.userPrompt }}",
        "modelId": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-4o"
        },
        "options": {
          "maxTokens": 2000,
          "temperature": 0.7,
          "systemMessage": "={{ $json.systemPrompt }}"
        },
        "resource": "text",
        "operation": "message"
      },
      "typeVersion": 2
    },
    {
      "id": "validate-output",
      "name": "Validate AI Output",
      "type": "n8n-nodes-base.code",
      "onError": "continueRegularOutput",
      "position": [
        2000,
        300
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const items = $input.all();\nconst aiResponse = items[0].json.message ? items[0].json.message.content : (items[0].json.response || items[0].json.text || '');\nconst buildPromptNode = $('Build AI Prompt');\nconst context = buildPromptNode.first().json.context;\n\nlet parsedContent;\ntry {\n  const jsonMatch = aiResponse.match(/\\{[\\s\\S]*\\}/);\n  if (jsonMatch) {\n    parsedContent = JSON.parse(jsonMatch[0]);\n  } else {\n    throw new Error('No JSON found in response');\n  }\n} catch (error) {\n  parsedContent = {\n    title: 'Generated Content',\n    sections: [\n      {\n        heading: 'Main Content',\n        content: aiResponse,\n        bullets: []\n      }\n    ],\n    metadata: {\n      tone: context.creatorProfile.brandVoice,\n      constraintsFollowed: true,\n      validationPassed: true\n    }\n  };\n}\n\nconst prohibitedTopics = context.creatorProfile.prohibitedTopics || [];\nconst contentString = JSON.stringify(parsedContent).toLowerCase();\n\nconst violations = prohibitedTopics.filter(topic => \n  contentString.includes(topic.toLowerCase())\n);\n\nif (violations.length > 0) {\n  parsedContent.metadata.constraintsFollowed = false;\n  parsedContent.metadata.violations = violations;\n  parsedContent.metadata.validationPassed = false;\n}\n\nparsedContent.branding = {\n  creatorName: context.creatorProfile.name,\n  brandVoice: context.creatorProfile.brandVoice,\n  industry: context.creatorProfile.industry\n};\n\nreturn [{\n  json: {\n    generatedContent: parsedContent,\n    validation: {\n      passed: parsedContent.metadata.validationPassed,\n      violations: violations,\n      constraintsChecked: prohibitedTopics.length\n    },\n    originalContext: context\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "validation-switch",
      "name": "Validation Check",
      "type": "n8n-nodes-base.switch",
      "position": [
        2220,
        300
      ],
      "parameters": {
        "mode": "rules",
        "rules": {
          "values": [
            {
              "outputKey": "valid",
              "conditions": {
                "options": {
                  "leftValue": "",
                  "caseSensitive": false
                },
                "combinator": "and",
                "conditions": [
                  {
                    "operator": {
                      "type": "boolean",
                      "operation": "true"
                    },
                    "leftValue": "={{ $json.validation.passed }}",
                    "rightValue": "true"
                  }
                ]
              },
              "renameOutput": true
            },
            {
              "outputKey": "invalid",
              "conditions": {
                "options": {
                  "leftValue": "",
                  "caseSensitive": false
                },
                "combinator": "and",
                "conditions": [
                  {
                    "operator": {
                      "type": "boolean",
                      "operation": "false"
                    },
                    "leftValue": "={{ $json.validation.passed }}",
                    "rightValue": "false"
                  }
                ]
              },
              "renameOutput": true
            }
          ]
        },
        "options": {}
      },
      "typeVersion": 3.3
    },
    {
      "id": "format-gamma",
      "name": "Format for Gamma",
      "type": "n8n-nodes-base.code",
      "onError": "continueRegularOutput",
      "position": [
        2440,
        220
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const items = $input.all();\nconst data = items[0].json.generatedContent;\nconst context = items[0].json.originalContext;\n\nconst gammaDocument = {\n  name: data.title,\n  theme: context.creatorProfile.industry.toLowerCase(),\n  slides: data.sections.map((section, index) => ({\n    id: `slide-${index + 1}`,\n    title: section.heading,\n    content: {\n      text: section.content,\n      bullets: section.bullets || []\n    },\n    layout: index === 0 ? 'title' : 'content',\n    notes: `Creator: ${data.branding.creatorName} | Voice: ${data.branding.brandVoice}`\n  })),\n  metadata: {\n    creator: data.branding.creatorName,\n    industry: data.branding.industry,\n    generatedAt: new Date().toISOString(),\n    validationStatus: 'passed'\n  },\n  settings: {\n    brandVoice: data.branding.brandVoice,\n    targetAudience: context.creatorProfile.targetAudience\n  }\n};\n\nreturn [{\n  json: {\n    gammaDocument,\n    creatorId: context.metadata.creatorId\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "create-gamma-doc",
      "name": "Create Gamma Document",
      "type": "n8n-nodes-base.httpRequest",
      "onError": "continueRegularOutput",
      "position": [
        2660,
        220
      ],
      "parameters": {
        "url": "https://api.gamma.app/v1/documents",
        "method": "POST",
        "options": {},
        "sendBody": true,
        "contentType": "json",
        "authentication": "genericCredentialType",
        "bodyParameters": {
          "parameters": [
            {
              "name": "document",
              "value": "={{ JSON.stringify($json.gammaDocument) }}"
            }
          ]
        },
        "genericAuthType": "httpHeaderAuth"
      },
      "typeVersion": 4.3
    },
    {
      "id": "log-success",
      "name": "Log Success to Airtable",
      "type": "n8n-nodes-base.airtable",
      "onError": "continueRegularOutput",
      "position": [
        2880,
        220
      ],
      "parameters": {
        "base": {
          "__rl": true,
          "mode": "id",
          "value": "appOutputTracking"
        },
        "table": {
          "__rl": true,
          "mode": "id",
          "value": "tblGeneratedDocs"
        },
        "columns": {
          "value": {
            "status": "completed",
            "createdAt": "={{ $now.toISO() }}",
            "creatorId": "={{ $('Format for Gamma').first().json.creatorId }}",
            "gammaDocId": "={{ $json.id }}",
            "gammaDocUrl": "={{ $json.url }}"
          },
          "schema": [],
          "mappingMode": "defineBelow",
          "matchingColumns": []
        },
        "options": {},
        "operation": "create"
      },
      "typeVersion": 2.1
    },
    {
      "id": "log-error",
      "name": "Log Validation Error",
      "type": "n8n-nodes-base.airtable",
      "onError": "continueRegularOutput",
      "position": [
        2440,
        380
      ],
      "parameters": {
        "base": {
          "__rl": true,
          "mode": "id",
          "value": "appOutputTracking"
        },
        "table": {
          "__rl": true,
          "mode": "id",
          "value": "tblValidationErrors"
        },
        "columns": {
          "value": {
            "status": "requires_review",
            "content": "={{ JSON.stringify($json.generatedContent) }}",
            "createdAt": "={{ $now.toISO() }}",
            "creatorId": "={{ $json.originalContext.metadata.creatorId }}",
            "violations": "={{ JSON.stringify($json.validation.violations) }}"
          },
          "schema": [],
          "mappingMode": "defineBelow",
          "matchingColumns": []
        },
        "options": {},
        "operation": "create"
      },
      "typeVersion": 2.1
    },
    {
      "id": "respond-success",
      "name": "Respond Success",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        3100,
        220
      ],
      "parameters": {
        "options": {},
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify({\n  success: true,\n  gammaDocUrl: $json.url,\n  message: 'Personalized Gamma document created successfully'\n}) }}"
      },
      "typeVersion": 1.4
    },
    {
      "id": "respond-error",
      "name": "Respond Validation Error",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        2660,
        380
      ],
      "parameters": {
        "options": {
          "responseCode": 400
        },
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify({\n  success: false,\n  error: 'Content validation failed',\n  violations: $json.validation.violations || [],\n  message: 'Content violates creator constraints and requires manual review'\n}) }}"
      },
      "typeVersion": 1.4
    }
  ],
  "connections": {
    "Build AI Prompt": {
      "main": [
        [
          {
            "node": "OpenAI Content Generation",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Webhook Trigger": {
      "main": [
        [
          {
            "node": "Parse Intake Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format for Gamma": {
      "main": [
        [
          {
            "node": "Create Gamma Document",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validation Check": {
      "main": [
        [
          {
            "node": "Format for Gamma",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Log Validation Error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Intake Data": {
      "main": [
        [
          {
            "node": "Route by Data Source",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge Context Data": {
      "main": [
        [
          {
            "node": "Build AI Prompt",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate AI Output": {
      "main": [
        [
          {
            "node": "Validation Check",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Airtable Data": {
      "main": [
        [
          {
            "node": "Lookup Creator Framework",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Typeform Data": {
      "main": [
        [
          {
            "node": "Lookup Creator Framework",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Log Validation Error": {
      "main": [
        [
          {
            "node": "Respond Validation Error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Route by Data Source": {
      "main": [
        [
          {
            "node": "Fetch Typeform Data",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Fetch Airtable Data",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Fetch Google Drive Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create Gamma Document": {
      "main": [
        [
          {
            "node": "Log Success to Airtable",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Google Drive Data": {
      "main": [
        [
          {
            "node": "Lookup Creator Framework",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Log Success to Airtable": {
      "main": [
        [
          {
            "node": "Respond Success",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Lookup Creator Framework": {
      "main": [
        [
          {
            "node": "Merge Context Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI Content Generation": {
      "main": [
        [
          {
            "node": "Validate AI Output",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}