WhatsApp Business Ops

Run WhatsApp Business outbound without losing quality score — 24h window, approved templates, duplicate guard. Use when answering inbound leads or sending fo...

Alexandre Bloch

@alexbloch-ia

What This Skill Does

Manages WhatsApp Business outbound messaging with template-only sends outside the 24-hour window, duplicate detection, and quality score protection. Handles inbound lead replies, scheduled follow-ups, and CRM logging through a configurable pipeline.

Replaces manual WhatsApp Business management and risky bulk messaging by enforcing Meta's 24-hour window, approved templates, and duplicate guard to protect your quality score.

When to Use It

  • Answer inbound WhatsApp leads with free-form replies inside the 24-hour window
  • Send first-contact approved templates to new opt-in leads
  • Schedule follow-up messages to cold leads using a cadence table
  • Log lead interactions and conversation state to a CRM sheet
  • Alert a Telegram or Slack channel when a new lead messages in
  • Detect and skip duplicate sends to the same phone number

Install

$ openclaw skills install @alexbloch-ia/whatsapp-business-ops

WhatsApp Business Management

WhatsApp is not a discovery channel — it is a conversion channel. The goal is not to send fast: it is one qualified hand-off per opt-in lead, no unsolicited bulk, no duplicate send. Reference BSP: Whatchimp (Meta Business Partner); any BSP works.

Access, data, and network — read before running

Every access is opt-in — each one stays off until you fill its placeholder in ## Configure. The default is no network.

AccessWhyDefault
BSP API token (<WA_API_KEY>)Send/read messagesNone — no network call without it
CRM sheet ID (<CRM_SHEET_ID>)One row per leadNone — CRM writes skipped
Alert channel / webhook (<ALERT_CHAT_PRIMARY>)Leaves your machine. Posts lead name + phone to Telegram/Slack/DiscordNone — recap stays local
Local workspace (<WORKSPACE_DIR>/memory/)State + duplicate guardLocal files only

Personal data persisted, in full: display name, case type, free-text situation, conversation timestamps, template-send log, alert log, per-run recaps. Phone numbers are stored raw ONLY in wa-crm-state.md (the system-of-record mirror); every duplicate register and the blacklist key on sha256(phone)[0:12]. That is lead PII on disk — ## Data minimization and retention is doctrine, not an appendix. Scope the API token to the one business number; never reuse a full-account token.

When to Use

TriggerAction
"answer the whatsapp leads"wa-inbound run — §Flow, free-form only inside open window
"send the first message to new leads"wa-outbound run — approved template only
"relance the cold leads"wa-followup run — cadence table
"quality score dropped" / "number got paused"Stop outbound. ## Troubleshooting, last two rows. Human review
"am I talking to a bot?" (asked by a lead)Answer honestly, immediately — ## Identity

Configure

PlaceholderExampleYour value
<BRAND_NAME>"Acme Studio"
<WA_BUSINESS_NUMBER>"+1 555 0000"
<WA_PHONE_NUMBER_ID>numeric ID from your BSP
<WA_API_KEY>scoped to one number
<WA_TEMPLATE_FIRST_CONTACT> / _FOLLOWUP_SOFT / _FOLLOWUP_HARD / _CLOSINGMeta-approved template IDs
<CRM_SHEET_ID>Sheet/Airtable ID— (omit → no CRM write)
<ALERT_CHAT_PRIMARY>alert chat ID— (omit → local recap only)
<WORKSPACE_DIR>"~/.openclaw/workspace/whatsapp-acme"
# <WORKSPACE_DIR>/config.yaml
whatsapp:
  phone_number_id: <WA_PHONE_NUMBER_ID>
  base_url: https://app.whatchimp.com/api/v1   # swap host for another BSP
templates:
  first_contact: <WA_TEMPLATE_FIRST_CONTACT>
  followup_soft: <WA_TEMPLATE_FOLLOWUP_SOFT>
  followup_hard: <WA_TEMPLATE_FOLLOWUP_HARD>
  closing: <WA_TEMPLATE_CLOSING>
