Generative UI in Slack, powered by OpenUI-lang.
Ask in plain English and the bot replies with a native, interactive Slack card - fields, buttons that actually do things, and real charts - instead of a wall of markdown. An LLM composes the interface as OpenUI-lang; this app compiles it to Slack Block Kit and runs the interaction loop.
/agent a card with a counter and +1 / -1 / reset buttons
/agent a bar chart of tasks by status: 5 todo, 3 in progress, 8 done, with a refresh button
@openui-agent give me a sprint status card with key metrics
Clicking a button updates the same message in place - state lives server-side, no new messages and no model call per click.
For how it all fits together, see ARCHITECTURE.md.
- Generative UI - the model composes each layout from a curated component set (headers, sections, field grids, context, dividers, images, buttons, charts).
- Interactive state - buttons carry actions (
@Set,@Reset) that run server-side and re-render the message viachat.update. - Real charts - bar / line / pie / area rendered to PNG with axes, value labels, and legends.
- Socket Mode - outbound WebSocket, so no public URL and no webhooks.
- Two entry points - the
/agentslash command and@mentions.
- Node.js 18+
- A Slack workspace where you can install an app
- An OpenAI API key
git clone <your-repo-url> openui-slack
cd openui-slack
npm installSocket Mode means no public URL - the bot opens an outbound WebSocket.
Go to api.slack.com/apps → Create New App → From an app manifest, pick your workspace, and paste:
display_information:
name: OpenUI Agent
features:
bot_user:
display_name: openui-agent
always_online: true
slash_commands:
- command: /agent
description: Ask the OpenUI agent
usage_hint: "[your request]"
should_escape: false
oauth_config:
scopes:
bot:
- commands # /agent slash command
- app_mentions:read # @mentions
- chat:write # post, chat.update, ephemeral
- files:write # upload chart images
- reactions:write # react to the triggering message
settings:
event_subscriptions:
bot_events:
- app_mention
interactivity:
is_enabled: true # button clicks -> block_actions over the socket
socket_mode_enabled: true
org_deploy_enabled: false
token_rotation_enabled: falseThen:
- App-level token - Basic Information → App-Level Tokens → Generate, scope
connections:write→ copy thexapp-…value. - Install - Install App → Install to Workspace → Allow → copy the Bot User OAuth Token
xoxb-…. - Invite the bot to a channel so it can post and be mentioned there:
/invite @openui-agent.
Copy .env.example to .env and fill in:
OPENAI_API_KEY=sk-...
OPENAI_MODEL=gpt-5.5
SLACK_BOT_TOKEN=xoxb-...
SLACK_APP_TOKEN=xapp-...
npm run dev # watch mode
# or
npm start # run onceThen in Slack: /agent a bar chart of tasks by status with a refresh button, or @openui-agent ….
npm run smoke # drive the reactive loop headlessly (state + action + re-render)
npm run chart # render each chart type to chart-<type>.png| Env var | Purpose |
|---|---|
OPENAI_API_KEY |
OpenAI credentials (required) |
OPENAI_MODEL |
Model id (default gpt-5.5) |
SLACK_BOT_TOKEN |
Bot User OAuth token xoxb-… (required) |
SLACK_APP_TOKEN |
App-level token xapp-… for Socket Mode (required) |
MCP_URL |
Optional. MCP server for live data via Query(), bridged with npx mcp-remote (e.g. https://mcp.linear.app/sse) |
- State is in-memory. Buttons on messages posted before a restart are acknowledged but do nothing.
Swap the
Mapinsrc/store.tsfor Redis to make it durable. - No modals or rich input forms - Slack routes those through modals, not yet supported here.
- Reply-only, single workspace - responds to commands and mentions; no proactive posting or multi-tenant OAuth.
Set MCP_URL to an MCP server and the agent's Query() calls run against it. On boot, index.ts
connects (via npx mcp-remote, which handles hosted OAuth on first run), registers the server as the
tool provider, and lists its tools in the prompt. Then a request like /agent a snapshot of my open issues fetches real data. Without MCP_URL, the agent invents plausible data.
- Mutations - run a
Mutation()tool from a button (@Run); reads (Query()) work today. - Query arrays into tables - a column-projection
Tablebound to query rows. - Streaming - stream model output and debounce
chat.update. - Durable state - Redis-backed message store.
MIT - see LICENSE.