Skip to main content

Identity & Principals

Every v1 acting userId emitted by chat history/search or the chat.message and chat.reaction webhooks resolves through user.info under the same credentials, and any accompanying userType or messageAuthorType equals user.info.type.

Roam uses one principal ID namespace for people and automated actors. A principal always has one of two public types:

PrincipaltypeAdditional signal
Workspace memberuserMember fields such as isAdmin are present
Guest (a person without membership in your account)userisGuest: true
Classic bot, agent, assistant, or coworkerbotbotCode and integrationId appear when that actor has them

Automated implementation details intentionally collapse to bot. Do not branch on display names or assume that every bot has botCode or integrationId.

Directory vs. hydration

An unfiltered user.list is the workspace member directory. It lists active members only; it never enumerates guests or bots.

Use user.list?ids=<id1,id2,...> to hydrate IDs you already hold. This mode accepts up to 100 comma-separated bare or tagged IDs, deduplicates them in first-seen order, and returns resolved principals in that order. It has no cursor and cannot be combined with q, limit, or cursor. Unknown IDs, groups, and principals outside your authorization boundary are silently omitted, so an empty result does not reveal whether an ID exists.

Explicit ID lookup is historical as well as current: user.info?id=... and user.list?ids=... can resolve archived or deactivated principals. This lets stored messages remain attributable after offboarding. Email lookup is different: it is member-only and requires user:read.email.

For user targets, a known UUID plus user:read is sufficient; prior shared-chat provenance is not required. Guests expose public display identity and isGuest: true, with email only when the credential also has user:read.email. Bots and agents are limited to the caller's Roam, assistants to the caller's account, and coworkers to the account of their owning principal. Singular unauthorized lookups return user_not_found; batch hydration silently omits them.

Avoid bot loops

History/search messages and chat.message webhooks include required userType. Reaction webhooks include both actor userType and messageAuthorType. Use these fields before doing any extra lookup:

if (event.data.userType === "bot") return;

When a surface supplies only an ID—such as group.members, reaction.list, poll voters, chat previews, or meeting participants—hydrate it with user.info or batch IDs through user.list?ids.

Address sidecars

Principals are only one kind of chat address. Mentions may also refer to groups or the literal all — the mention token's sigil tells you which: <@ID> is a principal (resolve with user.info), <!subteam^ID> is a group or channel (resolve with group.info).

For chat payloads, request expand=addresses on chat.list, chat.history, or chat.search to receive an address map keyed by UUID. chat.link.resolve does not support this expansion.

Slack capability mapping

Slack conceptRoam v1 equivalentImportant difference
users.listUnfiltered user.listRoam keeps this strictly as the active workspace member directory
users.infouser.infoRoam also hydrates guests and bot-like actors from the same userId namespace
users.info called repeatedlyuser.list?ids=Roam provides ordered bulk hydration for up to 100 IDs
is_restricted / guest userstype: "user", isGuest: trueRoam's guest signal means no membership in the caller's account
is_bot, bot_id, or app_idtype: "bot" plus optional botCode / integrationIdAgents, assistants, and coworkers intentionally share the bot vocabulary
Message bot subtype checksuserType / messageAuthorTypeRoam puts principal type directly beside actor IDs
<@U…> user mentions<@uuid>Same syntax; a |label suffix is accepted and ignored on write, reads are always bare
<!subteam^S…> group mentions<!subteam^uuid>Same syntax and semantics (notifies members); Roam also uses it for channel mentions, which Slack lacks
<!channel> broadcast<!channel>Same syntax and semantics; the legacy <@all> is accepted on write
<#C…> channel links, <!here>, <!everyone>Reserved — not tokensRoam has no channel links or presence/workspace-scoped notify yet; these stay literal text

The model is Slack-capable without copying Slack's split identity namespaces: one Roam userId is sufficient for attribution, loop prevention, and later hydration.