slack
v0.1.0 · compiled with katari 0.1.0
Overview
from the package READMEslack — a Slack bot capability over Socket Mode
A single module, slack, plus its FFI sidecar src/slack.ts: watch a channel's messages and post
replies — threads and file attachments included — the Slack twin of the Discord package. Socket Mode
means no public URL and no request-signature verification: the sidecar opens an outbound WebSocket
with the app-level token and Slack pushes events over it, while the bot token drives the Web API.
slack.provider(bot_token = ..., app_token = ...)— connects ONCE and serves the client handle for the extent of the continuation.slack.watch_messages(channel, deliver_to)— serve a channel forever, delivering each incoming message (channel/user/text/thread_ts/files) to your agent. Bot posts (this bot's own replies included) are not delivered, so replying cannot loop.slack.send_message(channel, text, files, thread_ts ?= null)— post to a channel; a message'stsasthread_tsreplies in its thread, andfilesupload as attachments.slack.send_files(channel, files, caption)— the tool shape of the above, for handing to a model.
Files are first-class on both directions: an incoming message's attachments arrive as file
values (downloaded with the bot token, since Slack file URLs are private), and outgoing file values
upload via files.uploadV2.
Threads: a delivered thread_ts is the thread the message was posted in, or null for a top-level
message — pass it straight back to send_message to reply where the message came from (in its thread
if it had one, in the channel otherwise).
Delivery is at-least-once: every event is acknowledged on arrival (so a slow handler does not turn into duplicates), but an acknowledgement lost to a dropped socket makes Slack re-send the event, and no dedup memory is kept here.
Slack app setup
- Create an app at api.slack.com/apps ("From scratch").
- Socket Mode: Settings → Socket Mode → enable it. Generate the app-level token with the
connections:writescope — this is thexapp-…token (SLACK_APP_TOKEN). - Scopes: Features → OAuth & Permissions → Bot Token Scopes: add
chat:write(post messages),files:write(upload attachments),files:read(download incoming attachments). - Events: Features → Event Subscriptions → enable, then under "Subscribe to bot events" add the
message events for the conversations you watch:
message.channels(public channels),message.groups(private channels),message.im(DMs). Socket Mode needs no Request URL. - Install the app to your workspace: OAuth & Permissions → Install. The Bot User OAuth Token is the
xoxb-…token (SLACK_BOT_TOKEN). - Invite the bot to the channel (
/invite @your-bot) and copy the channel id (theC…value in the channel's details).
Secrets / env
SLACK_BOT_TOKEN— the bot token (xoxb-…), used for every Web API call and attachment download.SLACK_APP_TOKEN— the app-level token (xapp-…), used only to open the Socket Mode connection.
Store both in the runtime: katari env set SLACK_BOT_TOKEN --secret and
katari env set SLACK_APP_TOKEN --secret. Each is a string of private, passed straight to the
sidecar and never surfaced elsewhere.
Sidecar dependencies
src/slack.ts imports @slack/socket-mode, @slack/web-api and @katari-lang/port. They are
declared in package.json; run pnpm install (or npm install) in this package so katari apply
can bundle the sidecar. (A pure-Katari consumer that never applies this package does not need them.)
Usage
import slack
// Echo every message back where it came from (its thread if it had one), attachments included.
agent echo(channel: string, user: string, text: string, thread_ts: string | null, files: array[file]) -> null {
slack.send_message(channel = channel, text = f"<@${user}> said: ${text}", files = files, thread_ts = thread_ts)
}
agent main() -> never {
use slack.provider(
bot_token = env.get_secret(key = "SLACK_BOT_TOKEN"),
app_token = env.get_secret(key = "SLACK_APP_TOKEN"),
)
slack.watch_messages(channel = "C0123456789", deliver_to = echo)
}Hand slack.send_files to an AI loop's tool list to let the model post images or documents on its
own.
slack
12 declarationsget_slack_client#
requestrequest get_slack_client() -> stringThe live Slack connection (the Socket Mode and Web API clients live in the sidecar; the handle is opaque).
Returns
Wire view (JSON Schema)
provider#
agentagent provider[R, effect E](bot_token: string of private, app_token: string of private, continuation: agent(value: null) -> R with {...E, get_slack_client}) -> R with E | io | prelude.throw[slack_error]Provide the Slack connection for the extent of the continuation: connect once over Socket Mode, then serve get_slack_client to everything downstream. A failed connect (a bad xapp-… token, a transient network fault) raises slack_error (auth_error | api_error) — the connection is fixed at start, so a bad credential surfaces here rather than on the first send.
Generics
Parameters
The bot token (xoxb-…) every Web API call authenticates with.
The app-level token (xapp-…) the Socket Mode WebSocket opens with.
Runs with get_slack_client served; its result is the provider's result.
Returns
Effects
Inferred type
agent(app_token: string of private, bot_token: string of private, continuation: agent(value: null) -> T0 with get_slack_client | E1) -> T0 with throw[api_error | auth_error] | E1 | io
Wire view — the wire shape is fixed at the call site, where the generic parameters are instantiated.
create_slack_client#
external agentexternal agent create_slack_client(bot_token: string of private, app_token: string of private) -> string with prelude.throw[slack_error]Connect to Slack — open the Socket Mode WebSocket and the Web API client — and return an opaque client handle. Raises slack_error (auth_error | api_error) when the connect fails — a bad app-level token is auth_error, a transient network fault api_error. Prefer the provider.
Parameters
The bot token (xoxb-…) every Web API call authenticates with.
The app-level token (xapp-…) the Socket Mode WebSocket opens with.
Returns
Effects
Wire view (JSON Schema)
slack_close#
external agentexternal agent slack_close(client: string) -> nullClose a Slack connection: disconnect the Socket Mode WebSocket and drop the client handle. The provider arms this as a finally, so the connection dies with the run — a socket left open keeps receiving events, and Slack round-robins each event across every socket open on the app token, so a dead run's zombie socket would swallow messages (acking them) that a live bot then never sees. Idempotent: closing an unknown or already-closed handle is a no-op, since a finalizer may run more than once.
Parameters
The opaque handle create_slack_client returned.
Returns
Wire view (JSON Schema)
auth_error#
datadata auth_error(message: string)An authentication / authorization failure: the bot token is invalid, revoked, or missing a scope the call needs (Slack invalid_auth, token_revoked, missing_scope, …); message carries Slack's error string. The bot cannot recover on its own — the operator must fix the token or scopes and restart the run (the connection is fixed at start). Let it surface so the bot stops loudly rather than silently dropping every reply.
Parameters
Wire view (JSON Schema)
api_error#
datadata api_error(message: string)A Slack API call failed for a non-auth reason: a rate limit, a transient network fault, a channel the bot is not in, a message too long; message carries Slack's error string. Usually per-message or transient, so a bot catches it to drop just that reply and keep serving.
Parameters
Wire view (JSON Schema)
slack_error#
typetype slack_error = auth_error | api_errorDefinition
slack_send#
external agentexternal agent slack_send(client: string, channel: string, text: string, thread_ts: string | null, files: array[file]) -> null with prelude.throw[slack_error]Low-level: post to a channel on a given client — a thread reply when thread_ts carries a ts, a channel post when it is null; any files upload as attachments. Raises auth_error or api_error (slack_error) when the Slack API call fails. Prefer the send_message agent.
Parameters
The opaque handle create_slack_client returned.
The id of the channel to post in (e.g. "C0123456789").
The message text (Slack mrkdwn); with files it becomes their caption.
The parent message's ts to reply in its thread, or null to post to the channel.
Files to upload as the message's attachments ([] for a plain text post).
Returns
Effects
Wire view (JSON Schema)
slack_watch#
external agentexternal agent slack_watch[effect E](client: string, channel: string, deliver_to: agent(channel: string, user: string, text: string, thread_ts: string | null, files: array[file]) -> null with E) -> never with ELow-level: listen on a channel and deliver each incoming message (channel / user / text / thread_ts / attachments as files) to a callback. Prefer the watch_messages agent.
Generics
Parameters
The opaque handle create_slack_client returned.
The id of the channel to listen on; messages elsewhere are ignored.
Called once per incoming message; its effects E flow to the caller's handlers.
Returns
Effects
Wire view — the wire shape is fixed at the call site, where the generic parameters are instantiated.
send_message#
agentagent send_message(channel: string, text: string, files: array[file], thread_ts: string | null ?= null) -> nullPost a message to a channel using the ambient Slack client. Pass a message's ts as thread_ts to reply in its thread (an incoming message's thread_ts passed straight back replies where it came from); files upload as attachments (pass [] for a plain text post — parameter defaults only take literals, so the empty array cannot default). Raises slack_error (auth_error | api_error) when the send fails — catch api_error to keep a bot serving past a transient failure, and let auth_error surface so a bad token stops the bot loudly.
Parameters
The id of the channel to post in (e.g. "C0123456789").
The message text (Slack mrkdwn); with files it becomes their caption.
Files to upload as the message's attachments ([] for a plain text post).
The parent message's ts to reply in its thread; omit (or pass null) to post to the channel.
Returns
Inferred type
agent(channel: string, files: array[file], text: string, thread_ts?: string) -> null with throw[api_error | auth_error] | get_slack_client | io
Wire view (JSON Schema)
send_files#
agentagent send_files(channel: string, files: array[file], caption: string) -> nullTool: post files (images, documents) to a Slack channel, with a caption. Use the channel id given in the conversation context; pass each file as its handle object — a bare {"$katari_ref": "<id>"} suffices.
Parameters
The id of the channel to post in, from the conversation context.
The files to upload, each passed as its handle object.
The text posted alongside the files.
Returns
Inferred type
agent(caption: string, channel: string, files: array[file]) -> null with throw[api_error | auth_error] | get_slack_client | io
Wire view (JSON Schema)
watch_messages#
agentagent watch_messages[effect E](channel: string, deliver_to: agent(channel: string, user: string, text: string, thread_ts: string | null, files: array[file]) -> null with E) -> never with E | get_slack_client | ioServe a channel forever, delivering each incoming message to deliver_to — shaped like time.watch / google_calendar.watch so it composes under parallel [ … ]. The bot's own posts (and other bots') are not delivered, so replying from deliver_to cannot loop. A delivered thread_ts is the thread the message was posted in, or null for a top-level message — pass it straight to send_message to reply where the message came from. Delivery is at-least-once: Slack re-sends an event whose acknowledgement was lost, and no dedup memory is kept here.
Generics
Parameters
The id of the channel to listen on (e.g. "C0123456789").
Called once per incoming message; its effects E flow to the caller's handlers.
Returns
Effects
Inferred type
agent(channel: string, deliver_to: agent(channel: string, files: array[file], text: string, thread_ts: null | string, user: string) -> null with E3) -> never with get_slack_client | E3 | io
Wire view — the wire shape is fixed at the call site, where the generic parameters are instantiated.