Imported from benjaminematton/vabene-agents (
skills/vabene-interview-recruiter/SKILL.md). Install upstream withnpx skills add benjaminematton/vabene-agents --skill vabene-interview-recruiter. Copyright stays with the author.
VaBene Interview Recruiter
Purpose
Close the gap between qualified leads (from vabene-interview-finder and, optionally, vabene-reddit-monitor) and processed transcripts (in vabene-discovery-synthesis). Today the middle (drafting personalized invites, tracking who's been contacted/replied/scheduled, gift-card delivery, stalled-thread follow-ups) is manual and untracked, so leads rot in MEMORY.md.
The recruiter drafts outreach (always for human dispatch — never auto-sends), tracks state via Telegram-reply commands, and pings Ben when threads stall.
Hard rules:
- This skill NEVER sends a Reddit comment, DM, or any other outbound message. All dispatch is manual after Telegram review.
- DM drafts must include three consent elements; if any is missing the agent regenerates instead of dispatching to Telegram.
- Comment-first is the default Reddit path. DM-first only via the explicit
skip-commentoverride (when the original post invites DMs or the subreddit allows research recruitment). - State transitions go through
recruit-status <id> <state>Telegram commands or manual MEMORY.md edits via SSH. No auto-progression.
State machine
pending lead exists in MEMORY.md (from interview-finder or reddit-monitor), recruiter not yet drafted
↓ (recruiter run)
comment_drafted public comment draft + Telegram dispatch
↓ (Ben sends, replies "recruit-status <id> commented")
commented
↓ (target replies to comment in-thread; Ben replies "recruit-status <id> engaged")
engaged
↓ (recruiter run)
dm_drafted DM draft with consent block + Telegram dispatch
↓ (Ben sends, replies "recruit-status <id> dm-sent")
dm_sent
↓
replied | scheduled | interviewed | declined | ghosted | gift_sent
Override: recruit-status <id> skip-comment jumps pending → dm_drafted directly. Use only when the original post explicitly invites DMs or the sub is known to allow research recruitment.
State changes always APPEND a new MEMORY.md entry. Never edit-in-place — the history of state transitions is the audit log.
Per-lead workflow
Step 1 — Discover leads
MEMFILE="$HOME/.openclaw/workspace/MEMORY.md"
[[ -f "$MEMFILE" ]] || { echo "ERROR: MEMORY.md missing at $MEMFILE" >&2; exit 1; }
Read MEMORY.md, extract entries from interview-finder (and optionally reddit-monitor if Ben wants it as a recruiter source). Group by lead_id. Filter to leads where the latest entry has outcome: "pending".
Step 2 — Cross-skill dedup
The same Reddit URL can be surfaced by both interview-finder (pain profile) and reddit-monitor (acquisition profile). Both skills emit a deterministic lead_id (sha256(normalized_url)[:12]); the recruiter dedupes on it.
For each lead_id:
- Take the highest-confidence source entry (HIGH > MED) as canonical.
- Record the merger:
merge_sources: ["interview-finder", "reddit-monitor"]on the recruiter draft entry. - Skip if any prior recruiter entry exists for this
lead_id(i.e., recruiter has already drafted/sent something — don't re-draft).
Step 3 — Draft per state
pending → comment_drafted (default Reddit path):
Draft a public comment that:
- Adds value to the thread (answers their question, validates their pain, shares a relevant resource).
- Does NOT pitch the interview directly.
- Establishes Ben as a person interested in the same problem space.
- Length: 2–4 sentences.
- Voice: first-person founder ("I'm building something in this space and seeing the same problem"), not corporate ("VaBene helps planners…"). Pre-PMF, first-person is the right call.
engaged → dm_drafted:
Draft a DM that contains all three consent elements (the agent self-checks; if any is missing, regenerates and does NOT dispatch):
- Recording disclosure: explicit statement that, with permission, the call may be recorded so notes can be reviewed afterward.
- Use-of-data sentence: how the data is used (private notes, no quotes shared externally without explicit follow-up consent).
- Opt-out language: participant can decline recording, stop the call, or pass on any question without explanation.
Plus:
- Specific detail from the original post (proves it's not a template blast).
- Time ask: "20-min call".
- Incentive: "$25 gift card" or whatever Ben sets via env var
INTERVIEW_INCENTIVE_USD. - Same first-person founder voice as the comment.
Step 4 — Send to Telegram (review-only)
For comment_drafted and dm_drafted states, send the draft to Telegram with the lead context, full draft text, and explicit "REVIEW BEFORE SENDING" framing. See "Telegram message formats" below.
Step 5 — Stalled-thread sweep (daily)
Trigger: stalled threads. Default cron 09:30 PT.
For each lead currently in commented or dm_sent:
- If
now - outcome_updated_at > 5dand no further state change → Telegram nudge ("Lead<id>no movement in 5d — comment follow-up Y/N? DM follow-up Y/N?") - If Ben replies
recruit-status <id> followup-commentorrecruit-status <id> followup-dm, draft a follow-up message with a different angle from the first (never identical re-send). - Never auto-resend.
Heartbeat: if zero stalled threads, send 🔁 Recruiter: no stalled threads. Telegram message.
Step 6 — Scheduled & interviewed cues
When state transitions to scheduled (via recruit-status <id> scheduled):
- Day-before reminder ping with the original post context refresher (so Ben goes into the call with the right backstory).
When state transitions to interviewed (via recruit-status <id> interviewed):
- Reminder check: look for a transcript file matching this lead in
~/.openclaw/agents/main/transcripts/. Filename convention isYYYY-MM-DD-<participant-handle>.md. - If no transcript file exists 24h after the
interviewedstate change → ping Ben to drop the transcript sovabene-discovery-synthesiscan process it.
Step 7 — Gift-card and close
recruit-status <id> gift-sent is the terminal state. Closes the audit chain.
MEMORY.md JSONL schema (pinned at v0.1)
Every state change appends one line:
{"schema_version":"0.1","ts":"2026-04-26T18:00:00-07:00","scan":"recruiter","lead_id":"a3f1b2c4d5e6","outcome":"comment_drafted","outcome_updated_at":"2026-04-26T18:00:00-07:00","merge_sources":["interview-finder"],"channel":"comment","draft_text":"<full draft, only on *_drafted states>","notes":"<free-text from Ben on state transition, optional>"}
Field reference:
schema_version— always"0.1"from this skill at v0.1.0.ts— ISO 8601 with PT offset; when this entry was written.scan— always"recruiter".lead_id— 12-char hex prefix ofsha256(normalized_url). Same hash function asinterview-finderandreddit-monitor(cross-skill dedup contract).outcome— one of:pending(only set by upstream skills, never recruiter),comment_drafted,commented,engaged,dm_drafted,dm_sent,replied,scheduled,interviewed,declined,ghosted,gift_sent.outcome_updated_at— when this state transition happened (typically same asts, but separate field so you can backdate manually for testing).merge_sources— array of upstreamscanvalues that contributed; usually one, sometimes bothinterview-finderandreddit-monitor.channel—"comment"or"dm". Tracks which artifact the recruiter is drafting at this state.draft_text— present only on*_draftedstates; full text of the drafted comment or DM.notes— optional free-text Ben can include in therecruit-statuscommand (e.g.,recruit-status <id> declined no time this month).
lead_id normalization (cross-skill contract)
input: https://www.reddit.com/r/SanFrancisco/comments/abc123/whatever_title/?utm_source=x#comment
output: sha256("reddit.com/r/sanfrancisco/comments/abc123")[:12] (lowercase host, canonical post ID, no query, no anchor)
Both interview-finder and reddit-monitor MUST use the same function. The recruiter trusts lead_id as an opaque dedup key; if upstream emits a hash with different normalization, the recruiter will treat the same URL as two leads. Fix at the source.
Telegram message formats
Comment draft ready
💬 Recruiter — comment draft (REVIEW BEFORE SENDING)
Lead: a3f1b2c4d5e6 | Sub: r/SanFrancisco | Score: HIGH (5/6)
🔗 https://reddit.com/r/SanFrancisco/comments/abc123/...
Original post snippet: "Turning 30 next month, 8 of us, can't agree on..."
Draft comment:
---
[2-4 sentence comment, first-person founder voice]
---
After you post: reply 'recruit-status a3f1b2c4d5e6 commented'
If you'd rather skip the comment and DM directly: 'recruit-status a3f1b2c4d5e6 skip-comment'
DM draft ready
✉️ Recruiter — DM draft (REVIEW BEFORE SENDING)
Lead: a3f1b2c4d5e6 | After comment engagement
🔗 https://reddit.com/r/.../...
Draft DM:
---
[DM text — must contain: recording disclosure, use-of-data sentence, opt-out language, original-post detail, 20-min ask, $25 incentive]
---
✓ recording disclosure present
✓ use-of-data present
✓ opt-out language present
After you send: reply 'recruit-status a3f1b2c4d5e6 dm-sent'
Stalled-thread nudge
🔁 Recruiter — stalled (no movement in 5d)
Lead: a3f1b2c4d5e6 | Last state: dm_sent (2026-04-21)
🔗 https://reddit.com/r/.../...
Reply with one of:
'recruit-status a3f1b2c4d5e6 followup-dm' → draft a different-angle follow-up DM
'recruit-status a3f1b2c4d5e6 ghosted' → close as no-response
'recruit-status a3f1b2c4d5e6 declined' → close as declined
Scheduled — day-before refresher
📅 Recruiter — interview tomorrow
Lead: a3f1b2c4d5e6 | u/<handle> | r/<sub>
Original post (refresher): "<snippet>"
Trigger event: <from finder>
Workaround they tried: <from finder>
Pain dollar value (if quantified): <from finder>
🔗 https://reddit.com/r/.../...
Suggested opening: ask them to walk you through the moment that made them start looking for a solution.
Interviewed — transcript drop reminder (T+24h)
📂 Recruiter — transcript not yet in watched dir
Lead: a3f1b2c4d5e6 (interviewed 24h ago)
Expected file: ~/.openclaw/agents/main/transcripts/<YYYY-MM-DD>-<handle>.md
Recommended source: Otter or Granola Markdown export.
discovery-synthesis won't pick this up until the file lands.
Empty stalled sweep heartbeat
🔁 Recruiter: no stalled threads.
Cron Setup
Deferred. Wire only after ≥ 3 successful manual runs covering all branches: comment_drafted, commented, engaged → dm_drafted, dm_sent, stalled-thread sweep, skip-comment override, cross-skill dedup. For reference / future creation:
# Per-run draft generation — every 4 hours during waking hours
openclaw cron add \
--name "VaBene Interview Recruiter" \
--cron "0 9,13,17,21 * * *" \
--tz "America/Los_Angeles" \
--session isolated \
--message "Run the vabene-interview-recruiter skill. Draft outreach for any pending leads, handle state transitions from Telegram replies. Never send anything automatically." \
--model claude-sonnet-4-6 \
--to <TELEGRAM_CHAT_ID> \
--announce \
--channel telegram
# Stalled-thread sweep — daily 9:30 PT
openclaw cron add \
--name "VaBene Recruiter Stalled Sweep" \
--cron "30 9 * * *" \
--tz "America/Los_Angeles" \
--session isolated \
--message "Run the vabene-interview-recruiter skill with trigger 'stalled threads'. Surface any commented/dm_sent leads with no movement >5d." \
--model claude-haiku-4-5-20251001 \
--to <TELEGRAM_CHAT_ID> \
--announce \
--channel telegram
Sonnet for drafting (voice + consent self-check matters); Haiku is fine for the stalled-thread sweep (just MEMORY.md filtering and a Telegram render).
Manual Telegram commands
| Command | What it does |
|---|---|
recruit |
Per-run draft pass for all pending leads |
draft invites |
Alias |
stalled threads |
Stalled-thread sweep (commented / dm_sent with no movement > 5d) |
recruiter status |
Counts by outcome over last 30d, list of leads in each terminal-non-final state |
recruit-status <lead_id> <state> |
State transition. States: commented, engaged, skip-comment, dm-sent, replied, scheduled, interviewed, declined, ghosted, gift-sent, followup-comment, followup-dm. Optional trailing free-text becomes notes. |
Tuning levers (in priority order)
- Stalled-thread threshold — default 5 days. Raise (7d) if Ben is following up too often; lower (3d) if leads are dying before nudges arrive.
INTERVIEW_INCENTIVE_USD— default 25. Raise if response rates are too low; lower if the recruiter pipeline is starting to feel paid-promotion-y.- Comment template voice — pre-PMF first-person ("I'm building…") is the v0.1 call. If post-PMF the brand is recognizable, switch to lower-key ("at VaBene we're trying to figure out…"). Document the change.
- Skip-comment heuristic — currently only via explicit Ben override. If a fraction of subs are confirmed DM-friendly, the recruiter could auto-skip-comment for those subs. Don't add this until the skip-comment manual path has been used > 5 times to validate.
Document any tuning change in this skill's README change log. Voice and incentive especially — they're easy to fiddle and hard to A/B retroactively.
Relationship to other skills
vabene-interview-finder— upstream lead source. The recruiter consumes itsoutcome: "pending"entries and writes back recruiter-namespaced state transitions.vabene-reddit-monitor— optional secondary lead source. A post can qualify as both an acquisition lead (reddit-monitor) and an interview candidate (interview-finder); cross-skill dedup vialead_idensures the recruiter drafts once.vabene-discovery-synthesis— downstream. The recruiter'sinterviewedstate triggers a transcript-drop reminder; once the file lands, discovery-synthesis picks it up on its next run.
The three skills together form the customer-development loop: find → recruit → synthesize. All three follow the append-only MEMORY.md JSONL convention with lead_id / transcript_id cross-references, so the lifecycle of any single conversation is auditable end to end.
Security
- Read-only on MEMORY.md for upstream entries; append-only for own entries.
- No outbound network calls beyond Telegram. No Reddit posting, no DM sending.
- DM drafts contain participant-specific detail extracted from public Reddit posts (no scraping of private profiles). Treat MEMORY.md as PII-bearing per the existing
memory/README.mdpolicy. - All Telegram messages route through the configured bot.