Cron asks “is it time?” Webhooks say “this just happened, act.” For agent work, that distinction matters. A scheduled dig through the inbox is different from “a Stripe dispute was created” or “a pull request opened against main.”
Hermes Agent webhooks turn authenticated HTTP POST events into agent runs whose results go to a configured delivery target. Used well, they are small doors with locks and clear jobs. Used poorly, they are an exposed port with one mega-prompt that tries to handle every JSON blob the internet sends.
This article covers route design, provider-appropriate authentication, the documented health check, and a practical smoke test. Pair it with the first-week hardening in /articles/hermes-first-week-memory-and-skills before you expose anything beyond localhost.
When webhooks are the right trigger
Prefer webhooks when:
- Latency matters (review a PR while the author is still context-switching).
- The source system already emits events (GitHub, GitLab, Jira, Stripe, internal forms).
- Each event should become one focused task, not a long conversational session.
Prefer cron when:
- You are polling for drift (“any cert expiring in 14 days?”).
- The source cannot push.
- You want a quiet periodic brief rather than per-event interrupts.
Official Hermes guidance matches this split: cron for scheduled checks and webhooks for event-triggered runs.
Architecture in one picture
Source system (GitHub / GitLab / n8n / custom app)
| HTTPS POST + source-appropriate authentication
v
Hermes webhook adapter (default port 8644)
| route: /webhooks/<name>
v
Named route config (filters + prompt + delivery)
v
Agent run (skills/tools under your approval policy)
v
Configured delivery (chat channel, GitHub comment, or log)
n8n can sit on the left as a validator and fan-in layer: validate fields, drop junk, then POST a minimized payload to Hermes. That is an illustrative architecture in /articles/hermes-vs-n8n-choose-by-job, not a documented turnkey vendor integration. If n8n needs the agent result back in its workflow, call the separate Hermes API server on default port 8642 with bearer authentication instead of treating the webhook adapter as a synchronous callback.
Setup path (verify against live docs)
The upstream webhook documentation describes this path:
- Enable the webhook platform (
hermes gateway setupor env such asWEBHOOK_ENABLED=true). - Configure a secret for every route. Use GitHub’s HMAC header, GitLab’s plain token header, or generic V2 timestamped HMAC as appropriate for the source.
- Create a named route in config or via
hermes webhook subscribe(command per current docs). - Health-check:
curl http://localhost:8644/health - Point the external system at
https://your-host/webhooks/<name> - Send an authenticated test payload; confirm the route, prompt, tool scope, and delivery target you expected.
Default documented port is 8644. The documented defaults also limit a route to 30 requests per minute and reject bodies larger than 1 MB. If you changed these values, test the configured limits rather than relying on defaults.
Static configuration changes may require the gateway lifecycle documented by your installed release. Dynamic routes created with
hermes webhook subscribeare hot-reloaded without a restart and receive an auto-generated secret. In either case, confirm the gateway process sees the intended profile and environment; a successful command in an interactive shell does not prove that the daemon has the same configuration.
Route authentication is not optional
Every route must inherit or define a secret, otherwise the adapter fails at startup. Authentication is provider-specific: GitHub uses X-Hub-Signature-256, GitLab uses an exact-match X-Gitlab-Token, and generic custom senders should use V2 timestamped HMAC. Sender authentication proves which holder of the secret sent the request; it does not make payload instructions trusted.
Rules that hold up in production:
- Generate a long random secret; store it in a secrets manager or an environment file with locked permissions, never in skill markdown the agent can read casually.
- Prefer per-route secrets when systems have different trust levels (GitHub app vs internal form vs partner webhook).
- Reject unsigned or invalid signatures at the edge; do not “log and continue.”
- Use
INSECURE_NO_AUTHonly for temporary loopback testing. The adapter refuses to start if that value is combined with a non-loopback bind such as0.0.0.0or a LAN address.
Use Hermes’s current generic V2 scheme for custom senders: X-Webhook-Timestamp is Unix seconds; X-Webhook-Signature-V2 is the lowercase hexadecimal HMAC-SHA256 digest of <timestamp>.<raw-body>. Hermes rejects timestamps outside a ±300-second window. V1 signs only the body and lacks replay protection, so do not build new senders on it (official webhook security contract).
Reproducible signed smoke test
After creating a route named support-triage, put a non-sensitive payload in payload.json. Have your approved secret-injection mechanism set WEBHOOK_SECRET before this shell starts; do not type a production secret into command history. The Node command below reads the key from the environment rather than expanding it into the process arguments and signs the exact file bytes:
: "${WEBHOOK_SECRET:?inject a disposable route secret before running this test}"
timestamp="$(date +%s)"
signature="$(TIMESTAMP="$timestamp" node -e '
const { createHmac } = require("node:crypto");
const { readFileSync } = require("node:fs");
const hmac = createHmac("sha256", process.env.WEBHOOK_SECRET);
hmac.update(`${process.env.TIMESTAMP}.`, "utf8");
hmac.update(readFileSync("payload.json"));
process.stdout.write(hmac.digest("hex"));
')"
curl --fail-with-body \
-H 'Content-Type: application/json' \
-H "X-Webhook-Timestamp: $timestamp" \
-H "X-Webhook-Signature-V2: $signature" \
--data-binary @payload.json \
http://127.0.0.1:8644/webhooks/support-triage
Then repeat without either signature header and with a timestamp older than 300 seconds. Both must be rejected. Do not paste a real secret into screenshots, tickets, or shell history; use a disposable route secret for documentation tests and rotate it afterward.
An exposed webhook that can place attacker-controlled text in front of a terminal-capable agent creates a remote tool-execution risk. Authentication limits who can submit events, but authenticated payload text can still be adversarial. Use TLS and network controls, minimize payloads, scope or disable terminal, file, and outbound-action tools, and isolate execution from the host. Approval prompts are an operator-intent guardrail, not a hostile-input sandbox.
What to put in the payload
Send the agent a contract, not a raw firehose:
{
"event_type": "github.pull_request.opened",
"repo": "acme/api",
"pr_number": 1842,
"title": "Add billing retry worker",
"author": "ada",
"base_ref": "main",
"html_url": "https://github.example.invalid/acme/agent-service/pull/1842",
"task": "Summarize risk for main. List missing tests. Do not approve or merge."
}
Strip unused fields. Huge workflow dumps waste context and invite confused tool use. “Small, explicit payloads with a clear task” is this article’s design recommendation, not a claim about an official n8n integration.
Route design: many small doors
Do not build /webhooks/everything. Build named routes with filters and prompts:
| Route name | Source | Job | Delivery |
|---|---|---|---|
gh-pr-opened | GitHub PR opened | Risk summary + test gaps | Engineering Telegram topic |
stripe-dispute | Stripe dispute created | Checklist draft | Finance Slack + log |
support-form | n8n after validation | Classify + draft reply | Configured private Slack channel |
uptime-alert | Monitoring webhook | Gather recent deploys context | On-call channel |
Each route should answer:
- Which events are accepted?
- What is the single expected output?
- Which tools are allowed for this route’s agent profile?
- Where does the result go?
- What happens on failure (retry? dead-letter? page a human?)?
Webhook payloads often contain emails, account IDs, or message bodies. Minimize fields before they reach Hermes. The route schema does not document a per-route memory-write switch. Use a dedicated profile with memory disabled or
memory.write_approvalenabled, and test what persists. If no agent reasoning is needed, use documenteddeliver_onlymode instead of running an agent.
Health checks and operability
Documented health endpoint: http://localhost:8644/health (or your host/port). Use it for:
- Local smoke tests after enable
- Docker/Kubernetes readiness probes
- External uptime checks against a private health URL, not against an unauthenticated webhook route
Also log:
- Signature failures (possible attack or misconfigured secret)
- Payload validation failures
- Agent run duration and tool-approval denials
- Downstream delivery failures (chat API down, etc.)
Without those signals, “the agent felt flaky” is your only incident report. These records improve observability; they are not automatically a complete, tamper-resistant audit trail.
Example: GitHub PR opened → focused run
Goal: When a PR opens against main, Hermes drafts a risk note for humans. It does not merge, approve, or comment unless you later add a reviewed delivery path.
- Create route
gh-pr-opened. For GitHub direct delivery, configure the shared secret used to verifyX-Hub-Signature-256; for a generic n8n relay, implement the V2 timestamped HMAC contract instead. - Filter to
pull_request/opened/ basemain. - Prompt contract: summarize purpose, blast radius, missing tests, rollout risk; mark unknowns; no merge instructions.
- Tools: read-only GitHub fetch if configured; shell disabled or approval-required.
- Delivery: post markdown to an internal channel; human decides next steps.
Shown shape of a good agent output (illustrative structure; your model wording will vary):
PR #1842: Add billing retry worker (ada to main)
Facts
- Touches billing worker and queue config (from title/files list provided).
- Linked URL: `https://github.example.invalid/acme/agent-service/pull/1842` (illustrative)
Risks
- Retry storms if backoff missing [inference; verify in diff]
- No mention of idempotency keys in title [unclear]
Missing tests to confirm
- Duplicate delivery / poison message behavior
- Alerting when retry budget exhausted
Do not merge from this note. Human review required.
That is an event-driven agent run with a stop rule. It is not an autonomous code owner.
Exercise: design three routes before you enable one
On paper (or in your runbook), write three webhook routes for your stack. For each, fill:
- Name
- Source + event filter
- Authentication method and secret owner
- Payload fields: start with at most 10 as a deliberately small exercise budget
- Prompt: start with at most 8 lines, then add only what the route’s evals require
- Tools allowed
- Delivery target
- Failure behavior
Only implement the lowest-risk route first, usually an internal alert or a draft-only PR summary. Run curl against /health, then an authenticated test POST and an invalid-auth test, then one real event in a non-production repo or staging project.
Failure modes to expect
- Secret mismatch after rotation: authentication fails; fix the environment used by the gateway process, not only your laptop shell.
- Over-broad prompt: the agent improvises tools; split the route.
- Retry storms: the source retries POSTs. Hermes caches delivery IDs for one hour, but meaningful deduplication requires a stable
X-GitHub-DeliveryorX-Request-ID. Customer-visible actions still need durable business idempotency whose retention matches the replay window. - Memory pollution: high-volume alerts reach durable memory; use a dedicated profile and explicit memory settings.
- Exposed port: health and webhooks are reachable without the intended TLS and network controls; fix the network before adding tools.
References worth keeping open
- Hermes webhook adapter
- Hermes API server
- Hermes security model
- Hermes documentation
- Internal: /articles/first-ai-agent-in-n8n, /articles/hermes-vs-n8n-choose-by-job
Event-driven agents earn their keep when each route is narrow, authenticated, and observable, with a configured delivery target. The webhook adapter is an event-ingress front door, not the bearer-authenticated request/response API. Design and test it accordingly.



