Skip to Content
SimulatorsSlackOverview

Simulators

Slack simulator

slack-twin is a local, stateful simulator of the Slack Web API for testing bots and agents. Conversations, chat, users, reactions, pins, bookmarks, files, views, canvases, lists, AI-app threads and search keep Slack’s own object shapes and its ok: false error envelope. A seeded workspace gives a bot something to read. The simulator behaves as Slack does unless a line says Differs from Slack.

The simulator never calls Slack and touches no real workspace. It accepts any token with a bot, user, app-level or workflow prefix (xoxb-, xoxp-, xapp-, xwfp-). It delivers an event only to an app you connect, once you choose how.

Quickstart

Install the CLI, sign in and boot the simulator with RYSTIC_URL pointing at it.

On Pro you sign in with an API key from your account .

curl -fsSL https://www.rystic.ai/install.sh | sh
rystic login rys_…   # your API key
export RYSTIC_URL=$(rystic run slack-twin -d --port 0 --scenario design_studio --json | jq -r .data.url)
curl -s "$RYSTIC_URL/_rystic/version"
# -> {..."product":"slack-twin"...}

On Enterprise, the licensed edition signs in with a license file in place of the key: rystic login ./rystic-license.json (licensing ).

First calls

Your token acts as the scenario’s bot, studiobot. Read the newest message in #help, post a reply and read the channel back:

curl -s "$RYSTIC_URL/api/conversations.history?channel=CWDC0000004&limit=1" -H 'Authorization: Bearer xoxb-local' | jq -c '.messages[0] | {user, text: .text[0:40]}'
# -> {"user":"UWDC0000022","text":"Reminder to nobody in particular: the sh"}
curl -s -X POST "$RYSTIC_URL/api/chat.postMessage" -H 'Authorization: Bearer xoxb-local' -d channel=CWDC0000004 -d 'text=hello from the quickstart' | jq -c '{ok, channel, ts}'
# -> {"ok":true,"channel":"CWDC0000004","ts":"1772323200.000001"}
curl -s "$RYSTIC_URL/api/conversations.history?channel=CWDC0000004&limit=1" -H 'Authorization: Bearer xoxb-local' | jq -c '.messages[0] | {user, text}'
# -> {"user":"UWDCBOT0001","text":"hello from the quickstart"}

Connect your client

Point an official Slack SDK at $RYSTIC_URL/api/ with any xoxb- token, and the rest of your code stays as it is. Python:

import os
from slack_sdk import WebClient

client = WebClient(token="xoxb-local", base_url=os.environ["RYSTIC_URL"] + "/api/")
result = client.conversations_history(channel="CWDC0000001", limit=10)

Node:

const { WebClient } = require("@slack/web-api");

const client = new WebClient("xoxb-local", {
  slackApiUrl: process.env.RYSTIC_URL + "/api/",
});
const result = await client.conversations.history({ channel: "CWDC0000001", limit: 10 });

For another Slack library, set its base URL as Point your client at it  describes. A library left on slack.com reaches the simulator only if the intercept gateway routes it.

Differs from Slack: every method is also served at /<method>, without Slack’s /api/ prefix, so a client configured with the bare origin works too.

Keeping Slack’s hostnames

To leave your client’s own configuration alone, boot with rystic run slack-twin -d --port 0 --intercept --scenario design_studio. The simulator then sits behind its project’s HTTPS gateway. Run your client with rystic exec -- <command>, or eval "$(rystic env)" in a shell, to configure clients that honor proxy and CA environment settings. Their calls to slack.com and slack-files.com then land on the simulator, and the URLs it returns keep Slack’s hostnames. Clients that bypass the gateway are not isolated by proxy settings or --strict.

File uploads and returned URLs

The three-step upload works as it does on Slack:

  1. files.getUploadURLExternal returns an upload_url.
  2. You POST the bytes to it, as a raw body or multipart, and get OK - <bytes>.
  3. files.completeUploadExternal shares the file.

Differs from Slack: every URL the simulator returns, such as upload_url, permalink and url_private, is on the origin your request reached. Called at $RYSTIC_URL, that is the simulator’s own address, never slack.com or slack-files.com. Set RYSTIC_PRESERVE_SERVICE_URLS=1 to keep the service hosts.

A GET of url_private or url_private_download returns:

RequestResponse
with a Bearer tokenthe uploaded bytes, under the file’s mimetype
without a token401
for a file whose bytes never reached the simulator404

