What it looks like
When a workflow hits a human gate like this:Approve Plan stageA team member clicks a button, the message updates to show the selection, and the workflow resumes on the chosen path.approveOpen in Fabro (context from the upstream stage, when available)[Approve][Revise]
Supported question types
| Question type | Slack UI |
|---|---|
| Yes/No | Two buttons: Yes, No |
| Confirmation | Two buttons: Yes, No |
| Multiple choice | One button per option |
| Multi-select | Checkboxes with a Submit button |
| Freeform | Prompt to reply in a thread |
@mention prefix stripped) becomes the answer.
Setup
1. Create a Slack App
- Go to api.slack.com/apps and click Create New App
- Choose From scratch, give it a name (e.g. “Fabro”), and select your workspace
2. Enable Socket Mode
- In the app settings sidebar, go to Socket Mode
- Toggle Enable Socket Mode on
- Create an App-Level Token with the
connections:writescope - Copy the token (starts with
xapp-) — this is yourFABRO_SLACK_APP_TOKEN
3. Add bot token scopes
- Go to OAuth & Permissions in the sidebar
- Under Bot Token Scopes, add the scopes you need:
chat:write— required to post and update Slack messageschat:write.public— optional, only needed to post lifecycle notifications or interview prompts to public channels without inviting the bot firstchannels:history— optional, only needed for freeform thread replies in public channelsgroups:history— optional, only needed for freeform thread replies in private channels
4. Enable interactivity
For human-in-the-loop buttons, multi-select menus, and other interactive controls:- Go to Interactivity & Shortcuts in the sidebar
- Toggle Interactivity on
5. Subscribe to events for freeform replies
Skip this step if you only use lifecycle notifications, buttons, confirmations, multiple choice, or multi-select questions.- Go to Event Subscriptions in the sidebar
- Toggle Enable Events on
- Under Subscribe to bot events, add:
message.channels— receive public channel messages, needed for freeform thread replies in public channelsmessage.groups— receive private channel messages, needed for freeform thread replies in private channels
6. Install the app
- Go to Install App in the sidebar
- Click Install to Workspace and authorize
- Copy the Bot User OAuth Token (starts with
xoxb-) — this is yourFABRO_SLACK_BOT_TOKEN
7. Configure Fabro
Add both tokens to the Fabro server vault:server.env. server.env is reserved for bootstrap secrets such as SESSION_SECRET, FABRO_DEV_TOKEN, and object-store credentials.
Restart the server after changing Slack credentials so the Socket Mode connection is recreated with the new tokens.
Enable Slack in your server configuration. You can leave default_channel out if you only use per-route lifecycle notification channels:
settings.toml
default_channel is used only for human-in-the-loop interview prompts. Run lifecycle notifications use per-run or per-workflow [run.notifications] routes instead.
8. Invite the bot
Invite the bot to any channel where you want it to post interview questions or lifecycle notifications:chat:write.public scope allows posting to public channels without an invite. Private channels always require an invite.
9. Verify startup
Start or restart the Fabro server and check its logs. A working Slack connection should include:<storage>/logs/server.log unless you configure [server.logging] differently.
You can also check Settings > Integrations in the web UI. The Slack row is computed from server configuration, vault credential presence, and the live Socket Mode connection state, so it can distinguish missing credentials, connecting, connected, and error states.
How it works
Fabro uses the same web interviewer that powers the web UI. When a human gate fires:- Fabro builds a Block Kit message from the question, stage hint, upstream context, optional run link, and answer controls
- Posts the message to the configured Slack channel
- The workflow blocks, waiting for an answer
- Button click — Slack delivers the interaction over the Socket Mode WebSocket. Fabro maps the button’s action ID back to the question and submits the answer. The original message is updated to show the selection.
- Thread reply (freeform) — The user replies in the message thread. Fabro receives the reply as a message event, matches it to the pending question via the thread timestamp, and submits the text as the answer.
Run lifecycle notifications
Slack run lifecycle notifications are opt-in per run or workflow through the[run.notifications] namespace. Notification settings do not live in server config.
workflow.toml
run.failed is a terminal run event. A stage can fail and still be followed by another graph edge that lets the run complete; in that case a route listening for run.completed fires, not run.failed.
The route-level Slack channel is required for lifecycle notifications. The channel may be a literal ("#deploys") or an environment interpolation ("{{ env.DEPLOYS_SLACK_CHANNEL }}"). If the channel is missing, empty, or cannot be resolved, Fabro logs a warning and skips that route without affecting the run or other notification routes.
Lifecycle notifications are one-way and fire-and-forget. They never accept answers, register reply threads, update prior messages, or interact with interview state.
Smoke test lifecycle notifications
To verify delivery end to end, add a Slack route to any workflow and run it on the server:workflow.toml
workflow.fabro
run.started and run.completed. To verify run.failed, replace the graph with a failing command stage that has no normal exit path, then run it again:
workflow.fabro
Environment variables
| Variable | Required | Description |
|---|---|---|
FABRO_SLACK_BOT_TOKEN | Yes | Bot User OAuth Token (xoxb-...). Used to post and update messages. |
FABRO_SLACK_APP_TOKEN | Yes | App-Level Token (xapp-...). Used to connect via Socket Mode. |
default_channel is not required for lifecycle notifications; it is only needed when you want interview prompts to use a default Slack channel.
Limitations
- Single workspace — Fabro connects to one Slack workspace at a time.
- One answer per question — The first person to click a button or reply in a thread provides the answer. Subsequent interactions on the same question are ignored.
- No Slack steering — Slack does not support steering running agents.
- Lifecycle notifications are one-way —
run.started,run.completed, andrun.failedmessages are posted once per matching route and are not updated later.