We build custom applications 5x faster and cheaper 🚀
Book a Free Consultation
Stuck on an error? Book a 30-minute call with an engineer and get a direct fix + next steps. No pressure, no commitment.
A direct answer: Build a small, explicit integration that does three things—authenticates to Google (YouTube Data API) using either an API key for public-channel polling or OAuth2 for private/channel-scoped access, receives new-video notifications by either polling or a WebSub (PubSubHubbub) push endpoint you host, and then forwards validated events into your OpenClaw skill by invoking the skill’s runtime invocation API (the exact invocation URL and auth mechanism are provided by ClawHub/OpenClaw documentation). Keep state (last-seen video id, backoff state, refresh tokens) outside the agent runtime (database or secret store), validate webhooks, handle rate limits and retries, and debug by checking HTTP responses, logs, credential scopes, and that the skill invocation call succeeds.
// Usage: set env: YT_API_KEY, CHANNEL_ID, SKILL_INVOKE_URL, SKILL_API_KEY, LAST_SEEN_KEY (DB key)
// This example uses an in-memory store for simplicity; in production, use a DB.
import fetch from "node-fetch";
const YT_API_KEY = process.env.YT_API_KEY;
const CHANNEL_ID = process.env.CHANNEL_ID;
const SKILL_INVOKE_URL = process.env.SKILL_INVOKE_URL;
const SKILL_API_KEY = process.env.SKILL_API_KEY;
let lastSeenVideoId = null; // <b>//</b> persist this externally in production
async function fetchLatestVideos() {
const url = `https://www.googleapis.com/youtube/v3/search?part=snippet&channelId=${encodeURIComponent(CHANNEL_ID)}&order=date&type=video&maxResults=5&key=${encodeURIComponent(YT_API_KEY)}`;
const res = await fetch(url);
if (!res.ok) throw new Error(`YouTube API error ${res.status}`);
const body = await res.json();
return body.items || [];
}
async function forwardToSkill(event) {
const res = await fetch(SKILL_INVOKE_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${SKILL_API_KEY}` // <b>//</b> replace with the auth method OpenClaw docs specify
},
body: JSON.stringify(event)
});
if (!res.ok) {
const text = await res.text();
throw new Error(`Skill invoke failed ${res.status}: ${text}`);
}
return res.status;
}
async function pollOnce() {
const videos = await fetchLatestVideos();
// items are in descending publishedAt order
for (const item of videos.reverse()) {
const videoId = item.id.videoId;
if (!videoId) continue;
if (lastSeenVideoId === videoId) continue;
// build a compact event for OpenClaw skill
const evt = {
type: "youtube.new_video",
channelId: CHANNEL_ID,
videoId,
title: item.snippet.title,
publishedAt: item.snippet.publishedAt,
url: `https://www.youtube.com/watch?v=${videoId}`
};
await forwardToSkill(evt);
lastSeenVideoId = videoId; // <b>//</b> persist after successful forward
}
}
// <b>//</b> Example single-run; run this on a schedule (cron, cloud scheduler)
pollOnce().catch(err => {
console.error("Poll error:", err);
process.exit(1);
});
import express from "express";
import bodyParser from "body-parser";
import fetch from "node-fetch";
const app = express();
app.use(bodyParser.text({ type: "*/*" })); // WebSub posts raw XML
const SKILL_INVOKE_URL = process.env.SKILL_INVOKE_URL;
const SKILL_API_KEY = process.env.SKILL_API_KEY;
const VERIFY_TOKEN = process.env.WEBSUB_VERIFY_TOKEN; // <b>//</b> your secret for subscribe validation
app.get("/websub/callback", (req, res) => {
const mode = req.query["hub.mode"];
const challenge = req.query["hub.challenge"];
const token = req.query["hub.verify_token"];
if (mode === "subscribe" && token === VERIFY_TOKEN) {
res.status(200).send(challenge);
} else {
res.status(403).send("Forbidden");
}
});
app.post("/websub/callback", async (req, res) => {
const rawBody = req.body; // XML notification from YouTube
// <b>//</b> parse the XML and extract video id and publishedAt
// For brevity, use a simple extraction — in production use a proper XML parser (xml2js, fast-xml-parser)
const match = rawBody.match(/<yt:videoId>([^<]+)<\/yt:videoId>/);
if (!match) {
res.status(400).send("No video id");
return;
}
const videoId = match[1];
const evt = {
type: "youtube.new_video",
videoId,
raw: rawBody
};
try {
await fetch(SKILL_INVOKE_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${SKILL_API_KEY}`
},
body: JSON.stringify(evt)
});
res.status(200).send("OK");
} catch (err) {
console.error("forward error", err);
res.status(502).send("Forward failed");
}
});
app.listen(3000, () => console.log("WebSub receiver listening on 3000"));
Speak one‑on‑one with a senior engineer about your no‑code app, migration goals, and budget. In just half an hour you’ll leave with clear, actionable next steps—no strings attached.
1
Add a watcher entry under the watchers section of claw-config.yaml that references your installed YouTube watcher skill, provides required environment variables (like YOUTUBE_API_KEY and CHANNEL_ID), and configures the trigger (poll or webhook). Below is a practical example pattern — adjust exact keys to match your OpenClaw version and installed skill name.
Typical requirements: supply secrets via env vars, choose trigger (poll/webhook), and enable the watcher.
watchers:
- id: youtube-watcher
skill: youtube-watcher
enabled: true
trigger:
type: poll
interval_seconds: 300
env:
YOUTUBE_API_KEY: ${YOUTUBE_API_KEY}
CHANNEL_ID: ${CHANNEL_ID}
2
The runtime rejects the YouTube Watcher plugin because the plugin declares a plugin API version that the OpenClaw agent runtime doesn’t support — a mismatch between the plugin’s expected SDK/manifest API and the installed runtime/ClawHub policy.
3
Map YouTube watcher payloads into the OpenClaw event-sink schema by validating and normalizing fields, converting timestamps to ISO8601, providing a stable id, a clear event_type, and a top-level payload object. Reject or transform unknown fields to avoid parse errors.
4
Reduce polling, batch and cache results, and implement exponential backoff + jitter in the YouTube Watcher skill. Move state (last-checked timestamps) outside the agent runtime, monitor real YouTube quota in Google Cloud Console, and request quota increases or switch to webhook-like flows to avoid polling.
This prompt helps an AI assistant understand your setup and guide you through the fix step by step, without assuming technical knowledge.
From startups to enterprises and everything in between, see for yourself our incredible impact.
Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We’ll discuss your project and provide a custom quote at no cost.Â