What inspired SentinelGrid
I wanted to solve a real organizational problem, not an imagined one. So I went to Reddit and read complaint after complaint about incident response chaos. That's where I found the quote that became my north star: "Three of us saw it. All three assumed someone else had replied." That's diffusion of responsibility — and it silently kills incident response teams. I decided to build something that makes it structurally impossible.
How I built it
SentinelGrid runs on the Slack CLI with Deno and TypeScript — a stack I had never touched before this hackathon. The architecture is event-driven: three separate workflows connected through a shared datastore, triggered by webhooks, reactions, and scheduled timers. SentinelGrid uses MCP to expose tools including presence checking, datastore operations, and message posting to Slack's runtime. The workflow orchestrates which tools to call, while the LLM makes the intelligent decision about which engineer to assign based on incident type and confidence scoring.
The LLM wasn't in the original scope. But when I thought about how to assign incidents to engineers, hardcoding the logic felt wrong — incident descriptions are natural language, and specializations don't map cleanly to keywords. That's where the LLM earned its place: reasoning about who's best suited rather than pattern-matching strings. I was also deep in reading about LLM evaluation at the time, which led me to add confidence scoring — the agent scores its own certainty and explains its reasoning rather than pretending it always knows best. The acknowledgment loop came from the same instinct: keep humans in the loop, don't let automation pretend the problem is solved just because a message was posted.
The hardest moment
The DynamoDB filter expression syntax for querying the Slack datastore.
I needed to find an incident record by its message timestamp — a
non-primary-key field. I attempted it six times. The issue was a single
character: value placeholders require a : prefix (:message_ts),
attribute name placeholders require a # prefix (#message_term),
and they must match exactly between the expression string and the values
map. I read the Slack documentation line by line until I understood why
the syntax worked, not just that it did. That moment mattered more to me
than getting it right.
What I learned
I learned a completely new stack from scratch — Deno, TypeScript, and the Slack CLI — and built my first event-driven architecture and my first production deployment. But the thing I'm most proud of is simpler: I can explain every design decision in SentinelGrid in depth. That's new for me. I've built hackathon projects before where I could demo but not defend. This one I can defend.
Built With
- automation
- datastores
- deno
- groq
- mcp
- slack
- slack-cli
- typescript
Log in or sign up for Devpost to join the conversation.