An upload over 1 MiB (blob_cap_bytes in GET /_rystic/version) is accepted but not stored, so a GET of its url_private returns 404. A seeded file has no bytes either.

Each client’s quickstart was run against this simulator in each way it connects, with the vendor’s own hosts blocked.

ClientVersionKindWorks withFails withCheckedKnown gap
slackapi/slack-cli (GitHub Releases)v4.11.0clidefault host—2026-11-06—
slack_sdk (PyPI)v3.45.0sdkbase URL, default host—2026-11-06—
@slack/web-api (npm)v8.2.0sdkbase URL, default host—2026-11-06—

Auth

Send Authorization: Bearer <token>, or the token as a form-encoded body’s token= field. When a request carries both, the form token is the one read, right or wrong, as on Slack, and of two form tokens the last. Neither ?token= nor a JSON body’s token is read.

A request without a usable token returns:

Requestapi.testEvery other method
no token, or a header under another scheme, such as Basicoknot_authed
a malformed Authorization header: Bearer with nothing after it, or a second headernot_authednot_authed

Differs from Slack: Slack reads Bearer alone as no token, so its api.test returns ok.

Differs from Slack: Slack refuses a second Authorization header with invalid_auth.

The token’s prefix is its kind, as on Slack:

PrefixKind
xoxb-, or a rotated xoxe.xoxb-bot token
xoxp-, or a rotated xoxe.xoxp-user token
xapp-app-level token
xwfp-workflow token

A token with none of these prefixes, Bearer x included, returns 200 {"ok":false,"error":"invalid_auth"}.

Differs from Slack: the simulator holds no issued tokens, so it accepts whatever follows a known prefix.

A method meant for the other kind returns 200 {"ok":false,"error":"not_allowed_token_type"}:

Token the method needsMethods
a user tokensearch.*, reminders.*, users.identity, users.profile.set, users.setPhoto, users.deletePhoto
a bot tokenchat.startStream, chat.appendStream, chat.stopStream, assistant.threads.setStatus, assistant.threads.setSuggestedPrompts, assistant.threads.setTitle, agents.sessions.setStatus, agents.sessions.rename

The token alone names the caller only when it is bound to a user. In a loaded scenario every other token acts as the scenario’s bot (UWDCBOT0001, studiobot, in design_studio), and X-Rystic-Actor: <user id> acts as a seeded user instead (Identities).

A user’s role decides what it may delete, as on Slack. A workspace owner’s or admin’s user token deletes other people’s messages and the files it can see; anyone else’s token deletes only its own (Deleting other people’s messages and files).

An empty workspace (no --scenario) holds one bot user, U0000000001 (team T0000000001), and every other token acts as it. It holds no team record, so auth.test returns invalid_auth. Every channel is channel_not_found until you seed one or load design_studio.

API version

Slack’s Web API has no version to pin: a method is served by name, so the simulator serves the current method set at /api/<method> and at /<method>. A request that names a version in a header of its own gets the same response, with the same shapes. Response shapes follow what Slack’s API returned when the simulator was measured, and how fidelity is measured says how that is checked.

Errors and limits

Error format

A method returns a failure with HTTP 200 and {"ok":false,"error":"<code>"}, so check ok as well as the status. The one exception is the seeded 429 under Rate limits. Some refusals of an argument add response_metadata.messages, worded per method. conversations.list refuses a limit that is not a number with {"ok":false,"error":"invalid_arguments","response_metadata":{"messages":["[ERROR] input must match schema: value for limit could not be parsed as an integer"]}}. Other methods return the code alone, as conversations.members returns invalid_limit.

A refused conversations.invite also lists each user it turned down in errors, each entry carrying its own error and user. A success can carry a warning: conversations.join on a channel you are already in returns ok: true with warning and response_metadata.warnings set to already_in_channel.

Rate limits

The simulator counts no requests, so no volume of calls returns 429.

Differs from Slack: Slack limits each method’s calls per minute by its rate tier.

Two calls return ratelimited all the same:

CallResponse
reactions.list, when the user the caller acts as has "rate_limited": true in its state_user record (Seeding state)429 with Retry-After: 30 and {"ok":false,"error":"ratelimited"}
users.profile.get with include_labels=true, every time200 {"ok":false,"error":"ratelimited"}

