Building Your First Skill: From Zero to Published
Chapter 30: Building Your First Skill: From Zero to Published
Theory must eventually meet practice. This chapter walks through a complete hands-on project, building a "Daily News Digest" Skill from scratch โ covering project initialization, SKILL.md authoring, tool call integration, local testing, debugging techniques, and every step to publishing on ClawHub. By the end of this chapter, you'll have a Skill that actually runs and can be shared.
30.1 Project Planning: Daily News Digest Skill
Feature Design
We're building: Daily News Digest
Description:
Fetches today's news on specified topics, generates structured
summaries, supports multilingual output, and can save to Markdown files.
Core capabilities:
1. Fetch today's news from multiple sources
2. Categorize and rank by topic/importance
3. Generate structured report with summaries, sources, timestamps
4. Optionally save to local file
Required tools:
- web_search (news search)
- fetch_url (article content retrieval)
- write_file (optional, save report)
- get_current_time (accurate date context)
Target File Structure
daily-news-digest/
โโโ SKILL.md
โโโ skill.json
โโโ tools/
โ โโโ __init__.py
โ โโโ news_fetcher.py
โ โโโ digest_formatter.py
โโโ prompts/
โ โโโ system_fragment.md
โโโ tests/
โ โโโ test_news_fetcher.py
โ โโโ test_formatter.py
โ โโโ fixtures/
โ โโโ sample_news_response.json
โโโ requirements.txt
โโโ README.md
30.2 Project Initialization
Step 1: Create Skill Scaffold with Hermes CLI
# Install Hermes CLI if not already installed
pip install hermes-cli
# Create new Skill project
hermes skill new daily-news-digest
# Output:
# โ Created directory: daily-news-digest/
# โ Generated: SKILL.md (template)
# โ Generated: skill.json (template)
# โ Generated: tools/__init__.py
# โ Generated: tests/test_basic.py
# โ Generated: requirements.txt
cd daily-news-digest
Step 2: Set Up Python Environment
python -m venv .venv
source .venv/bin/activate # macOS/Linux
pip install hermes-sdk anthropic pytest httpx python-dotenv
pip freeze > requirements.txt
Step 3: Configure Environment Variables
cat > .env << 'EOF'
ANTHROPIC_API_KEY=your_api_key_here
SEARCH_API_KEY=your_search_api_key_here
HERMES_MODEL=claude-3-5-sonnet-20241022
EOF
30.3 Writing SKILL.md
---
id: daily-news-digest
version: 1.0.0
name: Daily News Digest
description: >
Fetches today's top news on specified topics, generates structured
summaries with sources, and optionally saves to a Markdown file.
author: your-username
license: MIT
tags:
- news
- research
- summarization
- daily-digest
hermes_version: ">=3.0.0"
tools_required:
- web_search
- fetch_url
tools_optional:
- write_file
- get_current_time
language: en
created: 2024-11-20
updated: 2024-11-20
---
# Daily News Digest
## Overview
This skill enables Hermes to create a comprehensive daily news digest on any topic.
**When to use this skill:**
- User says "give me today's news on X"
- User wants "a summary of what happened with X today/this week"
- User requests "daily briefing", "news digest", or "news roundup"
- User wants to stay updated without browsing
**Do NOT use this skill when:**
- User asks a specific factual question (use direct search)
- User wants analysis/opinion rather than news reporting
- User asks about events older than 2 weeks (use general research)
- User is in an offline environment
## Usage
### Parameters
| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| `topics` | array[string] | Yes | โ | News topics to research (1โ5) |
| `time_range` | string | No | "today" | "today" / "this_week" / "24h" |
| `max_articles` | integer | No | 5 | Articles per topic (1โ10) |
| `output_language` | string | No | auto | Auto-detect from user |
| `save_to_file` | boolean | No | false | Save digest to Markdown file |
### Execution Process
**Step 1: Topic Analysis**
Extract topics, formulate 2 search queries per topic.
**Step 2: News Search**
For each topic, call `web_search`:
- Query 1: "[topic] news today [current_year]"
- Query 2: "[topic] latest developments [current_month] [current_year]"
**Step 3: Article Selection**
Top 3โ5 most recent, authoritative results. Prioritize major outlets.
**Step 4: Content Extraction**
Call `fetch_url` for each selected article. Fall back to snippet if fetch fails.
**Step 5: Synthesis**
Generate structured digest with executive summary, key developments, quotes.
**Step 6: Output**
Format in user's language. If `save_to_file`, call `write_file`.
## Examples
### Example 1: Single Topic
**User input:**
Give me today's news about AI regulation in Europe.
**Expected output:**
```markdown
# Daily News Digest: AI Regulation in Europe
*Generated: November 20, 2024*
## Executive Summary
The European Parliament is accelerating EU AI Act implementation...
## Key Developments
### 1. EU AI Act: New Compliance Deadlines
**Source:** Reuters | Nov 20, 2024
The European Commission confirmed...
## Sources
1. [Reuters] EU AI Act Update - https://... - Nov 20, 2024
Dependencies
web_search: News search. Recommended: Brave Search API or Bing News.fetch_url: Article content. Should handle JS-rendered pages gracefully.write_file(optional): For saving to local filesystem.
Limitations
- Cannot access paywalled content
- Breaking news (< 30 min) may not appear in results
- Accuracy depends on source material quality
Configuration
skills:
- id: daily-news-digest
version: "^1.0"
config:
default_max_articles: 5
trusted_sources:
- reuters.com
- bbc.com
- apnews.com
Changelog
1.0.0 (2024-11-20)
- Initial release
---
## 30.4 Implementing Tool Call Integration
### tools/news_fetcher.py
```python
"""News fetcher: wraps search and content extraction logic."""
import httpx
from dataclasses import dataclass
from typing import Optional
from urllib.parse import urlparse
@dataclass
class NewsArticle:
title: str
url: str
snippet: str
source: str
published_date: Optional[str]
full_content: Optional[str] = None
class NewsFetcher:
def __init__(self, search_api_key: str, timeout: int = 10):
self.api_key = search_api_key
self.timeout = timeout
self.client = httpx.Client(timeout=timeout)
def search_news(self, query: str, time_range: str = "today") -> list[NewsArticle]:
"""Search for news articles. Corresponds to web_search tool call."""
date_restrict = {"today": "pd", "24h": "pd", "this_week": "pw"}.get(time_range, "pd")
response = self.client.get(
"https://api.search.brave.com/res/v1/news/search",
params={"q": query, "count": 10, "freshness": date_restrict},
headers={"Accept": "application/json", "X-Subscription-Token": self.api_key}
)
response.raise_for_status()
data = response.json()
return [
NewsArticle(
title=r.get("title", ""),
url=r.get("url", ""),
snippet=r.get("description", ""),
source=urlparse(r.get("url", "")).netloc.replace("www.", ""),
published_date=r.get("age", "Unknown")
)
for r in data.get("results", [])
]
def fetch_article_content(self, url: str) -> Optional[str]:
"""Fetch full article text. Corresponds to fetch_url tool call."""
try:
response = self.client.get(url, headers={"User-Agent": "HermesNewsDigest/1.0"})
response.raise_for_status()
# Simple text extraction โ use trafilatura in production
from html.parser import HTMLParser
class Extractor(HTMLParser):
def __init__(self):
super().__init__()
self.parts = []
def handle_data(self, data):
if data.strip():
self.parts.append(data.strip())
ex = Extractor()
ex.feed(response.text)
return " ".join(ex.parts)[:2000] # truncate to avoid token overflow
except Exception:
return None # degrade gracefully, don't crash
tools/digest_formatter.py
"""Format news articles into structured digest output."""
from datetime import datetime
from typing import Optional
from .news_fetcher import NewsArticle
class DigestFormatter:
def format_digest(
self,
topic: str,
articles: list[NewsArticle],
max_articles: int = 5
) -> str:
selected = articles[:max_articles]
today = datetime.now().strftime("%B %d, %Y")
lines = [
f"# Daily News Digest: {topic}",
f"*Generated: {today}*", "",
"## Key Developments", ""
]
for i, a in enumerate(selected, 1):
lines += [
f"### {i}. {a.title}",
f"**Source:** {a.source} | {a.published_date}", "",
a.full_content or a.snippet, ""
]
lines += ["## Sources"]
for i, a in enumerate(selected, 1):
lines.append(f"{i}. [{a.source}] {a.title} โ {a.url}")
return "\n".join(lines)
30.5 Local Testing
tests/test_news_fetcher.py
import pytest
from unittest.mock import patch, MagicMock
import httpx
from tools.news_fetcher import NewsFetcher, NewsArticle
@pytest.fixture
def fetcher():
return NewsFetcher(search_api_key="test-key")
@pytest.fixture
def sample_response():
return {"results": [
{"title": "AI Act Update", "url": "https://reuters.com/ai",
"description": "The EU...", "age": "2 hours ago"},
{"title": "Industry Response", "url": "https://bbc.com/tech",
"description": "Companies...", "age": "5 hours ago"},
]}
class TestSearchNews:
def test_returns_articles(self, fetcher, sample_response):
with patch.object(fetcher.client, 'get') as mock_get:
mock_get.return_value = MagicMock(
json=lambda: sample_response,
raise_for_status=MagicMock()
)
articles = fetcher.search_news("AI regulation")
assert len(articles) == 2
assert isinstance(articles[0], NewsArticle)
def test_empty_results(self, fetcher):
with patch.object(fetcher.client, 'get') as mock_get:
mock_get.return_value = MagicMock(
json=lambda: {"results": []},
raise_for_status=MagicMock()
)
assert fetcher.search_news("xyz_nonexistent") == []
def test_timeout_returns_none(self, fetcher):
with patch.object(fetcher.client, 'get', side_effect=httpx.TimeoutException("timeout")):
content = fetcher.fetch_article_content("https://slow.example.com")
assert content is None
@pytest.mark.parametrize("time_range,expected", [
("today", "pd"), ("24h", "pd"), ("this_week", "pw"), ("unknown", "pd"),
])
def test_date_filter_mapping(self, fetcher, time_range, expected):
result = fetcher._build_date_filter(time_range)
assert result == expected
Running Tests
# Run all tests
pytest tests/ -v
# With coverage report
pytest tests/ --cov=tools --cov-report=html --cov-fail-under=80
# Validate Skill format
hermes skill validate
# Output:
# โ SKILL.md format valid
# โ skill.json valid
# โ Required tools declared: web_search, fetch_url
# โ Examples present (2 found)
# โ All checks passed!
30.6 Debugging Techniques
Technique 1: Hermes Debug Mode
import hermes, logging
logging.basicConfig(level=logging.DEBUG)
agent = hermes.Agent(
skills=["./daily-news-digest"],
debug=True,
trace_tokens=True
)
# Output:
# [DEBUG] Loading skill: daily-news-digest v1.0.0
# [DEBUG] Skill injected: 342 tokens added to system prompt
# [DEBUG] Tool called: web_search {"query": "AI regulation today 2024"}
# [DEBUG] Token usage: input=8412, output=623, cache_hit=False
Technique 2: Mock Tool Calls
from hermes.testing import SkillTestHarness
harness = SkillTestHarness(skill_path="./daily-news-digest")
harness.mock_tool("web_search", returns=[
{"title": "AI News Today", "url": "https://example.com", "snippet": "..."},
])
harness.mock_tool("fetch_url", returns="Full article content here...")
result = harness.run("Give me today's news about AI")
assert "AI" in result
assert "Sources" in result
assert harness.tool_call_count("web_search") >= 1
Technique 3: Step-Through Debugging
harness.run_with_steps(
"Morning briefing on crypto and AI",
on_tool_call=lambda tool, args: print(f"โ Calling {tool}: {args}"),
on_tool_result=lambda tool, result: print(f"โ {tool}: {str(result)[:100]}"),
)
30.7 Publishing to ClawHub
Pre-Publication Checklist
hermes skill validate # All checks must pass โ
pytest tests/ -v # All tests must pass โ
ls LICENSE README.md # Both must exist โ
hermes skill build # Creates: daily-news-digest-1.0.0.skill
Publishing
# Method 1: CLI publish
hermes skill publish
# ClawHub username: your-username
# ClawHub API token: [enter token]
# โ Published! View at: https://clawhub.io/skills/your-username/daily-news-digest
CI/CD Auto-Publish (GitHub Actions)
name: Publish to ClawHub
on:
push:
tags: ['v*']
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with: {python-version: '3.11'}
- name: Install CLI
run: pip install hermes-cli
- name: Validate
run: hermes skill validate
- name: Test
run: pytest tests/ -v
- name: Publish
env:
CLAWHUB_TOKEN: ${{ secrets.CLAWHUB_TOKEN }}
run: hermes skill publish --token $CLAWHUB_TOKEN
30.8 Summary
Through this complete hands-on project, you've mastered the full Hermes Skill development workflow:
- Planning: Define capability boundaries, required tools, target users
- SKILL.md: The soul of the Skill โ invest most effort in the "When to use" and process sections
- Tool implementation: Encapsulate business logic into clean Python modules aligned with tool interfaces
- Testing: Mock tool calls enable offline testing without API costs; target > 80% coverage
- Debugging: Use debug mode and step-through to observe actual execution
- Publishing: Via CLI or CI/CD automation to ClawHub
Discussion Questions
-
In SKILL.md's "Do NOT use" section, we say not to use this Skill for news older than 2 weeks. If a user asks about historical news, should the Skill error out or gracefully degrade to a general search? How would you express this in SKILL.md?
-
The
DigestFormatter._generate_executive_summarymethod notes "actually done by the LLM in real execution." Redesign this boundary โ which steps in a Skill should use code, and which should be done by the LLM? -
If the news digest Skill is used by 1,000 simultaneous users in production, search API rate limits become a bottleneck. How would you design a caching layer? What's the appropriate cache duration?
-
Design a Skill "security audit" checklist โ what security issues must be checked and cleared before publishing to ClawHub?