Overview
This workflow collects YouTube comments daily for monitored channels and videos, scores them for sentiment, and surfaces notable mentions, complaints, and feature requests. It replaces manual YouTube Studio checks with an automated pipeline that catches important audience feedback within 24 hours.
Trigger
Cron schedule (daily at 9 AM UTC)
Schedule
Runs daily at 9 AM UTC
Workflow Steps
Load monitored channels and keywords
Read the list of channels, video URLs, and brand keywords to monitor from configuration.
Search for recent videos
Query YouTube via Scavio for recent videos from monitored channels and keywords.
Extract comment data
Pull structured comment data including text, author, timestamp, and like count.
Score sentiment
Apply basic keyword-based sentiment scoring to categorize comments as positive, negative, or neutral.
Flag notable comments
Surface comments with high engagement, brand mentions, or negative sentiment for review.
Generate daily digest
Compile flagged comments into a digest and send to Slack or email.
Python Implementation
import requests
import json
from pathlib import Path
from datetime import datetime
API_KEY = "your_scavio_api_key"
POSITIVE_WORDS = ["love", "great", "amazing", "helpful", "best", "awesome", "excellent"]
NEGATIVE_WORDS = ["bad", "terrible", "broken", "hate", "worst", "scam", "awful", "disappointed"]
def search_youtube(query: str) -> list[dict]:
res = requests.post(
"https://api.scavio.dev/api/v1/search",
headers={"x-api-key": API_KEY},
json={"platform": "youtube", "query": query},
timeout=15,
)
res.raise_for_status()
return res.json().get("organic", [])
def score_sentiment(text: str) -> str:
lower = text.lower()
pos = sum(1 for w in POSITIVE_WORDS if w in lower)
neg = sum(1 for w in NEGATIVE_WORDS if w in lower)
if neg > pos:
return "negative"
if pos > neg:
return "positive"
return "neutral"
def extract_comments(videos: list[dict]) -> list[dict]:
comments = []
for video in videos:
for comment in video.get("comments", []):
sentiment = score_sentiment(comment.get("text", ""))
comments.append({
"video_title": video.get("title", ""),
"author": comment.get("author", ""),
"text": comment.get("text", ""),
"likes": comment.get("likes", 0),
"sentiment": sentiment,
"video_link": video.get("link", ""),
})
return comments
def run():
config = json.loads(Path("monitor_config.json").read_text())
keywords = config.get("keywords", ["your brand name"])
all_comments = []
for kw in keywords:
videos = search_youtube(kw)
comments = extract_comments(videos)
all_comments.extend(comments)
# Flag notable comments
notable = [c for c in all_comments if c["sentiment"] == "negative" or c["likes"] >= 10]
notable.sort(key=lambda x: x["likes"], reverse=True)
date = datetime.utcnow().strftime("%Y-%m-%d")
report = {
"date": date,
"total_comments": len(all_comments),
"positive": sum(1 for c in all_comments if c["sentiment"] == "positive"),
"negative": sum(1 for c in all_comments if c["sentiment"] == "negative"),
"neutral": sum(1 for c in all_comments if c["sentiment"] == "neutral"),
"notable": notable[:20],
}
Path(f"yt_comments_{date}.json").write_text(json.dumps(report, indent=2))
print(f"Collected {len(all_comments)} comments, {len(notable)} notable")
for c in notable[:5]:
print(f" [{c['sentiment']}] {c['text'][:80]}")
if __name__ == "__main__":
run()JavaScript Implementation
const API_KEY = "your_scavio_api_key";
const POSITIVE_WORDS = ["love", "great", "amazing", "helpful", "best", "awesome", "excellent"];
const NEGATIVE_WORDS = ["bad", "terrible", "broken", "hate", "worst", "scam", "awful", "disappointed"];
async function searchYouTube(query) {
const res = await fetch("https://api.scavio.dev/api/v1/search", {
method: "POST",
headers: { "x-api-key": API_KEY, "content-type": "application/json" },
body: JSON.stringify({ platform: "youtube", query }),
});
if (!res.ok) throw new Error(`scavio ${res.status}`);
return (await res.json()).organic ?? [];
}
function scoreSentiment(text) {
const lower = text.toLowerCase();
const pos = POSITIVE_WORDS.filter((w) => lower.includes(w)).length;
const neg = NEGATIVE_WORDS.filter((w) => lower.includes(w)).length;
if (neg > pos) return "negative";
if (pos > neg) return "positive";
return "neutral";
}
function extractComments(videos) {
const comments = [];
for (const video of videos) {
for (const comment of video.comments ?? []) {
comments.push({
videoTitle: video.title ?? "",
author: comment.author ?? "",
text: comment.text ?? "",
likes: comment.likes ?? 0,
sentiment: scoreSentiment(comment.text ?? ""),
videoLink: video.link ?? "",
});
}
}
return comments;
}
async function run() {
const fs = await import("fs/promises");
const config = JSON.parse(await fs.readFile("monitor_config.json", "utf8"));
const keywords = config.keywords ?? ["your brand name"];
const allComments = [];
for (const kw of keywords) {
const videos = await searchYouTube(kw);
allComments.push(...extractComments(videos));
}
const notable = allComments
.filter((c) => c.sentiment === "negative" || c.likes >= 10)
.sort((a, b) => b.likes - a.likes);
const date = new Date().toISOString().slice(0, 10);
const report = {
date,
totalComments: allComments.length,
positive: allComments.filter((c) => c.sentiment === "positive").length,
negative: allComments.filter((c) => c.sentiment === "negative").length,
neutral: allComments.filter((c) => c.sentiment === "neutral").length,
notable: notable.slice(0, 20),
};
await fs.writeFile(`yt_comments_${date}.json`, JSON.stringify(report, null, 2));
console.log(`Collected ${allComments.length} comments, ${notable.length} notable`);
for (const c of notable.slice(0, 5)) console.log(` [${c.sentiment}] ${c.text.slice(0, 80)}`);
}
run();Platforms Used
YouTube
Video search with transcripts and metadata