Network profiles on Pro and Enterprise can reject a Slack method with 429 and the body you give it. Give the profile’s response headers what your client’s backoff reads, such as {"Retry-After": "30"}: without them, slack_sdk backs off about a second.

Pagination

The cursor methods take limit and cursor, and a page with more to come carries response_metadata.next_cursor. Pass it back as cursor until it is empty or absent. A next_cursor is the simulator’s own opaque value, so never compare it with live Slack’s. These page sizes are the simulator’s, and “(unconfirmed)” marks those not yet checked against Slack:

MethodDefault limitLargest limitA cursor the simulator did not issue
conversations.history100999, a larger value cut to it (unconfirmed)invalid_cursor
conversations.list1001000, a larger value cut to itinvalid_cursor
conversations.members1001000, a larger value cut to it (unconfirmed); 0 or less returns invalid_limitinvalid_cursor
conversations.replies1000noneinvalid_cursor
users.conversationsevery conversation999, a larger value cut to itinvalid_cursor
users.listevery usernone (unconfirmed)invalid_cursor
reactions.listevery itemnone (unconfirmed)invalid_arguments
chat.scheduledMessages.list100none (unconfirmed)invalid_cursor
slackLists.items.listevery itemnone (unconfirmed)invalid_cursor
files.info, for the file’s commentsevery commentnone (unconfirmed)invalid_arguments

These methods page by page and count instead, and return a paging object with the page numbers and the total:

MethodDefault countLargest count (unconfirmed)
files.list100none
search.messages20100, a larger value cut to it
search.all, search.files20none

/_rystic/page-cap caps a method’s limit or count, so a test reaches page two without seeding a full page. Name the method without /api, as GET /conversations.list.

Idempotency

Not applicable: Slack’s Web API takes no idempotency key, so sending chat.postMessage twice posts two messages. A repeated state change is refused instead:

Repeated callError
reactions.addalready_reacted
pins.addalready_pinned
conversations.archivealready_archived
conversations.create with a taken namename_taken
usergroups.create with a taken namename_already_exists

Coverage

The simulator covers Slack’s conversations, chat, users, reactions, pins, bookmarks, files, views, canvases, lists, reminders, user groups, search and team methods. It also serves the AI-app thread methods assistant.threads.setStatus, setSuggestedPrompts and setTitle, and agents.sessions.setStatus and rename. It serves every method to both GET and POST, with form-encoded or JSON bodies; files.getUploadURLExternal takes a form only. An unknown method returns 200 {"ok":false,"error":"unknown_method","req_method":"<name>"}. Out of scope are admin.*, Slack Connect administration, DMs with external members, Enterprise Grid and files.upload, which Slack retired. So are interactions beyond a message’s buttons and a modal’s submit: select menus, shortcuts, slash commands and views.push.

Guests and Slack Connect channels shared with another organization are served (Guests and Slack Connect), and Search has Slack’s query modifiers. The Slack methods in the published spec that the simulator leaves out are under Limitations, and a running simulator lists its own routes at GET /_rystic/routes.

448 routes, counted from the simulator’s route table: 232 listed below, and 216 more that answer the same operations at other paths.

