InsightWorker Logo

Building a Virtual PM with InsightWorker — my 6 PM weekday digest, automated

← All posts
Product Management May 8, 2026 by Hitesh Talesra 11 min read

It was Thursday at 5:50 PM. My calendar pinged me to send the daily digest. I opened JIRA, started clicking through tickets, realized I'd forgotten to log out two stale ones from yesterday, copied the keys into Outlook, manually colored the headers, and shipped it at 6:18 PM after most of the engineering team had already gone home. The Friday version: I just didn't send it. Monday's standup, I winged it from memory.

If you've run sprints, you've lived this. The digest is the kind of work everyone agrees is valuable and no one can justify spending an hour on. So either you do it badly, or you do it inconsistently, or you stop. I'd done all three.

What 'Virtual PM' actually means

I'm not building a chatbot that answers what's the status of ticket X. I'm building an automation that takes the consistent, data-driven, time-bound parts of project management — the parts I was demonstrably bad at by hand — and runs them on a clock. Same JQL, same classification rules, same delivery channels, same time every day. The agent doesn't have to be smart. It has to be relentless.

I started with the daily JIRA sprint digest because it was the worst-offending thing on my plate. Once that worked, I added five more. This post is the first one.

The setup — config first, prompt second

InsightWorker has a built-in scheduler. The whole digest is described in two files in my project's .insightworker/ folder.

The schedule (when, how, who):

schedules: - name: sprint-digest cron: "0 18 * * 1-5" # 6 PM weekdays app: sprint-digest

And an app config that defines the actual project assumptions — none of it is hardcoded into the agent's logic, so when our team renames a board or changes recipients, I edit one file:

# .insightworker/apps/sprint-digest/config.yaml app: sprint-digest project: key: ENG board: 47 active_sprint_only: true max_results: 200 schedule: cron: "0 18 * * 1-5" stale_threshold_hours: 72 recipients: email: [eng-team@acme.com, em-leads@acme.com] slack_channel: "#eng-sprint" follow_up: enabled: true message_template: | Hi {assignee_first_name}, your ticket {ticket_key} ({ticket_summary}) hasn't been updated in 72h. Could you add a quick progress comment? Thanks!

The corresponding app prompt is the actual brain — the agent reads the config, fetches tickets via the jira_search tool, classifies them, composes the HTML, and sends it. Every step is something the agent does in its main loop with the tools we ship.

1. Search JIRA for tickets in the ENG project that are part of the active sprint. 2. Classify each ticket into one of the following buckets: - COMPLETED: status is "Done" and updated in the last 24h. - IN PROGRESS: status is "In Progress". - BLOCKED: label is "blocked" or status is "Blocked". - STALE: status is not "Done" and not updated in the last 72h. 3. For each bucket, extract the ticket key, summary, assignee, and priority. 4. Compose a professional HTML email with these sections, using color-coded headers (Green for Done, Blue for In Progress, Red for Blocked, Amber for Stale). 5. Send the email to the recipients defined in config.yaml. 6. For each STALE ticket, send a polite 1:1 Slack DM to the assignee asking for a status update.

How the digest classifies work

The split is borrowed from how my engineering manager actually thinks about the sprint, not from JIRA's status field. Four buckets:

  • Completed Today — status Done AND updated within the last 24 hours
  • In Progress — status In Progress
  • Blocked — label blocked OR status Blocked
  • Stale — not updated in 72 hours and not Done

The classification rules live in the prompt as plain English. The agent applies them deterministically because the JQL it generates is deterministic. This is the part where I had to stop being clever — there's no need for the LLM to reason about the right bucket. I just need it to apply my rules consistently. The fact that an LLM is in the loop is incidental.

What gets delivered

Each 6 PM run produces three things:

  • HTML email to the team distribution — color-coded sections, ticket keys clickable back to JIRA, owners + priorities inline
  • Slack post in #eng-sprint — same content, threaded, so engineers in different timezones can scroll back
  • Local archive at ~/sprint-digests/YYYY-MM-DD.html — for the audit trail and for whoever joins mid-quarter

If JIRA is unreachable or returns 0 active sprint tickets, the app logs the error and refuses to send a misleading empty digest. Better to send nothing than to send something wrong; I learned that the hard way in week one.

The useful PM nudge

The thing I'm most proud of is the stale-ticket nudge. Every weekday at 6 PM the app checks In Progress tickets that haven't been touched in 72 hours, resolves the assignee in Slack, and sends a 1:1 DM with the message template from the config:

Hi Maya, your ticket ENG-1432 (Refactor billing webhook to use idempotency keys) hasn't been updated in 72h. Could you add a quick progress comment? Thanks!

