Project Story

Inspiration

The idea started from a simple, uncomfortable observation: Slack workspaces exclude people constantly, and almost never on purpose. Someone posts a screenshot with no alt text. A status update goes out as a bare 🔴 with no label. A dense thread full of jargon gets pinned as "must read." Nobody intends to lock a screen-reader user, a colorblind teammate, or someone with ADHD out of the conversation — it just happens, because accessibility is nobody's job in the moment content gets created.

We wanted to build something that didn't require a policy, a training, or an admin setting — something that just showed up at the exact moment it was useful and got out of the way otherwise. That became our organizing model: three moments where accessibility actually breaks down (something exclusionary gets posted, someone can't parse what's there, someone doesn't know help exists), and an agent built to intervene at each one, quietly.

The Slack Agent Builder Challenge's "Slack Agent for Good" track was the right forcing function to actually build it instead of just talking about it.

What it does

Accessibility Co-Pilot watches a workspace and:

  • Suggests draft alt text (visible only to the uploader) the moment an image is posted, and offers OCR extraction for scanned PDFs with no text layer.
  • Flags color-only status signals (a bare 🔴 with no label) privately to the person who posted them.
  • Lets anyone right-click a message to get it rewritten in plain language, tuned to how they need it — shorter, every acronym explained, dyslexia-friendly, or ELI5 — via a customization modal instead of one fixed rewrite.
  • Lets anyone right-click a thread to get a linear, screen-reader-friendly digest.
  • Sends a single onboarding DM to new members explaining what's available, once, with no ongoing nagging.

How we built it

We started from Slack's bolt-python-assistant-template, scaffolded through the Slack CLI into a Developer Sandbox, and split the system into two pieces on purpose: a Bolt-Python app that owns everything Slack-facing (events, Block Kit, modals), and a standalone MCP server that owns everything generation-facing (five tools: generate_alt_text, extract_pdf_text, simplify_text, digest_thread, check_color_only_signal). The Bolt app talks to the MCP server as a client over stdio, spawning it as a local subprocess per call — simple enough to reason about during a live demo, and it keeps the "MCP server integration" requirement genuinely load-bearing rather than bolted on.

The harder design decision was making /simplify and /digest actually context-aware instead of a bare LLM wrapper. Before either tool rewrites anything, the Bolt app calls Slack's Real-Time Search API (assistant.search.context) to pull how a term or acronym has actually been used elsewhere in the workspace, and passes that into the Gemini 2.5 Flash prompt as grounding. That's the difference between "here's a plausible-sounding explanation of ARR" and "here's what your team actually means by ARR."

Every proactive suggestion — alt text, color-only nudges, PDF OCR offers — posts as an ephemeral message visible only to the person who can act on it. We wanted the agent to feel like a helpful colleague tapping you on the shoulder, not a bot narrating everyone's mistakes in public.

With limited time before the deadline, we prioritized ruthlessly: alt text first (highest demo value, simplest loop), then the RTS-grounded simplify/digest flow, then the onboarding DM last, since it's static and needed the least iteration.

Challenges we ran into

Alt text has no attach endpoint. We assumed there'd be a Slack API call to attach alt text to an already-uploaded file. There isn't one — alt text is only editable through the client's "Edit file details" UI. We pivoted to posting the generated alt text as a threaded reply instead, which turned out to be just as accessible (a screen reader reading the thread encounters it either way) and didn't require us to fake an integration that doesn't exist.

RTS quietly requires a different token than everything else. Bot-token calls to assistant.search.context need an action_token, which Slack only attaches to message/app_mention event payloads — not the message-shortcut payloads that /simplify and /digest are built on. We only found this after implementing it once and watching it fail silently. The fix was switching RTS calls to a user token (search:read.public scope) instead of the bot token, which meant re-reading OAuth scope documentation we'd assumed we already understood.

Slash commands don't work inside threads. Our original plan used /simplify and /digest as literal slash commands. Slack doesn't support invoking a slash command from inside a thread, which is exactly where you want to simplify a message or digest a conversation. We rebuilt both as message shortcuts (right-click → More actions) instead — a smaller change technically, but one that meant redoing the interaction design, including the customization modal for /simplify.

Failing gracefully mattered more than we expected. RTS lookups, Gemini calls, and file downloads can all fail mid-demo. We made a deliberate choice for search_workspace_context() to return an empty string instead of raising on failure, so /simplify and /digest degrade to a generic rewrite instead of crashing outright — important for a live, judged demo where we don't control network conditions.

What we learned

Coming from a cloud/platform engineering background (AWS, GCP, Kubernetes, Terraform), the Slack-specific plumbing was the real learning curve: Bolt's listener/event-subscription model, Block Kit's ephemeral-vs-threaded posting semantics, Socket Mode as a persistent-WebSocket alternative to standing up a public HTTPS endpoint, and the difference between message shortcuts and slash commands that cost us a redesign. We also came away with a much sharper sense of what "grounded" should mean for an LLM feature in a real product — RTS isn't just a checkbox for the hackathon rubric, it's the actual mechanism that keeps a simplify/digest rewrite honest to what a specific team actually said, instead of what a general-purpose model assumes.

The other lesson was about restraint: the strongest version of this project wasn't the one with the most features, it was the one where a small number of moments worked reliably enough to demo without anyone crossing their fingers.

What's next for Accessibility Co-Pilot

  • Workspace-level opt-in controls so admins can enable/disable individual moments per channel.
  • Expanding color-only detection beyond emoji to inline color formatting and image-embedded status indicators.
  • A lightweight feedback loop on generated alt text and digests, so accepted/edited suggestions can improve future prompts.
  • Investigating whether Slack's file API adds native alt-text attachment support, so we can drop the threaded-reply workaround.

Built With

Share this project:

Updates