Show all operations
ResourceOperations
Agent session (2)GET, POST /api/agents.sessions.rename; GET, POST /api/agents.sessions.setStatus
Assistant thread attributes (3)GET, POST /api/assistant.threads.setStatus; GET, POST /api/assistant.threads.setSuggestedPrompts; GET, POST /api/assistant.threads.setTitle
Bookmark (3)GET, POST /api/bookmarks.add; GET, POST /api/bookmarks.edit; GET, POST /api/bookmarks.remove
Canvas (6)GET, POST /api/canvases.create; GET, POST /api/canvases.delete; GET, POST /api/canvases.edit; GET, POST /api/canvases.getContent; GET, POST /api/canvases.sections.lookup; GET, POST /api/conversations.canvases.create
Canvas access grant (2)GET, POST /api/canvases.access.delete; GET, POST /api/canvases.access.set
Channel (conversation) (16)GET, POST /api/bookmarks.list; GET, POST /api/chat.getPermalink; GET, POST /api/chat.postEphemeral; GET, POST /api/conversations.archive; GET, POST /api/conversations.create; GET, POST /api/conversations.history; GET, POST /api/conversations.info; GET, POST /api/conversations.list; GET, POST /api/conversations.members; GET, POST /api/conversations.open; GET, POST /api/conversations.rename; GET, POST /api/conversations.replies; GET, POST /api/conversations.setPurpose; GET, POST /api/conversations.setTopic; GET, POST /api/conversations.unarchive; GET, POST /api/pins.list
Channel membership (7)GET, POST /api/conversations.close; GET, POST /api/conversations.invite; GET, POST /api/conversations.join; GET, POST /api/conversations.kick; GET, POST /api/conversations.leave; GET, POST /api/conversations.mark; GET, POST /api/users.conversations
Custom emoji (1)GET, POST /api/emoji.list
File (6)GET, POST /api/files.completeUploadExternal; GET, POST /api/files.delete; GET, POST /api/files.getUploadURLExternal; GET, POST /api/files.info; GET, POST /api/files.list; GET, POST /api/search.files
List access grant (2)GET, POST /api/slackLists.access.delete; GET, POST /api/slackLists.access.set
List export job (1)GET, POST /api/slackLists.download.start
List item (row) (4)GET, POST /api/slackLists.items.create; GET, POST /api/slackLists.items.delete; GET, POST /api/slackLists.items.deleteMultiple; GET, POST /api/slackLists.items.update
Message (9)GET, POST /api/chat.appendStream; GET, POST /api/chat.delete; GET, POST /api/chat.meMessage; GET, POST /api/chat.postMessage; GET, POST /api/chat.startStream; GET, POST /api/chat.stopStream; GET, POST /api/chat.update; GET, POST /api/search.all; GET, POST /api/search.messages
Pinned item (2)GET, POST /api/pins.add; GET, POST /api/pins.remove
Reaction (4)GET, POST /api/reactions.add; GET, POST /api/reactions.get; GET, POST /api/reactions.list; GET, POST /api/reactions.remove
Reminder (5)GET, POST /api/reminders.add; GET, POST /api/reminders.complete; GET, POST /api/reminders.delete; GET, POST /api/reminders.info; GET, POST /api/reminders.list
Scheduled message (3)GET, POST /api/chat.deleteScheduledMessage; GET, POST /api/chat.scheduleMessage; GET, POST /api/chat.scheduledMessages.list
Slack List (5)GET, POST /api/slackLists.create; GET, POST /api/slackLists.download.get; GET, POST /api/slackLists.items.info; GET, POST /api/slackLists.items.list; GET, POST /api/slackLists.update
Team (workspace) (2)GET, POST /api/team.info; GET, POST /api/team.profile.get
User (14)GET, POST /api/auth.test; GET, POST /api/dnd.info; GET, POST /api/dnd.teamInfo; GET, POST /api/users.deletePhoto; GET, POST /api/users.getPresence; GET, POST /api/users.identity; GET, POST /api/users.info; GET, POST /api/users.list; GET, POST /api/users.lookupByEmail; GET, POST /api/users.profile.get; GET, POST /api/users.profile.set; GET, POST /api/users.setActive; GET, POST /api/users.setPhoto; GET, POST /api/users.setPresence
User Group (7)GET, POST /api/usergroups.create; GET, POST /api/usergroups.disable; GET, POST /api/usergroups.enable; GET, POST /api/usergroups.list; GET, POST /api/usergroups.update; GET, POST /api/usergroups.users.list; GET, POST /api/usergroups.users.update
View (modal/home) (3)GET, POST /api/views.open; GET, POST /api/views.publish; GET, POST /api/views.update
Other (1)GET, POST /api/api.test

Also served, outside the model: POST /actions/{token}, POST /api/apps.connections.open (with SLACK_TWIN_SOCKET_MODE=1), POST /api/assistant.threads.open, POST /api/assistant.threads.setContext, POST /api/blocks.actions, POST /api/views.submit, POST /apps.connections.open (with SLACK_TWIN_SOCKET_MODE=1), POST /assistant.threads.open, POST /assistant.threads.setContext, POST /blocks.actions, GET /files-pri/{tf}/download/{name}, GET /files-pri/{tf}/{name}, GET /img/avatars/{name}, POST /upload/v1/{ticket}, PUT /upload/v1/{ticket}, POST /views.submit.

Scenarios

The simulator ships 1 scenario; the quickstart boots design_studio.

ScenarioWhat it is forWhat it seedsClock
design_studio (default)a whole web design studio’s Slack a year inbookmark 74, channel 60, connected_team 2, file 48, membership 451, message 2958, pin 45, reaction 324, scheduled_message 4, team 1, user 472026-03-01T00:00:00Z