Polite, low-shame, every day. The team gets called out by an algorithm at a predictable time, not their PM in a 1:1. Two nice consequences:

  • Far fewer awkward 1:1 conversations about why a ticket has been sitting. The nudge is impersonal, the response is just a comment, the friction is gone.
  • Engineers know exactly when the DM lands so they get into the rhythm of leaving a comment before it does. The DM volume has dropped over six weeks because the team got better at updating tickets — which is the actual goal.

Where human approval stays in place

InsightWorker's permission gate sits in front of send_email by default. For the digest, I set AUTO_APPROVE_TOOLS=true because the prompt is locked down — recipients are config-pinned, content is templated, no surface area for the agent to send something to the wrong list. For ad-hoc sends (e.g. when I ask the agent to email an exec a custom report), the permission prompt comes back. I want the safety belt on for those, off for the routine.

And before I trusted the digest with the real distribution, I ran it for a week with --dry-run. The agent rendered the email locally, opened it in my browser, and didn't send anything. I read each one and edited the app until I trusted it. --dry-run is the single most underrated feature for getting comfortable with an automation.

The first week — embarrassing edge cases

Three things broke in the first week of running it for real:

  • Day 1: the agent classified Done tickets that had been re-opened. I refined the JQL to require the most recent status transition to Done happen within the 24-hour window, not just the updated-at field.
  • Day 3: a sprint roll-over caused the active-sprint filter to return zero tickets. The agent helpfully sent an email titled "Sprint digest — 0 tickets in progress." Engineering had a good laugh. I added a no active sprint detected early-exit so it doesn't email noise.
  • Day 5: a backfilled ticket showed up as 12 "Completed Today" entries because someone bulk-edited a label. I tightened the 24h check to require the status transition to fall in the window, not just the modified-date.

Each fix was a 5-line edit to the prompt or the JQL. Total fix time across the three issues: about 30 minutes. The system was usable on day 1; it became reliable by day 7.

What the Virtual PM doesn't do — and what I still own

Three things stay with me:

  • Defining what 'blocked' means. We use a label and a status. Other teams might use just a label, or a custom field. The agent doesn't infer this — I tell it.
  • The recipient list. I don't trust an LLM with a distribution that includes a VP. The list is config-pinned and changes only by a human editing the file.
  • The narrative when something is genuinely on fire. The digest is a daily heartbeat. When a P0 ticket is in trouble, I write a separate Slack message and tag people directly. The Virtual PM is for the routine cadence; I'm for the moments that need human framing.

If you're worried an agent will replace your judgement, don't be. The judgement is the part that's hard. The cadence is the part that's monotonous. The agent does the cadence.

What I added next

After the digest stabilized, I bolted on four more apps that share the same scheduler:

  • Stale submission radar (we run an insurance broker partnership) — every weekday morning, list submission folders not touched in 7 days, email our broker manager.
  • Renewal radar — every Monday morning, find policies bound 11 months ago, queue them for renewal review with the prior submission docs already pulled into a folder.
  • Retro prep — every Friday at 4 PM, summarize the sprint's wins / blockers / learnings into a draft retro doc the team edits before Monday's retro meeting. Saved my Friday afternoons.
  • Standup pre-brief — every weekday at 8 AM, generate a 3-bullet 'what to ask in standup today' note just for me. Reduces my standup prep from 10 minutes to 2.

All four run on the same scheduler with their own YAML configs. None required new code from me — just new prompts and new schedules.

Other PM areas this approach handles cleanly

I haven't tried these yet but the pattern fits and I'll write about them when I do:

  • Velocity tracking + capacity planning — pull historical velocity, project completion, flag at-risk commitments before they slip.
  • Cross-team dependency tracking — daily scan for tickets blocked on tickets owned by another team, surface them to both PMs.
  • OKR check-ins — weekly nudge to OKR owners with current metric vs. target, ask for a one-line update.
  • Customer feedback triage — pull from support / NPS, classify by theme, route each batch to the right PM or EM.
  • Roadmap drift detection — compare current sprint commitments against roadmap milestones, flag drift early.
  • QBR / EBR prep — assemble the slide content from JIRA + Datadog + revenue data on a recurring cadence so the deck is 80% pre-built before I open it.
  • Sprint review demo prep — list completed tickets, identify which ones have demoable artifacts, draft a 1-line demo script per ticket for the engineers to flesh out.

What I'd tell another PM starting today

Pick the one PM app you do worst by hand. Mine was the daily digest; yours might be retros, OKR check-ins, or status updates to leadership. Set aside two days, write the config in YAML, write the app prompt in plain English, and run it with --dry-run for a week before you let it touch real recipients.

After the first cadence is reliable, the second one takes a quarter of the time. The third takes none — you copy the schedule entry, swap the prompt, and you're done. The first one is the one that teaches you the shape of the tool. Pick it carefully and accept that the first week is going to be embarrassing.

I put together the use-case page with the full app walkthrough and screenshots if you want to see what each step looks like.