crm: { sheet_id: <CRM_SHEET_ID>, tab: "Leads" }
alerts: { primary_chat: <ALERT_CHAT_PRIMARY>, channel: telegram }
retention: { lead_days: 90, recap_days: 30, template_log_days: 30 }
inbound_pipeline:
  shared_queue_file: <WORKSPACE_DIR>/../shared/leads-whatsapp.json
schedule:  # dm_check "*/2 9-22 * * *" · add_contacts "*/5 9-22 * * *" · follow_up "0 10 * * *"
  timezone: Europe/Paris

The 24h window — the central law

After any message a user sends you, you have 24 h to reply in free-form. After 24 h of user silence, only Meta-approved templates, each billed as a new conversation. Free-form to a cold conversation → API rejection or a marketing-conversation bill + a quality hit; exit code 2, always skipped. Check window state before every outbound send:

curl -s "https://app.whatchimp.com/api/v1/whatsapp/get/conversation" \
  -d "apiToken=<WA_API_KEY>" -d "phone_number_id=<WA_PHONE_NUMBER_ID>" \
  -d "phone_number=<DIGITS>" -d "limit=1"
# Output: {"status":1,"data":[{"sender":"user","created_at":"2026-07-16T09:12:04Z"}]}
# sender="user" and < 24h old  → free text OK. Otherwise → template.

Session check first on every cron:

curl -s "https://app.whatchimp.com/api/v1/whatsapp/subscriber/list" \
  -d "apiToken=<WA_API_KEY>" -d "phone_number_id=<WA_PHONE_NUMBER_ID>" \
  -d "limit=1" -d "offset=1"
# Output: {"status":1,"data":[...]}   401/403 → stop+alert · 429 → stop, wait next run

Timeout > 10 s → BSP down: one retry, then stop and alert. Never retry in a loop. A 200 OK carrying status=0 is a failure, not a send. Never send a "test" message to a real lead — use a dedicated test number. Never auto-rotate an API token from inside a cron; rotation is a human action.

Phase gating and quotas

Meta assigns each number a tier: T1 1k uniques/24h · T2 10k · T3 100k · T4 unlimited. New numbers start at T1. Tiers rise on volume + quality, and fall — or the number gets paused — when quality drops.

ConditionPhaseOutbound
Tier 1 or quality yellow/redAInbound replies + approved-template follow-ups. ≤ 50 first-contact/day
Tier 2+ and quality green, no drop in 14 dBFull doctrine, first week capped at 50% of Phase B max

Read memory/wa-state.md at start of every run. Phase B is a manual flip, never automatic. Any quality drop → revert to A immediately.

