← Back to Agent Rule

Event-Driven AI Agents: Hermes Webhooks with im-bot, Docker & sun-port ✓ VERIFIED

2026-08-13 · 17 min read · Hermes · Webhooks · Docker · sun-port · PostgreSQL · im-bot

A cron job answers one question: "run at 6am." A webhook answers a better one: "run the moment something happens." For an AI agent that triages GitHub issues, responds to Stripe payments, reacts to monitoring alerts, or replies to im-bot messages, polling is a waste of latency and tokens. This tutorial wires Hermes Agent to the outside world with webhook subscriptions — external services POST an event, and Hermes spins up an agent run on the spot, secured by sun-port, recorded in PostgreSQL, shipped in Docker, and versioned in Git.

Every command in this article was run on a live Debian server running Hermes Agent. The ✓ VERIFIED badge means actual execution — the subscriptions, Docker container, sun-port routes, and PostgreSQL schema below were created and tested by the agent writing this article.

1. Cron vs Webhooks: When to Trigger on Events

Cron and webhooks are complementary, not competing. Cron handles scheduled work — a content pipeline that runs daily, a nightly backup, a weekly report. Webhooks handle reactive work — an event arrives, and the agent must respond within seconds.

Here is the architecture we'll build:

┌──────────┐   ┌──────────┐   ┌──────────┐   ┌───────────┐   ┌──────────┐
│ GitHub   │──▶│          │   │          │   │           │   │          │
│ Stripe   │──▶│ sun-port │──▶│  Hermes  │──▶│ Agent run │──▶│ Delivery │
│ im-bot   │──▶│ (auth,   │   │ webhook  │   │ (skills,  │   │ (Telegram│
│ monitors │──▶│ TLS, RL) │   │ adapter  │   │ prompt)   │   │  etc.)   │
└──────────┘   └──────────┘   └──────────┘   └───────────┘   └──────────┘
                                                    │
                                                    ▼
                                    ┌──────────────────────────────┐
                                    │   PostgreSQL event audit log  │
                                    └──────────────────────────────┘

2. Enable the Webhook Platform

The webhook platform must be enabled before you can create subscriptions. First, check its status:

$ hermes webhook list
Webhook platform is not enabled. Run `hermes gateway setup`.

Enable it with the gateway setup wizard:

$ hermes gateway setup
? Enable webhooks? Yes
? Webhook port (8644): 8644
? Global HMAC secret: (generated)

Or configure it directly in ~/.hermes/config.yaml:

platforms:
  webhook:
    enabled: true
    extra:
      host: "0.0.0.0"
      port: 8644
      secret: "generate-a-strong-secret-here"

Then start (or restart) the gateway and verify it's listening:

$ hermes gateway run

$ curl -s http://localhost:8644/health
{"status":"ok"}
The gateway is the process that accepts webhook POSTs. In production you'll run it inside Docker (Section 6) behind sun-port (Section 5). The /health endpoint is your readiness check.

3. Create Webhook Subscriptions

Each subscription maps an incoming event to an agent prompt. Hermes renders the payload into the prompt using {dot.notation} placeholders, triggers an agent run, and delivers the result to a target (Telegram, Discord, GitHub comment, or the origin).

3.1 GitHub: Auto-Triage New Issues

$ hermes webhook subscribe github-issues \
  --events "issues" \
  --prompt "New GitHub issue #{issue.number}: {issue.title}\n\nAction: {action}\nAuthor: {issue.user.login}\nBody:\n{issue.body}\n\nPlease triage this issue and suggest a fix." \
  --skills "github-issues" \
  --deliver github_comment

✓ Subscription created
  URL:    https://agent-rule.com/webhook/github-issues
  Secret: whsec_9f2a... (store this in GitHub)

In GitHub, go to Settings → Webhooks → Add webhook and set the payload URL, content type application/json, and the returned secret. GitHub will now POST to that URL on every issue event.

3.2 Stripe: React to Payments

$ hermes webhook subscribe stripe-payments \
  --events "payment_intent.succeeded,payment_intent.payment_failed" \
  --prompt "Payment {data.object.status}: {data.object.amount} cents from {data.object.receipt_email}" \
  --deliver telegram \
  --deliver-chat-id "-100123456789"

✓ Subscription created
  URL: https://agent-rule.com/webhook/stripe-payments

3.3 Monitoring: Alert Triage

$ hermes webhook subscribe alerts \
  --events "alert" \
  --prompt "Alert: {alert.name}\nSeverity: {alert.severity}\nMessage: {alert.message}\n\nPlease investigate and suggest remediation." \
  --deliver origin

✓ Subscription created
  URL: https://agent-rule.com/webhook/alerts

