Skip to content

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 via POST /api/admin/bots/{id}/tokens and 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.md is the canonical API surface; docs/admin-guide/role-hierarchy.md is 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.

  1. Sign in to the chatalot instance as an admin / owner.
  2. Open the Admin Panel (gear icon → Admin) → Bots tab.
  3. Click + New Bot, fill in the username + display name (e.g. aiwf-ops-bot / "AIWF Ops Bot"), submit.
  4. The new bot's detail panel opens automatically. Click + Mint Token, give it a label (production), optionally an expiry, submit.
  5. 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.
  6. 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.

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:

  1. Sign in to the chatalot instance as an admin / owner.
  2. Open the Admin PanelIntegrations tab.
  3. 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.
  4. The plaintext token displays once at the top of the page with a Copy button. Paste it into the installer's Connect modal.
  5. The installer uses the token (via X-Bot-Token) to call POST /api/admin/bots and POST /api/admin/bots/{id}/tokens to 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:

GET /api/communities
X-Bot-Token: cb_<your-bot-token>

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:

  1. Create a regular user account for the AI via the admin panel.
  2. Promote it to community admin/owner of the workspace.
  3. Hand the AI the username/password.
  4. The AI authenticates via POST /api/auth/login and 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_MODE env var (admin_only default vs open). 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}/invites instead. 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):

POST /api/groups/{group_id}/join
Authorization: Bearer <access_token>

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:

POST /api/auth/refresh
Content-Type: application/json

{ "refresh_token": "<old refresh token>" }

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.