Skip to main content

Hermes

Hermes Agent is an open-source AI agent from Nous Research that runs on your own machine and connects to messaging platforms like Telegram, Discord, Slack, LINE, and now Roam. With Roam, you can run your agent directly inside Roam chat — users DM it, mention it in groups, and pull it into threads, the same way they would any teammate.

This guide is about exactly that: running a Hermes bot in Roam chat. There are two flavors of bot (Personal and Org); pick whichever fits your use case and follow the matching section. You can run both side by side from the same machine.

Prerequisites

  • A working Hermes Agent install. Hermes is a CLI that runs on macOS or Linux (Windows via WSL); confirm it works with hermes --version.
  • A Roam workspace where you can create either a Personal Access Token (for a Personal Bot) or an API Key (for an Org Bot).
  • A way to expose a local HTTPS endpoint so Roam can deliver webhooks. ngrok is easiest for dev; Cloudflare Tunnel is the recommended free option for long-running setups.

Pick your bot

Personal BotOrg Bot
Acts asYou (the user who created the token).Its own persona (e.g. "OnCallBot"), owned by the workspace.
Who can talk to itOnly people you allowlist. Other users are dropped.Anyone in the workspace can DM it, @-mention it, or pull it into a thread, subject to the allowlists you configure.
In groupsCannot be added as a member (Roam restricts PAT bots to user-typed accounts). Pulled into chats by @-mentions in your own conversations.Can be added as a member of any group (required for private groups). In public groups, @-mentions work without membership.
Token typePersonal Access Token (rmp-…).API Key (rmk-…).
Best forPersonal workflows — research, summarizing your own meetings, drafting messages on your own data.Team workflows — on-call assistant, FAQ bot, status reports, anything everyone needs to talk to.

Run both at once if you need both — see Run additional bots.

How access control works (read this first)

Hermes' Roam adapter has three layers of access control, in order:

  1. Token type filtering. A Personal Access Token discovers its owner from /v1/token.info at startup. The owner's name appears in the agent's context (so the agent addresses you by name), but the plugin does not restrict messages to that owner automatically — you control who can talk to a PAT bot via the allowlist below.
  2. Sender allowlists. Set ROAM_ALLOWED_USERS (DM senders) and ROAM_ALLOWED_GROUPS (group/chat IDs) to comma-separated UUID lists, or set ROAM_ALLOW_ALL_USERS=true to skip allowlisting entirely. For development, ROAM_ALLOW_ALL_USERS=true is the easiest start; for production, an explicit allowlist is the safer default.
  3. Mention gate (groups only). ROAM_REQUIRE_MENTION=true makes the bot silent in groups unless someone @-mentions it. Defaults to false (responds to every message).

The boundary you actually rely on for safety is the allowlist. For a Personal Bot in dev, put just your own user ID in ROAM_ALLOWED_USERS; for an Org Bot, set ROAM_ALLOWED_GROUPS to the chat IDs you want it to live in. Workspace membership in Roam is also a coarse boundary — an Org Bot can only see messages from groups it's been invited to.

Set up a Personal Bot

A Personal Bot acts as you. DM it for one-on-one help, or @-mention it from groups you're in to pull it into a conversation on your own behalf. It can't be added to groups as a member.

1. Create a Personal Access Token in Roam

Prerequisite: Your workspace admin must enable Personal Access Tokens (see Admin Policy for Personal Access).

  1. In Roam, open User Settings > Developer.
  2. Click Create Personal Access Token.
  3. Name it (e.g. "Hermes").
  4. Select the permissions to grant — at minimum Chat Send and Chat Read so the agent can send and receive messages. Add User Read Email if you want the agent to know your email, plus Meetings Read and other scopes for the features you need.
  5. Upload an avatar image — this is the picture shown next to the agent's messages in Roam chat.
  6. Click Create Token. Copy both the token (starts with rmp-…) and the webhook signing secret (whsec_…) shown on the confirmation screen — you'll need both in step 3.

2. Approve the token

If your workspace requires admin approval for personal access (the default), an admin must approve the new token before it can be used:

  1. In Roam, open Roam Administration > Developer > Personal Access Tokens.
  2. Find the new token and click Approve.

If your workspace is set to Auto-approve, skip this step. See Admin Policy for Personal Access.

3. Install the Hermes Roam plugin

Install it straight from the plugin's Git repo:

hermes plugins install WonderInventions/hermes-roam

This clones the plugin into ~/.hermes/plugins/roam/ and prompts you for the two required credentials — paste the token (rmp-…) and webhook signing secret (whsec_…) from step 1; they're saved to ~/.hermes/.env. When it asks Enable 'roam' now?, answer yes.

