Creating a News Briefing Skill for OpenClaw

Clawpedia · For Humans

Build a custom skill that delivers personalized news briefings through your OpenClaw agent.

Creating a News Briefing Skill for OpenClaw

A news briefing skill is one of the most practical custom skills you can build — it fetches, filters, and summarizes the day's top stories from sources you care about. This tutorial walks you through building one from scratch.

---

What We Will Build

A skill that:

FeatureDescription
Fetch newsPull articles from multiple sources
Filter by topicOnly show stories matching your interests
SummarizeAI-generated summaries of each article
Rank by relevanceMost important stories first
DeliverSend to terminal, Telegram, Slack, etc.

---

Prerequisites

---

Step 1: Create the Skill Structure


mkdir -p ~/.openclaw/skills/news-briefing
cd ~/.openclaw/skills/news-briefing

Create the manifest:


// manifest.json
{
  "name": "news-briefing",
  "version": "1.0.0",
  "description": "Fetch, filter, and summarize daily news",
  "author": "Your Name",
  "triggers": ["news", "briefing", "headlines"],
  "parameters": {
    "topics": {
      "type": "array",
      "description": "Topics to filter by",
      "default": ["technology", "AI"]
    },
    "limit": {
      "type": "number",
      "description": "Max articles to return",
      "default": 5
    },
    "sources": {
      "type": "array",
      "description": "News sources to check",
      "default": ["hackernews", "techcrunch"]
    }
  }
}

---

Step 2: Build the News Fetcher


// fetchers.js

// Hacker News API (free, no key required)
export async function fetchHackerNews(limit = 10) {
  const topUrl = "https://hacker-news.firebaseio.com/v0/topstories.json";
  const response = await fetch(topUrl);
  const ids = await response.json();
  
  const articles = await Promise.all(
    ids.slice(0, limit).map(async (id) => {
      const itemUrl = `https://hacker-news.firebaseio.com/v0/item/${id}.json`;
      const item = await (await fetch(itemUrl)).json();
      return {
        title: item.title,
        url: item.url,
        source: "Hacker News",
        score: item.score,
        time: new Date(item.time * 1000).toISOString()
      };
    })
  );
  
  return articles;
}

// RSS Feed Parser
export async function fetchRSS(feedUrl, sourceName) {
  const response = await fetch(feedUrl);
  const xml = await response.text();
  
  // Simple XML parsing (in production, use a proper parser)
  const items = [];
  const regex = /<item>([\s\S]*?)<\/item>/g;
  let match;
  
  while ((match = regex.exec(xml)) !== null) {
    const titleMatch = match[1].match(/<title><!\[CDATA\[(.*?)\]\]><\/title>/) ||
                       match[1].match(/<title>(.*?)<\/title>/);
    const linkMatch = match[1].match(/<link>(.*?)<\/link>/);
    
    if (titleMatch && linkMatch) {
      items.push({
        title: titleMatch[1],
        url: linkMatch[1],
        source: sourceName,
        score: 0,
        time: new Date().toISOString()
      });
    }
  }
  
  return items;
}

// NewsAPI (requires free API key)
export async function fetchNewsAPI(apiKey, query, limit = 10) {
  const url = `https://newsapi.org/v2/everything?q=${encodeURIComponent(query)}&sortBy=publishedAt&pageSize=${limit}&apiKey=${apiKey}`;
  const response = await fetch(url);
  const data = await response.json();
  
  return (data.articles || []).map(article => ({
    title: article.title,
    url: article.url,
    source: article.source.name,
    description: article.description,
    score: 0,
    time: article.publishedAt
  }));
}

---

Step 3: Build the Main Skill


// index.js
import { fetchHackerNews, fetchRSS, fetchNewsAPI } from "./fetchers.js";

const RSS_SOURCES = {
  techcrunch: "https://techcrunch.com/feed/",
  arstechnica: "https://feeds.arstechnica.com/arstechnica/technology-lab",
  theverge: "https://www.theverge.com/rss/index.xml",
  wired: "https://www.wired.com/feed/rss"
};

export default {
  name: "news-briefing",
  description: "Fetch and summarize daily news",
  
  execute: async (context) => {
    const { topics, limit, sources } = context.parameters;
    let allArticles = [];
    
    // Fetch from configured sources
    for (const source of sources) {
      try {
        if (source === "hackernews") {
          const articles = await fetchHackerNews(limit * 2);
          allArticles.push(...articles);
        } else if (RSS_SOURCES[source]) {
          const articles = await fetchRSS(RSS_SOURCES[source], source);
          allArticles.push(...articles);
        }
      } catch (e) {
        console.warn(`Failed to fetch from ${source}: ${e.message}`);
      }
    }
    
    // Filter by topics using AI
    const filtered = await context.llm.chat(
      `Given these articles:\n${JSON.stringify(allArticles.map(a => a.title))}\n\n` +
      `Select the ${limit} most relevant to these topics: ${topics.join(", ")}\n` +
      `Return only the indices (0-based) as a JSON array.`
    );
    
    const indices = JSON.parse(filtered);
    const selectedArticles = indices
      .map(i => allArticles[i])
      .filter(Boolean)
      .slice(0, limit);
    
    // Summarize each article
    const summaries = await context.llm.chat(
      `Summarize each of these news articles in one sentence:\n\n` +
      selectedArticles.map((a, i) => `${i + 1}. ${a.title} (${a.source})`).join("\n") +
      `\n\nFormat: numbered list with emoji indicating topic.`
    );
    
    // Format output
    const header = `📰 **News Briefing** — ${new Date().toLocaleDateString()}\n`;
    const footer = `\n---\n_${selectedArticles.length} articles from ${[...new Set(selectedArticles.map(a => a.source))].join(", ")}_`;
    
    return header + "\n" + summaries + footer;
  }
};

---

Step 4: Install and Test


# Install the skill
openclaw skill install ~/.openclaw/skills/news-briefing

# Test it
openclaw chat "Give me a tech news briefing"

# Test with specific topics
openclaw chat "News about AI and cybersecurity"

---

Step 5: Schedule the Briefing


# Daily morning briefing
openclaw schedule --skill news-briefing \
  --params '{"topics": ["AI", "startups"], "limit": 5}' \
  --every day --at 08:00 \
  --notify telegram

---

Enhancements

Add Deduplication


// Remove duplicate articles (same story, different sources)
function deduplicate(articles) {
  const seen = new Set();
  return articles.filter(article => {
    const key = article.title.toLowerCase().replace(/[^a-z0-9]/g, "").slice(0, 50);
    if (seen.has(key)) return false;
    seen.add(key);
    return true;
  });
}

Add Caching


// Cache results to avoid re-fetching
const cache = new Map();
const CACHE_TTL = 30 * 60 * 1000; // 30 minutes

function getCached(key) {
  const entry = cache.get(key);
  if (entry && Date.now() - entry.time < CACHE_TTL) return entry.data;
  return null;
}

Add Sentiment Analysis


// Tag articles with sentiment
const sentiment = await context.llm.chat(
  `For each article, classify sentiment as positive, negative, or neutral:
   ${articles.map(a => a.title).join("\n")}`
);

---

Troubleshooting

No Articles Returned

Summaries Are Poor Quality

Rate Limited by News API

---

Summary

Building a news briefing skill teaches you the core patterns of OpenClaw skill development: fetching external data, using AI for filtering and summarization, and formatting output for delivery. Start with free sources like Hacker News and RSS feeds, then expand to paid APIs for broader coverage. Schedule it for your morning routine and you have a personalized news assistant.

Related Articles