Simulators
Slack changelog
What changed in each Slack simulator release, newest first.
v0.0.20 — 2026-11-04
-
The state UI is included on Free.
/_rystic/uiis available on every plan, including Free. The Pro-only verified UI download is a separate feature. -
Setting the clock never stamps records before the timeline events it fired. When
POST /_rystic/clock {"time": ...}fires timeline events due at its target, the next request’s records stamp after theirs instead of reusing their instant; a set past every event still stamps one millisecond past its target, as before. -
A paced clock keeps wall time, and
rystic run --speed 0stops it. UnderRYSTIC_PACE, each tick moves the clock by the wall time that passed since the last, so a tick missed while the twin was busy no longer makes timeline events fire late.rystic runhands the twin only the pace it resolved, so--speed 0, a scenario without events,--tapeand--livekeep the clock manual even when your shell exportsRYSTIC_PACE. -
Request-URL delivery retries as Slack does. An event your app does not ack with a 2xx within 3 s is retried after 0 s, 1 min and 5 min, re-signed and numbered in
x-slack-retry-numwithx-slack-retry-reason, and a non-2xx answer carryingx-slack-no-retry: 1stops that event’s retries. -
rystic grid -evalsturns on Slack event push for an agent that asks for it. The archive’s manifest declares how: an agent withevents: socketgets a twin started withSLACK_TWIN_SOCKET_MODE=1, and one withevents: webhook <port> <path>getsSLACK_TWIN_EVENTS_URLpointed athttp://127.0.0.1:<port><path>. -
Events pushed over Socket Mode carry the twin’s URLs. A URL on a Slack host inside an event pushed over Socket Mode, or read from
GET /_rystic/egress, is on the twin’s origin, as the Web API and request-URL delivery already answer it;RYSTIC_PRESERVE_SERVICE_URLS=1keeps Slack’s hosts. -
Swagger UI to read and call the twin from the browser.
/_rystic/swagger, linked from the state UI header, is Swagger UI over the published API trimmed to the operations this twin serves; Try it out sends each request to the twin. -
Moving the clock past a request-URL retry sends it at once. A request-URL retry still waits its 0 s, 1 min or 5 min on the wall clock, and
POST /_rystic/clockpast its due now sends it straight away; a retry waiting out its delay no longer refuses a new event asqueue_full. Thedeliveryreport onGET /_rystic/egressaddsretries_armed,next_retry_atandnext_retry_clock. -
Requests stay fast as scheduled messages accumulate. A request no longer rescans every queued scheduled message: the twin settles them only once a write lands or the next
post_atcomes due. -
The docs page lists the clients known to work. A new known-clients table under Connect your client names each official client checked against this simulator, the release checked, the ways it connects that work and fail, the day it was checked, and any known gap that covers a failure.
-
The @slack/web-api client’s first call is checked on every round. The
auth.testcall @slack/web-api makes before anything else is now a test measured against real Slack in every full round, so a change that breaks how the client starts shows up before release. -
A state dump seeds back as it was, and the next record gets a fresh id.
GET /_rystic/inspectposted toPOST /_rystic/seedafter a reset restores every record where it was, per-caller records included, which used to land under a doubled key. The next record created after any seed passes every uuid and event id the simulator minted that the seed holds.GET /_rystic/inspect?view=sequenceslists every id counter, and a world document’s newsequenceskey restores them, so ids the simulator renders from a counter continue where the dump left off. -
The verified UI shows the controls real Slack shows. The rail carries Activity, Agents & tools, Admin, Create new and focus mode in Slack’s order; the top bar carries Slackbot AI and Help; the channel header carries huddle, notifications, search in channel and a More actions menu (details, star, copy link, leave or close); the tabs carry Add canvas and +; the thread pane’s header carries Summarize and a More menu with Copy link; the composer carries More options and the hover bar Forward. Each control works against the twin or renders disabled and says why on click. An avatar shows the person’s photo when the twin serves one on its own origin, and
?theme=darkopens Slack’s dark theme. -
The verified Slack UI redraws after a reset. An open page shows the twin’s new state when the twin is reset or a scenario or seed document loads, with Socket Mode on or off, and forgets the drafts, saved items and sidebar state it kept from before, so an agent starting its next episode never sees the last one’s state. No reload by hand.
-
Each Socket Mode app gets its own copy of every event. An app-level token shaped like Slack’s,
xapp-1-<app id>-…, connects as that app, whose envelopes carry itsapi_app_idand whose acks and redeliveries are its own; any other token connects as the default app,AWDCBOT0001, as before. Two bots, or a bot beside the verified UI, each receive every event instead of splitting them, and a bot’s own connections still share its events without duplicates. A bot whose token carries a real app id now sees that id inapi_app_id; setSLACK_TWIN_APP_IDto it to keep the default app. -
Every member in the shipped scenario has a profile picture. In
design_studio,users.info,users.listandusers.profile.getanswer each member’simage_24throughimage_512as a default avatar on the simulator’s own address, and the simulator serves each image, so a Slack client draws a picture instead of a blank tile. -
The verified UI has Threads and Activity. The sidebar’s Threads opens every thread you started or replied in, newest activity first, each with its latest replies and a reply box. The rail’s Activity lists mentions of you, replies in your threads and reactions to your messages, filtered by kind, and each item opens its message. Both read the twin’s public history and replies and update live.
-
A DM’s details name the other member, and say when you closed it.
conversations.infoanswers a DM’suseras the other member from whichever side reads it, and afterconversations.closeanswersis_open: falsefor you untilconversations.openreopens it; the other member’s view is unchanged. -
chat.scheduledMessages.listlists only your own scheduled messages. Each token sees the messages it scheduled, never another member’s, even in a channel you share. Thedesign_studioscenario’s queued messages each name who scheduled them. -
The verified UI hides a DM you closed. Closing a direct message takes it out of the sidebar and the DMs list, and it stays out across reloads until you open it again or, for one you closed in this browser, the other person writes to you. The Scheduled tab no longer carries a notice that the twin lists everyone’s scheduled messages.
-
One container for the simulator and its verified UI. The Slack Verified UI page builds a single image from the simulator’s and the UI’s release images, with one health check, and shows how to seed a workspace per task, reset it between episodes, read the end state for grading, which UI features work and what each environment costs to run.
-
Search, reminders, user groups and custom emoji. The twin serves
search.messages,search.filesandsearch.all;reminders.add,reminders.list,reminders.info,reminders.completeandreminders.delete; the sevenusergroups.*methods; andemoji.list, each measured against live Slack. Search and reminders take a user (xoxp-) token, as Slack’s do, and answer a bot tokennot_allowed_token_type. -
A person can press your bot’s button or submit its modal. A Slack client calls
blocks.actionsorviews.submitwithX-Rystic-Surface: client, and the app that owns the message or view receives Slack’sblock_actionsorview_submissionpayload, over Socket Mode as aninteractiveenvelope or as a signedpayload=form post toSLACK_TWIN_INTERACTIVITY_URL(else the events URL), with atrigger_idand aresponse_urlthat replaces, deletes or answers the message. The client’s call waits for your app’s 3 s ack and hands back what it sent, such as a submit’sresponse_action. -
The verified UI searches and shows user groups, custom emoji and reminders. The top bar’s search and the channel header’s Search in channel open a results view with Messages and Files tabs, the query’s words lit and each result opening its message or thread. Custom emoji from
emoji.listfill the emoji picker’s Custom section and draw in messages and reactions, aliases included. A user-group mention draws as its@handleand opens a card of its members. The composer’s@offers groups. Directories gains a User Groups tab for creating, editing, disabling and enabling groups and for setting their members. A message’s More actions offers Remind me. Later lists your reminders with Mark complete and Delete. -
The verified UI presses your bot’s buttons. A Block Kit button in a message, in an actions block or as a section’s accessory, sends
blocks.actionsas the person viewing, after the button’s confirm dialog when it has one, so your app receivesblock_actionsand whatever it does to the message throughresponse_urlorchat.updateshows as it happens. When your app is not connected, does not ack within 3 s or answers an error, the button shows Slack’s generic error. Selects, overflow menus and date pickers stay disabled and say why. A modal your app opens does not show in the UI yet. -
A slow client no longer holds up the others. The simulator reads a request’s whole body before it starts serving it and sends the answer after, so a client that uploads slowly, or reads a large answer slowly, no longer delays other clients’ requests. Answers are unchanged; one larger than 4 MiB still makes other requests wait while it is sent.
-
Message search answers only from conversations you can read.
search.messagesandsearch.alllist a message only from a public channel, or from a private channel, DM or group DM you’re in, as Slack does: another member’s DM or a private channel you’re not in no longer shows up, in the matches or the totals. A DM’s match is typedimand named for the other member. File search (search.filesand the file results ofsearch.all) is unchanged. -
Search results link a reply into its thread and mark the words that matched. A reply’s
search.messagespermalink ends in?thread_ts=and its thread’s timestamp, andhighlight=truewraps each matched word in U+E000 and U+E001, as Slack does. -
Message search applies Slack’s search modifiers.
search.messagesandsearch.allnarrow their message results byin:(a channel as#name,name,<#C…>or<#C…|name>, a DM as<@U…>or@name),from:(<@U…>,@name,nameorme) andhas:link,has:pinorhas:reaction, as Slack does. They also narrow bybefore:,after:,on:andduring:dates (YYYY-MM-DD,YYYY/MM/DD,today,yesterday, a month or a year). Words match whole and all must match."Phrases"keep their order and-excludes. Twoin:orfrom:terms match either.highlight=truemarks only the words you searched.has::emoji:and the file results are not filtered yet. -
Canvases and Lists. The twin serves
canvases.create,canvases.edit,canvases.delete,canvases.getContent,canvases.sections.lookup,canvases.access.set,canvases.access.deleteandconversations.canvases.create. It also serves the twelveslackLists.*methods for Lists, their items, access and CSV export. User groups now answer as a workspace that lets members manage them:usergroups.create,usergroups.updateandusergroups.users.updatesucceed, and a group carries Slack’s full set of fields. -
A request URL answers Slack’s url_verification before its first event. With
SLACK_TWIN_EVENTS_URLset, the twin sends Slack’s signedurl_verificationchallenge first and holds events until your app answers HTTP 200 with thechallenge, as text, JSON or a form, as Bolt does.GET /_rystic/egresssays why events wait underdelivery.handshake. A receiver that does not answer the challenge gets no events now: answer it, or setSLACK_TWIN_URL_VERIFICATION=off, which stands for a URL set byapps.manifest.update, to which Slack sends no challenge. -
Wait for your bot’s turn instead of sleeping.
GET /_rystic/settleanswers once no event or interaction awaits your bot’s ack and no Web API call has landed forquiet_msof wall time, or attimeout_mswith what is still open; calls made withX-Rystic-Surface: clientnever count, andprincipal=watches only your bot’s. Itslast_call_seqis the lastseqin/_rystic/calls, so the calls between a settle before you act and one after are your bot’s turn.rystic settleand the MCP server’ssettletool ask the same route. -
The verified UI opens canvases and Lists. Create new lists Slack’s entries and makes a canvas or a List, and the rail’s Files gains Canvases and Lists pages. A canvas shows its headings, lists, checklists, quotes, tables and code beside an outline of its headings; you add to it at its foot, edit or delete any part of it and tick its checklist items. A channel’s Add canvas tab makes the channel canvas and opens it in place, and the tab strip’s + menu shows Slack’s entries, with Link adding a bookmark. A List shows as a table with select pills, people, dates, checkboxes and a to-do List’s completion circles, and an Item panel beside it holds every field, Mark Complete and the subtasks. You add items and subtasks there, edit cells, delete one item or several, search the List and rename it. A List a channel names among its tabs opens as a tab. The Canvases and Lists pages, a canvas’s title, a channel canvas made elsewhere and Download as CSV show what the twin can’t answer yet.
-
The call log shows what each call asked for and whether Slack served it. Each
GET /_rystic/callsentry now carriesargs, the call’s arguments with tokens redacted (long values cut, file contents left out), andok. A refusal Slack answers with HTTP 200 hasok: falseand its code inerror,unknown_methodincluded, and a method the simulator does not have is also markedunknown_route: true.?tail=Nreturns only the last N calls, anddroppedcounts the calls the log no longer keeps. -
Your bot sees a member leave. Deactivate a member with a seed or a scenario timeline
seedevent settingdeleted: true, and every app receives oneuser_changewith the whole user, as on Slack; a reactivation, or a seed that changes another field of the user object, sends one too. The deactivated member leaves every public and private channel but keeps its DMs and stays inusers.listwithdeleted: true, andconversations.membersexcludes it. -
A scheduled message that falls due reaches your app, and a person’s posts as the person. When the clock reaches a
chat.scheduleMessagemessage’spost_at, the twin sends themessageevent, andapp_mentionwhen it mentions a bot user, over Socket Mode or your request URL, aschat.postMessagedoes. A message a person scheduled withX-Rystic-Surface: clientposts as they typed it, with itsclient_msg_idand nobot_id,app_idorbot_profile, in its preview, in history and in its events. -
The verified Slack UI stays signed in through a reset or a restart. After a reset, a scenario or seed load, a credential binding or a simulator restart, the UI binds every person’s token again before an open page redraws, so a post from the page lands as its person, never as the bot, and a harness no longer runs
setup-twin.mjsafter each reset. It binds with the new"keep": trueonPOST /_rystic/credentials, which adds only the credentials not bound yet, so a harness’s or scenario file’s own bindings stay. -
The docs are one page, with a page per feature. The archive ships
README.mdin place ofHOWTO.md, with the same sections in the same order as every Rystic simulator’s page, andCHANGELOG.mdbeside it.SCENARIOS.mdno longer ships; the Scenarios section of the page lists each scenario. Identities, the Events API, interactivity, search, the MCP server, the workspace UI and the verified UI (with its one-container environment) each have their own page underdocs/features/, and every example on these pages runs as written against a fresh simulator. -
The docs page links the admin token section. The page now sends you to the admin token’s section of the shared-simulators page.
v0.0.19 — 2026-10-12
-
A world can carry its own timeline. A
--seedfile, or a complete world document posted toPOST /_rystic/seed, may addcredentials, binding presented credentials to actors, andevents: calls the twin makes on its own API at set sim times as one of those actors, through the same handlers and egress as your requests. A top-levelcredentialsoreventskey the twin used to ignore now takes effect.RYSTIC_PACE=<speed>plays the clock, and the timeline, in real time, and an event’satmay be+<duration>./_rystic/inject,--injectandRYSTIC_INJECTnow needstate_setinstead ofscenarios. -
A timeline call can hand what it minted to later events. A
callevent may addbind,{"<name>": "<JSON Pointer>"}, saving fields of its answer, such as an ID the twin minted; a later event uses one as${name}in a call’s path, headers or body or in a seed event’s record keys and values (a register name cannot be bound). A schedule naming a${name}no earlier call binds is refused at load, and an event whose binding call failed records an error instead of firing. -
A clock jump fires each timeline event at its own time. When one
POST /_rystic/clockor paced tick passes several events, each fires with the clock at itsat, so the records it creates carry that time, and the clock then lands where the move asked. An event already due when the clock moves fires at the current time. -
A clock jump stamps each timeline event at its exact instant. When a clock move passes timeline events, the records each one makes carry exactly its
at, to the millisecond, whatever traffic came before it, so one jump leaves the same records as stepping the clock to each event in turn. -
A timeline call can carry raw bytes, XML or a JSON array. A
callevent may sendbody_textorbody_base64instead ofbody; the twin sends it as written, under the route’s own content type (XML, bytes, JSON or a form), so a scheduled call can upload an object’s bytes or post an XML document, and${name}fills inbody_text. Abodymay be a JSON array where the route reads one. A call that answers XML binds from its elements by JSON Pointer, such as/InitiateMultipartUploadResult/UploadId.
v0.0.18 — 2026-10-11
-
The verified UI installs and starts with
rystic ui slack. The Slack client built against Slack’s own client is a Pro and Enterprise deliverable: it ships as the imageregistry.rystic.ai/slack-twin-ui:<ver>beside the twin’s, pulled with the samedocker login, andrystic uiruns it against a running instance (or, without Docker, the UI’s archive on your Node 22+). A Free key is refused withfeature_not_in_plan, naming theverified_uientitlement; the Web API and the embedded state UI stay on every plan. -
The scripts in this archive are MIT-licensed.
check.shand the archive’s other scripts are published under the MIT License at https://github.com/Rystic-Inc/rystic-scripts , so you can copy, change and share them.LICENSE.txtno longer covers them; it still governs the twin itself.
v0.0.17 — 2026-10-10
-
Scheduled messages post when the twin’s clock reaches them. A
chat.scheduleMessageposts once, as its author and in its thread, when simulator time reachespost_at, and then leaveschat.scheduledMessages.list; a canceled message never posts. -
Ephemeral posts reach the activity stream, and stream starts keep their chunks. A successful
chat.postEphemeralemits oneslack.message.ephemeralevent on/_rystic/eventsnaming its recipient, and never enters history. Achat.startStreamwithchunksand nomarkdown_textstores the chunks’ text as the message text. -
Closing a DM closes it only for you.
conversations.closecloses the DM for the caller alone and leaves the other participants’ view untouched;conversations.openon it reopens it for the caller with their read position kept. -
Deleting a message removes its pin and its reactions, as Slack does. After
chat.delete,pins.liston the channel no longer lists the message, andreactions.geton it answersmessage_not_found. A thread parent kept as a tombstone, because it still has replies, keeps both; that case is not yet measured. The pin behavior was measured against a live workspace. -
Conversation answers name your workspace’s team, and list and info agree.
conversations.create,invite,join,rename,setTopicandsetPurposeanswershared_team_idswith your workspace’s team and leave out thelatestand unread counts Slack omits there;conversations.listandconversations.infogive one conversation the samecreatorandupdated, whoever asks, and a new group DM is namedmpdm-…from its members’ handles.setPurpose,renameandjoinleave their system message in channel history, and a streamed message carries your bot’sbot_id,app_idandbot_profile, reads back asin_progresswith the text sent so far untilchat.stopStream, and finishes ascompleted. -
A deleted file leaves a tombstone on its share, and a file names its uploader’s team. After
files.delete, a message that shared the file keeps it as{"id": …, "mode": "tombstone"}, as Slack does, andfiles.infoanswers the uploader’s team inuser_teaminstead of a placeholder. -
Refusals carry Slack’s diagnostics, and a published view names your app. Every
invalid_argumentsrefusal carriesresponse_metadata.messages,conversations.inviterefusals carry the per-usererrorsarray, andviews.publishanswers your app’sapp_idandbot_idrather than fixed placeholders. Apost_atthat is not a timestamp onchat.scheduleMessage, a presence other thanautoorawayonusers.setPresence, andmarkdown_textbesidechunksonchat.stopStreamanswerinvalid_arguments, as Slack does.conversations.invitewithforce=truepasses over an unknown user only while another named user can still be added, so beside members alone it answersuser_not_found. -
Answers carry what Slack’s documented responses fix. Where a handler leaves a field absent or null, the twin answers with the constant, the documented default or the
oktag the method’s documented response fixes for that status, error envelopes included; a value the handler wrote is never replaced. -
Tokens are read and refused the way Slack reads them. A token that is not a Slack token (
Bearer x,Bearer token) now answersinvalid_authinstead of being accepted. A malformedAuthorizationheader —Beareralone, or a second one — answersnot_authed, as live Slack answersBeareralone on every method butapi.test, which it answersok, though live Slack refuses a second header withinvalid_auth; a header under another scheme, such asBasic, presents no token and answersnot_authedtoo. A form-encodedtokennow wins over the header, and of two form tokens the last, as live Slack answers;api.teststill answers without a token but refuses an invalid one. Rotatedxoxe.xoxb-/xoxe.xoxp-, app-levelxapp-and workflowxwfp-tokens are recognized kinds, so an app-level token onusers.identityanswersnot_allowed_token_type. -
A write moves the records it relates to, as Slack does. A stream started in a thread (
chat.startStreamwiththread_ts), or a file shared into one (files.completeUploadExternalwiththread_ts, naming its one conversation inchannel_idorchannels), moves its parent’sreply_count,reply_users,reply_users_countandlatest_reply, as a posted reply does, and the file’s share message is a reply in that thread.pins.removetakes the channel off the message’spinned_to, soconversations.historycounts one pin fewer.conversations.infoanswers yourlast_read, whichconversations.markmoves. -
An empty workspace holds the bot every token acts as. A twin started without a scenario, and the workspace
POST /_rystic/resetwipes to, now hold one bot user,tb-member-user-idof teamtb-member-team-id, and every token acts as it. A method that names it now finds it:users.listlists it, andusers.infoandusers.profile.get, among others, answer it instead ofuser_not_found. A user token’susers.identityanswers it instead ofinvalid_auth, and what you create is authored by it: a channel’screator, and a message’suser,team,bot_idandapp_id, where a mintedU00000001of teamT00000002authored them before. The workspace still holds no team record, soauth.testanswersinvalid_authas before. A seed that keys its own caller undermemberreplaces this bot; one that keys every user by itsidkeeps it beside them, and every token still acts as it. -
HOWTO.md counts every route the twin answers. Its coverage sentence reads 274 routes where it read 73: each of the 67 modeled methods counts at
GETandPOSTon both/<method>and/api/<method>, asGET /_rystic/routeslists them. The twin answers what it did before. -
A file is shared only once its bytes are uploaded, and a /me message reads back as Slack’s.
files.completeUploadExternalon a file whose bytes never reached itsupload_urlstill answersok, but leaves the file untyped and unshared, posts no message and moves no thread parent, as Slack does: upload the bytes first to share a file. Achat.meMessagemessage reads back with your app’sapp_id, arich_textblock and noteam, andreactions.getanswers a message’spinned_towhile it is pinned. -
A first close answers its flags, an in-flight stream reads back empty, a thread parent carries its thread_ts, a broadcast reply and a /me edit answer no bot_profile, and an IM carries is_archived.
conversations.closeanswers the first close of an open DM withno_op: falseandalready_closed: falsebesideok.reactions.geton a stream still in flight answers an emptytextwithstreaming_state: in_progress, the bot’sbot_idandapp_id, andparent_user_idon a reply. A thread’s parent answersthread_tsequal to its owntsonce any reply joins it, a streamed reply included.conversations.historylists a broadcast reply (reply_broadcast) with nobot_profileorteam, andchat.updateof a/memessage answers neither. An IM’sconversations.infocarriesis_archived, and itslatestbot message’sbot_profilecarries aname.
v0.0.16 — 2026-10-02
-
Socket Mode envelope IDs come from their own range. The first envelope the twin sends carries
envelope_idcdf8b689-d7dc-46e6-8000-000000000001rather than00000001-0000-4000-8001-000000000001, and later ones count up in their last digits. A client that acknowledges the ID it receives sees no difference; a test that pinned an envelope ID needs the new one. -
Post as a person with
X-Rystic-Surface: client. Achat.postMessagemade as a seeded non-bot user with the headerX-Rystic-Surface: clientstores and emits the message that user would type in the Slack client:user,team,text,blocksand aclient_msg_id, with nobot_id,app_idorbot_profile, so an app that ignores bot messages starts a turn on itsmessageandapp_mentionevents. Posts without the header keep their app attribution, as on Slack.
v0.0.15 — 2026-10-01
-
check.shfails fast on a port that never answers. Its port check and boot wait give each/_rystic/versionprobe two seconds, so an address where something accepts connections but never replies fails the check with a message instead of hanging. -
Your bot receives Events API events, over Socket Mode or at a request URL. Run the twin with
SLACK_TWIN_SOCKET_MODE=1and point Bolt at it with anyxapp-token, or setSLACK_TWIN_EVENTS_URLandSLACK_TWIN_SIGNING_SECRETto have each event POSTed signed. Messages, edits, deletes, mentions, reactions, pins, file shares and channel joins, leaves, creates, renames and archives emit the events Slack sends for them.
v0.0.14 — 2026-09-30
-
Inspect offers an identity view, and an injected 429 can carry
Retry-After./_rystic/inspect?view=identitiesfolds identical actor-label aliases for display; the default export retains all records so a reset followed by seeding restores caller identity. A network profile’srejecttakes validatedheaders, so an injected429can send theRetry-Aftera client’s backoff reads. The docs now say what file downloads answer, howstate_reactionis keyed, and that a deleted file’s share message keeps its full file. -
File URLs serve the bytes. A
GETof a file’surl_privateorurl_private_downloadwith a Bearer token returns the bytes you uploaded, under the file’smimetype(the download form as an attachment); without a token it answers401, and a file whose bytes never reached the twin answers404.files.completeUploadExternalnow returns the file’smimetype,mode,pretty_type,is_publicandurl_private_download, as Slack does. -
Thread identities and query errors. Thread replies preserve an existing bot ID from the caller. Missing-argument errors from permalink, file-info, and email-lookup reads, and invalid-limit errors from reactions-list and user-conversations reads, include populated response diagnostics; replies and file-info reads reject non-integer
limitvalues. -
The state UI shows the workspace as a Slack client. On Pro and Enterprise,
/_rystic/uiopens a live Slack-style view of the twin — channels, threads, DMs, Block Kit, App Home and modals — where you can view as any seeded user and post, reply, react, pin, edit or join as them through the Web API, beside Your Bot, Scheduled messages and Views cards. -
The state UI takes the rystic.ai look.
/_rystic/uiand its Your Bot, Scheduled messages and Views cards now read in the site’s paper, ink and Space Grotesk instead of the dark theme; the workspace card keeps Slack’s own look. -
Unmodified clients reach the twin at Slack’s hostnames.
rystic run slack-twin --interceptboots the twin behind the rystic CLI’s HTTPS gateway, andrystic exec -- <command>routes that command’s calls toslack.comandslack-files.comto it, for supported SDKs using their default transports. -
Missing and invalid arguments answer Slack’s errors. A call missing a required argument answers
invalid_argumentswith Slack’sresponse_metadata.messages([ERROR] missing required field: channel), exceptusers.infowithoutuser, which answersuser_not_foundas Slack does.chat.postMessagewith no text, blocks or attachments answersno_text,reactions.addwith an emoji name the workspace doesn’t know answersinvalid_name, andusers.getPresencewithoutuserreports the caller’s own presence. -
Thread and message reads reflect earlier writes. A
reply_broadcastreply appears in channel history as athread_broadcastcarrying its parent asroot. Deleting a reply updates its parent’sreply_count,reply_usersandlatest_reply; deleting a parent that has replies leaves a tombstone (“This message was deleted.”, fromUSLACKBOT) that keeps its thread, and deleting it again answersmessage_not_found. An updated message carries noparseorlink_names, and a post answers nousernameunless you sent one. -
Pins list newest first, and Slackbot is a listed user.
pins.listlists the most recently pinned item first, pins made in the same second included, and each pinned message carriespinned_info.users.listlists the built-in Slackbot first, as the same objectusers.inforeturns forUSLACKBOT(is_botfalse, Slack’s avatars). Lists the twin doesn’t otherwise order come back in the order their records were created, the same on every run. -
History windows and reply reads follow Slack.
conversations.historywitholdestreturns the messages just after it, not the channel’s newest, and its cursor pages forward from there, so a bot that catches up from a checkpoint sees every message once.conversations.replieswith a reply’stsreturns that reply alone, not its whole thread. -
Long messages are truncated at 4,000 characters. A message longer than 4,000 characters is stored, and read back from history, as its first 4,000 characters; the post’s own answer still echoes the full text, as Slack’s does.
v0.0.13 — 2026-09-27
-
Several agents can share one workspace, each under its own token.
POST /_rystic/credentialsbinds a token to a user, and every call with that token acts as that user, with no Rystic header.GET /_rystic/callslists every request since reset with the identity that made it, for grading. See Identities in the Slack docs. -
Conversation access follows each identity. Private conversation reads respect current membership, including after leaving; human user tokens can read public history without joining. Opening a human–bot DM from either identity reuses the same conversation, including an existing seeded DM.
-
Private conversations stay hidden from non-members everywhere.
conversations.listshows a private channel, DM or group DM only to its members, as on Slack, and a non-member posting into one getschannel_not_found, the same answer reads give. A refused post no longer writes bot IDs onto the caller.
v0.0.12 — 2026-09-26
- HOWTO.md and
check.shstart from an API key. HOWTO.md leads withrystic login rys_…, says which control-plane routes Free, Pro and Enterprise reach and whatPOST /_rystic/resetdoes on each, documents seeding (twelve registers,state_triggerincluded, and an inspect dump posted back as it is) and the token rules as served (xoxb-/xoxp-kinds,not_allowed_token_type, the form-bodytoken=fallback), and states that the bundled MCP package, rebound todesign_studio, is not yet re-qualified. SCENARIOS.md namesdesign_studioas the world Free boots.check.shruns the Free and Pro builds on your API key (RYSTIC_TOKEN, or the keyrystic loginstored) as well as the Enterprise build on its license file, looked up in the CLI’s order.
v0.0.11 — 2026-09-26
- Only
design_studioships as a named scenario. The older scenarios are now test-only fixtures and are no longer accepted by--scenario,RYSTIC_SCENARIO, orPOST /_rystic/scenario. Tests and evaluations can load their unchanged register maps and clocks fromsim/testdata/scenarios/through the control plane.
v0.0.10 — 2026-09-25
-
Self-serve on the metered edition replaces the free edition. A Rystic account’s token runs the metered build:
rystic login rys_…once, thenrystic runordocker run -e RYSTIC_TOKEN=…. It carries every scenario and control-plane route your plan includes, and bills active 30-second blocks against the plan’s allowance. The fixed-world free download and its usage report are gone. -
design_studiois the same studio on every twin. Its people, clients and project dates now come from one shared world, so the clients, invoice numbers and dates#financenames match what the Stripe, PayPal, Resend and Lob twins hold for Website Design Contractors. The archived project channels were regenerated with each job’s real page count: the workspace now carries 2,951 messages (was 2,959).
v0.0.9 — 2026-09-22
- No change to the twin’s behavior. Same control plane, scenario pack,
seeds, worlds and license gate as v0.0.8, and the free edition’s
design_studioworld is unchanged. The bundled MCP package pins the same korotovsky/slack-mcp-server v1.3.0 and the same thirteen cases; each receipt its workflow checker writes now carries aschema_version.
v0.0.8 — 2026-09-22
- A fourth world,
design_studio: somebody else’s whole Slack../slack-twin --scenario design_studioboots into a web design studio a year in — its own team (TWDC0000001), its own cast of forty-three people and bots, and its own bot token,studiobot. 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, carrying 2,959 messages with the pins, bookmarks, files, threads, reactions and queued messages a year leaves behind. Six questions are waiting in it that only a reader of this workspace can answer — who is on call, when a client launches, how a site is rolled back — and every one is answered by a record inside the binary. The threeRystic Shopworlds are unchanged, message for message, so a suite pinned to theirtsvalues keeps passing.SCENARIOS.mdhas the workspace’s channels, people and the six questions.
v0.0.7 — 2026-09-21
- No change to the licensed twin’s behavior. Same control plane,
scenario pack, seeds, worlds and license gate as v0.0.6.
/_rystic/versiongains anedition:line — empty on a licensed build — so a caller can tell a licensed twin from the free edition without inspecting anything else.
v0.0.6 — 2026-09-21
-
A pinned native MCP integration. The release includes the korotovsky/slack-mcp-server v1.3.0 bot-token profile, native caller, bounded local gateway, workflow checker and exact thirteen-case pack. The declared tools discover channels, read history and threads, and post replies. Package manifests pin the binaries, tool manifest, model and shipped fixture files. See the bundled MCP README for configuration and explicit exclusions, including hosted Slack MCP and search.
-
Protected runtime contract 9. A trusted harness can require a private gateway credential on all twin requests and preserve service URL identities while routing the native caller locally. Service credentials supplied in accepted form bodies follow the same modeled authentication rules.
-
Measured thread and message fixes. Posting updates stored parent thread registers; history and replies preserve those values. Repeated-parent cursor pages, public block metadata and the single-section Markdown fallback used by the pinned caller follow the controlled workflow evidence. The API test response no longer echoes the bearer token.
Measured scope. The native local workflow passes 13/13 cases, a fresh read-only comparison passes 8/8, and the Linux caller cell passes seven containment checks. The original controlled live comparison and exact local replay retain their separate model pins. These are scoped MCP results, not a new full-surface Slack fidelity score. Released Harbor reset/isolation and a real-agent rollout remain separate qualification gates; packaging alone does not certify them.
Qualification addendum, 2026-09-21. The published 0.0.6 package now passes 13/13 shipped cases without a compiler in the offline Linux amd64 Harbor image. Two-episode isolation, three repeated resets, in-flight cancellation, native crash, gateway timeout and cleanup pass. The unchanged held-out task’s MCP reference earns 1.0; null, wrong-thread, duplicate-reply and forbidden-edit controls fail their intended checks. The support card and dated receipts identify exact artifacts and limitations. One real held-out Opus rollout earns 1.0 with 17 MCP replies under the original grader. The source workflow ended red because its checker compared a short row ID with Harbor’s qualified name; the retained original trace passes the corrected exact-name check. Both receipts and source hashes are retained. No learning/generalization claim is made, and the release archives remain unchanged.
v0.0.3 — 2026-09-19
- A third world,
refund_desk_xl../slack-twin --scenario refund_desk_xlboots into the same#refund-desk(CRYSOPS0003) over a busier week: twelve users, 45 top-level messages and five threads across five days, and seventeen open requests naming fourteen objects in stripe-twin’sbusy_account— the ninerefund_desknames plus the paused v1 webhook endpoint, a completed and an expired checkout session, an unpaid cart and the production endpoint. The shapes an ops bot has to leave alone are all on the record: a status question already answered in its thread, the bot’s own earlier summary naming a charge, a refund word with no payment ID behind it, the same customer asked about twice, and two objects in one message.refund_deskandops_channelare unchanged, message for message, so a suite pinned to theirtsvalues keeps passing.SCENARIOS.mdlists the new world’s requests and what each resolves to on the Stripe twin.
v0.0.2 — 2026-09-18
-
Two named worlds, baked in.
./slack-twin --scenario refund_deskboots into#refund-desk: one workspace, nine users, and a channel where eight people have left 19 messages — chatter, Stripe status questions and five refund requests, every payment ID a real record in stripe-twin’sbusy_accountworld — with the refund handbook pinned and one request already answered in its thread.ops_channelis#ops: the same people, 14 messages of chatter and status questions and not one refund request, several of the questions answerable only from another twin. Yourxoxb-token is the workspace’s ops bot (auth.test→UBOTOPS0001) and already a member, soconversations.historyanswers without a join. Slack has no seed API and a bot cannot post as somebody else, so a channel with a history other people wrote is exactly the state a bot token cannot reach on its own.--list-scenariosnames them;RYSTIC_SCENARIO=<name>does the same in a container. -
SCENARIOS.md. One page in the archive naming each world, its channel and cast, what every request points at on the Stripe twin, and how to re-pin the handbook with a different refund window. -
conversations.historylists top-level messages only. Thread replies no longer appear in a channel’s history; they live underconversations.replies, as on Slack, withthread_broadcastreplies the one exception. A bot’s own posts now carry thebot_idandapp_idthatauth.testreports for its token, and each user’stz_label/tz_offsetfollow theirtz.
v0.0.1 — 2026-09-17
The first release of the Slack twin — an early cut, shipped so integrations can start against it while fidelity work continues. Read the measurement paragraph below before relying on a method’s exact response shape.
-
A local Slack Web API for your bot’s integration tests. 67 methods in the bot dialect across 11 resources:
conversations.*(create, join, invite, kick, leave, archive, rename, topic and purpose, history, replies, members, mark, open, close, info, list),chat.*(postMessage, update, delete, postEphemeral, meMessage, getPermalink, scheduled messages, and the startStream/appendStream/stopStream trio),users.*,reactions.*,pins.*,bookmarks.*, the external file upload pair withfiles.info/list/delete,views.open/publish/update,auth.test,api.test,team.info,team.profile.get,dnd.infoanddnd.teamInfo. Point your Slack SDK’s base URL at the twin and your existing suite runs against it unchanged — form-encoded and JSON bodies both work, as they do live. -
Slack’s error envelope, faithfully. Slack answers failures as HTTP 200 with
{"ok": false, "error": "channel_not_found"}, so a client branches on the error string and never the status code. The twin does the same for every modeled method — a missing channel, a missing token (not_authed), a user-token-only method called with a bot token — so your error handling is exercised against the shape it will meet live. -
It never reaches the network, and it needs no app. Any well-formed bot token (
xoxb-…) is accepted and the twin dials nothing — which is the point: a suite that hitsslack.com/apifails when a channel it created last week cannot be deleted (Slack has no channel delete), and one that posts to a shared test workspace fails when a colleague’s run posts first. -
Seed the workspace your test needs.
POST /_rystic/seedtakes a register map — channels, memberships, messages, users, the team, reactions, pins, bookmarks, files, scheduled messages, views — and lands it directly.GET /_rystic/registerslists what this twin accepts a seed for; a register the model does not carry is refused with a status rather than silently dropped. -
The same seed twice is the same workspace twice.
POST /_rystic/resetempties the twin,POST /_rystic/seed-rngpins the generator so channel IDs repeat run to run, andPOST /_rystic/clockfixes the twin’s notion of now — which matters more here than elsewhere, because a message’stsis both its ID and its clock reading. Two identical seed flows leave byte-identical/_rystic/inspectdumps.
What is measured, and what is not. Fidelity is scored differentially
against real slack.com/api on a throwaway workspace, per response field,
not by eyeball: the last full-suite round was 161 of 221 probes on
2026-09-17. That is an early number and this release ships it as-is. The
misses cluster where the model is known to be thin: response fields the
Slack SDK samples list but a live response omits, cursor and ts values
that are volatile by design, and the reads whose live evidence came from a
newly seeded workspace. HOWTO.md lists the known shortfalls under
Limitations. Rounds are re-run as the model is repaired and each release
states its own number.
Not in this release. The Events API and Socket Mode, admin.*
(Enterprise Grid), Slack Connect and shared channels, user tokens (xoxp-)
and the methods only they can succeed at — users.profile.set,
users.setPhoto, users.deletePhoto, users.identity — which answer their
documented ok:false branch, as they do live under a bot token.