AI Workspace Setup Runbook
This is the operator-and-AI runbook for provisioning a chatalot workspace from zero — community, groups, channels, role assignments, the works — when you want an autonomous agent (Hivemind, an LLM-driven script, etc.) to handle setup rather than clicking through the web UI.
It's the cousin of docs/integrations/hivemind.md (which covers the
narrower webhook-only output case). This doc covers the full
read/write API an AI needs to design and provision a workspace.
Who this is for
An autonomous agent that needs to:
- Create a community (one per organization on a self-hosted instance)
- Lay out groups for organizational separation (admins, devs, qa, ops, customer success, etc.)
- Create the channels that live within each group
- Mint invites that pre-provision the right role on accept
- Enforce that members of one group can't see another group's channels
This is the "AIWF AI sets up its own chat workspace" use case. By extension, anyone running their own chatalot install whose ops AI manages workspace lifecycle.
Prerequisites
The following must be in place before the AI can drive the API:
- A bot account on the chatalot instance, provisioned by an
instance admin via
POST /api/admin/bots(CHAT-10 Phase 1, ships in v0.25.10+). The admin then mints a long-lived API token viaPOST /api/admin/bots/{id}/tokensand hands the plaintext to the AI exactly once. See "Provisioning the bot" below. - An understanding of which user IDs the AI will be assigning roles to.
Either the AI is provisioning empty groups (and inviting humans
separately) or it's been given a roster of
{user_id, role, group}tuples by the operator. - The chatalot instance URL (e.g.
https://chat.aiworkforcetx.com). - A docs reference:
docs/developer-guide/api-reference.mdis the canonical API surface;docs/admin-guide/role-hierarchy.mdis the permission model the AI should respect.
Provisioning the bot (operator side, one-time)
The operator (a real human admin) does this once per AI workspace, before handing anything to the AI. Two equivalent paths.
There's a third path — bot:provision-scoped integration tokens
(CHAT-62b4cd3a, v0.25.21+) — when the consumer is an automation that
needs to mint its OWN bot user (e.g. Hivemind Connect). See
Integration tokens below the two paths.
Option A: Web UI (recommended, v0.25.10+)
- Sign in to the chatalot instance as an admin / owner.
- Open the Admin Panel (gear icon → Admin) → Bots tab.
- Click + New Bot, fill in the username + display name (e.g.
aiwf-ops-bot/ "AIWF Ops Bot"), submit. - The new bot's detail panel opens automatically. Click + Mint
Token, give it a label (
production), optionally an expiry, submit. - The plaintext token displays once at the top of the panel with a Copy button. This is the only time you can see it — copy it now and store it somewhere the AI can read it.
- Add the bot to a community (v0.25.17+, CHAT-160): from the same Bots admin tab, select the bot and use the "Add to Community" action to add it to one or more communities. The bot auto-joins that community's public channels and becomes a real member. You can also add it to specific private channels from those channels' member settings. Once added, the bot can use the full plaintext management API surface (webhooks, member lists, channel management) from within the community.
To revoke later: open the bot's detail panel, click Revoke on the token row.
Integration tokens (v0.25.21+, recommended for productized installers)
If the consumer is an installer that does its own bot + token
provisioning (Hivemind Connect is the canonical case), don't give it
your admin session JWT. Mint a bot:provision-scoped integration
token instead:
- Sign in to the chatalot instance as an admin / owner.
- Open the Admin Panel → Integrations tab.
- Click + Generate Integration Token. Label it
hivemind(or whatever the consumer is). Scope: bot:provision. Pick an expiry if the integration only needs it once. - The plaintext token displays once at the top of the page with a Copy button. Paste it into the installer's Connect modal.
- The installer uses the token (via
X-Bot-Token) to callPOST /api/admin/botsandPOST /api/admin/bots/{id}/tokensto create its own per-workspace bot + long-lived bot token. After that, it can revoke the integration token — or leave it active and revoke from the Integrations tab when the installer is done.
bot:provision tokens cannot send messages, manage communities, or
hit any other API route. If the token leaks, the blast radius is
"create more bot users on this instance" — and the operator can
revoke from the same Integrations tab they minted it on.
Option B: API (scripted, equivalent surface)
# 1. Create the bot account. Returns a bot user record.
curl -X POST https://chat.example.com/api/admin/bots \
-H "Authorization: Bearer $YOUR_ADMIN_JWT" \
-H "Content-Type: application/json" \
-d '{
"username": "aiwf-ops-bot",
"display_name": "AIWF Ops Bot",
"bio": "Manages AIWF chat workspace; provisions channels, mints webhooks."
}'
# => 201 { "id": "<bot-uuid>", "username": "aiwf-ops-bot", ... }
# 2. Mint a long-lived token for the bot. RESPONSE INCLUDES PLAINTEXT
# TOKEN — capture it now; the server stores only the hash.
curl -X POST https://chat.example.com/api/admin/bots/<bot-uuid>/tokens \
-H "Authorization: Bearer $YOUR_ADMIN_JWT" \
-H "Content-Type: application/json" \
-d '{ "label": "production", "expires_in_hours": null }'
# => 201 {
# "id": "<token-uuid>",
# "label": "production",
# "token_prefix": "cb_EXAMPLE",
# "token": "cb_<your-bot-token>", <-- give this to the AI
# "expires_at": null,
# "created_at": "..."
# }
# 3. (Optional) Promote the bot to community admin / community owner of
# the workspace it's about to manage. Either by inviting it to the
# community (POST /api/communities/{cid}/invites with grants_role)
# and accepting the invite as the bot, or by direct DB role-set.
# For the AIWF case where the bot creates the community, it
# auto-becomes community owner at creation time, so this step is
# skippable — the bot just creates the community in step 2 of the
# Provisioning Recipe.
The operator hands the plaintext token value to the AI as a config
secret. The AI uses it on every API call:
No login flow, no JWT refresh, no expiring access tokens. The token is
the credential. To revoke, the operator calls
POST /api/admin/bots/{id}/tokens/{tid}/revoke and the next request
the AI makes returns 401.
What if you're on chatalot < v0.25.10?
CHAT-10 Phase 1 (bot accounts + long-lived tokens) ships in v0.25.10. If you're running an older managed-update channel, the interim path is:
- Create a regular user account for the AI via the admin panel.
- Promote it to community admin/owner of the workspace.
- Hand the AI the username/password.
- The AI authenticates via
POST /api/auth/loginand manages refresh-token rotation in its session loop (see "Token rotation in an AI session loop" below).
The interim path works against today's chatalot. Anywhere this doc
shows X-Bot-Token: <token>, substitute Authorization: Bearer
<access_token> and add a refresh-token loop around it.
The provisioning recipe
The AI's setup loop follows this order. Each step has the canonical HTTP call, the permission required, and what the response gives you.
1. Authenticate
POST /api/auth/login
Content-Type: application/json
{
"username": "aiwf-bot",
"password": "<from operator>"
}
Response:
{
"access_token": "<JWT>",
"refresh_token": "<hex>",
"user": { "id": "<uuid>", "username": "aiwf-bot", ... }
}
Save user.id (you'll be the auto-assigned community owner of any
community you create), access_token (use as Authorization: Bearer …
on every subsequent request), and refresh_token (hold for rotation).
2. Create the community
POST /api/communities
Authorization: Bearer <access_token>
Content-Type: application/json
{
"name": "AIWF",
"description": "AI Workforce Solutions internal chat",
"is_discoverable": false
}
Response includes the new id (community UUID). The authenticated user
is automatically assigned as community Owner at level 3.
Permission gate: Whether community creation is open or admin-only is controlled by the instance's
COMMUNITY_CREATION_MODEenv var (admin_onlydefault vsopen). If you're locked out, ask the instance operator to either flip the mode or provision the community on your behalf.
3. Lay out the groups
For each organizational unit (Admins, Devs, QA, Operations, Customer Success, Announcements):
POST /api/groups
Authorization: Bearer <access_token>
Content-Type: application/json
{
"community_id": "<community uuid>",
"name": "Engineering",
"description": "Devs + tech leads",
"is_public": false,
"allow_invites": false
}
is_public=false is the default and what you want for separation: only
explicitly-added members see the group's channels. If you set
is_public=true, every community member can see and join the group's
channels — useful for an "Announcements" group everyone reads, less
useful for a private "QA" group.
allow_invites=false (default) means only community moderators+ can
manage group invites. Set true if a group should be self-service.
The group is auto-assigned its creator (the AI's user) as Owner at
level 3 within the group. Save the returned id for the channel-create
step.
4. Create channels within each group
POST /api/groups/{group_id}/channels
Authorization: Bearer <access_token>
Content-Type: application/json
{
"name": "general",
"topic": "Eng team general discussion",
"channel_type": "text"
}
Repeat per channel per group. Conventional layout:
Admins
├─ leadership (text, private)
└─ strategy (text, private)
Engineering
├─ general
├─ code-review
└─ incidents
QA
├─ general
├─ release-testing
└─ bug-triage
Operations
├─ general
├─ monitoring
└─ on-call
Customer Success
└─ general
Announcements
└─ announcements (text, READ-ONLY for non-admins)
For the announcements channel specifically (read-only for most), set the read-only flag after creation:
PATCH /api/channels/{channel_id}
Authorization: Bearer <access_token>
Content-Type: application/json
{
"read_only": true
}
Now only channel owner + admin can post; moderators/members are
read-only. This pairs well with the webhook pattern (see
docs/integrations/hivemind.md) — mint a webhook on the channel, hand
the URL to the AI, and the AI posts announcements via that webhook
without needing send-message permission as a user.
5. Mint role-granting invites for human members
This is where CHAT-123's grants_role field shines. You can hand each
team a single invite link that auto-elevates them on accept:
POST /api/communities/{community_id}/invites
Authorization: Bearer <access_token>
Content-Type: application/json
{
"max_uses": null,
"expiry_hours": 720,
"grants_role": "moderator"
}
Roles you can grant: member (default), moderator, admin. You can
only grant a role strictly lower than your own. Since the AI is
community owner, all three are available.
Hand the resulting invite URL (https://your-chatalot.example.com/invite/<code>)
to the right team. They accept it and land in the community already at
the right tier — no manual promotion required after the fact.
Per-group invites. The above mints a community-level invite. For group-scoped invites (when groups are private), use
POST /api/groups/{id}/invitesinstead. Same shape; member is added to that group only with no community-wide elevation.
6. Add the AI's user back to the right groups
The AI is community owner but not automatically a member of every group it creates. If you want the AI in the announcements channel itself (so it can post messages directly without a webhook):
For private groups, the AI as community owner can add itself unconditionally (instance/community owner override). For groups where that override doesn't apply, mint a one-use group invite first.
7. Verify the layout works
After provisioning, the AI should run a sanity smoke before declaring the workspace ready:
GET /api/communities/{community_id}/groups
GET /api/groups/{group_id}/channels
GET /api/channels/{channel_id}/members
Cross-check: the right groups exist, each has the expected channels, and the channel member counts match what you intended. If you've also provisioned test users via invite, log in as one of them (briefly) and confirm they can only see groups they were added to.
Token rotation in an AI session loop
The access token expires (default 30 min). Refresh tokens are single-use and rotated on every refresh:
Response: new access_token and new refresh_token. The old refresh
token is now revoked. If your AI loses track of which refresh token
is current (e.g. crashes mid-rotation), the operator has to log it in
again to get a new pair.
Suggested loop, pseudocode:
on every API call:
if access_token expires within 60s:
try refresh()
if 401:
re-login from stored credentials
(or escalate to operator if credentials fail)
For long-lived sessions (a Hivemind agent running for days), persist the refresh token to a small encrypted local store between cycles. Don't keep it only in memory unless the agent process is genuinely stateless and the operator is fine re-logging-in on every restart.
Permission cascade — the model the AI must respect
Quick reference (full detail in docs/admin-guide/role-hierarchy.md):
Instance Owner (5) — bypasses everything
Instance Admin (4) — bypasses everything
Community Owner (3) — full control of one community
Community Admin (2) — manage settings, members, groups, channels
Moderator (1) — kick/ban/warn/timeout members
Member (0) — participate in channels they have access to
Same shape repeats at the Group scope (group owner / admin / member, no moderator) and at the Channel scope (owner / admin / moderator / member).
Critical rules the AI must encode:
- Strict-greater for moderation. A moderator can kick a member; not another moderator. An admin can kick a moderator; the owner can kick the admin. Role levels are integers; the actor's level must be strictly greater than the target's.
- Group membership gates channel visibility. A user in the "Engineering" group cannot see channels in the "QA" group simply by being community-level admin — they have to be a member of the group. (Instance admin/owner can see everything; that's the synthetic-role override.)
- Sender-key rotation on exit. When you remove a member from a community, group, or channel, the server rotates the group sender key for forward secrecy. The leaver retains chain-key seeds for pre-removal traffic but cannot decrypt anything new. This is automatic — the AI doesn't need to do anything to trigger it.
- Owner-only role changes at channel scope. An admin can change community-level roles (member ↔ moderator); only the owner can change channel-level roles. Plan accordingly when delegating to multiple admins.
Webhooks vs the user-API for posting
Two ways the AI can post messages in a channel:
| Mechanism | Auth | Granularity | Use when |
|---|---|---|---|
| Webhook | Per-channel token (no user session) | Channel-only, output only | One-way bots (announcements, alerts, scheduled reports) |
| User-API + send-message | The AI's bot token (X-Bot-Token) |
Full plaintext management surface in any channel the bot is a member of | Channel management, member lists, webhook creation, non-E2E ops |
For the announcements use case, prefer the webhook. It's operationally cleaner: no token rotation, no membership management, no risk of the AI accidentally using its broader user privileges. Mint one webhook per announcement channel, hand the URL to the AI, done.
For channel management, member administration, and webhook lifecycle, use the
bot-token path (X-Bot-Token). Long-lived bot tokens (v0.25.10+) are the
correct auth mechanism for all these operations.
E2E message content — out of the box vs. the bot-as-client path
Out of the box, a bot cannot read or send end-to-end-encrypted message content: it has no crypto identity (no identity key, signed prekey, or sender keys), because nothing generates them for it. It can interact with channels via webhooks (plaintext messages rendered server-side) and via the management API surface, but it cannot decrypt the E2E-encrypted messages that human members exchange. For announcement/output and channel/member management, that is the intended, simplest path — keep using webhooks and the bot token.
To make a bot a true E2E participant (read + post encrypted messages as a
member), ADR-002 is decided: Option B (bot-as-client) — the bot runs its
own crypto client and holds its own keys off-server, so E2E is preserved
end-to-end. The decision is recorded in ADR-002
(adr-002-chatalot-bot-e2e-messaging). Option A
(server-side bot keys) was rejected because it would weaken E2E for every
member of a bot's channels.
The chatalot-side contract a bot client implements — key registration, X3DH,
sender-key lifecycle, the WS message + rotation events, and the wire-format
gotchas (byte fields are JSON number arrays, not base64; chain_id is i64;
sends are WebSocket-only) — is specified in
developer-guide/bot-e2e-client.md.
The bot crypto client itself is built in Hivemind (HIVE-55/56/57), not in
chatalot; chatalot only ever holds public keys, opaque sender-key
distributions, and ciphertext. Until that client ships, design bot
integrations around webhooks (output) and the management API
(channel/member control).
Worked example: AIWF workspace from scratch
End-to-end provisioning script (Python pseudocode), assuming the AI has
been given username, password, and base_url:
import requests
s = requests.Session()
base = base_url
# 1. Authenticate
r = s.post(f"{base}/api/auth/login", json={"username": username, "password": password})
tok = r.json()["access_token"]
s.headers["Authorization"] = f"Bearer {tok}"
# 2. Community
r = s.post(f"{base}/api/communities", json={
"name": "AIWF", "description": "AI Workforce Solutions internal", "is_discoverable": False,
})
cid = r.json()["id"]
# 3. Groups + 4. Channels
groups = {
"Admins": ["leadership", "strategy"],
"Engineering": ["general", "code-review", "incidents"],
"QA": ["general", "release-testing", "bug-triage"],
"Operations": ["general", "monitoring", "on-call"],
"Customer Success": ["general"],
"Announcements": ["announcements"],
}
gids = {}
chids = {}
for gname, channels in groups.items():
r = s.post(f"{base}/api/groups", json={
"community_id": cid, "name": gname, "is_public": gname == "Announcements",
})
gids[gname] = r.json()["id"]
for cname in channels:
r = s.post(f"{base}/api/groups/{gids[gname]}/channels", json={
"name": cname, "channel_type": "text",
})
chids[(gname, cname)] = r.json()["id"]
# Make the announcements channel read-only
s.patch(f"{base}/api/channels/{chids[('Announcements', 'announcements')]}",
json={"read_only": True})
# 5. Role-granting invites for each team (operator hands these out)
invites = {}
for gname in ["Admins", "Engineering", "QA", "Operations", "Customer Success"]:
role = "admin" if gname == "Admins" else "moderator" if gname in {"Engineering", "Operations"} else "member"
r = s.post(f"{base}/api/communities/{cid}/invites", json={
"max_uses": None, "expiry_hours": 720, "grants_role": role,
})
invites[gname] = r.json()["code"]
# 6. AI joins announcements + the groups it'll post to
for gname in ["Announcements", "Engineering", "Operations"]:
s.post(f"{base}/api/groups/{gids[gname]}/join")
# 7. Smoke
r = s.get(f"{base}/api/communities/{cid}/groups")
assert len(r.json()) == 6, f"expected 6 groups, got {len(r.json())}"
print("OK")
print("Hand these invite codes out:", invites)
Run this once, hand the printed invite codes to the right teams, and the workspace is provisioned. The AI now has a community-owner session it can keep refreshing, and it can post announcements either via webhook (preferred) or directly as itself.
Errors you'll actually hit
A short field guide to the most common 4xx responses:
| Error | What it means | Fix |
|---|---|---|
401 Unauthorized |
Access token expired or invalid | Refresh the token; on persistent 401, re-login |
403 Forbidden |
You're authenticated but the role isn't high enough | Check whether the AI's user has the right role at the right scope; instance admins/owners bypass community gates but not the strict-greater moderation rule |
404 Not Found |
The community/group/channel doesn't exist OR you don't have access to it (chatalot returns 404 instead of 403 for some endpoints to avoid leaking existence) | Verify the UUID and that you're a member |
409 Conflict |
Duplicate name (community, group, or channel name already exists at this scope) | Pick a different name; chatalot enforces uniqueness within scope |
400 Validation |
A field is missing or fails validation (e.g. name too long, invalid grants_role) |
Inspect the response body — chatalot returns a specific error string |
If you're driving this from an AI agent, plumb each of these into a retry policy: 401 → refresh; 5xx → exponential backoff; 4xx → give up and surface to the operator, don't loop.
Related docs
- API Reference — full HTTP surface
- Role Hierarchy — permission cascade detail
- Group Permissions — group-scope role rules
- Channel Permissions — channel-scope role rules
- Webhooks — the simpler one-way bot mechanism
- Hivemind integration — the operator-side runbook for wiring Hivemind agents to channels via webhook