kdeps-templates-whatsapp-bot
v1.0.1kdeps template: whatsapp-bot
set templates / shelf workflow
PurposeProject scaffolds for kdeps new, and the knowledge vault's files (routing table, schema, graph page).
LLM context: none - kdeps uses it without sending it to the model
Where it runs
Part of the templates set, used in workflow and agent mode. Project scaffolds for kdeps new, and the knowledge vault's files (routing table, schema, graph page).
- kdeps new, or the knowledge vault's first use
- the template is renderedtemplates/whatsapp-bot
- the project or vault files are written
Loaded by: kdeps (agent loop), kdeps new, kdeps run, kdeps knowledge build
In the code: kdeps newtemplates.(*Generator).GenerateProject
Install
A kdeps asset: installed with the first-run profile that includes its set, or the first time a command needs it, and updated without a new release.
Pin a version with kdeps install templates/whatsapp-bot@<version>.
Contents
README.md.j2
# {{ name }}
{{ description }}
A WhatsApp bot as a plain kdeps workflow: Meta's webhook calls `/webhook`, the
`llm` resource answers the message, `reply` sends the answer with the WhatsApp
Cloud API, and `respond` acknowledges the webhook.
```text
WhatsApp -> POST /webhook -> llm -> reply (Graph API /messages) -> 200 ok
Meta -> GET /webhook -> respond (hub.challenge when the verify token matches)
```
## Quick Start
1. Set the credentials from your Meta app (WhatsApp > API Setup):
```bash
export WHATSAPP_PHONE_NUMBER_ID=1234567890 # the sending number's ID
export WHATSAPP_ACCESS_TOKEN=EAAG... # system user or temporary token
export WHATSAPP_VERIFY_TOKEN=pick-any-string # also entered in Meta's webhook form
export WHATSAPP_APP_SECRET=abc123... # App settings > Basic; signs each POST
export KDEPS_API_AUTH_TOKEN=dev-token # /webhook is public; other routes need it
```
2. Run it and expose port {{ port }} over HTTPS (Meta only calls HTTPS URLs):
```bash
kdeps run workflow.yaml
# e.g. cloudflared tunnel --url http://localhost:{{ port }}
```
3. In Meta's webhook settings, set the callback URL to `https://<host>/webhook`
and the verify token to `WHATSAPP_VERIFY_TOKEN`, then subscribe to `messages`.
## How it works
- `GET /webhook` returns `hub.challenge` as plain text only when
`hub.verify_token` equals `WHATSAPP_VERIFY_TOKEN` (403 otherwise).
- `POST /webhook` is checked against `X-Hub-Signature-256`
(`hmacSHA256(WHATSAPP_APP_SECRET, request.rawBody)`); a forged POST gets 403
before any model call. Leave `WHATSAPP_APP_SECRET` unset only for local tests.
- Status callbacks (sent, delivered, read) have no text message: `llm` and
`reply` are skipped and the webhook gets `ok`.
- The first text message of each notification is answered.
Change the model or persona in `resources/llm.yaml`. `WHATSAPP_API_URL`
overrides the Graph API base (default `https://graph.facebook.com/v19.0`).
resources/llm.yaml.j2
actionId: llm
name: Generate Reply
validations:
methods: [POST]
routes: [/webhook]
# Status callbacks (sent, delivered, read) carry no text message: nothing to answer.
skip:
- "{% raw %}(request.body.entry?.[0]?.changes?.[0]?.value?.messages?.[0]?.text?.body ?? '') == ''{% endraw %}"
# Reject a forged POST before spending a model call: Meta signs the body with the app secret.
check:
- "{% raw %}env('WHATSAPP_APP_SECRET', '') == '' || request.headers['X-Hub-Signature-256'] == 'sha256=' + hmacSHA256(env('WHATSAPP_APP_SECRET'), request.rawBody){% endraw %}"
error:
code: 403
message: "invalid signature"
chat:
model: llama3.2:1b # llamafile alias - auto-downloaded to ~/.kdeps/models (~1.1 GB, first run only)
scenario:
- role: assistant
prompt: Answer in a few short sentences, in the language of the message.
prompt: "{% raw %}{{ request.body.entry[0].changes[0].value.messages[0].text.body }}{% endraw %}"
timeout: 60s
resources/reply.yaml.j2
actionId: reply
name: Send WhatsApp Reply
requires:
- llm
validations:
methods: [POST]
routes: [/webhook]
skip:
- "{% raw %}(request.body.entry?.[0]?.changes?.[0]?.value?.messages?.[0]?.text?.body ?? '') == ''{% endraw %}"
# WhatsApp Cloud API: send the answer back to the sender's number.
component:
name: http_request
with:
method: POST
url: "{% raw %}{{ env('WHATSAPP_API_URL', 'https://graph.facebook.com/v19.0') }}/{{ env('WHATSAPP_PHONE_NUMBER_ID') }}/messages{% endraw %}"
headers:
Authorization: "{% raw %}Bearer {{ env('WHATSAPP_ACCESS_TOKEN') }}{% endraw %}"
Content-Type: application/json
data:
messaging_product: whatsapp
to: "{% raw %}{{ request.body.entry[0].changes[0].value.messages[0].from }}{% endraw %}"
type: text
text:
body: "{% raw %}{{ get('llm').message.content }}{% endraw %}"
failOnError: true
timeout: 30s
resources/respond.yaml.j2
actionId: respond
name: Webhook Response
requires:
- reply
validations:
routes: [/webhook]
# GET is Meta's verification handshake: echo hub.challenge (digits only)
# when hub.verify_token matches.
check:
- "{% raw %}request.method != 'GET' || (request.query['hub.mode'] == 'subscribe' && request.query['hub.verify_token'] == env('WHATSAPP_VERIFY_TOKEN', '') && env('WHATSAPP_VERIFY_TOKEN', '') != '' && request.query['hub.challenge'] matches '^[0-9]+$'){% endraw %}"
error:
code: 403
message: "forbidden"
apiResponse:
success: true
headers:
Content-Type: text/plain; charset=utf-8
response: "{% raw %}{{ request.method == 'GET' ? request.query['hub.challenge'] : 'ok' }}{% endraw %}"
workflow.yaml.j2
apiVersion: kdeps.io/v1
kind: Workflow
metadata:
name: {{ name }}
description: {{ description }}
version: "{{ version }}"
targetActionId: respond # its result is the webhook's HTTP response
settings:
apiServer:
hostIp: "0.0.0.0"
portNum: {{ port }}
routes:
- path: /webhook
methods: [GET, POST] # GET: Meta's verification handshake; POST: messages
public: true # Meta sends no bearer token; the POST signature is checked instead
version: 1.0.1
files:
README.md.j2: |
# {{ name }}
{{ description }}
A WhatsApp bot as a plain kdeps workflow: Meta's webhook calls `/webhook`, the
`llm` resource answers the message, `reply` sends the answer with the WhatsApp
Cloud API, and `respond` acknowledges the webhook.
```text
WhatsApp -> POST /webhook -> llm -> reply (Graph API /messages) -> 200 ok
Meta -> GET /webhook -> respond (hub.challenge when the verify token matches)
```
## Quick Start
1. Set the credentials from your Meta app (WhatsApp > API Setup):
```bash
export WHATSAPP_PHONE_NUMBER_ID=1234567890 # the sending number's ID
export WHATSAPP_ACCESS_TOKEN=EAAG... # system user or temporary token
export WHATSAPP_VERIFY_TOKEN=pick-any-string # also entered in Meta's webhook form
export WHATSAPP_APP_SECRET=abc123... # App settings > Basic; signs each POST
export KDEPS_API_AUTH_TOKEN=dev-token # /webhook is public; other routes need it
```
2. Run it and expose port {{ port }} over HTTPS (Meta only calls HTTPS URLs):
```bash
kdeps run workflow.yaml
# e.g. cloudflared tunnel --url http://localhost:{{ port }}
```
3. In Meta's webhook settings, set the callback URL to `https://<host>/webhook`
and the verify token to `WHATSAPP_VERIFY_TOKEN`, then subscribe to `messages`.
## How it works
- `GET /webhook` returns `hub.challenge` as plain text only when
`hub.verify_token` equals `WHATSAPP_VERIFY_TOKEN` (403 otherwise).
- `POST /webhook` is checked against `X-Hub-Signature-256`
(`hmacSHA256(WHATSAPP_APP_SECRET, request.rawBody)`); a forged POST gets 403
before any model call. Leave `WHATSAPP_APP_SECRET` unset only for local tests.
- Status callbacks (sent, delivered, read) have no text message: `llm` and
`reply` are skipped and the webhook gets `ok`.
- The first text message of each notification is answered.
Change the model or persona in `resources/llm.yaml`. `WHATSAPP_API_URL`
overrides the Graph API base (default `https://graph.facebook.com/v19.0`).
resources/llm.yaml.j2: |
actionId: llm
name: Generate Reply
validations:
methods: [POST]
routes: [/webhook]
# Status callbacks (sent, delivered, read) carry no text message: nothing to answer.
skip:
- "{% raw %}(request.body.entry?.[0]?.changes?.[0]?.value?.messages?.[0]?.text?.body ?? '') == ''{% endraw %}"
# Reject a forged POST before spending a model call: Meta signs the body with the app secret.
check:
- "{% raw %}env('WHATSAPP_APP_SECRET', '') == '' || request.headers['X-Hub-Signature-256'] == 'sha256=' + hmacSHA256(env('WHATSAPP_APP_SECRET'), request.rawBody){% endraw %}"
error:
code: 403
message: "invalid signature"
chat:
model: llama3.2:1b # llamafile alias - auto-downloaded to ~/.kdeps/models (~1.1 GB, first run only)
scenario:
- role: assistant
prompt: Answer in a few short sentences, in the language of the message.
prompt: "{% raw %}{{ request.body.entry[0].changes[0].value.messages[0].text.body }}{% endraw %}"
timeout: 60s
resources/reply.yaml.j2: |
actionId: reply
name: Send WhatsApp Reply
requires:
- llm
validations:
methods: [POST]
routes: [/webhook]
skip:
- "{% raw %}(request.body.entry?.[0]?.changes?.[0]?.value?.messages?.[0]?.text?.body ?? '') == ''{% endraw %}"
# WhatsApp Cloud API: send the answer back to the sender's number.
component:
name: http_request
with:
method: POST
url: "{% raw %}{{ env('WHATSAPP_API_URL', 'https://graph.facebook.com/v19.0') }}/{{ env('WHATSAPP_PHONE_NUMBER_ID') }}/messages{% endraw %}"
headers:
Authorization: "{% raw %}Bearer {{ env('WHATSAPP_ACCESS_TOKEN') }}{% endraw %}"
Content-Type: application/json
data:
messaging_product: whatsapp
to: "{% raw %}{{ request.body.entry[0].changes[0].value.messages[0].from }}{% endraw %}"
type: text
text:
body: "{% raw %}{{ get('llm').message.content }}{% endraw %}"
failOnError: true
timeout: 30s
resources/respond.yaml.j2: |
actionId: respond
name: Webhook Response
requires:
- reply
validations:
routes: [/webhook]
# GET is Meta's verification handshake: echo hub.challenge (digits only)
# when hub.verify_token matches.
check:
- "{% raw %}request.method != 'GET' || (request.query['hub.mode'] == 'subscribe' && request.query['hub.verify_token'] == env('WHATSAPP_VERIFY_TOKEN', '') && env('WHATSAPP_VERIFY_TOKEN', '') != '' && request.query['hub.challenge'] matches '^[0-9]+$'){% endraw %}"
error:
code: 403
message: "forbidden"
apiResponse:
success: true
headers:
Content-Type: text/plain; charset=utf-8
response: "{% raw %}{{ request.method == 'GET' ? request.query['hub.challenge'] : 'ok' }}{% endraw %}"
workflow.yaml.j2: |
apiVersion: kdeps.io/v1
kind: Workflow
metadata:
name: {{ name }}
description: {{ description }}
version: "{{ version }}"
targetActionId: respond # its result is the webhook's HTTP response
settings:
apiServer:
hostIp: "0.0.0.0"
portNum: {{ port }}
routes:
- path: /webhook
methods: [GET, POST] # GET: Meta's verification handshake; POST: messages
public: true # Meta sends no bearer token; the POST signature is checked instead
Versions
| Version | Published | Status |
|---|---|---|
| 1.0.1 | 10/21/2026 | active |
| 1.0.0 | 10/14/2026 | active |
Details
- Author
- kdeps
- License
- Apache-2.0
- Latest Version
- 1.0.1
- Published
- 10/14/2026