What each scenario holds:

  • design_studio:
    • Sixty conversations — five company channels, five craft channels, ten live client projects, two Slack Connect channels shared with clients, four private channels, five DMs, a group DM and twenty-eight archived projects — and 2,958 messages from a cast of forty-five, with the pins, bookmarks, files, threads and queued messages a year leaves behind.
    • The token is studiobot, and every question the workspace asks it — who is on call, when a client launches, how a site is rolled back — is answered by a record inside this binary.

Boot one by name, or load one into a running simulator:

rystic run slack-twin -d --port 0 --scenario design_studio
curl -s -X POST "$RYSTIC_URL/_rystic/scenario" -d '{"name":"design_studio"}'

What the scenarios do not hold

  • design_studio is the only scenario. Its token acts as studiobot (UWDCBOT0001). #general is CWDC0000001 and #help is CWDC0000004.
  • Without --scenario, the simulator boots an empty workspace with no team and one bot user, and POST /_rystic/reset returns to it. Seed the workspace you need (Seeding state).
  • The refund and ops scenarios of earlier releases are not shipped; rebuild such a workspace with POST /_rystic/seed.

Seeding state

Each family of records is one register named state_<family>, such as state_team, state_user, state_channel or state_message. A register holds Slack’s own object shapes, each filed under a key. Most are keyed by id. Some are not, such as these:

RegisterKey
state_messagechannel + ts
state_membershipchannel + user
state_reactionchannel + timestamp + name: one record per emoji, carrying the reacting users and their count

Membership is its own register. A channel with messages but no state_membership row returns not_in_channel to every reader, as on Slack. Seed a workspace with one user, one channel and one message, then read it back:

curl -s -X POST "$RYSTIC_URL/_rystic/reset" -o /dev/null -w '%{http_code}\n'
# -> 204
curl -s -X POST "$RYSTIC_URL/_rystic/seed" -o /dev/null -w '%{http_code}\n' -d '{"state_team":{"T0SEED":{"id":"T0SEED","name":"Seed Workspace","domain":"seed"}},"state_user":{"member":{"id":"U0SEED001","name":"seeduser","real_name":"Seed User","team_id":"T0SEED"}},"state_channel":{"C0SEED001":{"id":"C0SEED001","name":"seeded","is_channel":true,"is_archived":false,"created":1767225600,"creator":"U0SEED001"}},"state_membership":{"C0SEED001/U0SEED001":{"channel":"C0SEED001","user":"U0SEED001"}},"state_message":{"C0SEED001/1767225600.000100":{"channel":"C0SEED001","ts":"1767225600.000100","type":"message","text":"seeded hello","user":"U0SEED001"}}}'
# -> 204
curl -s "$RYSTIC_URL/api/conversations.history?channel=C0SEED001" -H 'Authorization: Bearer xoxb-local' | jq -c '{ok, messages: [.messages[] | {text, user}]}'
# -> {"ok":true,"messages":[{"text":"seeded hello","user":"U0SEED001"}]}
curl -s "$RYSTIC_URL/api/auth.test" -H 'Authorization: Bearer xoxb-local' | jq -c '{team_id, user_id}'
# -> {"team_id":"T0SEED","user_id":"U0SEED001"}

Key the user your app acts as under member, as this seed keys U0SEED001: every token then acts as it. Keyed under its id alone, the user joins the empty workspace’s bot, U0000000001, which every token still acts as. views.open needs a seeded state_trigger for the trigger_id it names.

The simulator accepts any JSON-object record in a known register without checking its fields.

GET /_rystic/inspect keeps actor-label aliases such as member, so a dump posted back after a reset restores who each caller acts as; ?view=identities, a display projection that folds them, is no backup.

Scenarios and seeding covers the seed response, refusals and boot files.

Webhooks