Action (hard limits)Phase APhase B
Inbound replies / cron run2050
First-contact templates / 24 h50500
Follow-up templates / 24 h20200
Unique recipients / 24 h800 (under T1's 1000 cap)tier cap − 10%
Conversations / cron run3080
Same user — outbound spacingmin 24 h between any two outbound

Follow-up cadence: J+1 soft · J+3 soft · J+7 hard · J+15 hard · J+20 closing → status=lost. Stop after the closing template. Continuing past J+20 is ban-risk territory.

Duplicate guard — the most important section

Quality-score collapse is dominated by duplicate template sends. Three registers, read before every outbound action:

RegisterRow formatRule
wa-alerts-sent.md{"phone_hash":"<sha256:12>","qualified_date":"YYYY-MM-DD"}Phone present → never alert twice
wa-template-log.md{"phone_hash":"<sha256:12>","template_id":"<ID>","sent_at":"<ISO>","result":"ok|error"}Same (phone, template) in last 24 h → SKIP
wa-crm-state.mdone row per phone, never twoOnly wa-outbound appends; every other cron UPDATEs

If in doubt, SKIP. It is always better to skip than to send a duplicate. This one rule is the difference between a number that stays Tier 2 for years and one paused in 30 days.

Alert only when a lead replies AND qualifies — never after a first template send. Alerting on first contact generates dozens of false alerts and trains the human team to ignore the channel.

Data minimization and retention

The registers above hold lead PII — a regulated store, not logs.

RuleConcretely
MinimizeStore phone + name + case type + status. Never store ID numbers, health details, case documents, or payment data in memory files — keep those in the system of record.
MaskKey the duplicate registers on sha256(phone)[0:12], not the raw number. Lookups work identically; a leaked log exposes nothing.
Restrictchmod 700 <WORKSPACE_DIR>/memory. One number = one workspace. Never share wa-template-log.md across numbers or brands.
RetainLeads 90 d after last contact, recaps 30 d, template log 30 d — then delete. Prune at the start of the daily run, not "someday": find "<WORKSPACE_DIR>/memory" -name 'wa-recaps.md' -mtime +30 -delete (silent — pruned, not archived).
Alert payloadName + case type + phone only. Never paste conversation transcripts into the alert channel — it leaves your machine and is usually a wider audience than the case file.
EraseOn an opt-out or deletion request: purge every register, add the hash to wa-blacklist.md, confirm to the lead. Blacklist stores hashes only.
Sensitive nichesLegal, medical, financial: confirm with your DPO/counsel before any cron writes lead data. This skill does not make that call for you.

Identity — disclose, never dodge

If a lead asks whether they are talking to a bot, a human, or an AI: say so plainly, in the same message, and offer the human.

You're talking to <BRAND_NAME>'s automated assistant. I can take your details
now and a specialist will call you back — or I can pass you to them right away.

No ambiguity, no pivot, no "I'm part of the team". Honest disclosure is not a cost here: a lead who discovers the automation later is a complaint, a block, and a quality-score hit. Blocks and reports are exactly what Meta measures. Disclosure is account safety. Escalate to a human — stop the automated replies for that thread and alert — when the lead asks for a human, asks twice about identity, or raises anything the templates do not cover.

Reply rules (free-form, inside open window)

  • Match tone. Short messages. One ask per message. Reassuring on stressful topics, never dramatizing. Reread the full visible history before composing.
  • Never: definitive price quotes (soft "starting at" only if asked) · outcome guarantees · confidential client info · documents/PDFs to non-clients · anything outside the brand's scope.

Skeletons: A greeting + "how can we help" · B empathy + one contextual question · C hand-off: Thank you for these details. A specialist from <BRAND_NAME> will call you back. Could you confirm your name and the best time to reach you?

Classify every thread before drafting — case type is what the alert payload and the recap carry. Structure below, questions are yours:

Case typeSignalAction
A — high urgencyPending deadline, acute problemAsk date, location, urgency → collect contact → qualified
B — mid intentOngoing situation, no deadline5-7 contextual questions → collect contact → qualified
C — out of scopeNot what the brand doesRedirect to the right channel. Never force qualification
D — existing clientPhone-match in wa-clients-known.mdAnswer with empathy, redirect to the standard client support line. Never paste the prospect CTA — a paying client who gets a sales pitch blocks, and a block is a quality hit

"Too many questions" failsafe — after 5 questions with no contact info, switch: "To answer precisely, a specialist needs to call you back. Could you confirm your name + best time?" This converts more question-only threads than any other tactic.

Qualification requires: thread open · not blacklisted · not already "qualified — awaiting callback" · no auto-reply in the last 60 s (anti-cascade). Any check fails → skip.

Flow

wa-inbound (every 2 min)

1. Session check. 2. subscriber/list unseen_count>0 — if 0 unread → STOP.
3. Per unread (≤20 A / ≤50 B): get/conversation limit=20 → if last sender="bot" SKIP
   → qualify → draft → normalize encoding → POST /whatsapp/send → verify status=1.
4. If qualified: check wa-alerts-sent.md → if absent, alert primary chat, append hash.
   UPDATE the CRM row (wa-inbound never APPENDs). 5. Recap.

wa-outbound (every 5 min)

1. Session check. 2. Read shared queue, filter status="pending" — if 0 → STOP.
3. Per lead: template-log 24h check → subscriber/create → POST /whatsapp/send/template
   template_id=<WA_TEMPLATE_FIRST_CONTACT> → verify status=1 → queue status="first_message_sent"
   → APPEND CRM row (only cron that appends) → append template log.
4. Never alert from this cron. 5. Recap.

wa-followup (daily 10:00)

1. Session check. 2. CRM: status in {first_message_sent, follow_up_sent}, idle at a J+N mark.
3. Per candidate: template-log check → send cadence template → update CRM + log. 4. Recap.

WhatsApp Web via Playwright — gray area, not recommended

Running Playwright against web.whatsapp.com is outside WhatsApp's Business Terms and risks a permanent number ban. It is documented here only as a read-only contingency while BSP approval is pending, and it is not recommended for production. Requires an explicit human decision per incident — an agent must not choose this path on its own. Sessions rotate the QR link unpredictably (daily manual re-login); no template sends are possible; quality score still applies at the number level. If a login challenge or automation check appears: stop and hand the session to a human. Never tune timing to avoid a detection check. Migrate back to the BSP API as soon as approval lands.

Exit codes

CodeMeaningRecap
0Sent, or nothing to do (zero unread)status: ok
1Fatal (API 5xx, JSON parse fail)status: error, alert
224h-window violation attempted → skippedstatus: skip, log
3Auth fail (401/403)status: blocked, alert now
4Duplicate-guard SKIPstatus: ok

Output Format — mandatory recap

Every run ends with this, verbatim, to the alert channel and appended to memory/wa-recaps.md:

[Job name] — [status: ok|partial|blocked|skipped]
Inbound replies: [N or "—"]
Templates sent: [N or "—"]
Qualified leads: [count + first names only]
Phase / Tier: [A|B / Tier 1|2|3|4]
Quality score: [green|yellow|red]
Blockers: [text OR "—"]
Next action: [1 line]

Filled instance:

wa-inbound 10:32 — status: ok
Inbound replies: 7
Templates sent: —
Qualified leads: 2 (Marc, Lina)
Phase / Tier: A / Tier 1
Quality score: green
Blockers: —
Next action: human callbacks due before 12:00

Better silence than spam. Better a blockage report than a fake success. Never fake a successful send.

Troubleshooting

SymptomRoot causeFix
200 OK, message never deliveredphoneNumberID vs phone_number_id — BSP casing differs per endpoint. No error is raised, so you'll believe your code is rightCheck casing per endpoint. Verify status=1 in the body, not just HTTP 200
Send rejected for unknown reasonLeading + in the number. 33612345678 works, +33612345678 often does notStrip + before every call
Accents vanish in delivered messagesSome BSPs munge non-ASCII silentlyRoundtrip test once (send accented → fetch back). If it munges: strip accents, emojis, typographic quotes, em-dashes before send
Message body shows an empty slotTemplate variable {{1}} sent with no parameter — silently renders blankPass every slot explicitly
Templates sent from the wrong numberMultiple bots on one BSP account, active bot not verifiedVerify active bot before the first send of every cron
"outside 24h window" errorFree-form on a closed conversationSwitch to template, retry once
Inbound message seen after 24 hWebhook latency — the window closed while nothing errored, so no alarm firedReopen with a template: UTILITY category if one exists, MARKETING otherwise (different billing and Meta tolerance). Explain the delay in a variable slot. Log in wa-incidents.md, audit webhook latency
"template not approved"Meta rejected or paused itStop using it. Pick the fallback template. Never re-approve by editing content post-approval
Quality score yellow → red overnightScore lags hours, not minutes — one bad run degrades it for 1-2 daysYellow: flip to Phase A, audit template content. Red: inbound only, alert, manual review of 7 days
Number paused by MetaUsually: outbound to non-opted-in leads, duplicate templates, or content driftStop everything. Review last 200 actions. Appeal via BSP. Never spin up a second number to get around it — Meta cross-checks businesses

Memory files

memory/: wa-state.md (phase/tier/quality) · wa-alerts-sent.md · wa-template-log.md · wa-crm-state.md · wa-blacklist.md (hashes) · wa-clients-known.md · wa-recaps.md · wa-learnings.md · wa-incidents.md. Shared: ../shared/leads-whatsapp.json (written by upstream social agents, read by wa-outbound).

mkdir -p "<WORKSPACE_DIR>/memory" && chmod 700 "$_" && cd "$_" && for f in wa-recaps wa-state \
  wa-alerts-sent wa-template-log wa-crm-state wa-blacklist wa-clients-known wa-learnings \
  wa-incidents; do [ -f "$f.md" ] || : > "$f.md"; done
mkdir -p "<WORKSPACE_DIR>/../shared" && Q="<WORKSPACE_DIR>/../shared/leads-whatsapp.json"
[ -f "$Q" ] || echo '[]' > "$Q"   # wa-outbound reads this at step 2 — absent = first run crash
# Output: (silent, idempotent) — existing files are never truncated

First-run checklist

  • Placeholders filled; token scoped to one number.
  • BSP account approved, business number verified by Meta, ≥3 templates approved.
  • memory/ exists, chmod 700, retention values set in config.yaml.
  • Alert channel tested with "hello" — and confirmed as an acceptable destination for lead names.
  • Opt-in pipeline documented: how each lead consented, and where that proof lives.
  • Phase A confirmed, quality green.
  • Human team review: who reads alerts, callback SLA, escalation path, who handles deletion requests.

Scope

This skill ONLY: operates a WhatsApp Business number through a BSP API you own · sends free-form inside an open 24h window and Meta-approved templates outside it · contacts leads who opted in on another channel · qualifies and hands off to a human · keeps a masked, retention-bounded local state to avoid duplicates.

This skill NEVER: sends unsolicited bulk or non-opted-in outbound · sends free-form to a cold conversation · hides or denies the automation when a lead asks · adapts timing or behavior to slip past an automated check — when one appears, it stops and a human takes over · stores case documents or special-category data in memory files · retains lead data past the configured window · creates a second number to work around a Meta pause · rotates tokens or restarts infrastructure by itself.

Meta's WhatsApp Business Terms, the WhatsApp Business Messaging Policy, and your jurisdiction's privacy law are binding constraints on every run in this document. Where this doctrine and those rules disagree, those rules win.

Top skills in this category

CMO / Chief Marketing Officer

@ivangdavila

Operates as a chief marketing officer (CMO): sets marketing strategy, owns the pipeline number, allocates budget, picks channels, builds the team. Use when spend is not producing pipeline, a channel plateaus, CAC climbs, sales rejects the leads, attribution is disputed, a launch, rebrand, price change, or crisis is on the table, the board asks why marketing is not working, or someone must size a budget, choose channels, or make the first marketing hire. Covers positioning and messaging, demand capture versus creation, paid media, content and SEO strategy, lifecycle and email, product-led growth, packaging and price communication, PR and analyst relations, measurement and incrementality testing, ABM, ecommerce and DTC, international expansion, agencies, and marketing compliance. Not for executing a single ad, post, or landing-page test.

164.2k

Daily Report

@visualdeptcreative

Generate daily, weekly, and alert reports tracking leads, outreach, cost, priorities, and issues, saving structured summaries for progress monitoring.

66.3k

Error Guard — Control‑Plane Safety

@amar1432

System safety and control-plane skill that prevents agent deadlocks and freezes. Provides non-LLM control commands to inspect task state, flush message queues, cancel long-running work, and recover safely without restarting the container. Use when implementing or operating long-running tasks, sub-agents, benchmarks, background monitors (e.g., Moltbook, PNR checks), or when the system becomes unresponsive and needs immediate recovery controls.

84.1k

Lead Hunter

@galacticpuffin

Automated lead generation + enrichment for AI agents. Find prospects, enrich with emails/socials/company data, score & prioritize. Your agent builds pipeline while you sleep.

64.9k

ActiveCampaign

@byungkyu

ActiveCampaign API integration with managed OAuth. Marketing automation, CRM, contacts, deals, email campaigns, automations, tags, lists, users, accounts, custom fields, notes, and webhooks. All write operations require explicit user approval. Webhook creation sends account event data to external URLs — confirm the destination before creating. User and account management are administrative operations that affect shared resources. Use this skill when users want to manage contacts, deals, tags, lists, automations, campaigns, or account settings in ActiveCampaign. For other third party apps, use the api-gateway skill (https://clawhub.ai/byungkyu/api-gateway). Requires network access and valid Maton API key. Calls run through the `maton` CLI with OAuth login; default to read and list calls, and confirm every write or new connection with the user.

45.3k