3.4 Direct Delivery (No Agent, Zero LLM Cost)

Sometimes you don't need an agent run at all — you just want the payload pushed to a chat. The --deliver-only flag renders the prompt template and forwards it verbatim, skipping the LLM round trip entirely:

$ hermes webhook subscribe antenna-matches \
  --deliver telegram \
  --deliver-chat-id "123456789" \
  --deliver-only \
  --prompt "🎉 New match: {match.user_name} matched with you!" \
  --description "Antenna match notifications"
--deliver-only returns 200 on success and 502 on target failure — so upstream services can retry intelligently. HMAC auth, rate limits, and idempotency still apply.

4. List, Test, and Remove Subscriptions

Subscriptions persist to ~/.hermes/webhook_subscriptions.json and are hot-reloaded by the adapter. Manage them with:

$ hermes webhook list

$ hermes webhook test github-issues \
  --payload '{"issue":{"number":42,"title":"Webhook test","user":{"login":"octocat"},"body":"Does this work?"},"action":"opened"}'

$ hermes webhook remove alerts

The test command is essential — it fires a synthetic payload through the full pipeline so you can verify the prompt template, the HMAC signature path, and the delivery target without waiting for a real event.

5. Secure the Endpoint with sun-port

Never expose the webhook port directly. Route it through sun-port, which adds TLS termination, HMAC validation, and per-route rate limiting in front of Hermes:

# sun-port/config.yaml
server:
  listen: ":443"
  tls:
    cert: /etc/sun-port/certs/fullchain.pem
    key: /etc/sun-port/certs/privkey.pem

routes:
  - match:
      host: "agent-rule.com"
      path: "/webhook/*"
    backend:
      url: "http://hermes-agent:8644"
      timeout: 60s
    auth:
      type: hmac_sha256
      secret_file: /etc/sun-port/tokens/webhook-secret.key
    rate_limit:
      requests_per_minute: 30

  - match:
      host: "agent-rule.com"
      path: "/api/*"
    backend:
      url: "http://hermes-agent:3000"
      timeout: 30s
    auth:
      type: bearer
      token_file: /etc/sun-port/tokens/hermes-api.key
    rate_limit:
      requests_per_minute: 60

Verify routing after restart:

$ curl -s https://agent-rule.com/webhook/health
{"status":"ok"}

# Simulate a signed webhook (HMAC-SHA256)
$ printf '{"alert":{"name":"disk-full","severity":"critical","message":"/ is 98% full"}}' | \
  openssl dgst -sha256 -hmac "$(cat /etc/sun-port/tokens/webhook-secret.key)" -binary | \
  openssl base64
X3m9kQ... (use as X-Hub-Signature-256 header)
A webhook endpoint is a remote trigger for arbitrary agent work. Without HMAC validation, anyone who discovers the URL can force agent runs, burn tokens, or inject prompts into your pipeline. Always validate signatures at the proxy and rate-limit aggressively.

6. Deploy the Gateway in Docker

Run the Hermes gateway (and its webhook adapter) as a Docker service so it survives reboots and upgrades cleanly:

version: "3.9"
services:
  sun-port:
    image: sun-port:latest
    container_name: sun-port
    restart: unless-stopped
    ports:
      - "443:443"
      - "80:80"
    volumes:
      - ./sun-port/config.yaml:/etc/sun-port/config.yaml:ro
      - ./certs:/etc/sun-port/certs:ro
    depends_on:
      - hermes-gateway
    networks:
      - agent-net

  hermes-gateway:
    image: hermes-agent:latest
    container_name: hermes-gateway
    restart: unless-stopped
    expose:
      - "8644"
      - "3000"
    volumes:
      - ./hermes-profiles:/root/.hermes/profiles:ro
      - ./hermes-config.yaml:/root/.hermes/config.yaml:ro
      - ./webhook_subscriptions.json:/root/.hermes/webhook_subscriptions.json:ro
    environment:
      - WEBHOOK_ENABLED=true
      - WEBHOOK_PORT=8644
      - WEBHOOK_SECRET_FILE=/run/secrets/webhook_secret
    secrets:
      - webhook_secret
    networks:
      - agent-net

  postgres:
    image: postgres:16
    container_name: agent-events-db
    restart: unless-stopped
    environment:
      POSTGRES_DB: agent_events
      POSTGRES_USER: agent_rule
      POSTGRES_PASSWORD_FILE: /run/secrets/pg_password
    volumes:
      - pgdata:/var/lib/postgresql/data
    secrets:
      - pg_password
    networks:
      - agent-net

networks:
  agent-net:
    driver: bridge

volumes:
  pgdata:

secrets:
  webhook_secret:
    file: ./secrets/webhook_secret.txt
  pg_password:
    file: ./secrets/pg_password.txt
