Predict Jira Sprint Slip Risk with n8n and Notify Team via Slack
Scheduled Jira active sprint pull → compute health metrics → OpenRouter AI risk assessment → conditional Slack team alert
- Who
- Scrum teams, PMs, or engineering managers using Jira Software Cloud and Slack
- You get
- Automatically fetch active sprint status each weekday morning, assess slip risk with AI, and post a motivating Slack alert only when risk is not on_track
- You need
- n8n · jira · slack
Constraints
Prerequisites: n8n instance; Jira Software Cloud credentials (board and sprint access); OpenRouter API credential (tool-calling chat model); Slack API credential (bot can post to target channel). Set JIRA_DOMAIN, JIRA_BOARD_ID, STORY_POINTS_FIELD (e.g., customfield_10016), DONE_STATUSES, and SLACK_CHANNEL in the Config node. Default schedule is weekdays at 08:00 (cron: 0 8 * * 1-5).
What they did
Author salimbr published an n8n workflow template that automatically predicts Jira sprint slip risk. The template triggers via Schedule Trigger or manually, uses a Config node for Jira and Slack parameters, then calls the Jira Agile API to fetch the active sprint and its issues, computes metrics (completion, velocity gap, blockers) in a Code node, assesses risk level via an OpenRouter-hosted OpenAI model, and—only when risk is not on_track—drafts and posts a motivating Slack message to the team channel via a second AI Agent.
How it went
The source (template description and workflow.json) shows complete node structure and parameter configuration, but provides no actual execution logs or effectiveness data. The template claims automatic team notification when risk is detected, with silent fallback on parse failure (author_report).
Worth knowing
1. Default model is openai/gpt-oss-20b:free; OpenRouter free models may have rate limits or change availability—monitor accordingly. 2. Jira custom field ID (e.g., customfield_10016) varies by instance and must be updated to match your environment. 3. Blocker detection relies on labels or status names containing 'block'; adjust Compute Sprint Metrics Code if your team uses different naming. 4. On parse failure, the default risk_level is on_track with no notification—a non-blocking fallback that may mask AI output anomalies. 5. The Team Notifier Agent's systemMessage requires single-asterisk Slack bold; double asterisks will render literally and look broken. 6. This site has not tested execution; verify with Manual Trigger before enabling scheduled runs.
n8n workflow template
Structure checked · Not run-testedUse “Import from File” in n8n, or copy the JSON and paste it into the editor. Connect your own accounts, then try test data. The template is inactive by default.
Setup and test checklist
Credentials to connect
- Fetch Active Sprint node: connect jiraSoftwareCloudApi credentials.
- Send a message in Slack node: connect Slack API credentials.
- Fetch Sprint Issues node: connect jiraSoftwareCloudApi credentials.
- OpenAI Slip Predictor node: connect openRouterApi credentials.
- OpenAI Team Notifier node: connect openRouterApi credentials.
Configuration to replace
- Source template is n8n public workflow #16739 (~19 nodes). Keep inactive after import; verify triggers, expressions and external API endpoints.
- Sticky-note guidance is informational; replace placeholders with your own accounts, sheet schemas or channel IDs.
Test input and expected output
- Manually run the main path once in a test workspace and confirm credentialed nodes authenticate without errors.
- Drive one synthetic/redacted sample through and check output fields and downstream writes before enabling in production.
Template changes
- Author credential bindings, webhookId and execution data are removed; the workflow is inactive. Nodes, connections and original prompts are preserved. Not run-tested in n8n.
- No business-logic edits; verify keys, quotas and privacy compliance before use.
Template retrieved: 2026-10-25
Steps
Trigger and Configuration
After importing the template into n8n, configure the trigger and global variables. Supports manual execution (When clicking 'Execute workflow') or scheduled trigger (Schedule Trigger, default weekdays 08:00). Open the Config node (Set) and enter your JIRA_DOMAIN (e.g., your-instance.atlassian.net), JIRA_BOARD_ID (number), STORY_POINTS_FIELD (your Jira custom field ID for story points, e.g., customfield_10016), DONE_STATUSES (e.g., DONE), WIP_STATUSES (e.g., TO DO, IN PROGRESS, TO REVIEW), RISK_THRESHOLD (default at_risk), and SLACK_CHANNEL (e.g., #sprint-alerts).
Fetch Sprint Data
The workflow uses two sequential HTTP Request nodes to call the Jira Software Cloud Agile API. The first node, Fetch Active Sprint, requests /rest/agile/1.0/board/{boardId}/sprint?state=active to get the current active sprint. The second node, Fetch Sprint Issues, uses the returned sprint ID to request /rest/agile/1.0/sprint/{sprintId}/issue?fields=summary,status,labels,{storyPointsField}&maxResults=100 for all issues in that sprint. Both nodes use the jiraSoftwareCloudApi credential.
Compute Sprint Metrics
The Compute Sprint Metrics Code node (Code: JavaScript) processes the returned issues and sprint info, calculating: total sprint days, remaining days, time elapsed percentage, total story points, done points, in-progress points, todo points, completion percentage, ideal completion percentage, slip margin (negative means behind), remaining points, required daily velocity, actual daily velocity, velocity gap, blocker count and list. Blockers are flagged if labels contain 'block' or status name contains 'block'.
Execution prompt · Source text。JS Code field of the Compute Sprint Metrics Code node
// --- Inputs --- const issues = $input.first().json.issues; const config = $('Config').first().json; const sprint = $('Fetch Active Sprint').first().json.values[0]; // --- Sprint timing --- const sprintStart = new Date(sprint.startDate); const sprintEnd = new Date(sprint.endDate); const now = new Date(); const totalMs = Math.max(1, sprintEnd - sprintStart); const elapsedMs = Math.min(totalMs, Math.max(0, now - sprintStart)); const remainingMs = Math.max(0, sprintEnd - now); const totalDays = totalMs / 86400000; const remainingDays = remainingMs / 86400000; const elapsedDays = elapsedMs / 86400000; const timeElapsedPct = (elapsedMs / totalMs) * 100; // --- Points by status category (universal keys) --- const spField = config.STORY_POINTS_FIELD; let totalPoints = 0, donePoints = 0, inProgressPoints = 0, todoPoints = 0; const blockers = []; for (const issue of issues) { const pts = issue.fields?.[spField] ?? 0; const catKey = issue.fields?.status?.statusCategory?.key ?? "new"; const labels = issue.fields?.labels ?? []; totalPoints += pts; if (catKey === "done") donePoints += pts; else if (catKey === "indeterminate") inProgressPoints += pts; else todoPoints += pts; // Flag blocked tickets (label or status name containing "block") const statusName = (issue.fields?.status?.name ?? "").toLowerCase(); if (labels.some(l => l.toLowerCase().includes("block")) || statusName.includes("block")) { blockers.push({ key: issue.key, summary: issue.fields?.summary, points: pts }); } } // --- Core slip signals --- const completionPct = totalPoints > 0 ? (donePoints / totalPoints) * 100 : 0; const idealCompletionPct = timeElapsedPct; const slipMargin = completionPct - idealCompletionPct; // negative = behind const remainingPoints = totalPoints - donePoints; const requiredPointsPerDay = remainingDays > 0 ? remainingPoints / remainingDays : remainingPoints; const actualPointsPerDay = elapsedDays > 0 ? donePoints / elapsedDays : 0; const velocityGap = actualPointsPerDay - requiredPointsPerDay; // negative = too slow return [{ json: { sprint_name: sprint.name, sprint_goal: sprint.goal || "(no goal set)", total_days: Math.round(totalDays * 10) / 10, remaining_days: Math.round(remainingDays * 10) / 10, time_elapsed_pct: Math.round(timeElapsedPct), total_points: totalPoints, done_points: donePoints, in_progress_points: inProgressPoints, todo_points: todoPoints, completion_pct: Math.round(completionPct), ideal_completion_pct: Math.round(idealCompletionPct), slip_margin: Math.round(slipMargin), remaining_points: remainingPoints, required_points_per_day: Math.round(requiredPointsPerDay * 100) / 100, actual_points_per_day: Math.round(actualPointsPerDay * 100) / 100, velocity_gap: Math.round(velocityGap * 100) / 100, blocker_count: blockers.length, blockers: blockers } }];AI Risk Assessment
The Slip Predictor Agent (AI Agent node) sends the computed metrics to an OpenRouter model (default openai/gpt-oss-20b:free). The Agent's user text injects sprint metrics; the systemMessage defines it as a sprint risk analyst, instructing it not to recalculate numbers but to interpret them and return structured JSON: risk_level (on_track/at_risk/critical), risk_score (1-10), headline, diagnosis, recommendations, tickets_to_watch.
Execution prompt · Source text。text field (user message) of the Slip Predictor Agent node
==Analyze this sprint's health and predict slip risk. <sprint_metrics> Sprint: {{ $json.sprint_name }} Goal: {{ $json.sprint_goal }} Days remaining: {{ $json.remaining_days }} of {{ $json.total_days }} Time elapsed: {{ $json.time_elapsed_pct }}% Completion: {{ $json.completion_pct }}% done ({{ $json.done_points }}/{{ $json.total_points }} pts) Ideal completion at this point: {{ $json.ideal_completion_pct }}% Slip margin: {{ $json.slip_margin }} points Work distribution: - Done: {{ $json.done_points }} pts - In progress: {{ $json.in_progress_points }} pts - To do: {{ $json.todo_points }} pts Velocity: - Required from now: {{ $json.required_points_per_day }} pts/day - Actual so far: {{ $json.actual_points_per_day }} pts/day - Gap: {{ $json.velocity_gap }} pts/day Blockers: {{ $json.blocker_count }} {{ JSON.stringify($json.blockers) }} </sprint_metrics> Return the risk assessment JSON.Execution prompt · Source text。systemMessage field of the Slip Predictor Agent node
You are a sprint risk analyst. You receive computed metrics about an active sprint and must assess the risk that the sprint will not complete on time. You do NOT recalculate the numbers — they are already computed and reliable. Your job is to interpret them, assign a risk level, and recommend action. Key signals to weigh: - slip_margin: completion % minus ideal % based on time elapsed. Negative = behind. - velocity_gap: actual points/day minus required points/day. Negative = too slow. - remaining_points vs remaining_days: is the remaining work realistic? - blocker_count: blocked tickets are a leading indicator of slip. - Early sprint (time_elapsed_pct < 30) → be lenient, small gaps are normal. - Late sprint (time_elapsed_pct > 70) → be strict, gaps are hard to recover. Assign one risk level: - "on_track" — sprint will likely complete; no action needed - "at_risk" — recoverable slip; PM should act now - "critical" — sprint will likely miss its goal without intervention Output a JSON object with this exact structure: { "risk_level": "on_track" | "at_risk" | "critical", "risk_score": integer 1 to 10, "headline": "one-line summary of the sprint health", "diagnosis": "2-3 sentences explaining WHY, citing the specific metrics", "recommendations": ["2-4 concrete, prioritized actions the PM should take"], "tickets_to_watch": ["keys of the most at-risk or blocking tickets"] } Be direct and specific. Cite real numbers from the metrics. Frame recommendations as PM actions (rescope, reassign, unblock, escalate), not vague advice. Return only the JSON. No preamble, no markdown fences.Parse Prediction Result
The Parse Prediction Code node (Code: JavaScript) cleans the AI response (removes markdown code block markers) and attempts JSON.parse. On success, it merges the prediction with original metrics; on failure, it falls back non-blocking to risk_level=on_track with error=true, preventing crashes and suppressing Slack notification.
Execution prompt · Source text。JS Code field of the Parse Prediction Code node
const response = $input.first().json.output ?? $input.first().json.text ?? ""; const cleaned = response .replace(/^```json\s*/i, "") .replace(/^```\s*/i, "") .replace(/\s*```$/i, "") .trim(); try { const parsed = JSON.parse(cleaned); // Merge prediction with the original metrics for downstream use const metrics = $('Compute Sprint Metrics Code').first().json; return [{ json: { ...parsed, sprint_name: metrics.sprint_name, completion_pct: metrics.completion_pct, remaining_days: metrics.remaining_days, total_points: metrics.total_points, done_points: metrics.done_points } }]; } catch (error) { // Non-blocking fallback — no alert on parse failure rather than crash return [{ json: { risk_level: "on_track", risk_score: 0, headline: "Prediction parse failed — no alert sent", diagnosis: `Invalid JSON returned by Slip Predictor Agent: ${error.message}`, recommendations: [], tickets_to_watch: [], error: true } }]; }Conditional Team Notification
The If node checks whether risk_level is not equal to on_track. If at_risk or critical, the flow enters the Team Notifier Agent, where a second OpenRouter model drafts a motivating team message and posts it via the Send a message in Slack tool to the configured channel. If risk_level is on_track (or parse failure default), the No Operation branch ends the workflow without notification. The Team Notifier Agent's systemMessage specifies a warm, collective, energizing tone, requires single-asterisk Slack bold, blank-line separation, emoji header structure, and mandates posting via the Slack tool exactly once.
Execution prompt · Source text。text field (user message) of the Team Notifier Agent node
=Compose and post a motivating team message about this sprint's status. <sprint_status> Sprint: {{ $json.sprint_name }} Risk level: {{ $json.risk_level }} ({{ $json.risk_score }}/10) Completion: {{ $json.completion_pct }}% ({{ $json.done_points }}/{{ $json.total_points }} pts) Remaining: {{ $json.total_points - $json.done_points }} pts to do, {{ $json.remaining_days }} day(s) left Analyst headline: {{ $json.headline }} Analyst diagnosis: {{ $json.diagnosis }} Recommended actions: {{ $json.recommendations.map((r, i) => `${i + 1}. ${r}`).join('\n') }} </sprint_status> Post the message to the team channel now.Execution prompt · Source text。systemMessage field of the Team Notifier Agent node
You are a team-focused engineering manager. YOUR PRIMARY TASK: compose a motivating Slack message about a sprint that is falling behind, then IMMEDIATELY post it to the team channel by calling the Slack tool. Posting the message is mandatory — a message that is written but not posted is a failure. You MUST call the Slack tool exactly once. Once posted, reply with a one-line confirmation of what you sent. --- Below are the rules for HOW to write the message. Tone rules: - Warm, collective, energizing. Use "we"/"nous", never "you" as blame. - Frame the delay as a shared, recoverable challenge — never a failure. - Honest about the situation but always end on a confident note. - Write in the same language as the sprint data (French if tickets are French). - Reframe the analyst's recommendations as opportunities, not orders. CRITICAL Slack formatting rules: - Slack uses SINGLE asterisks for bold: *like this*. NEVER use double asterisks **like this** — they render literally and look broken. - Separate every logical block with a BLANK LINE for readability. - Use a clear visual structure with section emojis as headers, like: :rocket: *[Punchy headline]* [1-2 sentence framing of the situation] :dart: *Notre plan d'action* 1. [action] 2. [action] 3. [action] :bar_chart: *Où on en est* [completion snapshot in one line] :fire: *Prochaine étape* [concrete next step — usually the next stand-up] [One short motivating closing line] - Keep each line concise. Avoid dense paragraphs. - Use natural domain terms: say "le sprint" or "le board Jira", never "le tableau". - Do not invent metrics. Use only the numbers provided. Process: 1. Read the sprint status. 2. Compose the message following the structure above. 3. Call the "Send a message in Slack" tool ONCE to post it. 4. Respond with a brief confirmation. Do not post more than one message.
Execution prompt · Source text。text field of the Send a message in Slack node (populated by Team Notifier Agent via fromAI)
={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('Message_Text', ``, 'string') }}
Prompt you can copy
Setup guidance · Editorial suggestion。Setup assistant to help user import template and verify configuration
This block is the prompt. Ads are never placed here.
You are an n8n workflow setup assistant. The user wants to replicate the "Predict Jira Sprint Slip Risk and Slack Notify" template. Assist in this order: 1. Confirm the user has an n8n instance, Jira Software Cloud credentials, OpenRouter API Key, and Slack Bot Token; 2. Guide importing the template (from n8n.io/workflows/16739 or workflow.json); 3. Check that Config node values JIRA_DOMAIN, JIRA_BOARD_ID, STORY_POINTS_FIELD, DONE_STATUSES, and SLACK_CHANNEL are replaced with actual values; 4. Explain how to find the Jira custom field ID (e.g., from an issue's fields lookup for customfield_xxx); 5. Recommend testing with Manual Trigger first, and only enabling Schedule Trigger after Fetch Active Sprint and Fetch Sprint Issues return expected data; 6. If the AI prediction returns an unexpected format, note that Parse Prediction Code has a fallback defaulting to on_track with no notification.
After importing, fill the Config node first, then run manually to verify Jira data returns correctly
Source
Predict sprint slip risk in Jira with OpenAI via OpenRouter and Slack alerts · salimbr · n8n.io · 2026-06-30
Comments & feedback
Tried this method? Share results, problems or improvements. Comments appear after moderation. Please avoid sensitive information.
Loading…
Agent feedback
Reports are submitted by Agents and shared after user confirmation and moderation. They are not platform-verified.