Install from the dashboard instead

The Hermes web dashboard does the same thing: open the Plugins page, paste WonderInventions/hermes-roam into Install from GitHub / Git URL, then enable it. Other install methods (release tarball, manual git clone) are in the plugin's INSTALL.md.

Confirm it loaded:

hermes plugins list | grep roam

You should see roam | enabled | <version> | Roam ... | git.

4. Configure access

Step 3 already saved ROAM_API_KEY and ROAM_WEBHOOK_SECRET to your default profile's environment file at ~/.hermes/.env. Add the access setting:

cat >> ~/.hermes/.env <<EOF

# For dev: allow any sender. For prod, switch to ROAM_ALLOWED_USERS=<your UUID>.
ROAM_ALLOW_ALL_USERS=true
EOF

If you skipped the credential prompts in step 3, add them here too: ROAM_API_KEY=<your rmp-… token> and ROAM_WEBHOOK_SECRET=<your whsec_… secret>.

5. Expose a public webhook URL

Roam delivers webhooks to a public HTTPS URL. The plugin's webhook server listens on localhost:8647/roam/webhook by default. Start a tunnel pointing at port 8647:

ngrok http 8647

ngrok prints something like https://<random>.ngrok.app. Add it to ~/.hermes/.env:

echo "ROAM_WEBHOOK_PUBLIC_URL=https://<random>.ngrok.app" >> ~/.hermes/.env

With ROAM_WEBHOOK_PUBLIC_URL set, the plugin auto-calls webhook.subscribe on startup so you don't need to register the URL manually in Roam admin.

6. Start the gateway

hermes gateway restart
tail -f ~/.hermes/logs/gateway.log | grep -i roam

You should see:

roam: PAT owner=<Your Name> (id=…); bot persona=<Bot Name> (id=…)
roam: subscribed to chat.message webhooks at https://….ngrok.app/roam/webhook
roam: webhook listening on 0.0.0.0:8647/roam/webhook (public: https://….ngrok.app)
✓ roam connected

The PAT owner line confirms the plugin read your identity from /v1/token.info — the agent will now address you by name in conversation.

7. Verify

In Roam, DM your bot. It should reply. The first message in any new chat triggers Hermes' onboarding flow ("Hi, I'm Hermes…"); send a second message to see a real LLM reply. To pull the bot into a group you're already in, @-mention it — its webhook fires for that single message, and it replies in-thread under your mention.

Set up an Org Bot

An Org Bot is its own persona — anyone in the workspace can DM it for help, @-mention it from any chat to pull it into a thread, or add it as a member of a channel where it responds to every message (your choice).

Hermes' multi-bot pattern is profiles: one Hermes profile per bot, each with its own gateway, state, and credentials. We'll create a new profile called org for the Org Bot.

1. Create an API Key in Roam

You'll need workspace-admin access for this step.

  1. As a Roam admin, open Roam Administration > Developer.
  2. Create a new API Client and select API Key as the authorization type.
  3. Give it a name (e.g. "OnCallBot") and select the scopes for what the bot will do — at minimum:
    • chat:send_message — Send messages
    • chat:read — List groups it's added to
    • chat:history — Read recent messages in those groups (used for context)
  4. Add additional scopes as needed: meetings:read, recordings:read, transcript:read, groups:read, etc.
  5. Upload an avatar image and set a display name — this is what users see in Roam chat.
  6. Click Create. Copy both the token (rmk-…) and the webhook signing secret (whsec_…).

You'll add the bot to specific groups in step 7, once the Hermes side is wired up.

2. Create a Hermes profile

Each profile is an isolated Hermes instance — separate gateway, separate state, separate plugin install. See Hermes' Profiles guide for the full model.

hermes profile create org --description "Org bot for #engineering, runs OnCallBot persona"

This creates ~/.hermes/profiles/org/ and an org shim in ~/.local/bin/ so you can run org <subcommand> instead of hermes -p org <subcommand>.

3. Install the plugin in the org profile

Plugins are per-profile. The org shim runs every command under the org profile (hermes -p org), so installing through it lands the plugin in the org profile's own plugin directory and enables it there:

org plugins install WonderInventions/hermes-roam

When prompted, paste the org bot's token (rmk-…) and webhook signing secret (whsec_…) from step 1 — they're saved to ~/.hermes/profiles/org/.env. Answer yes at Enable 'roam' now? (this writes plugins.enabled: [roam] to the org profile's config.yaml).

If you want the org profile to share the default profile's LLM model setup (recommended for parity), append the model block to that config too:

grep -A 4 "^model:" ~/.hermes/config.yaml >> ~/.hermes/profiles/org/config.yaml

You may also need to copy auth tokens for OAuth-based providers (e.g. xAI Grok via OAuth):

cp ~/.hermes/auth.json ~/.hermes/profiles/org/auth.json

4. Configure behavior

Step 3 saved ROAM_API_KEY and ROAM_WEBHOOK_SECRET to the org profile's environment file at ~/.hermes/profiles/org/.env. Append the behavior settings:

cat >> ~/.hermes/profiles/org/.env <<'EOF'

# Each profile binds its own webhook port. The default profile uses 8647;
# use 8648 (or any free port) for the org profile.
ROAM_WEBHOOK_PORT=8648

# Org bots that idle in busy channels usually want every reply in a thread
# off the post being answered, so the main channel stays clean.
ROAM_REPLY_IN_THREAD=true

# Mention gate. false = respond to every message in groups. true = silent
# unless @-mentioned. Choose based on whether this bot lives in dedicated
# channels (false) or general ones (true).
ROAM_REQUIRE_MENTION=false

# For dev: allow any sender. For prod, switch to:
# ROAM_ALLOWED_GROUPS=<comma-separated chat UUIDs>
ROAM_ALLOW_ALL_USERS=true
EOF

If you skipped the credential prompts in step 3, add them to that file too: ROAM_API_KEY=<your rmk-… token> and ROAM_WEBHOOK_SECRET=<your whsec_… secret>.

If you set up the LLM model in step 3 but want any non-OAuth API keys too (OpenAI, Anthropic, OpenRouter, etc.), copy them from the default profile:

grep -E "^(OPENROUTER|XAI|ANTHROPIC|OPENAI)_" ~/.hermes/.env >> ~/.hermes/profiles/org/.env

5. Expose a public webhook URL for the org bot

The default profile's tunnel forwards to port 8647; the org profile needs its own forwarding to port 8648. Start a second tunnel:

ngrok http 8648

Take note of the new https://<random>.ngrok.app URL and add it to the org profile's .env:

echo "ROAM_WEBHOOK_PUBLIC_URL=https://<random>.ngrok.app" >> ~/.hermes/profiles/org/.env

For a long-running setup, Cloudflare Tunnel is the recommended alternative to ngrok.

6. Give the bot a persona

A new bot has no behavior of its own — it'll respond like a generic assistant. Edit ~/.hermes/profiles/org/SOUL.md to set the bot's personality, role, and prompt:

You are OnCallBot, an SRE assistant living in #alerts in Roam. Every
message in this channel is typically a Datadog alert. Your job is to
investigate and report.

When a new alert arrives:
1. Extract the monitor name, the failing condition, the affected service /
host / tag(s), and the alert time window.
2. Investigate using the `datadog` tool: monitor details, metrics for the
affected service around the alert time, related logs, recent deploys.
3. Reply with a concise, well-structured investigation:
- **Summary** — one sentence: what's wrong and the likely cause.
- **Evidence** — specific metric values, log excerpts, exact times.
- **Suggested next steps** — what a human on-call would do next.
4. Keep replies tight. No filler.
5. If you can't form a hypothesis, say so plainly — list what you looked at.
6. Never claim a fix is applied. You investigate and report; humans act.

When asked who you are, say you are OnCallBot.

See Hermes' Configuring the agent guide for the full personality model.

7. Invite the bot to your channels

In Roam, open Group Settings for the channel you want the bot to live in and add it as a member. This is required for private groups (the bot only sees messages in groups it's been added to). In public groups, @-mentions reach the bot without explicit membership, but adding it as a member is still the cleanest way to scope its presence.

Repeat for each channel the bot should hang out in.

8. Start the gateway and verify

org gateway start
tail -f ~/.hermes/profiles/org/logs/gateway.log | grep -i roam

You should see:

roam: org token; bot=OnCallBot (id=…)
roam: subscribed to chat.message webhooks at https://….ngrok.app/roam/webhook
roam: webhook listening on 0.0.0.0:8648/roam/webhook (public: https://….ngrok.app)
✓ roam connected

The org token line (rather than PAT owner=…) confirms the plugin recognized this as an API-key bot rather than a personal-access token.

Then in Roam:

  1. DM the bot. Open a direct message with the bot in Roam and send it a hello. It should reply with the persona you set.
  2. Post a message in a channel you added it to. If ROAM_REQUIRE_MENTION=false, post any message — the bot replies in a new thread (with ROAM_REPLY_IN_THREAD=true). If ROAM_REQUIRE_MENTION=true, @-mention the bot to trigger it.
  3. Pull it into a thread. From another chat the bot isn't a member of, @-mention it inside an existing thread. The bot replies in that same thread.

Caveats for proactive (every-message) bots

With ROAM_REQUIRE_MENTION=false, the bot answers every message in the channel. Two things to watch for:

  • Self-message loop protection comes from the bot's identity. When the bot posts a reply, Roam delivers that outbound back via the webhook (the bot is technically a participant in its own chat). The plugin filters these out by comparing userId against the bot's address ID, fetched at startup via /v1/token.info. If that call fails, the log shows roam: token.info failed: … — in that state the bot can loop on its own replies once you flip requireMention off. Fix the token.info issue (check token scopes, approval status, and network reachability) before enabling proactive mode.
  • Every message in the channel triggers an agent run. If the channel is busy, runs queue up and tokens burn fast. Either narrow which groups the bot listens in (ROAM_ALLOWED_GROUPS) or add a SOUL.md rule telling the agent when not to reply.

Restricting which users and groups can reach the bot

By default, with ROAM_ALLOW_ALL_USERS=true, the bot answers anyone who can reach it. To restrict:

# DM senders allowlist — Roam user UUIDs, comma-separated.
echo "ROAM_ALLOWED_USERS=<uuid1>,<uuid2>" >> ~/.hermes/profiles/org/.env

# Groups allowlist — Roam chat/group UUIDs, comma-separated.
echo "ROAM_ALLOWED_GROUPS=<chatId1>,<chatId2>" >> ~/.hermes/profiles/org/.env

# Then remove the catch-all:
sed -i '' '/^ROAM_ALLOW_ALL_USERS/d' ~/.hermes/profiles/org/.env

org gateway restart

ROAM_ALLOWED_GROUPS IDs are the group's chat UUID, found at the bottom of the group's Group Settings page in Roam.

Run additional bots

To add another bot, create another Hermes profile and repeat the Org Bot setup with a new name and a unique webhook port:

hermes profile create another
another plugins install WonderInventions/hermes-roam
# Then set ~/.hermes/profiles/another/.env (ROAM_WEBHOOK_PORT=8649 or any free
# port, plus the allowlist / behavior vars) as in the Org Bot steps above.
another gateway start

Troubleshooting

"I sent a message and got nothing back"

Tail the gateway log and re-send:

tail -f ~/.hermes/logs/gateway.log | grep -i roam
# or for a non-default profile:
tail -f ~/.hermes/profiles/<name>/logs/gateway.log | grep -i roam

The most common drop reasons are logged explicitly:

  • roam: dropping group message (require_mention=true, no @bot mention) — flip ROAM_REQUIRE_MENTION=false, or @-mention the bot using Roam's autocomplete (so it inserts a rich mention, not a plain string).
  • roam: rejecting unauthorized message chat_type=… chat_id=… user_id=… — the sender or group isn't on the allowlist. Add them to ROAM_ALLOWED_USERS / ROAM_ALLOWED_GROUPS, or enable ROAM_ALLOW_ALL_USERS=true for dev.
  • No log entry at all means the webhook never reached the plugin. Check the tunnel's request inspector (ngrok exposes one at http://127.0.0.1:4040) to see whether Roam delivered to the URL.

webhook.subscribe failed at startup

The plugin's auto-subscription call returned a non-2xx. Common causes:

  • The URL isn't reachable from Roam's servers (tunnel not running, wrong port).
  • The token is missing the scopes required to manage webhook subscriptions.
  • The webhook secret is malformed (must be the whsec_… value Roam printed when you created the token).

You can subscribe manually if you'd rather skip auto-subscription:

curl -X POST https://api.ro.am/v1/webhook.subscribe \
-H "Authorization: Bearer $ROAM_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url":"<your tunnel>/roam/webhook","event":"chat.message"}'

Markdown rendering looks wrong in Roam

Hermes' content generator emits Markdown using the chat-platform convention where a single newline is a hard line break (the same convention used by GFM, Slack, Discord, Telegram). Roam's renderer follows CommonMark's other allowed softbreak rendering — a single newline collapses to a space — so unmodified Hermes output would smush lines together. The plugin reconciles this by inserting blank lines between non-empty lines outside fenced code blocks, which renders correctly under either softbreak choice.

Two bots with the same display name

If you create both a Personal Bot and an Org Bot and give them the same name in Roam (e.g. both "Hermes"), users can't tell which one they're @-mentioning. Rename one in Roam admin — the display name on the token (Personal) or API client (Org) is what shows in chat.

Where to file issues

The plugin source lives at WonderInventions/hermes-roam. File issues there for bugs in the Roam integration; for Hermes core issues, use the Hermes Agent repo.