n8n Social Media Automation: Multi-Platform Publishing and Analytics
Managing social media presence across multiple platforms is time-consuming. This guide shows you how to build comprehensive social media automation workflows with n8n for publishing, scheduling, and analytics.
Multi-Platform Publishing Workflow
Unified Content Publisher
// n8n Function Node - Content Formatter
const content = $input.first().json;
// Platform-specific formatting
const formatForTwitter = (text, maxLength = 280) => {
if (text.length <= maxLength) return text;
return text.substring(0, maxLength - 3) + '...';
};
const formatForLinkedIn = (text) => {
// LinkedIn supports up to 3000 characters
const hashtags = extractHashtags(text);
return {
text: text.substring(0, 2900),
hashtags: hashtags.slice(0, 5)
};
};
const formatForInstagram = (text) => {
// Instagram caption limit is 2200
return {
caption: text.substring(0, 2100),
hashtags: generateRelevantHashtags(text, 30)
};
};
function extractHashtags(text) {
const matches = text.match(/#\w+/g) || [];
return [...new Set(matches)];
}
function generateRelevantHashtags(text, count) {
const keywords = text.toLowerCase()
.replace(/[^\w\s]/g, '')
.split(/\s+/)
.filter(w => w.length > 4);
const topKeywords = [...new Set(keywords)].slice(0, count);
return topKeywords.map(k => `#${k}`);
}
// Format for each platform
return [
{
json: {
platform: 'twitter',
content: formatForTwitter(content.message),
media: content.images?.slice(0, 4) || [],
scheduledTime: content.publishAt
}
},
{
json: {
platform: 'linkedin',
...formatForLinkedIn(content.message),
media: content.images?.[0] || null,
scheduledTime: content.publishAt
}
},
{
json: {
platform: 'instagram',
...formatForInstagram(content.message),
media: content.images?.[0] || null,
scheduledTime: content.publishAt
}
}
];Content Queue Manager
// n8n Function Node - Queue Manager
const newContent = $input.first().json;
const existingQueue = $('Get Queue').first().json.queue || [];
// Add to queue with priority
const queueItem = {
id: generateId(),
content: newContent,
priority: calculatePriority(newContent),
status: 'queued',
createdAt: new Date().toISOString(),
scheduledFor: newContent.publishAt || calculateOptimalTime(),
platforms: newContent.platforms || ['twitter', 'linkedin'],
retryCount: 0
};
// Insert based on priority and time
const updatedQueue = [...existingQueue, queueItem]
.sort((a, b) => {
if (a.priority !== b.priority) return b.priority - a.priority;
return new Date(a.scheduledFor) - new Date(b.scheduledFor);
});
function generateId() {
return `post_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
function calculatePriority(content) {
let priority = 5; // Base priority
if (content.isUrgent) priority += 3;
if (content.hasMedia) priority += 1;
if (content.isPromotion) priority += 2;
if (content.engagement?.expected > 100) priority += 2;
return Math.min(priority, 10);
}
function calculateOptimalTime() {
// Find optimal posting time based on analytics
const now = new Date();
const hour = now.getHours();
// Best times: 9am, 12pm, 5pm, 8pm
const optimalHours = [9, 12, 17, 20];
for (const h of optimalHours) {
if (h > hour) {
now.setHours(h, 0, 0, 0);
return now.toISOString();
}
}
// Schedule for next day
now.setDate(now.getDate() + 1);
now.setHours(9, 0, 0, 0);
return now.toISOString();
}
return { json: { queue: updatedQueue, added: queueItem } };Engagement Monitoring System
Real-Time Engagement Tracker
// n8n Function Node - Engagement Aggregator
const twitterData = $('Twitter Mentions').all();
const linkedinData = $('LinkedIn Notifications').all();
const instagramData = $('Instagram Comments').all();
const engagements = [];
// Process Twitter engagements
for (const tweet of twitterData) {
engagements.push({
platform: 'twitter',
type: tweet.json.type, // mention, reply, retweet, like
user: {
id: tweet.json.user.id,
handle: tweet.json.user.screen_name,
followers: tweet.json.user.followers_count
},
content: tweet.json.text,
sentiment: analyzeSentiment(tweet.json.text),
timestamp: tweet.json.created_at,
postId: tweet.json.in_reply_to_status_id,
requiresResponse: needsResponse(tweet.json),
priority: calculateEngagementPriority(tweet.json)
});
}
// Process LinkedIn engagements
for (const notification of linkedinData) {
engagements.push({
platform: 'linkedin',
type: notification.json.type,
user: {
id: notification.json.actor.id,
name: notification.json.actor.name,
title: notification.json.actor.title
},
content: notification.json.comment?.text || '',
sentiment: analyzeSentiment(notification.json.comment?.text || ''),
timestamp: notification.json.timestamp,
postId: notification.json.postId,
requiresResponse: notification.json.type === 'COMMENT',
priority: calculateEngagementPriority(notification.json)
});
}
// Process Instagram engagements
for (const comment of instagramData) {
engagements.push({
platform: 'instagram',
type: 'comment',
user: {
id: comment.json.from.id,
username: comment.json.from.username
},
content: comment.json.text,
sentiment: analyzeSentiment(comment.json.text),
timestamp: comment.json.timestamp,
postId: comment.json.media.id,
requiresResponse: true,
priority: calculateEngagementPriority(comment.json)
});
}
function analyzeSentiment(text) {
if (!text) return 'neutral';
const positiveWords = ['great', 'awesome', 'love', 'amazing', 'excellent', 'thanks'];
const negativeWords = ['bad', 'terrible', 'hate', 'awful', 'disappointed', 'issue'];
const lowerText = text.toLowerCase();
const positiveCount = positiveWords.filter(w => lowerText.includes(w)).length;
const negativeCount = negativeWords.filter(w => lowerText.includes(w)).length;
if (positiveCount > negativeCount) return 'positive';
if (negativeCount > positiveCount) return 'negative';
return 'neutral';
}
function needsResponse(item) {
// Check if engagement requires a response
if (item.type === 'mention' || item.type === 'reply') return true;
if (item.text?.includes('?')) return true;
return false;
}
function calculateEngagementPriority(item) {
let priority = 5;
// High follower count = higher priority
if (item.user?.followers_count > 10000) priority += 3;
else if (item.user?.followers_count > 1000) priority += 1;
// Questions get higher priority
if (item.text?.includes('?')) priority += 2;
// Negative sentiment = urgent
if (analyzeSentiment(item.text) === 'negative') priority += 3;
return Math.min(priority, 10);
}
// Sort by priority
engagements.sort((a, b) => b.priority - a.priority);
return { json: { engagements, count: engagements.length } };Auto-Response System
// n8n Function Node - Response Generator
const engagement = $input.first().json;
// Response templates by type and sentiment
const templates = {
positive: {
twitter: [
"Thank you so much! 🙏 We're glad you found it helpful!",
"Thanks for the kind words! Let us know if you have any questions.",
"We appreciate the feedback! 💪"
],
linkedin: [
"Thank you for your thoughtful comment! We appreciate you engaging with our content.",
"Thanks for sharing your perspective! Feel free to reach out if you'd like to discuss further."
],
instagram: [
"Thank you! 🙌 We're so glad you enjoyed it!",
"Thanks for the love! ❤️"
]
},
negative: {
twitter: [
"We're sorry to hear that. Could you DM us with more details so we can help?",
"Thanks for the feedback. We'd love to make this right - please reach out via DM."
],
linkedin: [
"Thank you for bringing this to our attention. We'd like to address your concerns - could you send us a direct message?",
"We appreciate your feedback and take it seriously. Please reach out directly so we can assist you."
],
instagram: [
"We're sorry you had this experience. Please DM us so we can help! 🙏",
"Thanks for letting us know. We'd love to make it right - check your DMs!"
]
},
question: {
twitter: [
"Great question! [ANSWER]. Let us know if you need more details!",
"Thanks for asking! [ANSWER]"
],
linkedin: [
"Thank you for your question! [ANSWER]. Feel free to follow up if you need additional information.",
"Great question! [ANSWER]"
],
instagram: [
"Thanks for asking! [ANSWER] 😊",
"[ANSWER] Hope that helps! 🙌"
]
}
};
function selectResponse(platform, sentiment, isQuestion) {
const category = isQuestion ? 'question' : sentiment;
const platformTemplates = templates[category]?.[platform] || templates[category]?.twitter;
if (!platformTemplates) return null;
return platformTemplates[Math.floor(Math.random() * platformTemplates.length)];
}
function generateAnswer(question) {
// In production, this could call an AI API for smart responses
// For now, return a placeholder
return "We'll get back to you with detailed information shortly";
}
const isQuestion = engagement.content.includes('?');
let response = selectResponse(engagement.platform, engagement.sentiment, isQuestion);
if (isQuestion && response) {
const answer = generateAnswer(engagement.content);
response = response.replace('[ANSWER]', answer);
}
// Determine if auto-response is appropriate
const shouldAutoRespond =
engagement.priority < 7 && // Don't auto-respond to high priority
engagement.sentiment !== 'negative' && // Manual review for negative
!isQuestion; // Manual review for questions
return {
json: {
engagement,
response,
shouldAutoRespond,
requiresManualReview: !shouldAutoRespond,
timestamp: new Date().toISOString()
}
};Analytics Aggregation
Cross-Platform Analytics Collector
// n8n Function Node - Analytics Aggregator
const twitterAnalytics = $('Twitter Analytics').first().json;
const linkedinAnalytics = $('LinkedIn Analytics').first().json;
const instagramAnalytics = $('Instagram Insights').first().json;
const period = $input.first().json.period || '7d';
// Normalize metrics across platforms
const normalizeMetrics = (platform, data) => {
switch (platform) {
case 'twitter':
return {
impressions: data.impressions || 0,
engagements: data.engagements || 0,
clicks: data.url_clicks || 0,
likes: data.likes || 0,
shares: data.retweets || 0,
comments: data.replies || 0,
followers: data.followers_count || 0,
followerGrowth: data.follower_growth || 0
};
case 'linkedin':
return {
impressions: data.impressions || 0,
engagements: data.engagement || 0,
clicks: data.clicks || 0,
likes: data.reactions || 0,
shares: data.shares || 0,
comments: data.comments || 0,
followers: data.followers || 0,
followerGrowth: data.follower_change || 0
};
case 'instagram':
return {
impressions: data.impressions || 0,
engagements: data.total_interactions || 0,
clicks: data.website_clicks || 0,
likes: data.likes || 0,
shares: data.shares || 0,
comments: data.comments || 0,
followers: data.followers_count || 0,
followerGrowth: data.follower_growth || 0
};
default:
return {};
}
};
const twitter = normalizeMetrics('twitter', twitterAnalytics);
const linkedin = normalizeMetrics('linkedin', linkedinAnalytics);
const instagram = normalizeMetrics('instagram', instagramAnalytics);
// Calculate totals
const totals = {
impressions: twitter.impressions + linkedin.impressions + instagram.impressions,
engagements: twitter.engagements + linkedin.engagements + instagram.engagements,
clicks: twitter.clicks + linkedin.clicks + instagram.clicks,
likes: twitter.likes + linkedin.likes + instagram.likes,
shares: twitter.shares + linkedin.shares + instagram.shares,
comments: twitter.comments + linkedin.comments + instagram.comments,
totalFollowers: twitter.followers + linkedin.followers + instagram.followers,
totalGrowth: twitter.followerGrowth + linkedin.followerGrowth + instagram.followerGrowth
};
// Calculate engagement rates
const calculateEngagementRate = (metrics) => {
if (metrics.impressions === 0) return 0;
return ((metrics.engagements / metrics.impressions) * 100).toFixed(2);
};
const analytics = {
period,
generatedAt: new Date().toISOString(),
platforms: {
twitter: {
...twitter,
engagementRate: calculateEngagementRate(twitter)
},
linkedin: {
...linkedin,
engagementRate: calculateEngagementRate(linkedin)
},
instagram: {
...instagram,
engagementRate: calculateEngagementRate(instagram)
}
},
totals: {
...totals,
overallEngagementRate: calculateEngagementRate(totals)
},
topPerforming: identifyTopContent([
...twitterAnalytics.posts || [],
...linkedinAnalytics.posts || [],
...instagramAnalytics.posts || []
])
};
function identifyTopContent(allPosts) {
return allPosts
.map(post => ({
id: post.id,
platform: post.platform,
content: post.text?.substring(0, 100) || '',
engagements: post.engagements || post.total_interactions || 0,
impressions: post.impressions || 0,
engagementRate: post.impressions > 0
? ((post.engagements || 0) / post.impressions * 100).toFixed(2)
: 0
}))
.sort((a, b) => b.engagements - a.engagements)
.slice(0, 10);
}
return { json: analytics };Weekly Report Generator
// n8n Function Node - Report Generator
const analytics = $input.first().json;
const previousWeek = $('Previous Week Analytics').first().json;
function calculateChange(current, previous) {
if (previous === 0) return current > 0 ? 100 : 0;
return (((current - previous) / previous) * 100).toFixed(1);
}
function formatNumber(num) {
if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M';
if (num >= 1000) return (num / 1000).toFixed(1) + 'K';
return num.toString();
}
const report = {
title: `Social Media Weekly Report - ${new Date().toLocaleDateString()}`,
summary: {
totalImpressions: formatNumber(analytics.totals.impressions),
impressionsChange: calculateChange(
analytics.totals.impressions,
previousWeek?.totals?.impressions || 0
),
totalEngagements: formatNumber(analytics.totals.engagements),
engagementsChange: calculateChange(
analytics.totals.engagements,
previousWeek?.totals?.engagements || 0
),
totalFollowers: formatNumber(analytics.totals.totalFollowers),
followerGrowth: analytics.totals.totalGrowth,
engagementRate: analytics.totals.overallEngagementRate + '%'
},
platformBreakdown: Object.entries(analytics.platforms).map(([platform, data]) => ({
platform,
impressions: formatNumber(data.impressions),
engagements: formatNumber(data.engagements),
engagementRate: data.engagementRate + '%',
followers: formatNumber(data.followers),
growth: data.followerGrowth
})),
topContent: analytics.topPerforming.slice(0, 5).map(post => ({
platform: post.platform,
preview: post.content,
engagements: formatNumber(post.engagements),
engagementRate: post.engagementRate + '%'
})),
recommendations: generateRecommendations(analytics, previousWeek)
};
function generateRecommendations(current, previous) {
const recommendations = [];
// Check engagement rate trends
const currentRate = parseFloat(current.totals.overallEngagementRate);
const previousRate = parseFloat(previous?.totals?.overallEngagementRate || 0);
if (currentRate < previousRate) {
recommendations.push({
type: 'warning',
message: 'Engagement rate decreased. Consider reviewing content strategy and posting times.'
});
}
// Platform-specific recommendations
for (const [platform, data] of Object.entries(current.platforms)) {
if (parseFloat(data.engagementRate) < 1) {
recommendations.push({
type: 'improvement',
message: `${platform} engagement is low. Try more visual content and questions to boost interaction.`
});
}
}
// Growth recommendations
if (current.totals.totalGrowth < 10) {
recommendations.push({
type: 'growth',
message: 'Follower growth is slow. Consider collaboration, hashtag optimization, or paid promotion.'
});
}
return recommendations;
}
// Generate HTML report
const htmlReport = `
<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }
.metric { display: inline-block; padding: 20px; margin: 10px; background: #f5f5f5; border-radius: 8px; }
.metric-value { font-size: 32px; font-weight: bold; }
.metric-label { color: #666; }
.change-positive { color: #22c55e; }
.change-negative { color: #ef4444; }
table { width: 100%; border-collapse: collapse; margin: 20px 0; }
th, td { padding: 12px; text-align: left; border-bottom: 1px solid #ddd; }
.recommendation { padding: 10px; margin: 5px 0; border-radius: 4px; }
.warning { background: #fef3c7; border-left: 4px solid #f59e0b; }
.improvement { background: #dbeafe; border-left: 4px solid #3b82f6; }
.growth { background: #dcfce7; border-left: 4px solid #22c55e; }
</style>
</head>
<body>
<h1>${report.title}</h1>
<h2>Summary</h2>
<div class="metrics">
<div class="metric">
<div class="metric-value">${report.summary.totalImpressions}</div>
<div class="metric-label">Impressions</div>
<div class="${parseFloat(report.summary.impressionsChange) >= 0 ? 'change-positive' : 'change-negative'}">
${report.summary.impressionsChange}%
</div>
</div>
<div class="metric">
<div class="metric-value">${report.summary.totalEngagements}</div>
<div class="metric-label">Engagements</div>
<div class="${parseFloat(report.summary.engagementsChange) >= 0 ? 'change-positive' : 'change-negative'}">
${report.summary.engagementsChange}%
</div>
</div>
<div class="metric">
<div class="metric-value">${report.summary.engagementRate}</div>
<div class="metric-label">Engagement Rate</div>
</div>
</div>
<h2>Platform Breakdown</h2>
<table>
<tr>
<th>Platform</th>
<th>Impressions</th>
<th>Engagements</th>
<th>Rate</th>
<th>Followers</th>
</tr>
${report.platformBreakdown.map(p => `
<tr>
<td>${p.platform}</td>
<td>${p.impressions}</td>
<td>${p.engagements}</td>
<td>${p.engagementRate}</td>
<td>${p.followers} (${p.growth >= 0 ? '+' : ''}${p.growth})</td>
</tr>
`).join('')}
</table>
<h2>Top Performing Content</h2>
<table>
<tr>
<th>Platform</th>
<th>Content</th>
<th>Engagements</th>
<th>Rate</th>
</tr>
${report.topContent.map(c => `
<tr>
<td>${c.platform}</td>
<td>${c.preview}...</td>
<td>${c.engagements}</td>
<td>${c.engagementRate}</td>
</tr>
`).join('')}
</table>
<h2>Recommendations</h2>
${report.recommendations.map(r => `
<div class="recommendation ${r.type}">
${r.message}
</div>
`).join('')}
</body>
</html>
`;
return {
json: {
report,
html: htmlReport
}
};Content Calendar Integration
Calendar Sync Workflow
// n8n Function Node - Calendar Manager
const scheduledPosts = $('Get Scheduled Posts').all();
const calendarEvents = $('Google Calendar Events').all();
// Sync posts to calendar
const postsToSync = [];
const eventsToCreate = [];
const eventsToUpdate = [];
for (const post of scheduledPosts) {
const postData = post.json;
const existingEvent = calendarEvents.find(e =>
e.json.extendedProperties?.private?.postId === postData.id
);
const eventData = {
summary: `📱 ${postData.platforms.join(', ')}: ${postData.content.substring(0, 50)}...`,
description: `
Platform(s): ${postData.platforms.join(', ')}
Status: ${postData.status}
Priority: ${postData.priority}
Content:
${postData.content}
${postData.media?.length ? `Media: ${postData.media.length} file(s)` : ''}
`.trim(),
start: {
dateTime: postData.scheduledFor,
timeZone: 'UTC'
},
end: {
dateTime: new Date(new Date(postData.scheduledFor).getTime() + 15 * 60000).toISOString(),
timeZone: 'UTC'
},
colorId: getColorByPlatform(postData.platforms[0]),
extendedProperties: {
private: {
postId: postData.id,
platforms: postData.platforms.join(','),
status: postData.status
}
}
};
if (existingEvent) {
eventsToUpdate.push({
eventId: existingEvent.json.id,
...eventData
});
} else {
eventsToCreate.push(eventData);
}
}
function getColorByPlatform(platform) {
const colors = {
twitter: '9', // Blue
linkedin: '7', // Cyan
instagram: '6', // Orange
facebook: '1' // Lavender
};
return colors[platform] || '8'; // Gray default
}
return [
{ json: { action: 'create', events: eventsToCreate } },
{ json: { action: 'update', events: eventsToUpdate } }
];Hashtag Research Automation
// n8n Function Node - Hashtag Analyzer
const topic = $input.first().json.topic;
const trendingHashtags = $('Get Trending').first().json.hashtags || [];
const industryHashtags = $('Industry Hashtags DB').first().json.hashtags || [];
// Analyze and score hashtags
function scoreHashtag(hashtag, trending, industry) {
let score = 50; // Base score
// Trending bonus
const trendingMatch = trending.find(t =>
t.name.toLowerCase() === hashtag.toLowerCase()
);
if (trendingMatch) {
score += Math.min(trendingMatch.volume / 1000, 30);
}
// Industry relevance
const industryMatch = industry.find(i =>
i.tag.toLowerCase() === hashtag.toLowerCase()
);
if (industryMatch) {
score += industryMatch.relevance * 20;
}
// Penalize overused hashtags
if (trendingMatch?.volume > 1000000) {
score -= 20; // Too competitive
}
return Math.min(Math.max(score, 0), 100);
}
// Generate hashtag suggestions
const generateSuggestions = (topic) => {
const words = topic.toLowerCase().split(/\s+/);
const suggestions = [];
// Direct hashtags
suggestions.push(`#${words.join('')}`);
suggestions.push(`#${words.join('_')}`);
// Related industry hashtags
const related = industryHashtags
.filter(h => words.some(w => h.tag.toLowerCase().includes(w)))
.slice(0, 10);
suggestions.push(...related.map(h => h.tag));
// Trending related
const trendingRelated = trendingHashtags
.filter(h => words.some(w => h.name.toLowerCase().includes(w)))
.slice(0, 5);
suggestions.push(...trendingRelated.map(h => h.name));
return [...new Set(suggestions)];
};
const suggestions = generateSuggestions(topic);
const scoredHashtags = suggestions.map(hashtag => ({
hashtag,
score: scoreHashtag(hashtag, trendingHashtags, industryHashtags),
isTrending: trendingHashtags.some(t =>
t.name.toLowerCase() === hashtag.toLowerCase().replace('#', '')
)
})).sort((a, b) => b.score - a.score);
// Recommend optimal mix
const recommended = {
high: scoredHashtags.filter(h => h.score >= 70).slice(0, 5),
medium: scoredHashtags.filter(h => h.score >= 40 && h.score < 70).slice(0, 10),
niche: scoredHashtags.filter(h => h.score < 40).slice(0, 5)
};
return {
json: {
topic,
allHashtags: scoredHashtags,
recommended,
optimalSet: [
...recommended.high.slice(0, 3),
...recommended.medium.slice(0, 4),
...recommended.niche.slice(0, 3)
].map(h => h.hashtag)
}
};Best Practices
Workflow Organization
- Separate concerns: Create distinct workflows for publishing, monitoring, and analytics
- Error handling: Implement retry logic for API failures
- Rate limiting: Respect platform API limits with delays between requests
- Credential management: Use n8n's credential system for secure token storage
Automation Guidelines
- Schedule posts during peak engagement hours (platform-specific)
- Always review auto-responses before enabling
- Monitor sentiment trends for brand protection
- Keep content queue 1-2 weeks ahead
- Regular analytics review for strategy optimization
Platform-Specific Tips
Twitter: Use threads for longer content, leverage polls for engagement LinkedIn: Professional tone, focus on industry insights Instagram: Visual-first approach, use Stories for casual content
Social media automation with n8n enables consistent presence across platforms while freeing time for strategic content creation and genuine community engagement.