We’ve all been there.
It’s 10:30 AM. You drop your update in the #daily-standup Slack channel: "Finally squashed that weird auth bug on the login portal, moving on to the database migration."
Two hours later, your Product Manager DMs you: "Hey, awesome job on the auth bug! Did you remember to move the ticket to Done in Jira?"
Context switching is the absolute worst part of being a developer. We are already communicating our status naturally in Slack—why do we have to open a new tab, wait for Jira or Linear to load, find the right sprint, and click a button just to say the exact same thing?
Last weekend, I decided I’d had enough. I built a Slack bot that reads my daily updates, figures out which ticket I’m talking about, and auto-closes it for me.
Here is how I built it using Next.js, the Slack Events API, and Claude 3.5 Sonnet.
The Architecture
The flow is surprisingly simple, but the execution requires some strict timing:
- Developer posts a message in a monitored Slack channel.
- Slack fires a webhook to my Next.js API route.
- The API fetches the user's currently open tasks from Jira/Linear.
- Claude 3.5 evaluates the Slack message against the open tasks to see if there's a match.
- If confidence is > 95%, the bot hits the PM tool's API to mark it "Done" and replies in Slack.
Challenge 1: Beating the Slack 3-Second Timeout
Slack's Events API is notoriously strict. When they send a webhook payload, you have exactly 3 seconds to respond with a 200 OK, or Slack assumes your app is dead and retries the event (which leads to duplicate AI calls and chaos).
Because calling the Anthropic API and the Jira API takes way longer than 3 seconds, you cannot await the AI processing in the main thread.
To solve this in Next.js (deployed on Vercel), I used the waitUntil function from @vercel/functions. This allows the serverless function to return the 200 OK immediately to Slack, while keeping the execution environment alive to finish the AI pipeline in the background:
// src/app/api/webhooks/slack/route.ts
import { waitUntil } from '@vercel/functions';
import { NextRequest, NextResponse } from 'next/server';
import { processSlackMessage } from '@/lib/ai/pipeline';
export const POST = async (request: NextRequest): Promise<NextResponse> => {
// ... verify signatures and parse payload ...
const event = payload.event;
if (event.type === 'message') {
// 1. Queue the heavy AI processing in the background
waitUntil(
processSlackMessage({
teamId: payload.team_id,
channelId: event.channel,
userId: event.user,
messageText: event.text,
messageTs: event.ts,
}).catch(console.error)
);
}
// 2. Immediately return 200 OK to satisfy Slack's 3-second rule
return NextResponse.json({ ok: true });
};
Challenge 2: Stopping the AI from Hallucinating Tasks
The scariest part of this build was the idea of an AI accidentally closing the wrong ticket. If a developer says "I'm going to start working on the header tomorrow", the AI shouldn't close the "Header Redesign" ticket just because the words match.
To fix this, I engineered a highly specific system prompt for Claude 3.5. Instead of just asking "does this match?", I force the LLM to return a strict JSON object with a calculated confidence_score.
I gave Claude very specific scoring guidelines:
// src/lib/ai/prompts.ts
const SYSTEM_PROMPT = `You are NudgeBot's task-matching engine. Given a Slack message and a list of the sender's open tasks, determine whether the message indicates that one or more tasks have been completed.
Return ONLY valid JSON in this exact shape:
{
"matches": [
{
"task_id": "<uuid>",
"confidence_score": <0.0–1.0>,
"reasoning": "<1–2 sentences>"
}
]
}
Scoring guidelines:
0.95–1.00 Explicit: "finished X", "deployed X", PR merged for X
0.80–0.94 Strong: past-tense action verbs ("shipped", "pushed") + context
0.65–0.79 Moderate: implicit completion language, partial name overlap
0.50–0.64 Weak: tangentially related, ambiguous phrasing
Rules:
- Past tense, "just", "finally", deployment language are positive signals.
- Questions, future tense ("will", "going to"), and negations ("not done") are NOT completions.
- Return empty matches if nothing matches above 0.50.`;
In the backend pipeline, if Claude returns a score of 0.95 or higher, the app automatically closes the ticket. If it's between 0.70 and 0.94, the bot sends an ephemeral (private) message to the user in Slack with a button asking: "Looks like you finished the Auth Bug. Should I close the ticket for you?"
The "Trust" Feature (Audit Trails)
If you automate project management, your PMs will eventually get paranoid about why things are moving.
Whenever the bot closes a ticket, it makes a secondary API call to Jira/Linear to leave a comment on the ticket: ✅ Marked as done by NudgeBot based on a Slack update from @Aditya: "Finally squashed that weird auth bug."
Complete transparency, zero extra clicks.
Don't want to build it yourself?
Building this was a super fun weekend project, but managing OAuth flows for Slack, Jira, Linear, Asana, and Trello is a massive headache.
Because we are developers and we appreciate not reinventing the wheel, I actually packaged this exact codebase into a 1-click installable SaaS.
If you want to stop your PMs from nagging you, you can invite the bot to your workspace and use it for free here: NudgeBot.xyz.
Let me know in the comments if you'd trust an AI to manage your project board!
Top comments (0)