The simulator renders an event for each Web API call that causes one, and delivers it to your app only when you choose a mode. Events API has the full event table, retries and the ways delivery differs from Slack.

  • Delivery switch. --env SLACK_TWIN_SOCKET_MODE=1 serves Socket Mode: apps.connections.open returns a one-time ws:// URL on the simulator. --env SLACK_TWIN_EVENTS_URL=<your URL> posts each event_callback to a request URL instead, and needs --env SLACK_TWIN_SIGNING_SECRET=<secret> beside it: the simulator refuses to start without one. With neither mode set, nothing is delivered.
  • Signing. A request-URL delivery is signed with SLACK_TWIN_SIGNING_SECRET, so Bolt’s verification passes. Delivery starts with Slack’s signed url_verification request, and your app replies with its challenge.
  • Retries. An event your app does not ack within 3 seconds is redelivered after 0 seconds, 1 minute and 5 minutes. A Socket Mode envelope carries retry_attempt and retry_reason, and a request-URL delivery the x-slack-retry-num and x-slack-retry-reason headers. A non-2xx response carrying x-slack-no-retry: 1 ends the retries.
  • The log. GET /_rystic/egress records every event your calls render, delivered or not, and GET /_rystic/settle waits until your bot’s turn is over.
  • Interactions. A person’s button press or modal submit reaches your app as block_actions or view_submission (Interactivity).

Features

Each feature has its own page:

  • AI-app threads: Set an AI app thread’s status, title and suggested prompts as Bolt’s Assistant class does, and open the app’s container as a person so your app hears assistant_thread_started.
  • Bot turns: GET /_rystic/settle tells your driver when your bot’s turn is over, and the call log shows what the bot did in it.
  • Events API: Slack’s Events API for your bot over Socket Mode or a request URL, with retries and app mentions.
  • Guests and Slack Connect: Guests whose own tokens see only their channels and the people in them, and Slack Connect channels shared with members of another organization, as a bot reads them.
  • Identities: Several agents share one workspace, each under its own token, acting as a seeded user, with a call log for grading.
  • Interactivity: A person presses a button on your bot’s message or submits its modal, and your app receives Slack’s block_actions or view_submission payload.
  • MCP server: The unmodified korotovsky Slack MCP server runs against the simulator through four tools: channel discovery, history, thread replies and posting a reply.
  • Search: search.messages, search.all and search.files with Slack’s query modifiers, checking access for messages; files are searched by text, and only a guest’s search.files is limited to its own files and its conversations’ files.
  • Verified UI tasks: Seed a task’s workspace and timed events, reset between episodes, grade what the agent did and size how many environments fit on one box.
  • Verified UI: A Slack client that runs on the simulator’s Web API, run beside the simulator or as one container per task.
  • Workspace UI: A browser view of the workspace at /_rystic/ui that draws the simulator’s state as a Slack client and redraws as your bot calls the API.

Determinism and reset

A message’s ts is both its id and its timestamp, and it comes from the simulator’s clock, so pin the clock before a run you want byte-identical.

Schedule relative to the scenario’s clock: a chat.scheduleMessage post_at from your wall clock is more than 120 days ahead of design_studio and returns time_too_far. post_at=1772326800, an hour past the scenario’s clock (1772323200), is accepted. An empty workspace’s clock starts at boot time (initial_clock in GET /_rystic/version).

Reset clears the token bindings. The clock moves:

  • a scheduled message, posted once when the clock reaches its post_at (Events API for the events it sends);
  • a request-URL redelivery, which also runs on the wall clock;
  • an interaction’s trigger_id (3 seconds) and response_url (30 minutes) (Interactivity);
  • a timeline event a scenario file or POST /_rystic/inject queues (Verified UI tasks).

Reset and determinism covers reset, the clock and the RNG.

Limitations

Known issues.

  • assistant_thread_started and assistant_thread_context_changed, which assistant.threads.open and assistant.threads.setContext send, follow Slack’s documented examples. They were never measured against Slack: the Slack app the simulator is measured with has no Agent experience, which cannot be turned off once on.

Beyond the known issues, 246 of the 354 operations in the published spec are not served, 1 success response may leave out fields the spec requires, 763 documented responses have no test yet and 132 behaviors are not modeled. The details follow.

Show all limitations