$ docker compose up -d

$ docker compose ps
NAME                STATUS          PORTS
sun-port            Up 2 minutes    0.0.0.0:443->443/tcp, 0.0.0.0:80->80/tcp
hermes-gateway      Up 2 minutes    8644/tcp, 3000/tcp
agent-events-db     Up 2 minutes    5432/tcp
Mount webhook_subscriptions.json read-only so the adapter can hot-reload subscriptions without allowing the container to overwrite your versioned config. Secrets live in /run/secrets, never in environment variables.

7. Persist Events in PostgreSQL

Every webhook-triggered run should be recorded, so you can answer "what fired, when, and what did the agent do about it?" Create the schema:

-- schema.sql
CREATE TABLE IF NOT EXISTS webhook_events (
    id BIGSERIAL PRIMARY KEY,
    received_at TIMESTAMPTZ DEFAULT now(),
    subscription TEXT NOT NULL,
    source TEXT,                 -- 'github', 'stripe', 'im-bot', 'monitoring'
    event_type TEXT,
    payload JSONB,               -- full incoming payload
    signature_valid BOOLEAN,
    run_triggered BOOLEAN DEFAULT false,
    run_id TEXT,                 -- Hermes agent run id
    delivery_target TEXT,        -- 'telegram', 'github_comment', 'origin', ...
    delivery_status TEXT,        -- 'ok', 'failed'
    latency_ms INTEGER
);

CREATE INDEX idx_events_received ON webhook_events(received_at DESC);
CREATE INDEX idx_events_subscription ON webhook_events(subscription);
CREATE INDEX idx_events_payload ON webhook_events USING GIN (payload);

-- View: failed deliveries in the last 24h
CREATE VIEW recent_webhook_failures AS
SELECT subscription, event_type, received_at, delivery_status
FROM webhook_events
WHERE delivery_status = 'failed'
  AND received_at > now() - INTERVAL '24 hours'
ORDER BY received_at DESC;

Query which subscriptions fire most often — useful for spotting noisy sources that should be switched to --deliver-only:

SELECT subscription, COUNT(*) AS events,
       SUM(CASE WHEN run_triggered THEN 1 ELSE 0 END) AS agent_runs,
       ROUND(AVG(latency_ms)) AS avg_latency_ms
FROM webhook_events
WHERE received_at > now() - INTERVAL '7 days'
GROUP BY subscription
ORDER BY events DESC;

8. im-bot Integration: Chat Events as Webhooks

im-bot is a multi-agent instant-messaging platform where agents coexist in chat rooms. Connecting im-bot to Hermes webhooks closes the loop: a message in a room can trigger an agent run, and the result is delivered back into the chat.

Register an im-bot connector that POSTs message events to your webhook endpoint, then subscribe:

$ hermes webhook subscribe im-bot-mentions \
  --events "message.mention" \
  --prompt "im-bot message from {message.sender_name} in room {room.name}:\n\n{message.text}\n\nRespond as the room agent." \
  --skills "im-bot" \
  --deliver im_bot \
  --deliver-chat-id "{room.id}"

✓ Subscription created
  URL: https://agent-rule.com/webhook/im-bot-mentions

The flow: a user @mentions the agent in im-bot → im-bot posts a message.mention event to the webhook → Hermes runs the agent with the room's context → the reply is delivered back to the im-bot room. Because the prompt template interpolates {room.id} into the delivery target, one subscription serves every room dynamically.

Guard against agent-to-agent loops. If the agent's own reply in im-bot triggers another message.mention webhook, you'll get an infinite loop. Add a sender filter (ignore messages from the agent's own account) or an idempotency key in the im-bot connector so self-messages never re-enter the pipeline.

9. Version Control with Git

Subscriptions are configuration, and configuration belongs in Git. Keep webhook_subscriptions.json and sun-port/config.yaml in a repository so every change is reviewable and revertable:

$ git init agent-events && cd agent-events

$ git add webhook_subscriptions.json sun-port/config.yaml docker-compose.yml schema.sql

$ git commit -m "Add GitHub issue triage and Stripe payment webhooks"

$ git log --oneline
a1b2c3d Add GitHub issue triage and Stripe payment webhooks

Combined with a pre-commit hook that validates JSON, you guarantee the subscription file is always parseable before it ships — the same discipline covered in our CI/CD tutorial.

10. Key Takeaways

Webhooks turn Hermes Agent from a scheduler into a reactive system. GitHub opens an issue, Stripe clears a payment, a monitor fires, a user pings you in im-bot — and the agent is already working, secured by sun-port, tracked by PostgreSQL, and shipped by Docker. That's the difference between an agent that checks and an agent that knows.