When a user sends a message to a Facebook Page via Messenger, the CLI webhook receives it, debounces messages, then calls OpenClaw via its /hooks/agent endpoint. OpenClaw runs an isolated agent turn with a meta-cli skill that teaches it to use meta-cli commands (RAG search, conversation history, send reply). The agent decides what to do autonomously.
Based on source code review (openclaw/src/gateway/hooks.ts, server/hooks.ts, cron/isolated-agent/run.ts):
- POST
/hooks/agentaccepts JSON withmessage(required),name,deliver,sessionKey,model,thinking,timeoutSeconds - Validates auth via
Authorization: Bearer <token>orx-openclaw-tokenheader - Returns
200 { ok: true, runId: "..." }immediately (async) - Internally creates an isolated agent turn (via
runCronIsolatedAgentTurn) - The agent run uses pi (coding agent) as the runtime — it has
exec/bashtool access - The agent sees workspace skills in its system prompt and can execute CLI commands
- After the run completes, a summary is posted to the main session
Key: deliver: false since the agent will send replies itself via meta-cli messenger send. We don't want OpenClaw to also deliver to a chat channel.
User sends FB message(s)
→ FB Webhook delivers to meta-cli server
→ meta-cli stores message(s) in SQLite
→ Debouncer collects messages per PSID (configurable, default 3s)
→ Timer fires (no new messages within window)
→ meta-cli POSTs to OpenClaw /hooks/agent:
{
"message": "<rendered prompt with batched messages>",
"name": "FB Messenger",
"deliver": false,
"sessionKey": "hook:fb:<psid>"
}
→ OpenClaw spins up isolated agent turn
→ Agent loads meta-cli-fb skill
→ Agent runs CLI commands:
meta-cli rag search "user's question" → knowledge base
meta-cli messenger history --psid USER → conversation context
meta-cli messenger send --psid USER -m "..." → send reply
→ Reply delivered through Messenger API
- Webhook server (FB message receive, HMAC validation, SQLite storage)
- Message store (save/list/exists/markAutoReplied)
- RAG engine (TF-IDF search via
meta-cli rag search) - Messenger send (
meta-cli messenger send) - Auth/config/pages/daemon — all working
messenger historyCLI command (so agent can read conversation context)- Message debouncer (per-PSID timer, configurable window)
- OpenClaw hooks caller (POST to
/hooks/agentafter debounce) - Config fields (debounce_seconds, hooks_endpoint, hooks_token, auto_reply, prompt_template)
- Wire debouncer + hooks into webhook handler
- OpenClaw skill (
SKILL.mdteaching the agent meta-cli commands)
File: internal/config/types.go
type Config struct {
// ... existing fields ...
DebounceSeconds int `json:"debounce_seconds,omitempty"` // default: 3
HooksEndpoint string `json:"hooks_endpoint,omitempty"` // e.g. "http://127.0.0.1:18789/hooks/agent"
HooksToken string `json:"hooks_token,omitempty"` // OpenClaw hooks auth token
AutoReply bool `json:"auto_reply,omitempty"` // enable/disable auto-reply
PromptTemplate string `json:"prompt_template,omitempty"` // Go template for the agent prompt
}File: internal/config/config.go — update Default() to set DebounceSeconds: 3.
File: cmd_impl/config.go — add all 5 new keys to setConfigField, getConfigField, and configListCmd.
Default prompt template:
New message(s) from Facebook Messenger user (PSID: {{.PSID}}) on page {{.PageID}}:
{{range .Messages}}- {{.Text}}
{{end}}
Use the meta-cli skill to help this user. Search the knowledge base if needed, check conversation history for context, and send a helpful reply.
The prompt template is configurable via meta-cli config set prompt_template "..." so users can customize agent behavior per their use case.
File: internal/config/config_test.go — add:
TestDefaultDebounceSeconds— verify default is 3TestSaveAndLoadAutoReply— round-trip new fields through save/loadTestSetConfigFieldNewKeys— verify all 5 new keys work insetConfigFieldTestGetConfigFieldNewKeys— verify all 5 new keys work ingetConfigField
File: internal/messenger/store.go
Add method:
func (s *Store) RecentMessages(pageID, psid string, limit int) ([]Message, error)Returns last N messages (both in/out) for a PSID, ordered chronologically (oldest first).
File: cmd_impl/messenger.go
Add messenger history subcommand:
meta-cli messenger history --psid USER_PSID [--limit 20] [--json]Output format (table):
DIRECTION TEXT RECEIVED_AT
in How do I reset my pw? 2026-03-07T09:00:00Z
out Go to Settings > ... 2026-03-07T09:00:05Z
in Thanks! 2026-03-07T09:01:00Z
This is what OpenClaw's agent will call to get conversation context.
File: internal/messenger/store_test.go — add:
TestRecentMessages— insert messages for multiple PSIDs, verify filtering by PSID + pageTestRecentMessagesOrder— verify chronological order (oldest first)TestRecentMessagesLimit— verify limit is respectedTestRecentMessagesBothDirections— insert in + out messages, verify both returnedTestRecentMessagesEmpty— verify empty result for unknown PSID
New package: internal/debounce/
internal/debounce/
debounce.go
debounce_test.go
Interface:
type Message struct {
ID string
Text string
}
type Callback func(psid string, messages []Message)
type Debouncer struct { ... }
func New(window time.Duration, cb Callback) *Debouncer
func (d *Debouncer) Add(psid string, msg Message)
func (d *Debouncer) Stop() // clean shutdown, cancel all pending timersBehavior:
msg1 from user_1 at T=0 → start timer(user_1, 3s)
msg2 from user_1 at T=1 → reset timer(user_1, 3s)
msg3 from user_1 at T=2 → reset timer(user_1, 3s)
T=5 (3s after last msg) → callback(user_1, [msg1, msg2, msg3])
Thread-safe. Per-PSID independent timers. Uses sync.Mutex + time.AfterFunc.
File: internal/debounce/debounce_test.go:
TestSingleMessage— one message, verify callback fires after windowTestDebounceResets— multiple messages within window, verify callback fires once with all messagesTestMultiplePSIDs— two users sending concurrently, verify independent timers and separate callbacksTestMessageOrder— verify messages are delivered in arrival orderTestStop— call Stop(), verify pending timers are cancelled and no callback firesTestStopIdempotent— calling Stop() twice doesn't panicTestAddAfterStop— adding after Stop() doesn't panic (no-op or graceful)TestZeroWindow— zero duration fires callback immediatelyTestConcurrentAdds— multiple goroutines adding to same PSID simultaneously (race detector)
New package: internal/hooks/
internal/hooks/
hooks.go
hooks_test.go
Interface:
type Client struct {
endpoint string
token string
httpClient *http.Client
}
func NewClient(endpoint, token string) *Client
func NewClientWithHTTP(endpoint, token string, hc *http.Client) *Client // for testing
func (c *Client) CallAgent(ctx context.Context, prompt string, psid string) errorThe POST body follows OpenClaw's exact /hooks/agent contract:
{
"message": "<rendered prompt>",
"name": "FB Messenger",
"deliver": false,
"sessionKey": "hook:fb:<psid>"
}deliver: false→ agent sends reply itself viameta-cli messenger sendsessionKey: "hook:fb:<psid>"→ each FB user gets their own isolated session, so agent remembers per-user context across turns- Response:
200 { ok: true, runId: "..." }— fire and forget, async
Also add prompt template rendering:
func RenderPrompt(tmpl string, psid, pageID string, messages []debounce.Message) (string, error)File: internal/hooks/hooks_test.go:
TestCallAgentSuccess— httptest server returns 200{"ok":true,"runId":"xxx"}, verify no errorTestCallAgentRequestFormat— verify POST body contains correct JSON fields (message, name, deliver=false, sessionKey)TestCallAgentAuthHeader— verifyAuthorization: Bearer <token>header is sentTestCallAgentServerError— server returns 500, verify error returnedTestCallAgentUnauthorized— server returns 401, verify error returnedTestCallAgentTimeout— server hangs, cancelled context, verify errorTestCallAgentBadEndpoint— invalid URL, verify errorTestRenderPrompt— verify template rendering with PSID, PageID, multiple messagesTestRenderPromptDefault— verify default template renders correctlyTestRenderPromptInvalid— invalid Go template, verify error
File: internal/messenger/webhook.go
Add to WebhookHandler:
type WebhookHandler struct {
// ... existing fields ...
Debouncer *debounce.Debouncer // NEW (nil = auto-reply disabled)
}Change processPayload:
- Parse message → store in DB (same as now)
- If message is inbound (not echo) AND
Debouncer != nil:Debouncer.Add(senderPSID, msg)
- Everything else unchanged
File: cmd_impl/webhook.go
Update webhook serve:
- If
auto_replyis true (config or--auto-replyflag):- Validate
hooks_endpointandhooks_tokenare set - Initialize hooks client
- Build prompt renderer from
prompt_template - Initialize debouncer with callback:
func(psid string, msgs []debounce.Message) { prompt := renderPrompt(template, psid, pageID, msgs) if err := hooksClient.CallAgent(ctx, prompt, psid); err != nil { log.Printf("hooks/agent error for %s: %v", psid, err) } }
- Pass debouncer into WebhookHandler
- Defer
debouncer.Stop()for graceful shutdown
- Validate
New flags:
--auto-reply Enable auto-reply (overrides config)
--debounce <seconds> Debounce window in seconds (overrides config)
--hooks-endpoint <url> OpenClaw hooks endpoint (overrides config)
--hooks-token <token> OpenClaw hooks token (overrides config)
File: internal/messenger/webhook_test.go — add:
TestWebhookWithDebouncer— send valid webhook POST with debouncer set, verify message is fed to debouncerTestWebhookWithoutDebouncer— Debouncer is nil, verify messages still stored but no debounce called (backward compatible)TestWebhookEchoNotDebounced— echo messages should NOT be fed to debouncerTestWebhookAutoReplyMissingConfig— verify error when auto_reply=true but hooks_endpoint/hooks_token missing (test in cmd_impl level)
File: skill/meta-cli-fb/SKILL.md
---
name: meta-cli-fb
description: >
Manage Facebook Page Messenger conversations using meta-cli.
Search knowledge base via RAG, read conversation history,
and send replies. Use when handling Facebook Messenger webhook messages.
metadata: {"openclaw": {"requires": {"bins": ["meta-cli"]}}}
---
# meta-cli Facebook Messenger Skill
You manage a Facebook Page's Messenger inbox. When triggered by a
webhook, you receive batched user messages and respond using the
page's knowledge base.
## Commands
### Search Knowledge Base
\`\`\`bash
meta-cli rag search "<query>" [--dir <path>] [--top 5] [--json]
\`\`\`
Search documents for relevant answers. Always do this before replying.
### Read Conversation History
\`\`\`bash
meta-cli messenger history --psid <PSID> [--limit 20] [--json]
\`\`\`
Get recent messages (both directions) with this user.
### Send Reply
\`\`\`bash
meta-cli messenger send --psid <PSID> --message "<reply>"
\`\`\`
Send a Messenger reply to the user.
### List Recent Messages
\`\`\`bash
meta-cli messenger list [--limit 50] [--json]
\`\`\`
## Workflow
1. Parse the PSID and message(s) from the prompt
2. Search knowledge base: `meta-cli rag search "<user question>"`
3. If needed, check history: `meta-cli messenger history --psid <PSID>`
4. Compose a reply based on knowledge base results
5. Send: `meta-cli messenger send --psid <PSID> --message "<reply>"`
## Rules
- Always search the knowledge base before replying
- Keep replies concise, friendly, and helpful
- If the knowledge base has no relevant info, say so honestly
and suggest the user contact a human
- Use conversation history to maintain context
- Do NOT make up information not found in the knowledge base# Copy to OpenClaw workspace
cp -r skill/meta-cli-fb ~/.openclaw/workspace/skills/
# Or link it
ln -s $(pwd)/skill/meta-cli-fb ~/.openclaw/workspace/skills/meta-cli-fbAdd to the Quick Start section:
# --- Auto-Reply (with OpenClaw) ---
meta-cli config set auto_reply true
meta-cli config set hooks_endpoint http://127.0.0.1:18789/hooks/agent
meta-cli config set hooks_token YOUR_TOKEN
meta-cli config set rag_dir ./knowledge-base
meta-cli webhook serve --auto-reply --daemonAdd to Commands table:
| messenger history | List conversation history with a user |
Add to Config section — document new config fields:
| debounce_seconds | Debounce window for message batching (default: 3) |
| hooks_endpoint | OpenClaw webhook endpoint URL |
| hooks_token | OpenClaw webhook auth token |
| auto_reply | Enable auto-reply pipeline (true/false) |
| prompt_template | Go template for agent prompt |
Add new Architecture section entry:
internal/
debounce/ # Message debouncing (per-user timer)
hooks/ # OpenClaw /hooks/agent caller
Update webhook serve flags table to include new flags.
Add under Messenger Commands:
Full documentation matching the existing command doc style:
- Usage, flags (
--psid,--limit), output format - Note that this reads from local DB
- Add to Command Requirements Matrix (requires Page ID, no auth)
Add under Webhook Commands > webhook serve:
- Document new flags:
--auto-reply,--debounce,--hooks-endpoint,--hooks-token - Document auto-reply behavior
Add new config keys to Config Commands > config set supported keys list.
Add new section ## Auto-Reply Pipeline after the existing Data Flow Summary:
Document:
- Overview of the auto-reply flow
- Message debouncing explanation with timing diagram
- OpenClaw integration (how
/hooks/agentis called) - Prompt template system
- Configuration reference
- Troubleshooting (OpenClaw not running, missing config, etc.)
Update the architecture diagram:
Facebook Messenger
│
▼
Meta Platform ──── POST /webhook ────► meta-cli webhook server
│
┌─────────┼─────────┐
▼ ▼ ▼
Signature Parse Store in
Validation Payload SQLite DB
│
▼
Debouncer
(per-PSID)
│
▼ (after quiet period)
POST /hooks/agent
(OpenClaw)
│
▼
Agent runs:
- rag search
- messenger history
- messenger send
Update the WebhookHandler struct documentation to include the Debouncer field.
Add to Package Dependency Graph:
cmd_impl
├── internal/debounce (message debouncing)
├── internal/hooks (OpenClaw hook caller)
... (existing)
Add to the Architecture diagram:
internal/
├── debounce/ # Per-PSID message debouncing
├── hooks/ # OpenClaw /hooks/agent integration
... (existing)
Update the Execution Flow for webhook serve to show the auto-reply initialization path.
Add new config fields to the Config Fields table:
| debounce_seconds | int | 3 | Seconds to wait before batching messages |
| hooks_endpoint | string | "" | OpenClaw /hooks/agent endpoint URL |
| hooks_token | string | "" | Bearer token for OpenClaw hooks auth |
| auto_reply | bool | false | Enable auto-reply via OpenClaw |
| prompt_template | string | "" | Go template for agent prompts |
Update the Example Config JSON to include new fields.
Update the Config Structure go code block.
Add to Project Structure:
├── internal/
│ ├── debounce/ # Message debouncing
│ │ ├── debounce.go # Per-PSID timer with batching
│ │ └── debounce_test.go
│ ├── hooks/ # OpenClaw integration
│ │ ├── hooks.go # /hooks/agent caller + prompt rendering
│ │ └── hooks_test.go
Add to the "Adding a New Command" section — mention messenger history as an example of a command that reads local DB without API access.
Create a dedicated auto-reply guide covering:
- Overview — what it does, the full flow diagram
- Prerequisites — OpenClaw installed and running, hooks enabled, meta-cli authenticated
- Setup Guide — step-by-step:
- Configure meta-cli (config set commands)
- Install the OpenClaw skill
- Configure OpenClaw hooks
- Start the webhook server
- Test with a message
- Configuration Reference — all config fields with descriptions
- Prompt Template — Go template syntax, available variables (
.PSID,.PageID,.Messages), examples - Debouncing — how it works, why 3s default, how to tune
- OpenClaw Skill — what commands the agent uses, how to customize
- Troubleshooting — common issues:
- "hooks/agent error" in logs → check OpenClaw is running, token matches
- No replies being sent → check skill is installed, rag_dir is set
- Replies are slow → check debounce_seconds, OpenClaw model speed
- Duplicate replies → check message deduplication in store
Update docs/README.md table of contents to include the new doc:
| Auto-Reply Guide | OpenClaw integration for automatic Messenger replies |
Add:
install-skill:
mkdir -p ~/.openclaw/workspace/skills/
cp -r skill/meta-cli-fb ~/.openclaw/workspace/skills/| File | Tests |
|---|---|
internal/debounce/debounce_test.go |
9 tests (single msg, debounce reset, multi-PSID, order, stop, stop idempotent, add after stop, zero window, concurrent adds) |
internal/hooks/hooks_test.go |
10 tests (success, request format, auth header, server error, unauthorized, timeout, bad endpoint, render prompt, render default, render invalid) |
| File | New Tests |
|---|---|
internal/config/config_test.go |
4 tests (default debounce, save/load auto-reply fields, set new keys, get new keys) |
internal/messenger/store_test.go |
5 tests (recent messages, order, limit, both directions, empty) |
internal/messenger/webhook_test.go |
4 tests (with debouncer, without debouncer, echo not debounced, auto-reply missing config) |
Based on existing tests:
- Use
httptest.NewServerfor API mocking (seegraph_test.go,posts_test.go) - Use
":memory:"SQLite for store tests (seestore_test.go) - Use
t.TempDir()+t.Setenv("HOME", tmp)for config tests (seeconfig_test.go) - Use
graph.NewWithHTTPClient(srv.URL, token, srv.Client())for graph client tests - Keep tests independent — no shared state between test functions
- Test both success and error paths
- For the debouncer, use short windows (10ms-50ms) to keep tests fast
# All tests
make test
# Specific package
go test ./internal/debounce/...
go test ./internal/hooks/...
# With race detector (important for debouncer)
go test -race ./internal/debounce/...
# Verbose
go test -v ./...Modified:
internal/config/types.go → add 5 new config fields
internal/config/config.go → update Default()
internal/config/config_test.go → 4 new tests
internal/messenger/webhook.go → add Debouncer field, feed inbound msgs
internal/messenger/webhook_test.go → 4 new tests
internal/messenger/store.go → add RecentMessages()
internal/messenger/store_test.go → 5 new tests
cmd_impl/config.go → add new config keys to set/get/list
cmd_impl/webhook.go → wire debouncer + hooks, add flags
cmd_impl/messenger.go → add messenger history command
Makefile → add install-skill target
README.md → document auto-reply setup + new commands
docs/README.md → add auto-reply.md to table of contents
docs/commands.md → add messenger history, new webhook flags, new config keys
docs/webhooks.md → add Auto-Reply Pipeline section, update diagrams
docs/architecture.md → add debounce + hooks packages
docs/storage.md → add new config fields, update example
docs/development.md → add new packages to structure
New:
internal/debounce/debounce.go
internal/debounce/debounce_test.go → 9 tests
internal/hooks/hooks.go
internal/hooks/hooks_test.go → 10 tests
skill/meta-cli-fb/SKILL.md
docs/auto-reply.md → dedicated auto-reply guide
No removals.
The prompt template is stored as a single string in config. Should we also support:
- A) Single string in config only (simplest — user does
meta-cli config set prompt_template "...") - B) Also support a file path (e.g.
prompt_template_filepointing to a .txt file for longer prompts)
Should the pipeline skip certain inbound messages?
- A) No filtering — always trigger (simplest, let the agent decide)
- B) Skip non-text messages only (stickers, reactions, images with no text)
- C) Configurable skip patterns
When OpenClaw /hooks/agent returns non-200 or is unreachable:
- A) Log error, skip silently (messages stay in DB for manual handling)
- B) Configurable fallback message in config (empty = silent skip)
- C) Retry with backoff, then silent skip
Each /hooks/agent call uses sessionKey: "hook:fb:<psid>". This means:
- Same user gets continuity across turns (agent remembers prior interactions)
- Different users are fully isolated
- Is this the right default? Or should all FB messages share one session?
Where should the skill live?
- A) In this repo under
skill/— user copies to OpenClaw workspace - B) Auto-installed via
meta-cli config set auto_reply true(copies skill automatically) - C) Published to ClawHub for
clawhub install meta-cli-fb
# 1. Install & auth meta-cli (already works)
meta-cli auth login --app-id APP_ID --app-secret APP_SECRET
meta-cli pages set-default PAGE_ID
# 2. Configure auto-reply
meta-cli config set auto_reply true
meta-cli config set hooks_endpoint http://127.0.0.1:18789/hooks/agent
meta-cli config set hooks_token YOUR_OPENCLAW_HOOKS_TOKEN
meta-cli config set rag_dir ./knowledge-base
meta-cli config set debounce_seconds 3
# 3. Install the skill into OpenClaw
make install-skill
# or: cp -r skill/meta-cli-fb ~/.openclaw/workspace/skills/
# 4. Make sure OpenClaw has hooks enabled in ~/.openclaw/openclaw.json:
# { "hooks": { "enabled": true, "token": "YOUR_OPENCLAW_HOOKS_TOKEN" } }
# 5. Start webhook server
meta-cli webhook serve --auto-reply --daemon
# Done! Messages now flow:
# FB → webhook → debounce → OpenClaw → rag search → replyWhen implementing, follow this order strictly. Each phase builds on the previous.
- Phase 1 — Config changes (foundation for everything)
- Phase 2 — Messenger history command (standalone, no dependencies on other new code)
- Phase 3 — Debouncer package (standalone, tested independently)
- Phase 4 — Hooks caller package (standalone, tested independently)
- Phase 5 — Wire into webhook (depends on Phase 1, 3, 4)
- Phase 6 — Skill file (standalone, no code changes)
- Phase 7 — Documentation (after all code is done)
- Phase 8 — Final test pass, run
make testandgo test -race ./...
After each phase, run make test to verify nothing is broken.