Quick Start
- Enable via
mibyan gateway setupor environment variables - Define routes in
config.yamlor create them dynamically withmibyan webhook subscribe - Point your service at
http://your-server:8644/webhooks/<route-name>
Setup
There are two ways to enable the webhook adapter.Via setup wizard
Via environment variables
Add to~/.mibyan/.env:
Verify the server
Once the gateway is running:Configuring Routes
Routes define how different webhook sources are handled. Each route is a named entry underplatforms.webhook.extra.routes in your config.yaml. Adapter settings (port, host, secret, routes) may also be written directly under platforms.webhook: — both spellings reach the adapter; a value nested under extra: wins if the same key appears in both places.
Route properties
Full example
Payload Filters
Usefilters when a provider sends a broad event stream but only some payloads should wake the agent or trigger deliver_only delivery. Filters run after signature validation, body parsing, and events, but before prompt rendering, idempotency, agent dispatch, or direct delivery.
exists: true|falsemissing: trueequals/not_equalscontainsfor strings, lists, and dict keysinfor inline listsin_filefor JSON arrays, JSON objects (keys are used), or newline-delimited text filesregexall,any, andnotgroups
payload.foo reads from a top-level payload object when one exists, or from the root webhook body for flat payloads. event / event_type match the resolved event type, and headers.<Name> reads request headers.
Event Coalescing
Providers often fire several distinct events for the same logical entity in quick succession — five rapid pushes to one pull request, a burst of edits to one ticket, a flapping monitoring alert. Each event carries a fresh delivery ID, so the idempotency cache cannot suppress them and every event wakes a separate agent run. Setcoalesce on a route to debounce these into a single run per entity:
- Events are grouped per route by the rendered
key. A bare dotted field (pull_request.number) or a full template ({repository.full_name}#{pull_request.number}) both work. - Each new event replaces the pending one and pushes the quiet-window timer back. When
window_secondspass with no new event, the group dispatches one agent run using the latest event’s payload, prompt, and delivery templates. max_wait_secondscaps total buffering from the group’s first event, so a steady event stream cannot postpone dispatch forever.- When more than one event was coalesced, the prompt gets a short note telling the agent how many earlier events were superseded.
- If the
keydoes not resolve for an event (the payload lacks the field — e.g. anissue_commentevent on a route keyed bypull_request.number), that event is dispatched immediately instead of being coalesced, so unrelated entities never collapse into one group. Pick a key present on every event type the route accepts. - Coalesced requests return HTTP 202 with
{"status": "coalesced"}. Delivery-ID idempotency still runs first, so provider retries of the same delivery are dropped rather than counted. - Pending groups are flushed (dispatched immediately) when the adapter disconnects — a gateway reconnect or
mibyan gateway stop— not dropped. Buffered events live in memory, so a hard process kill loses at most the current window’s buffered burst. coalesceapplies to agent-mode routes only; combining it withdeliver_onlyorcron_jobis rejected at startup.
Script Filters and Transforms
Usescript when declarative filters are not enough. Scripts must live under ~/.mibyan/scripts/ for the active profile; relative paths resolve there, and path traversal outside that directory is blocked. .sh and .bash scripts run with bash, and all other extensions run with the current Python interpreter.
The route payload is sent to stdin as JSON:
- JSON object stdout replaces the payload used by
promptanddeliver_extra. - Non-JSON text stdout is added to the payload as
script_output. - Empty stdout, exact
[SILENT],{"__mibyan_ignore__": true}, timeout, missing script, or nonzero exit code returns HTTP 200 with{"status":"ignored","reason":"script"}.
Prompt Templates
Prompts use dot-notation to access nested fields in the webhook payload:{pull_request.title}resolves topayload["pull_request"]["title"]{repository.full_name}resolves topayload["repository"]["full_name"]{__raw__}— special token that dumps the entire payload as indented JSON (truncated at 4000 characters). Useful for monitoring alerts or generic webhooks where the agent needs the full context.- Missing keys are left as the literal
{key}string (no error) - Nested dicts and lists are JSON-serialized and truncated at 2000 characters
{__raw__} with regular template variables:
prompt template is configured for a route, the entire payload is dumped as indented JSON (truncated at 4000 characters).
The same dot-notation templates work in deliver_extra values.
Forum Topic Delivery
When delivering webhook responses to Telegram, you can target a specific forum topic by includingmessage_thread_id (or thread_id) in deliver_extra:
chat_id is not provided in deliver_extra, the delivery falls back to the home channel configured for the target platform.
GitHub PR Review (Step by Step)
This walkthrough sets up automatic code review on every pull request.1. Create the webhook in GitHub
- Go to your repository → Settings → Webhooks → Add webhook
- Set Payload URL to
http://your-server:8644/webhooks/github-pr - Set Content type to
application/json - Set Secret to match your route config (e.g.
github-webhook-secret) - Under Which events?, select Let me select individual events and check Pull requests
- Click Add webhook
2. Add the route config
Add thegithub-pr route to your ~/.mibyan/config.yaml as shown in the example above.
3. Ensure gh CLI is authenticated
The github_comment delivery type uses the GitHub CLI to post comments:
4. Test it
Open a pull request on the repository. The webhook fires, Mibyan processes the event, and posts a review comment on the PR.GitLab Webhook Setup
GitLab webhooks work similarly but use a different authentication mechanism. GitLab sends the secret as a plainX-Gitlab-Token header (exact string match, not HMAC).
1. Create the webhook in GitLab
- Go to your project → Settings → Webhooks
- Set the URL to
http://your-server:8644/webhooks/gitlab-mr - Enter your Secret token
- Select Merge request events (and any other events you want)
- Click Add webhook
2. Add the route config
Delivery Options
Thedeliver field controls where the agent’s response goes after processing the webhook event.
For cross-platform delivery, the target platform must also be enabled and connected in the gateway. If no
chat_id is provided in deliver_extra, the response is sent to that platform’s configured home channel.
Replying to a delivery
By default a delivery is fire-and-forget: each webhook event runs in its own session, so if you reply to the delivered message in that chat, the agent there has no record of what was sent. Setmirror_to_session: true on the route (or pass --mirror-to-session to mibyan webhook subscribe) and the delivered text is also appended to the target chat’s session as [Webhook delivery: <route>] followed by the message, so a follow-up (“so he’s out?”) has the context.
- The mirror is best-effort: it never fails the delivery, and it is skipped when the chat has no gateway session yet (nobody has talked to the agent there).
- On a
/p/<profile>/route it is written into that profile’s session for the chat, never another profile’s. - The mirrored text enters the chat’s history as if you had sent it. On a
deliver_onlyroute it is the raw rendered payload, so only enable it for sources whose content you trust to sit in your conversation (your own services, not a public issue tracker).
Direct Delivery Mode
By default, every webhook POST triggers an agent run — the payload becomes a prompt, the agent processes it, and the agent’s response is delivered. This costs LLM tokens on every event. For use cases where you just want to push a plain notification — no reasoning, no agent loop, just deliver the message — setdeliver_only: true on the route. The rendered prompt template becomes the literal message body, and the adapter dispatches it directly to the configured delivery target.
When to use direct delivery
- External service push — Supabase/Firebase webhook fires on a database change → notify a user in Telegram instantly
- Monitoring alerts — Datadog/Grafana alert webhook → push to a Discord channel
- Inter-agent pings — Agent A notifies Agent B’s user that a long-running task finished
- Background job completion — Cron job finishes → post result to Slack
- Zero LLM tokens — the agent is never invoked
- Sub-second delivery — a single adapter call, no reasoning loop
- Same security as agent mode — HMAC auth, rate limits, idempotency, and body-size limits all still apply
- Synchronous response — the POST returns
200 OKonce delivery succeeds, or502if the target rejects it, so your upstream service can retry intelligently
Example: Telegram push from Supabase
https://your-server:8644/webhooks/antenna-matches. The webhook adapter validates the signature, renders the template from the payload, delivers to Telegram, and returns 200 OK.
Example: Dynamic subscription via CLI
Response codes
Configuration gotchas
deliver_only: truerequiresdeliverto be a real target.deliver: log(or omittingdeliver) is rejected at startup — the adapter refuses to start if it finds a misconfigured route.- The
skillsfield is ignored in direct delivery mode (no agent runs, so there’s nothing to inject skills into). - Template rendering uses the same
{dot.notation}syntax as agent mode, including the{__raw__}token. - Idempotency uses the same
X-GitHub-Delivery/X-Request-IDheader — retries with the same ID returnstatus=duplicateand do NOT re-deliver.
Event-Triggered Cron Jobs
Setcron_job on a route to fire an existing cron job whenever an event arrives — instead of polling on a fixed cadence or starting a fresh webhook agent session. This turns any scheduled job into an event-driven task: keep the schedule as a fallback sweep (or make it a rarely-firing one) and let the webhook fire it the moment something actually changes.
How it works:
- The event passes the same HMAC auth, rate limiting,
events/filters/scriptfiltering, and idempotency as any other route. - The route’s
prompttemplate is rendered from the payload and injected into the job as transient per-run context (the same rail ascronjob(action='run', prompt=...)— the job’s stored prompt is never mutated). - The job fires through the same at-most-once claim the scheduler uses, so a webhook burst cannot double-fire a job that is already running, and the job’s own delivery target receives the output.
Example: fire a PR-review job on review feedback
Via the CLI
Notes
cron_jobanddeliver_onlyare mutually exclusive (the adapter refuses to start if a route sets both). A cron job handles its own delivery.- The route-level
deliver,deliver_extra, andskillsfields are ignored oncron_jobroutes — the job’s own settings apply. - Paused/disabled jobs are not fired; the event is logged and dropped.
- The POST returns
202 Acceptedimmediately; the job runs in the background.
Dynamic Subscriptions (CLI)
In addition to static routes inconfig.yaml, you can create webhook subscriptions dynamically using the mibyan webhook CLI command. This is especially useful when the agent itself needs to set up event-driven triggers.
Create a subscription
List subscriptions
Remove a subscription
Test a subscription
How dynamic subscriptions work
- Subscriptions are stored in
~/.mibyan/webhook_subscriptions.json - The webhook adapter hot-reloads this file on each incoming request (mtime-gated, negligible overhead)
- Static routes from
config.yamlalways take precedence over dynamic ones with the same name - Dynamic subscriptions use the same route format and capabilities as static routes (events, prompt templates, skills, delivery)
- No gateway restart required — subscribe and it’s immediately live
Agent-driven subscriptions
The agent can create subscriptions via the terminal tool when guided by thewebhook-subscriptions skill. Ask the agent to “set up a webhook for GitHub issues” and it will run the appropriate mibyan webhook subscribe command.
Per-route toolsets
Webhook agent runs default to a deliberately constrained toolset (web_search, web_extract, vision_analyze, clarify) because webhook payloads can carry untrusted third-party content — a public PR title or issue comment should never be able to prompt-inject its way into your terminal.
For trusted routes — a localhost monitoring daemon pushing system alerts, an internal CI system — you can grant a wider toolset to that route only, without widening every other webhook route:
toolsets key by editing ~/.mibyan/webhook_subscriptions.json directly:
- The route list replaces the platform-level webhook toolset resolution for that route’s runs (it is not merged).
- Names are validated through the same path as
platform_toolsetsconfig — unknown names and platform-restricted toolsets are dropped. mibyan webhook subscribedeliberately does not accept a toolsets flag. Granting elevated tools is a manual config-file edit, so an agent creating its own subscription at runtime cannot self-grantterminal.- Only grant elevated toolsets to routes whose senders you fully control, with a real HMAC secret. Anyone who can POST a validly-signed payload to that route is effectively running an agent with those tools.
Security
The webhook adapter includes multiple layers of security:HMAC signature validation
The adapter validates incoming webhook signatures using the appropriate method for each source:- GitHub:
X-Hub-Signature-256header — HMAC-SHA256 hex digest prefixed withsha256= - GitLab:
X-Gitlab-Tokenheader — plain secret string match - Standard Webhooks:
webhook-id,webhook-timestamp, andwebhook-signatureheaders — signed content is{id}.{timestamp}.{raw_body}with av1,<base64-hmac-sha256>signature - Generic (V2, recommended):
X-Webhook-Signature-V2+X-Webhook-Timestampheaders — HMAC-SHA256 hex digest of<timestamp>.<body>. The timestamp (Unix seconds) must be within ±300 seconds of the server clock, which prevents captured requests from being replayed later. - Generic (V1, legacy):
X-Webhook-Signatureheader — raw HMAC-SHA256 hex digest of the body only. Still accepted for backward compatibility, but it has no replay protection (a captured request replays indefinitely); the gateway logs a deprecation warning once per route. Switch senders to V2.
Secret is required
Every route must have a secret — either set directly on the route or inherited from the globalsecret. Routes without a secret cause the adapter to fail at startup with an error. For development/testing only, you can set the secret to "INSECURE_NO_AUTH" to skip validation entirely.
When multi-profile routing is enabled, the route’s profile field also
binds that secret to one execution target. A route without profile is
default-profile-only. A request carrying a valid route signature is still
rejected if its /p/<profile>/ prefix does not match the route binding.
INSECURE_NO_AUTH is only accepted when the gateway is bound to a loopback host (127.0.0.1, localhost, ::1). If it is combined with a non-loopback bind such as 0.0.0.0 or a LAN IP, the adapter refuses to start — this prevents accidentally exposing an unauthenticated endpoint on a public interface.
Rate limiting
Each route is rate-limited to 30 requests per minute by default (fixed-window). Configure this globally:429 Too Many Requests response.
Idempotency
Delivery IDs (fromX-GitHub-Delivery, svix-id, webhook-id, X-Request-ID, or a random per-request ID) are cached for 1 hour. Duplicate deliveries (e.g. webhook retries) are silently skipped with a 200 response, preventing duplicate agent runs.
Body size limits
Payloads exceeding 1 MB are rejected before the body is read. Configure this:Authenticated does not mean trusted
Troubleshooting
Webhook not arriving
- Verify the port is exposed and accessible from the webhook source
- Check firewall rules — port
8644(or your configured port) must be open - Verify the URL path matches:
http://your-server:8644/webhooks/<route-name> - Use the
/healthendpoint to confirm the server is running
Signature validation failing
- Ensure the secret in your route config exactly matches the secret configured in the webhook source
- For GitHub, the secret is HMAC-based — check
X-Hub-Signature-256 - For GitLab, the secret is a plain token match — check
X-Gitlab-Token - Check gateway logs for
Invalid signaturewarnings
Event being ignored
- Check that the event type is in your route’s
eventslist - GitHub events use values like
pull_request,push,issues(theX-GitHub-Eventheader value) - GitLab events use values like
merge_request,push(theX-GitLab-Eventheader value) - If
eventsis empty or not set, all events are accepted
Agent not responding
- Run the gateway in foreground to see logs:
mibyan gateway run - Check that the prompt template is rendering correctly
- Verify the delivery target is configured and connected
Duplicate responses
- The idempotency cache should prevent this — check that the webhook source is sending a delivery ID header (
X-GitHub-Delivery,svix-id,webhook-id, orX-Request-ID) - Delivery IDs are cached for 1 hour
gh CLI errors (GitHub comment delivery)
- Run
gh auth loginon the gateway host - Ensure the authenticated GitHub user has write access to the repository
- Check that
ghis installed and on the PATH

