Slack Daily Digest
Produce a summarized daily digest of Slack activity across configured channels and DMs for a given date. Outputs a structured JSON file per day and an HTML viewer to browse all digests.
First-time setup
Before first use, copy each .example.json file to its real name and fill in your values:
user_config.example.json→user_config.json(in the workspace root, not this skill's directory) — your identity and environment (optional:jira_email,jira_account_idfor reliable Jira @mention search in Step 6c; optionalobsidian_digest_pathfor where Markdown is written at the end of Step 7)slack_channels_config.example.json→slack_channels_config.json— Slack channels to monitorfeature_watchlist.example.json→feature_watchlist.json— Jira feature keys to track (optional); optionalepic_child_refreshcontrols how often child issues are refetched (Step 6b)close_circle.example.json→close_circle.json— Slack handles of people in your close circle for viewer filtering (optional)
You also need the following MCP servers configured in Cursor:
- Slack MCP — commonly
user-slackoruser-user-slack(providesconversations_history,conversations_search_messages, etc.); use whatever identifier Cursor shows for your Slack integration - Atlassian MCP — commonly
user-atlassian(providesjira_get_issue,jira_search); use your Cursor MCP identifier
MCP Servers
This skill uses only the MCP servers configured in Cursor. Workflow steps below use placeholders {slack_mcp_server} and {atlassian_mcp_server} in every server: field—substitute the actual server names from your Cursor MCP configuration (Settings → MCP), not the example names above.
Do NOT use any other Slack MCP server than the one configured for your workspace.
Required Input
The user must provide a date for the digest (e.g., 2026-03-21, yesterday, today). If not provided, ask for it before proceeding. Normalize the date to YYYY-MM-DD format.
Optional: Person activity search
The user may also provide a person identifier — a Slack handle, email address, or display name (e.g., your-rh-handle@redhat.com, @your-rh-handle, Jane Smith). When provided, Step 3d runs to search what that person said across all public channels on the target date. This can be used:
- Alongside a full digest — the person's activity is appended to the digest JSON and presented after the executive summary
- On its own — if the user only asks what a specific person said (without requesting a full digest), skip Steps 1–3c and 6b–6c, and only run Steps 0, 1b, 3d, 4, 5, 6, 6a, 7 (including the Markdown export at the end of Step 7), and 8
Workflow
Step 0 — Load user configuration
Read user_config.json (in the workspace root) to get the user's identity and environment. All subsequent steps use these values instead of hardcoded names or URLs.
{
"slack_username": "your-slack-username",
"jira_display_name": "Your Full Name",
"jira_username": "your-jira-username",
"jira_email": "you@company.com",
"jira_account_id": "712020:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"slack_domain": "your-company.enterprise.slack.com",
"slack_dm_domain": "your-company-internal.slack.com",
"jira_base_url": "https://your-company.atlassian.net",
"obsidian_digest_path": ""
}
Use {slack_username}, {jira_display_name}, {jira_username}, {jira_email}, {jira_account_id}, {slack_domain}, {slack_dm_domain}, and {jira_base_url} as placeholders in the steps below. Substitute the actual values from the workspace-root user_config.json at runtime.
MCP server placeholders: Use {slack_mcp_server} for every Slack tool invocation (channels_list, conversations_history, conversations_search_messages, conversations_replies, users_search, etc.) and {atlassian_mcp_server} for every Atlassian/Jira tool. Read the actual names from Cursor’s MCP panel (e.g. user-slack, user-user-slack, user-atlassian).
Optional Jira fields: jira_email and jira_account_id are optional. Omit them or leave empty if unused.
Obsidian Markdown output directory: obsidian_digest_path — optional absolute path to a folder inside your Obsidian vault. If omitted or empty, Step 7 writes Markdown under this skill’s markdown/ directory (you can symlink that folder into a vault).
Atlassian MCP identity: JQL currentUser() refers to whoever authenticated the {atlassian_mcp_server} MCP connection (OAuth or API token owner). That is usually your own Atlassian account when you connect the integration in Cursor. If comment ~ currentUser() returns no rows but you know you have mentions, or results clearly belong to a different identity, treat the connection as non-matching and rely on jira_account_id (see Step 6c).
Resolve jira_account_id when missing: Before Step 6c, if jira_account_id is absent or blank, call jira_get_user_profile on {atlassian_mcp_server} with user_identifier set to {jira_email} if configured, otherwise {jira_username}. From the response, read the Atlassian account ID field (commonly accountId). Use that value for AAID-based JQL in Step 6c and suggest the user persist it in user_config.json.
Note: Some Slack workspaces use different domains for channel links vs DM links (e.g., redhat.enterprise.slack.com for channels but redhat-internal.slack.com for DMs). Use {slack_dm_domain} for all DM thread links (self-DMs and DMs with others). If slack_dm_domain is not set, fall back to {slack_domain}.
Step 1 — Load channel configuration
Read slack_channels_config.json (in this skill's directory) to get the list of channels and whether DMs should be included.
{
"channels": ["#team-channel-one", "#wg-channel-two", ...],
"mention_groups": ["@your-usergroup-one", "@your-usergroup-two"],
"include_dms": true
}
To add or remove channels or mention groups, edit slack_channels_config.json directly.
Step 1b — Resolve channel IDs
Slack deep links require channel IDs (e.g., C09S4J8TV5Y), not channel names. Before fetching messages, use channels_list on {slack_mcp_server} to get a mapping of channel names to IDs:
server: {slack_mcp_server}
tool: channels_list
args:
channel_types: "public_channel,private_channel"
limit: 999
Also fetch all IM (1:1 DM) rows and all MPIM rows so self-DM resolution, DM/MPDM thread_link ids, and slack-dm-list (invoked from Step 3b) have complete maps:
server: {slack_mcp_server}
tool: channels_list
args:
channel_types: "im"
limit: 999
server: {slack_mcp_server}
tool: channels_list
args:
channel_types: "mpim"
limit: 999
For both im and mpim, paginate with cursor until no further cursor—do not assume one page lists every conversation.
The self-DM channel is the IM entry named @{slack_username} (e.g., D06BXAVPNA2 for @dcawley). Record its D… id as self_dm_channel_id for Step 3 and Step 3b (exclude self-DM from the Step 3b “with others / group” bucket).
Build a lookup map from configured public/private channel name → channel ID. Use these IDs in all thread_link fields for configured channels. For 1:1 DMs use D… with {slack_dm_domain}; for MPDMs use C… with the domain rules in Step 5.
ID maps for Step 3b (slack-dm-list + digest links): From channels_list (im) results, for each IM row record dm_channel_id (D…) and dm_peer_scope (Name column, e.g. @handle). Build an optional @handle → D… map for resolving bulk-search rows. From mpim results, map mpdm_name (row Name / #mpdm-…) → mpdm_channel_id (C…) and a display string from Purpose / Topic. dm_peer_scope / dm_channel_id are used for legacy fallback in Step 3b only if bulk is:dm fails.
(Do not use these maps to change Step 2 or Step 3c.)
Step 2 — Fetch channel messages for the target date
For each channel in the config, use the conversations_history MCP tool on {slack_mcp_server}:
server: {slack_mcp_server}
tool: conversations_history
args:
channel_id: "#channel-name"
limit: "1d"
The limit: "1d" fetches one day of history. If the target date is not today, use conversations_search_messages with date filters instead:
server: {slack_mcp_server}
tool: conversations_search_messages
args:
filter_in_channel: "#channel-name"
filter_date_on: "YYYY-MM-DD"
limit: 100
Step 3 — Fetch self-DMs (notes to self)
Always fetch messages the user sent to themselves. These are personal reminders, action items, and reference links that should appear in the digest as actions.
For today, use conversations_history on {slack_mcp_server}:
server: {slack_mcp_server}
tool: conversations_history
args:
channel_id: "@{slack_username}"
limit: "1d"
For historical dates, use conversations_search_messages on {slack_mcp_server}:
server: {slack_mcp_server}
tool: conversations_search_messages
args:
filter_in_im_or_mpim: "@{slack_username}"
filter_date_on: "YYYY-MM-DD"
limit: 100
Treat every self-DM as an action item. Set urgency based on the message content (default to "today" unless the message clearly refers to a future date). Include these in the digest under a channel entry named "Self (notes/reminders)".
Step 3b — Fetch DMs with others and group DMs (if enabled)
If include_dms is true, delegate DM discovery and bulk fetch to the slack-dm-list skill, then merge results into the digest pipeline. This does not change Step 2 (channels), Step 3 (self-DM), or Step 3c (usergroup searches).
MCP alignment: Read the conversations_search_messages tool descriptor on {slack_mcp_server} before calling (required by slack-dm-list and this step).
Primary path — invoke slack-dm-list
- Read slack-dm-list/SKILL.md in full at the start of this step (same workspace:
.cursor/skills/slack-dm-list/SKILL.md). - Run slack-dm-list Workflow steps 1 — Bulk search and 2 — Dedupe by conversation using digest date
Das the target date and the same{slack_mcp_server}as the rest of this digest. - Digest-specific: Unlike a standalone slack-dm-list user reply (which may present only a table), for the digest you must retain all message rows from the bulk search, grouped by
Channel, for Steps 4 (conversations_replies) and 5 (summarization). Do not drop message text after deduping. - Run slack-dm-list step 3 — Labels to set digest
channeldisplay names ("DM with @peer","MPDM: …", etc.). Omit the self-DM conversation from Step 3b’s grouped results: useself_dm_channel_idfrom Step 1b so self-DM content stays only under Step 3’s"Self (notes/reminders)". - Resolve
channel_idand archive ids forthread_linkusing slack-dm-list step 4 — Optional archive links (optionalchannels_list) plus Step 1b ID maps. Apply{slack_dm_domain}/{slack_domain}per Step 5 in this skill. - Empty vs error: Follow slack-dm-list Failure handling and daily-digest Safe write rules: bulk search completes with zero rows ⇒ no DM/MPDM activity for
Din search scope (valid empty). Do not treat as Slack-unhealthy. - Optional supplements (only if the bulk
is:dmpath errors or achannel_idcannot be resolved after slack-dm-list fallbacks): you may use narrowconversations_history/ client-side filtering to calendarD; markkey_highlightsbest-effort when you rely on this.
Legacy fallback (daily-digest only — if bulk is:dm is unsupported or errors on {slack_mcp_server}): Do not use the slack-dm-list primary path. Instead: use Step 1b paginated im rows, exclude self-DM; for each remaining IM run conversations_search_messages with filter_date_on: D and filter_in_im_or_mpim try-order (A) dm_peer_scope, (B) dm_channel_id if the MCP documents D… for that parameter, (C) record IM as skipped if both fail. For MPDMs on D not returned by a failed bulk attempt, use channels_list (mpim) and scoped search only where needed. There is no 50-IM cap when paginating the full im list. List skipped or partial DMs in key_highlights.
Do not use conversations_unreads as the primary source for DM activity on date D — it reflects unread state, not full history for D. You may still call conversations_unreads for diagnostics only; it must not replace Steps 1–3 above for building the digest content.
Step 3c — Search for usergroup mentions
For each group in mention_groups, search for messages that @mention that group on the target date. This catches messages in channels not in the configured list that still need attention.
server: {slack_mcp_server}
tool: conversations_search_messages
args:
search_query: "@openshift-ai-exploring-managers"
filter_date_on: "YYYY-MM-DD"
limit: 100
Deduplicate against messages already fetched from configured channels. For any new messages found, include them in the digest under a channel entry named "Mentions (@group-name)" — e.g. "Mentions (@openshift-ai-exploring-managers)". The channel name must start with "Mentions (" so the viewer can identify these as tagged items. The viewer automatically labels these actions as "Tagged" and routes them to the My Actions tab (since the user was directly mentioned via a group they belong to). Treat these as action items with urgency "this_week" unless the content indicates higher urgency.
Step 3d — Person activity search (OPTIONAL)
Only run this step if the user provided a person identifier. This searches all public channels for what a specific person said on the target date.
3d.1 — Resolve the person
Use users_search on {slack_mcp_server} to resolve the person's identity:
server: {slack_mcp_server}
tool: users_search
args:
query: "<email or handle provided by user>"
limit: 5
From the results, identify the correct user. Extract:
- slack_user_id — the Slack user ID (e.g.,
U1234567890) - slack_username — their Slack username (e.g.,
your-rh-handle) - display_name — their display name (e.g.,
Jane Smith) - email — their email address
If multiple results are returned, pick the best match. If no results, inform the user and stop.
3d.2 — Search for the person's messages
Use conversations_search_messages on {slack_mcp_server} to find all messages from the person on the target date across all public channels:
server: {slack_mcp_server}
tool: conversations_search_messages
args:
filter_users_from: "@<slack_username>"
filter_date_on: "YYYY-MM-DD"
limit: 100
This searches across all public channels — no channel list needed.
If the results are paginated (a cursor is returned), fetch additional pages until all are retrieved.
3d.3 — Fetch full thread context
For each message that is part of a thread (has a thread_ts), fetch the full thread:
server: {slack_mcp_server}
tool: conversations_replies
args:
channel_id: "<channel_id from search result>"
thread_ts: "<thread_ts>"
limit: "50"
Deduplicate threads — if the person sent multiple messages in the same thread, only fetch the thread once.
3d.4 — Summarize each thread
For each thread the person participated in, produce:
- summary — 1-3 sentence summary of the overall thread conversation
- person_said — 1-3 sentence summary specifically of what the target person contributed (their messages, opinions, decisions, commitments)
- participants — list of Slack usernames who participated in the thread
- thread_link — Slack deep link:
https://{slack_domain}/archives/{channel_id}/p{thread_ts_no_dot}
For standalone messages (not in a thread), treat each as its own entry.
3d.5 — Build person activity output
Assemble the person activity into the person_activity object (see schema in Step 6) and also write a standalone report to reports/YYYY-MM-DD-<slack_username>.json in this skill's directory.
Update the reports manifest by reading all files in reports/ and writing viewer/reports-manifest.json:
{
"last_updated": "2026-04-01T15:30:00Z",
"reports": [
{ "date": "2026-04-01", "person": "jsmith", "display_name": "Jane Smith", "file": "2026-04-01-jsmith.json" }
]
}
Sort entries newest-first. The viewer reads this manifest to populate the People tab under To Do.
Report schema:
{
"person": {
"email": "your-rh-handle@redhat.com",
"slack_username": "@your-rh-handle",
"display_name": "Jane Smith",
"slack_user_id": "U1234567890"
},
"date": "2026-04-01",
"generated_at": "2026-04-01T15:30:00Z",
"channels": [
{
"channel": "#team-dashboard-crimson",
"channel_id": "C1234567890",
"threads": [
{
"thread_ts": "1234567890.123456",
"thread_link": "https://{slack_domain}/archives/C1234567890/p1234567890123456",
"participants": ["@your-rh-handle", "@alice", "@bob"],
"summary": "Team discussed the new PatternFly 6 migration timeline.",
"person_said": "Jane proposed a phased migration starting with layout components."
}
]
}
],
"overall_summary": "Jane was active in 3 channels across 5 threads...",
"stats": {
"total_channels": 3,
"total_threads": 5,
"total_messages": 12
}
}
3d.6 — Present person activity
After saving the JSON, present an inline summary of the person's activity:
- Person banner — who was searched, the date, message count
- Per-channel breakdown — for each channel, list threads with the
person_saidsummary - Overall summary
- File location — where the report was saved
Format example:
Slack activity for Jane Smith (@your-rh-handle) on 2026-04-01 3 channels · 5 threads · 12 messages
#team-dashboard-crimson (2 threads)
- PatternFly 6 migration: Jane proposed a phased migration starting with layout components...
- Sprint retro: Jane flagged CI pipeline flakiness as the top concern...
Step 4 — Fetch thread replies
For any message that is a thread parent (has reply_count > 0 or thread_ts), fetch the full thread using {slack_mcp_server}:
server: {slack_mcp_server}
tool: conversations_replies
args:
channel_id: "#channel-name"
thread_ts: "1234567890.123456"
limit: "1d"
Step 5 — Analyze and summarize
For each thread or standalone message, produce:
- summary — 1-3 sentence summary of the conversation
- actions_needed — List of action items extracted from the conversation (empty array if none)
- action_owner — Who likely needs to perform each action (use Slack handles, e.g.
@alice). Assign@{slack_username}when the user is clearly the right owner — they were asked to do something, volunteered, or the action falls under their responsibility based on context. If no specific individual can be reasonably identified as the owner, use@team— do not default to@{slack_username}just because they are reading the digest. - urgency — One of:
"today","this_week","later" - thread_link — Slack deep link to the thread:
https://{domain}/archives/{channel_id}/p{thread_ts_no_dot}— IMPORTANT:{channel_id}must be the Slack channel ID (e.g.,C09S4J8TV5Y) resolved in Step 1b, NOT the channel name. For DMs (channel IDs starting withD), use{slack_dm_domain}as the domain. For channels (IDs starting withC), use{slack_domain}. Links with channel names will not open in Slack.
Urgency classification rules
- today: Explicit deadlines for today, blockers, urgent requests, production issues, review requests with same-day expectations
- this_week: Items with near-term deadlines, follow-ups expected this week, PR reviews, meeting action items
- later: FYI messages, long-term planning discussions, ideas, nice-to-haves, informational updates
Step 6 — Build the daily digest JSON
Write the digest to digests/YYYY-MM-DD.json in this skill's directory. Before writing, follow the “Safe write rules (digests)” section below (merge-by-source, preserve on MCP failure). Use this schema:
{
"date": "2026-03-21",
"generated_at": "2026-03-21T15:30:00Z",
"channels": [
{
"channel": "#team-dashboard-crimson",
"channel_id": "C1234567890",
"threads": [
{
"thread_ts": "1234567890.123456",
"thread_link": "https://{slack_domain}/archives/C1234567890/p1234567890123456",
"participants": ["@alice", "@bob"],
"summary": "Discussed the new model registry API changes...",
"actions_needed": [
{
"action": "Review PR #456 for model registry changes",
"owner": "@alice",
"urgency": "this_week"
}
]
}
]
},
{
"channel": "DM with @bob",
"channel_id": "D9876543210",
"threads": [
{
"thread_ts": "1234567891.654321",
"thread_link": "https://{slack_domain}/archives/D9876543210/p1234567891654321",
"participants": ["@you", "@bob"],
"summary": "Bob asked about deployment timeline for gen-ai package...",
"actions_needed": [
{
"action": "Reply with updated deployment schedule",
"owner": "@you",
"urgency": "today"
}
]
}
]
}
],
"person_activity": {
"person": {
"email": "your-rh-handle@redhat.com",
"slack_username": "@your-rh-handle",
"display_name": "Jane Smith",
"slack_user_id": "U1234567890"
},
"channels": [
{
"channel": "#team-dashboard-crimson",
"channel_id": "C1234567890",
"threads": [
{
"thread_ts": "1234567890.123456",
"thread_link": "https://{slack_domain}/archives/C1234567890/p1234567890123456",
"participants": ["@your-rh-handle", "@alice"],
"summary": "Discussed the new model registry API changes...",
"person_said": "Jane proposed using OpenAPI spec and volunteered to write the migration guide."
}
]
}
],
"overall_summary": "Jane was active in 3 channels across 5 threads...",
"stats": { "total_channels": 3, "total_threads": 5, "total_messages": 12 }
},
"jira_mentions": [
{
"issue_key": "RHOAIENG-12345",
"issue_summary": "Fix the widget",
"issue_status": "In Progress",
"issue_link": "{jira_base_url}/browse/RHOAIENG-12345",
"comment_author": "Jane Doe",
"comment_date": "2026-03-21T14:00:00.000+0000",
"comment_text": "@you can you review this?"
}
],
"executive_summary": {
"my_actions": [
{
"action": "Reply with updated deployment schedule",
"urgency": "today",
"channel": "DM with @bob",
"thread_link": "https://{slack_domain}/archives/D9876543210/p1234567891654321"
}
],
"others_actions": [
{
"action": "Review PR #456 for model registry changes",
"owner": "@alice",
"urgency": "this_week",
"channel": "#team-dashboard-crimson",
"thread_link": "https://{slack_domain}/archives/C1234567890/p1234567890123456"
}
],
"key_highlights": [
"Model registry API redesign discussion kicked off in #team-dashboard-crimson — consensus to use OpenAPI spec",
"E2E pipeline fixed after 2-day outage — root cause was stale envtest binaries on self-hosted runners"
]
},
"summary_stats": {
"total_channels": 7,
"total_threads": 23,
"total_actions": 5,
"actions_today": 2,
"actions_this_week": 2,
"actions_later": 1,
"jira_mentions": 1
},
"slack_fetch_status": "ok",
"jira_fetch_status": "ok",
"last_successful_slack_merge_at": "2026-03-21T15:30:00Z",
"last_successful_jira_merge_at": "2026-03-21T15:30:00Z"
}
Optional provenance fields (viewer ignores; set when using safe merge):
| Field | Values | Meaning |
|---|---|---|
slack_fetch_status |
"ok" | "failed" | "skipped" |
Slack MCP / steps completed successfully, errored, or not run (e.g. person-only run) |
jira_fetch_status |
"ok" | "failed" | "skipped" |
Jira steps completed successfully, errored, or skipped |
last_successful_slack_merge_at |
ISO timestamp | When Slack-derived digest content was last successfully written |
last_successful_jira_merge_at |
ISO timestamp | When Jira-derived sections for this digest were last successfully merged |
Safe write rules (digests)
Goal: Never replace a richer existing digest with emptier data because Slack or Jira MCP broke, auth failed, or tools returned no results for a bad reason.
Detect source health
- Slack unhealthy if: MCP/tool unavailable, auth failed, rate-limit abort, or any step in 1b–3c did not complete successfully for configured channels. Do not infer failure from “zero messages” alone if each channel was successfully fetched (no activity is valid). The same applies to Step 3b: if the slack-dm-list bulk
is:dmpath (or, when used, legacy per-IM attempts) completed without tool error, zero DM/MPDM rows forDis valid empty, not Slack-unhealthy—do not assume “there are always DMs.” - Slack healthy if:
conversations_history/conversations_search_messages(etc.) completed per channel (or per scope) for dateDwithout tool error, including Step 3b slack-dm-list bulk search pagination finishing without MCP outage (or legacy fallback completing / deterministically skipping IMs without outage). - Jira unhealthy if:
jira_search/jira_get_issue/jira_get_user_profilethrows, auth fails, or MCP returns an explicit error. Do not treat “0 search results” as failure by itself—empty results can be legitimate. - Jira healthy if: Steps 6b–6c complete without tool errors (even when zero mentions).
Never clobber good data
- If
digests/D.jsonalready exists with non-emptychannels(or non-zero Slack-derivedsummary_stats/executive_summarycontent), and this run would write emptychannelsbecause Slack was unhealthy → retain the previous file’schannels,person_activity(if any), and Slack-related parts ofexecutive_summaryandsummary_stats. Optionally merge: keep priorexecutive_summaryentirely if splitting Slack vs Jira bullets is ambiguous. - If Slack is healthy and the day truly had no messages in scope → empty
channelsis correct. - If Jira unhealthy for this run → do not clear existing
jira_mentionsindigests/D.json; do not wipejira_mentions.jsonon tool error. Preserve prior digest Jira rows and merged file contents unless Jira was healthy and you intentionally re-filtered mentions forD. - User override: Only replace preserved content if the user explicitly asks to reset/clear that day’s digest.
Merge algorithm (when writing digests/D.json)
- Read existing
digests/D.jsonif present; parseslack_fetch_status/jira_fetch_statusif set. - Run Slack pipeline (Steps 1–5, etc.). Set
slack_fetch_statusto"ok"or"failed"/"skipped"as appropriate. - Run Jira pipeline (6b–6c). Set
jira_fetch_statusto"ok"or"failed"/"skipped". - Channels / person_activity / Slack stats: If Slack is
ok, use newly built content and setlast_successful_slack_merge_attogenerated_at. If Slack is not ok and existing file has Slack content → copy forward from existing file; keep priorlast_successful_slack_merge_at(or leave unchanged). - Jira mentions in digest: If Jira is
ok, use newly filtered mentions forDand setlast_successful_jira_merge_at. If Jira not ok → copy forwardjira_mentionsfrom existing file. executive_summary/summary_stats: Rebuild from the merged Slack + Jira content when either source is fresh; if only one source is fresh, merge highlights so you do not drop the other source’s prior bullets. If both failed, keep priorexecutive_summaryentirely.- Append a
key_highlightsline when content was retained due to failure, e.g.Slack refresh failed; earlier Slack content for this date was preserved. - Set
generated_atto now even on partial merges.
Optional backup before replace
Before overwriting digests/D.json, copy it to digests/.backups/D-<ISO8601>.json (same directory). Keeps a recoverable snapshot if the merge logic mis-fires. See File Structure below.
Step 6a — Build executive summary (MANDATORY)
After assembling all channel threads and actions, build the executive_summary object at the top level of the digest JSON. This is what the user sees first, so make it concise and actionable.
When applying safe merge (see Safe write rules (digests) above), build executive_summary from the final merged Slack + Jira content (after copy-forward), not from a failed partial fetch alone.
Split actions into two lists based on whether the owner is @{slack_username} (from user_config.json):
- my_actions — Actions where
@{slack_username}is the owner because they were asked, volunteered, or are clearly responsible based on context. Omit theownerfield (it's implicit). Includeaction,urgency("today"or"this_week"),channel, andthread_link. Sort by urgency:"today"items first, then"this_week". - others_actions — Actions where the owner is someone else (including
@teamfor actions with no clear individual owner). Includeaction,owner,urgency,channel, andthread_link. Sort the same way:"today"first, then"this_week". - key_highlights — Write 3-7 bullet points (plain strings) capturing the most important things that happened across all channels. Focus on decisions made, blockers raised, milestones hit, risks surfaced, and notable team events. Each bullet should be a single sentence. Do not repeat action items here — highlights are informational context.
Also present this executive summary directly to the user in your response (not just in the JSON). Format it as:
- A one-line stats banner (channels, threads, action counts)
- My Actions numbered list (items with urgency "today" get a TODAY label)
- Others' Actions numbered list (items with urgency "today" get a TODAY label)
- Key Highlights bulleted list
Step 6b — Update active feature tracker (MANDATORY)
This step MUST run on every digest invocation. It refreshes progress data for the feature tracker panel in the viewer.
First, read feature_watchlist.json (in this skill's directory) to get the canonical list of Jira keys and optional refresh settings:
{
"features": ["RHAISTRAT-545", "RHAISTRAT-172"],
"epic_child_refresh": "every_run"
}
- features — Jira issue keys to track (required).
- epic_child_refresh — optional, default
"every_run". Use"weekly"to refresh child-issue lists and progress at most once per rolling 7-day window (see delta logic below). Parent issue fields still refresh every run when the parent’supdatedchanges.
The user adds/removes keys in this file. Then read active_features.json and jira_local_state.json (create if missing) and reconcile:
- Add any key present in
feature_watchlist.jsonbut missing fromactive_features.json(initialize with empty fields — they will be populated below). - Remove any entry in
active_features.jsonwhose key is no longer infeature_watchlist.json.
Goal: Minimize Jira calls by batching searches and skipping redundant child searches when safe.
MCP guardrail: If the Atlassian MCP is configured with projects_filter (or env) restricting projects, a batched key in (...) query only returns issues in allowed projects. Missing keys are not errors—note any expected key absent from results.
6b.1 — Batch-fetch parent issues (replace N × jira_get_issue)
Build JQL key in (KEY1, KEY2, …) for all watchlist keys. Chunk keys into groups of at most 50 per jira_search call (API result limit is 50 issues per page). Paginate with start_at / page_token until all keys are covered.
server: {atlassian_mcp_server}
tool: jira_search
args:
jql: "key in (RHAISTRAT-545, RHAISTRAT-172, RHAISTRAT-1112)"
fields: "summary,status,assignee,fixVersions,updated,comment"
limit: 50
Use comment from search results for last_comment when the API returns enough; if not, call jira_get_issue only for keys that need a fresher comment (see delta below).
6b.2 — Delta refresh (optional, fewer get_issue / child searches)
For each feature key, compare the Jira updated field from the batch result with jira_issue_updated stored on the previous active_features.json entry (see schema below).
- If
updatedis unchanged andepic_child_refreshis"weekly"and a full child refresh ran within the last 7 days (seelast_children_full_refresh_atinjira_local_state.json), reuse the prior entry’s epics array and progress; still refresh whats_next from the current digest’s Slack context. - If
updatedchanged, or the key is new, or weekly mode is due for children, fetch children as in 6b.3. - children_fingerprint (optional): a stable string of sorted
childKey|statusfrom the last child search. If parentupdatedis unchanged and fingerprint matches a quick no-op check, you may skip the childjira_searcheven inevery_runmode (advanced; optional).
6b.3 — Batch-fetch child issues (replace N × per-parent jira_search)
For keys that need fresh children, build one JQL per chunk of parent keys (chunk to stay within JQL length limits and result sizes), e.g.:
((parent in (KEY1, KEY2, …)) OR ("Epic Link" in (KEY1, KEY2, …)))
Some projects use different epic linking; if the batch returns incomplete data for a key, fall back to the per-key query from the older workflow:
parent = KEY OR "Epic Link" = KEY
Paginate until all child issues are retrieved. Compute progress as: (closed or resolved issues / total issues) * 100, rounded to nearest integer.
When epic_child_refresh is "weekly", run the full batched child search only if last_children_full_refresh_at is older than 7 days or missing; otherwise reuse stored epics / progress from active_features.json and set updated_at on the file.
6b.4 — Write active_features.json
Overwrite the file with a top-level object the viewer expects: { "features": [ … ] } (see viewer/index.html loadFeatureTracker). Each feature object includes:
- key, summary, status, assignee, target_version, progress, last_comment, epics, whats_next — same meaning as before.
- jira_issue_updated — Jira’s issue
updatedtimestamp (ISO string) from the API for delta logic next run.
Set file-level updated_at to the current timestamp. When using weekly child refresh, update jira_local_state.json last_children_full_refresh_at whenever you perform a full batched child search.
Fields: Keep requests minimal — never use *all for this step.
Step 6c — Find Jiras mentioning me in comments
Search for issues where you were @mentioned or otherwise referenced in comments, using jira_search on {atlassian_mcp_server}.
Why display-name-only search fails: Jira Cloud stores @mentions as [~accountid:AAID]. Indexed search matches the AAID, not display name or username. See Atlassian: search @mentions with JQL.
6c.0 — Time window window_start (digest-aware, fewer candidates)
Let D be the digest date (YYYY-MM-DD) and T be today’s date in the same calendar system you use for the digest.
- If
|D − T| ≤ 1day (digest is today or yesterday): setwindow_starttoDminus 3 calendar days (short window; still tolerates timezone drift). - Else (backfilled digest): set
window_starttoDminus 14 calendar days.
Always combine with: updated >= "{window_start}" (never updated >= -3d relative to run time).
6c.1 — Primary mention search: run one of (A) or (B), not both
Goal: Avoid duplicate JQL when currentUser() and AAID would return the same issues.
- (A) If
jira_account_idis set after Step 0 — run only:
server: {atlassian_mcp_server}
tool: jira_search
args:
jql: "comment ~ \"{jira_account_id}\" AND updated >= \"{window_start}\""
fields: "summary,status,comment"
limit: 50
- (B) If
jira_account_idis not set — run only:
server: {atlassian_mcp_server}
tool: jira_search
args:
jql: "comment ~ currentUser() AND updated >= \"{window_start}\""
fields: "summary,status,comment"
limit: 50
Run the other query only when troubleshooting identity mismatch (e.g. MCP token is a service account).
6c.2 — Plain-text fallbacks (conditional — only if primary returned zero issues)
Only if the primary search returned no issues, run one or both (order does not matter):
jql: "comment ~ \"{jira_display_name}\" AND updated >= \"{window_start}\""
jql: "comment ~ \"{jira_username}\" AND updated >= \"{window_start}\""
These catch literal text, not @mention tokens.
6c.3 — Pagination cap
If results hit the 50-issue cap, paginate with start_at or page_token, but stop after at most 5 pages (250 issues) unless the user explicitly asks for full history—prevents runaway API usage.
6c.4 — Optional: same-digest cache (jira_local_state.json)
Read jira_local_state.json (same directory as this skill). If step_6c_last.digest_date equals the current D and step_6c_last.window_start equals the computed window_start, you may reuse step_6c_last.issue_keys as the deduped key list and skip repeating primary JQL—still run jira_get_issue if you need fresh comment bodies. Update step_6c_last after a full search.
6c.5 — Fetch issue comments
For each unique issue key from searches, use jira_get_issue:
server: {atlassian_mcp_server}
tool: jira_get_issue
args:
issue_key: "PROJ-123"
fields: "summary,status,comment"
comment_limit: 50
update_history: false
Keep fields minimal; avoid *all.
6c.6 — Optional: jira_batch_get_changelogs (Jira Cloud only)
For large issues where comment_limit: 50 is heavy, you may use jira_batch_get_changelogs on {atlassian_mcp_server} with comma-separated issue_ids_or_keys and fields filtering to comment-related history to spot recent comment activity with less payload than loading all comments—only when you are confident parsing changelog entries. If unsure, keep jira_get_issue.
server: {atlassian_mcp_server}
tool: jira_batch_get_changelogs
args:
issue_ids_or_keys: "PROJ-123,PROJ-456"
limit: 20
Use optional fields on this tool only if you know which issue fields to filter (see MCP tool schema). Cloud only; not available on Server/Data Center.
6c.7 — Scan comment bodies
Include a comment when:
{jira_account_id}is known and the body contains that id or[~accountid:{jira_account_id}]- The body contains
{jira_username}or
…(truncated)