Not served. 246 of the 354 operations in the published spec are not served.

  • agents, 2 of 11 served: POST /api/agents.conversations.archive, POST /api/agents.conversations.create, POST /api/agents.conversations.getCanvas, POST /api/agents.conversations.listViews, POST /api/agents.conversations.removeView, POST /api/agents.conversations.setCanvasContent, POST /api/agents.conversations.setCommands, POST /api/agents.conversations.setProperties, … and 1 more
  • assistant, 3 of 5 served: POST /api/assistant.search.context, POST /api/assistant.search.info
  • auth, 1 of 3 served: GET /api/auth.revoke, POST /api/auth.teams.list
  • chat, 12 of 13 served: POST /api/chat.unfurl
  • conversations, 19 of 28 served: POST /api/conversations.acceptSharedInvite, POST /api/conversations.approveSharedInvite, GET /api/conversations.declineSharedInvite, POST /api/conversations.externalInvitePermissions.set, POST /api/conversations.inviteShared, POST /api/conversations.listConnectInvites, POST /api/conversations.requestSharedInvite.approve, POST /api/conversations.requestSharedInvite.deny, … and 1 more
  • dnd, 2 of 5 served: POST /api/dnd.endDnd, POST /api/dnd.endSnooze, POST /api/dnd.setSnooze
  • files, 5 of 15 served: POST /api/files.comments.delete, POST /api/files.remote.add, GET /api/files.remote.info, GET /api/files.remote.list, POST /api/files.remote.remove, GET /api/files.remote.share, POST /api/files.remote.update, POST /api/files.revokePublicURL, … and 2 more
  • team, 2 of 9 served: GET /api/team.accessLogs, GET /api/team.billableInfo, POST /api/team.billing.info, POST /api/team.externalTeams.disconnect, GET /api/team.externalTeams.list, GET /api/team.integrationLogs, POST /api/team.preferences.list
  • users, 12 of 13 served: POST /api/users.discoverableContacts.lookup
  • views, 3 of 4 served: POST /api/views.push
  • Not served at all: admin (128), apps (23), blocks (1), bots (1), calls (6), dialog (1), entity (3), functions (8), migration (1), oauth (6), openid (2), rtm (2), slackbot (7), stars (3), tooling (1), workflows (8)

Missing response fields. The spec requires these fields in a success response, and the simulator may leave them out:

  • GET, POST /api/conversations.leave (200): error

Not tested. 884 tests compare the simulator with the real service. 763 documented responses have no test yet, and 227 of them need a credential our test account doesn’t hold (200 ×3, 401 ×74, 403 ×148, 404 ×2). The other 536:

OperationStatusWhy
GET, POST /api/agents.sessions.rename200a well-formed request does not provoke it (3 cases)
GET, POST /api/agents.sessions.rename200no known request reaches it
GET, POST /api/agents.sessions.rename429no request a client sends provokes it
GET, POST /api/agents.sessions.setStatus200a well-formed request does not provoke it (2 cases)
GET, POST /api/agents.sessions.setStatus200no known request reaches it
GET, POST /api/agents.sessions.setStatus429no request a client sends provokes it
GET, POST /api/api.test200a well-formed request does not provoke it (2 cases)
GET, POST /api/api.test429no request a client sends provokes it
GET, POST /api/assistant.threads.setStatus200a well-formed request does not provoke it (2 cases)
GET, POST /api/assistant.threads.setStatus429no request a client sends provokes it
GET, POST /api/assistant.threads.setSuggestedPrompts200a well-formed request does not provoke it (2 cases)
GET, POST /api/assistant.threads.setSuggestedPrompts200no known request reaches it (2 cases)
GET, POST /api/assistant.threads.setTitle200a well-formed request does not provoke it (2 cases)
GET, POST /api/assistant.threads.setTitle200no known request reaches it (2 cases)
GET, POST /api/assistant.threads.setTitle429no request a client sends provokes it
GET, POST /api/auth.test200a well-formed request does not provoke it (2 cases)
GET, POST /api/auth.test429no request a client sends provokes it
GET, POST /api/bookmarks.add200a well-formed request does not provoke it (3 cases)
GET, POST /api/bookmarks.add200no known request reaches it (2 cases)
GET, POST /api/bookmarks.add200a test against the service contradicted the documentation
… and 271 more

