/haro — Daily journalist-query response generator
Captures the highest-velocity backlink-acquisition opportunity: journalists actively requesting sources for stories that will publish in the next 3–14 days. A single response landing in a Forbes / WSJ / industry-outlet quote earns one DA-80+ editorial backlink — equivalent to ~6 months of organic-link attrition for a small site.
Important context: the original "HARO" (Help A Reporter Out) was shut down by Cision in December 2024 after a brief rebrand to "Connectively". The skill name /haro is retained per the v2 spec (familiar shorthand for the category), but the underlying source is configurable per site. As of 2026, the active sources are:
| Source | URL | Format | Notes |
|---|---|---|---|
| Featured.com (ex-Terkel) | featured.com | Web UI + daily digest email | Most popular post-HARO; ~50–150 queries/day across categories |
| Source of Sources | sourceofsources.com | Daily email digest | Peter Shankman's relaunch (HARO's original founder) |
| Qwoted | qwoted.com | Web UI | Paid (~$99/mo); journalist-vetted only |
| Help a B2B Writer | helpab2bwriter.com | Web UI + weekly digest | B2B-focused, lower volume |
Each site picks 1–3 sources via site-info.md → "Journalist sources" block.
Inputs
Required (asked at start if not given):
- Site — must match a folder under
sites/[name]/.
Optional (per-run overrides):
--source=X— pull from only one source this run (default: all configured).--min-match-score=N— minimum match score to draft a response (default: 70 out of 100).--max-drafts=N— cap responses per run (default: 5).
Workflow
Step 0 — Cron-mode detection (if invoked with --cron)
Cron mode is the DEFAULT pattern for this skill — it's designed for daily firing. Required behavior:
- Idempotency check: if
sites/{site}/_inbox/haro-responses-{date}.mdexists for today, exit withidempotent-skip. - Escalation: any error pulling a source (API down, auth expired, rate-limit, source ToS change) → write
_inbox/haro-source-error-{source}-{date}.mdwith full error context, continue with remaining sources. - Audit log: standard
appendRun({ exit, ... })perscripts/lib/audit-log.mjs. - Backoff: at start,
checkBackoff({ routine: 'haro-daily' }). If true → pause + escalate per standard contract.
Step 1 — Read context (Rule 1)
Load in order:
CLAUDE.mdsites/{site}/site-info.md— especially "Journalist sources" block, "Authority" block (what site is on-the-record about), "Person profile" if applicablesites/{site}/references/stats.md— for the numbers that make a quote citablesites/{site}/references/stories.md— for relevant anecdotessites/{site}/references/voice.md— for tone consistencysites/{site}/backlinks.md— for "Banned outreach targets" + outreach history
If any required file is missing → escalate per cron contract.
Step 2 — Source authentication check
For each source listed in site-info.md → "Journalist sources":
- Verify the corresponding API key / session cookie is present in
.env(cron) or available via auth flow (interactive). - If missing → escalate to
_inbox/haro-auth-missing-{source}.mdwith setup instructions. Skip that source for this run.
Supported auth methods:
- Featured.com: API key in
.envasFEATURED_API_KEY. Get from featured.com → Settings → API. - Source of Sources: email-digest parsing (forward to
[site-specific]+sos@yourforwarder.com; cron polls IMAP). - Qwoted: session cookie scraped via Chrome MCP (Qwoted blocks API access on lower tiers).
- Help a B2B Writer: email-digest parsing (same pattern as Source of Sources).
Step 3 — Pull today's queries
For each authenticated source, fetch queries submitted in the last 24 hours. Normalize to a common schema:
- query_id: featured-2026-05-17-abc123
source: featured
outlet: Forbes
reporter: jane.doe@forbes.com
reporter_handle: "@jane_doe"
category: "Business / AI"
topic: "Examples of AI ROI in SMBs"
query_body: "[full text of the journalist's request]"
word_count_requested: 150
deadline: 2026-05-19T17:00:00Z
embargo: null
preferred_credentials: "founders, AI consultants, SMB ops leaders"
pull_time: 2026-05-17T09:00:00Z
Step 4 — Match-score each query against site expertise
For each query, compute a 0–100 match score from:
| Signal | Weight |
|---|---|
| Topic overlap with site's "Authority" block (semantic similarity of query topic to site's documented expertise areas) | 35 |
| Preferred-credentials match (does the byline / Person profile match what the reporter wants?) | 25 |
Available stat in stats.md that directly answers the query |
20 |
Available story in stories.md that anecdotally answers the query |
10 |
| Outlet authority (DA 80+ = 10, DA 50–79 = 6, DA < 50 = 3) | 10 |
Threshold (default --min-match-score=70): only queries scoring ≥ 70 generate a draft response. Lower-scoring queries are logged in the daily summary but no draft is generated.
Step 5 — Draft response for high-match queries
For each query above threshold, generate a draft pitch:
Format: 100–200 words (or word_count_requested ± 20%), structured as:
- Lead with a number from
stats.md— never round, never invent. - One concrete example from
stories.md(anonymized unless consented) — 1–2 sentences. - One contrarian or specific opinion backed by the stat — the unique angle that makes the quote citable.
- Byline matching the site's documented byline policy (per memory: site-a → "Site A", site-b → co-byline OK, site-c → real founder name).
- Contact line with the reporter's preferred contact method.
Voice constraints: universal rules from references/voice.md apply — banned words, no exclamation marks, no AI tells. Pitch must read like a real expert wrote it, not like an AI. Run the anti-AI checklist before saving.
No links in the pitch itself — reporters explicitly reject pitches containing URLs as spam. The link gets included only IF the reporter follows up requesting more context.
Step 6 — Quality refusals
Refuse to draft a pitch when:
- Query asks for credentials the site doesn't have (e.g., reporter wants "PhD in computational biology" and site has no such person).
- Query requires fabrication (the answer can't be sourced from
stats.mdorstories.md). - Outlet is in
backlinks.md"Banned outreach targets" list. - Reporter has previously declined a pitch from this site (cross-check
backlinks.md"Outreach response tracking"). - Query embargo or deadline already passed.
Refused queries are logged with reason in the daily summary; no draft generated.
Step 7 — Output
Write to sites/{site}/_inbox/haro-responses-{date}.md:
# HARO daily responses — {site} — {date}
Pulled {N} queries across {M} sources. Generated {K} drafts ({K2} refused).
## Drafts awaiting your approval
### 1. Forbes — Jane Doe — "Examples of AI ROI in SMBs"
**Match score:** 82
**Deadline:** 2026-05-19 17:00 UTC (48h)
**Outlet DA:** 95
**Source:** Featured.com
**Draft pitch:**
> [pitch body, 150 words]
**To send:** copy the draft into Featured.com query response field. Reply to this inbox item with `sent` to log the outreach. The skill will create the outreach pipeline row in `backlinks.md`.
---
### 2. ...
## Queries refused (logged for transparency)
- "Topic X" — Forbes — refused: no PhD-qualified author available (matching skipped)
- "Topic Y" — Outlet Z — refused: outlet in banned-list (notes.md: prior bad-faith interaction 2025-09-04)
## Queries below match threshold (≥ 50, < 70)
(no draft generated; listed for transparency)
- "Topic A" — Outlet B — score 64 (`stats.md` has no number to anchor a 150-word pitch)
- ...
Step 8 — Update outreach pipeline (on user approval — separate skill)
The cron only DRAFTS. When user replies sent to an inbox item, a follow-up skill (/outreach-log) appends a row to backlinks.md → "Outreach pipeline" with status sent and the 14-day follow-up trigger.
Step 9 — Audit log
Standard appendRun({ exit: 'shipped' | 'idempotent-skip' | 'escalated', metadata: { queries_pulled: N, drafts_generated: K, refusals: K2 } }).
Refusal conditions
This skill refuses to run if:
- Site has no "Journalist sources" block in
site-info.md. - Site has no
references/stats.mdorreferences/stories.md(no anchor for citable pitches). - All configured sources are unauthenticated.
Setup checklist (one-time per site)
Before this skill can run on a new site:
- Add to
site-info.md:## Journalist sources - **Featured.com:** {profile_url}, API key in .env as FEATURED_API_KEY_{SITE} - **Source of Sources:** subscribed to digest at {forwarder_email} - **Authority block:** [3–5 areas this site is on-the-record about] - **Preferred byline format:** {per byline policy memory} - Add
_inbox/to site folder if not present. - Add cron entry to
.claude/schedules.yml(portfolio-daily-haro). - Add API keys to
.env+ trigger prompt (perremote-cron-no-dotenv-accessmemory — cron secrets must be in trigger prompt, not.env).
Routine versioning + idempotency contract
Idempotency key: (site, date). The skill runs once per site per day. Re-running on the same date skips with idempotent-skip and logs the score snapshot.
Routine version: 1.0.0 (2026-05-17). Increment on schema changes to haro-responses-{date}.md output format.