WardrikSlack
Copy MarkdownSlack for Wardrik. Agents answer in threads, and ask there before a call they can't make alone.
maria: @Wardrik what time is it on the server? :eyes: ↳ Wardrik: Waiting on @maria to approve
clock. Wardrik, to maria:clock, from a thread in #ops.[Approve][Deny]↳ Wardrik: Approved by maria. ↳ Wardrik: It's 14:02 UTC.
Make the Slack app
At api.slack.com/apps, choose Create New App, then From a
manifest. Pick your workspace and paste one of the manifests below. Change Wardrik to the
name your team will see.
For testing, over Socket Mode
Socket Mode needs no public address. Your app opens the connection to Slack.
display_information:
name: Wardrik
description: Agents that answer in threads, and ask before they act.
features:
app_home:
home_tab_enabled: false
messages_tab_enabled: true
messages_tab_read_only_enabled: true
bot_user:
display_name: Wardrik
always_online: true
oauth_config:
scopes:
bot:
- channels:history
- channels:read
- chat:write
- chat:write.public
- groups:history
- groups:read
- reactions:write
- users:read
- users:read.email
settings:
event_subscriptions:
bot_events:
- app_uninstalled
- channel_left
- group_left
- member_joined_channel
- message.channels
- message.groups
- tokens_revoked
interactivity:
is_enabled: true
org_deploy_enabled: false
socket_mode_enabled: true
token_rotation_enabled: falseThen:
- Choose Install to Workspace. Copy the Bot User OAuth Token (
xoxb-...) from OAuth & Permissions. This istoken. - Under Basic Information › App-Level Tokens, make a token with the
connections:writescope (xapp-...). This isapp_token.
Give Wardrik a Slack app of its own for Socket Mode. Slack spreads an app's events across every open connection.
For production, over HTTP
Slack sends events to your app's public address. Use the manifest above, with these settings.
Change agents.example.com to your address:
settings:
event_subscriptions:
request_url: https://agents.example.com/v1/channels/slack/events
bot_events:
- app_uninstalled
- channel_left
- group_left
- member_joined_channel
- message.channels
- message.groups
- tokens_revoked
interactivity:
is_enabled: true
request_url: https://agents.example.com/v1/channels/slack/actions
org_deploy_enabled: false
socket_mode_enabled: false
token_rotation_enabled: falseslack in each URL is the channel's name in your Wardrik config. Slack checks the events URL
when you save, so your app must already be running there.
Copy the Signing Secret from Basic Information › App Credentials. This is
signing_secret.
Try it locally
From this repository, with the Socket Mode tokens and a model key:
SLACK_BOT_TOKEN=xoxb-... SLACK_APP_TOKEN=xapp-... ANTHROPIC_API_KEY=sk-ant-... \
mix run --no-halt examples/socket_mode.exs
This starts Wardrik in memory with the agent acme/assistant. The agent may read a clock once
someone approves it.
- In Slack,
/invite @Wardrikinto a channel. - Mention it:
@Wardrik what time is it?
Set SLACK_APPROVERS=@you to name the approvers. Without it, whoever asked approves.
Wire it up
def deps do
[
{:wardrik_slack, "~> 0.1.0-rc.1"}
]
endAdd the channel to your Wardrik, under a name of your choice:
config :my_app, MyApp.AgentRuntime,
channels: [
slack:
{WardrikSlack,
token: System.fetch_env!("SLACK_BOT_TOKEN"),
signing_secret: System.fetch_env!("SLACK_SIGNING_SECRET"),
owner: "acme",
approvers: ["@maria", "jon@example.com"]}
]See WardrikSlack for every option.
Over HTTP, serve it with Wardrik.HTTP. Slack signs the exact bytes it sends, so if your app
parses bodies first, keep the raw body:
plug Plug.Parsers,
parsers: [:json, :urlencoded],
json_decoder: JSON,
body_reader: Wardrik.HTTP.body_reader()
forward "/", Wardrik.HTTP, wardrik: MyApp.AgentRuntimeList the channel in an agent's definition:
Wardrik.Agent.define(scope, "acme/triage",
model: "anthropic:claude-haiku-4-5",
description: "Triages incidents.",
channels: ["slack"]
)Invite the app into a Slack channel. If one agent is offered, it answers there from then on: mention the app, and it replies in a thread under your message. If several are offered, pick one. See WardrikChannel for rooms, threads and approvals.
Approvers
Name approvers by @handle, email address or Slack member ID, or use :anyone. Email
addresses need the users:read.email scope. Whoever asked for a turn may always decide it.
When an agent waits on a call:
Each approver gets the call in the app's Messages tab, with a link to its thread.
The thread says who it waits on:
↳ Wardrik: Waiting on @maria or @jon to approve
create_bill.The first to answer decides.
If Slack can reach none of the approvers, the call is asked in the thread, with the same buttons.
Notices
An agent nobody watches, such as one a trigger runs every morning, can post to a room when it
finishes, fails or waits on someone. Route it to slack:#room:
Wardrik.Agent.define(scope, "acme/digest",
model: "anthropic:claude-haiku-4-5",
notify: %{to: ["slack:#updates"], on: ["finished", "failed"]}
)To ask approvers about a parked turn, add a target to your Wardrik's notify, and route to it:
config :my_app, MyApp.AgentRuntime,
notify: [targets: [ops: {Wardrik.Notify.Room, channel: "slack", room: "#ops", approvers: ["@maria"]}]]
Wardrik.Agent.define(scope, "acme/triage", model: "anthropic:claude-haiku-4-5", notify: "ops")The notice is posted in the room with Approve and Deny. Each approver also gets it in the Messages tab, with a link. Only the channel's own approvers may decide.
- The app posts in any public channel, since the manifest asks for
chat:write.public. Invite it into private ones. - It never answers its own posts, so a room where the agent answers every message doesn't loop.
See Wardrik.Notify for webhooks and other targets.
Many workspaces
One app can serve many workspaces, one for each org:
slack:
{WardrikSlack,
client_id: System.fetch_env!("SLACK_CLIENT_ID"),
client_secret: System.fetch_env!("SLACK_CLIENT_SECRET"),
redirect_uri: "https://agents.example.com/slack/installed",
signing_secret: System.fetch_env!("SLACK_SIGNING_SECRET"),
installations: :stored}client_id and client_secret are under Basic Information. Set redirect_uri under
OAuth & Permissions. Each workspace's token is sealed in Wardrik's vault, so Wardrik needs
:keys. See WardrikSlack.Install.