Not modeled. Behavior the simulator does not model, by operation:

  • feature_disabled (the agent tasks feature is off for the workspace) and no_permission: documented in x-slack-errors but never observed; the workspace answers not_authorized instead. (GET, POST /api/agents.sessions.rename)
  • icon_emoji, icon_url and username are documented as cleared by passing null; the clearing semantics are unmeasured since no success is reachable. (GET, POST /api/agents.sessions.setStatus)
  • No in-scope read returns the stored status, loading_messages, icon or username, so the write is unverifiable by readback; only the ok/error envelope is measured. (GET, POST /api/assistant.threads.setStatus)
  • The status’s documented two-minute timeout (the service removes the status if no message is sent) is not modelled: no trace measures the expiry. (GET, POST /api/assistant.threads.setStatus)
  • 429 rate-limited response (Retry-After header): the spec declares the status with no body and no capture shows one, so no branch is declared. (GET, POST /api/assistant.threads.setSuggestedPrompts)
  • No in-scope read returns the stored prompts, so the write is unverifiable by read-back; its effect rests on the operation’s documented purpose. (GET, POST /api/assistant.threads.setSuggestedPrompts)
  • invalid_charset / invalid_post_type / missing_post_type / invalid_form_data: service-wide encoding refusals not captured on this method. (GET, POST /api/assistant.threads.setSuggestedPrompts)
  • missing_thread_ts: in the error enum but never observed — the thread_ts-less call answered not_agent_app on every capture. (GET, POST /api/assistant.threads.setSuggestedPrompts)
  • static_prompts_configured: documented for a thread_ts-less call by an agent app with static prompts configured; unreachable for the capture app, which answers not_agent_app first. (GET, POST /api/assistant.threads.setSuggestedPrompts)
  • No in-scope read returns the stored title, so the write is unverifiable by read-back; only the ok/error answer is measured. (GET, POST /api/assistant.threads.setTitle)
  • Whether a title has a maximum length at all is unknown: 300 characters answered ok and no larger value was sent. (GET, POST /api/assistant.threads.setTitle)
  • invalid_form_data (a body mislabelled as form data) is listed in x-slack-errors but no observation on this method isolates it, so no branch is declared for it. (GET, POST /api/assistant.threads.setTitle)
  • invalid_bookmark_type: bookmarks.edit exposes no type argument, so no request shape in scope reaches this error. (GET, POST /api/bookmarks.edit)
  • Whether the service accepts channel_ids and user_ids together, or only exactly one of them: invalid_parameters says ‘One of user_ids and channel_ids must be defined’ without stating that both is a failure. (GET, POST /api/canvases.access.delete)
  • Permission, plan and enterprise refusals (access_denied, no_permission, restricted_action, canvas_disabled_user_team, enterprise_is_restricted, team_access_not_granted, ekm_access_denied, org_login_required) are listed in x-slack-errors with no condition that distinguishes them on this method. (GET, POST /api/canvases.access.set)
  • The 1..20 bound on channel_ids / user_ids has no documented error code. (GET, POST /api/canvases.access.set)
  • The access_level value space (read/write/owner) is documented in prose, but the error a rejected value produces (invalid_arguments?) is not stated, so no branch is declared for it. (GET, POST /api/canvases.access.set)
  • Whether every listed channel or user must resolve before any grant is written (all-or-nothing) or the method writes what it can and lists the rest in failed_to_update_*_ids is not documented. (GET, POST /api/canvases.access.set)
  • failed_to_update_user_ids / failed_to_update_channel_ids appear both as success-envelope lists (partial failure) and as a documented error code; which conditions produce the ok-with-failures form versus the error form is not documented, so only the ok form is modeled. (GET, POST /api/canvases.access.set)
  • The platform-wide failure codes the error enum lists but no behaviour here distinguishes (access_denied, accesslimited, account_inactive, deprecated_endpoint, ekm_access_denied, enterprise_is_restricted, fatal_error, internal_error, invalid_arg_name, invalid_arguments, invalid_array_arg, invalid_charset, invalid_post_type, method_deprecated, missing_post_type, no_permission, org_login_required, request_timeout, restricted_action, service_unavailable, team_access_not_granted, team_added_to_org, token_expired, token_revoked, two_factor_setup_required). (GET, POST /api/canvases.create)
  • … and 112 more

By design.

  • Fidelity is measured, not total. A response field with no live example may differ from Slack’s. Assert on the fields your bot reads, and never byte-compare ts against live Slack.
  • One workspace per process. Your token acts in one team; the other organizations in its Slack Connect channels are records your bot reads, not workspaces a token acts in. Run one isolated workspace per episode; concurrent episodes need separate processes or containers.
  • Little moves without a request or a clock move. Presence changes only through explicit calls or seeded state, and ephemeral messages are transient activity for the workspace UI, not durable history or Events API deliveries.
  • Events and interactions are opt-in. Nothing is delivered until a run chooses a mode; assert on state through the Web API or /_rystic/inspect when you do not need delivery.
  • Token kinds are enforced, not sourced. The prefix decides what a token may call. views.open takes only the trigger_id of a seeded state_trigger, and the one a press delivers returns expired_trigger_id. users.setActive is a no-op.

Release notes: Changelog

Last updated on