CLAUDE LABJP
FORK — Claude Code 2.1.212 changes what /fork does: it copies your conversation into a new background session with its own row in claude agents, so you can keep working. The old in-session subagent is now /subtaskLIMITS — WebSearch calls are now capped at 200 per session by default, and subagent spawns get the same 200 ceiling, so a runaway search or delegation loop stops on its ownMCPBG — MCP tool calls running past two minutes now move to the background automatically, keeping the session usable. Tune the threshold with CLAUDE_CODE_MCP_AUTO_BACKGROUND_MSPLANFIX — Fixed plan mode auto-running file-modifying Bash commands such as touch and rm without a permission prompt or an SDK canUseTool callbackSONNET5 — Claude Sonnet 5 is running on introductory pricing of $2 per million input tokens and $10 per million output. After August 31 it moves to $3 and $15IPO — Bankers are reportedly lining up investor meetings for Anthropic ahead of a possible public listing as soon as OctoberFORK — Claude Code 2.1.212 changes what /fork does: it copies your conversation into a new background session with its own row in claude agents, so you can keep working. The old in-session subagent is now /subtaskLIMITS — WebSearch calls are now capped at 200 per session by default, and subagent spawns get the same 200 ceiling, so a runaway search or delegation loop stops on its ownMCPBG — MCP tool calls running past two minutes now move to the background automatically, keeping the session usable. Tune the threshold with CLAUDE_CODE_MCP_AUTO_BACKGROUND_MSPLANFIX — Fixed plan mode auto-running file-modifying Bash commands such as touch and rm without a permission prompt or an SDK canUseTool callbackSONNET5 — Claude Sonnet 5 is running on introductory pricing of $2 per million input tokens and $10 per million output. After August 31 it moves to $3 and $15IPO — Bankers are reportedly lining up investor meetings for Anthropic ahead of a possible public listing as soon as October
Articles/API & SDK
API & SDK/2026-05-05Intermediate

Stop Writing Weekly Reports Manually — Automate Them with Claude API, GitHub, Linear, and Slack

Automate your team's weekly Slack progress reports using Claude API. This guide walks through a Node.js system that pulls GitHub and Linear data, formats it with Claude API, and posts it to Slack automatically.

Claude API115automation95GitHub5Slack3Node.js3weekly reportLinear

Every Friday afternoon, engineers across the world open Jira, then GitHub, then dig through notes — spending 30 minutes or more compiling a progress report that people skim for 10 seconds. I did this for about a year before finally asking: why am I doing this manually?

Weekly progress reports matter. They keep stakeholders informed, help teams reflect on what they accomplished, and surface blockers early. But the actual work of collecting data and formatting it has zero creative value. That's exactly the kind of task Claude API was built for.

This guide walks through a complete Node.js system that pulls data from GitHub (commits and merged PRs) and Linear (completed issues), sends it to Claude API for intelligent formatting, and posts the result to Slack — fully automated via GitHub Actions every Friday.

System Overview

The flow is straightforward:

GitHub API → Fetch this week's commits and merged PRs
Linear API → Fetch this week's completed issues
↓
Claude API → Format data into a readable, human-sounding report
↓
Slack Webhook → Post to the designated channel

This runs as a scheduled GitHub Actions workflow (every Friday at 5 PM JST). No server required, and it fits comfortably within GitHub's free tier.

The key design decision: instead of using a fixed template, we let Claude API decide how to frame the week's work. This means the report adapts to what actually happened — a quiet week sounds different from a week with a major launch.

Setup and Prerequisites

You'll need Node.js v18+ and API keys for all four services:

  • Anthropic API key: Get one at console.anthropic.com
  • GitHub Personal Access Token: repo scope only
  • Linear API key: Settings → API in your Linear workspace
  • Slack Incoming Webhook URL: Create one via your Slack App configuration

Install dependencies:

npm init -y
npm install @anthropic-ai/sdk node-fetch dotenv

Create a .env file locally (use GitHub Secrets in production):

ANTHROPIC_API_KEY=YOUR_ANTHROPIC_API_KEY
GITHUB_TOKEN=YOUR_GITHUB_TOKEN
LINEAR_API_KEY=YOUR_LINEAR_API_KEY
SLACK_WEBHOOK_URL=YOUR_SLACK_WEBHOOK_URL
GITHUB_OWNER=your-org-name
GITHUB_REPO=your-repo-name

Fetching GitHub Data

We use the GitHub REST API to grab the past 7 days of activity:

// src/github.js
import { Octokit } from '@octokit/rest';
 
const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN });
 
export async function getWeeklyGitHubData() {
  const since = new Date();
  since.setDate(since.getDate() - 7);
  const sinceISO = since.toISOString();
 
  const owner = process.env.GITHUB_OWNER;
  const repo = process.env.GITHUB_REPO;
 
  // Fetch merged PRs from the past week
  const { data: pulls } = await octokit.pulls.list({
    owner,
    repo,
    state: 'closed',
    sort: 'updated',
    direction: 'desc',
    per_page: 50,
  });
 
  const mergedPRs = pulls
    .filter(pr => pr.merged_at && new Date(pr.merged_at) >= since)
    .map(pr => ({
      title: pr.title,
      number: pr.number,
      author: pr.user.login,
      mergedAt: pr.merged_at,
      labels: pr.labels.map(l => l.name),
    }));
 
  // Fetch commits (excluding bots)
  const { data: commits } = await octokit.repos.listCommits({
    owner,
    repo,
    since: sinceISO,
    per_page: 100,
  });
 
  const humanCommits = commits.filter(c =>
    c.author && !c.author.login.includes('[bot]')
  );
 
  return {
    mergedPRs,
    commitCount: humanCommits.length,
    activeContributors: [...new Set(humanCommits.map(c => c.author?.login).filter(Boolean))],
  };
}

The bot-filtering step is easy to miss. Without it, Dependabot and GitHub Actions commits inflate your numbers significantly — I've seen "50 commits this week!" turn out to be mostly automated dependency updates.

Fetching Linear Issues

Linear uses a GraphQL API, which feels unfamiliar if you're used to REST, but it's actually quite expressive once you write it out:

// src/linear.js
const LINEAR_API_URL = 'https://api.linear.app/graphql';
 
export async function getWeeklyLinearData() {
  const since = new Date();
  since.setDate(since.getDate() - 7);
  const sinceISO = since.toISOString();
 
  const query = `
    query WeeklyIssues($since: DateTime!) {
      issues(
        filter: {
          completedAt: { gte: $since }
          state: { type: { eq: "completed" } }
        }
        orderBy: updatedAt
        first: 50
      ) {
        nodes {
          title
          identifier
          assignee { name }
          labels { nodes { name } }
          completedAt
          estimate
        }
      }
    }
  `;
 
  const response = await fetch(LINEAR_API_URL, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: process.env.LINEAR_API_KEY,
    },
    body: JSON.stringify({ query, variables: { since: sinceISO } }),
  });
 
  const { data, errors } = await response.json();
  if (errors) {
    console.error('Linear API error:', errors);
    throw new Error('Failed to fetch Linear data');
  }
 
  return data.issues.nodes.map(issue => ({
    title: issue.title,
    identifier: issue.identifier,
    assignee: issue.assignee?.name ?? 'Unassigned',
    labels: issue.labels.nodes.map(l => l.name),
    estimate: issue.estimate ?? null,
  }));
}

The estimate field captures story points, which lets Claude reference velocity in the report — "the team completed 20 story points this week" carries more weight than a raw issue count.

Generating the Report with Claude API

This is where the system earns its value. We're not filling a template — we're asking Claude to read the data and decide how to communicate it:

// src/report.js
import Anthropic from '@anthropic-ai/sdk';
 
const client = new Anthropic();
 
export async function generateWeeklyReport(githubData, linearIssues) {
  const today = new Date().toLocaleDateString('en-US', {
    weekday: 'long',
    year: 'numeric',
    month: 'long',
    day: 'numeric',
  });
 
  const prompt = `
You're writing a weekly engineering progress report for Slack (${today}).
 
## GitHub Activity
- Merged PRs: ${githubData.mergedPRs.length}
- Total commits: ${githubData.commitCount}
- Active contributors: ${githubData.activeContributors.join(', ')}
 
### Merged PRs this week:
${githubData.mergedPRs.map(pr => `- #${pr.number}: ${pr.title} (by ${pr.author})`).join('\n')}
 
## Linear Issues Completed
- Issues closed: ${linearIssues.length}
- Total story points: ${linearIssues.reduce((sum, i) => sum + (i.estimate ?? 0), 0)}
 
### Completed issues:
${linearIssues.map(issue => `- [${issue.identifier}] ${issue.title} (${issue.assignee})`).join('\n')}
 
## Formatting rules:
- Use Slack mrkdwn format (*bold*, :emoji:, etc.)
- Start with a 2-3 sentence highlight that *reads from the data* (don't just list numbers — tell the story of what happened this week)
- Briefly summarize both PRs and Linear issues
- End with a brief forward-looking note
- Keep it to 250-350 words total
- Sound like a human, not a bot
`;
 
  const message = await client.messages.create({
    model: 'claude-sonnet-4-6',
    max_tokens: 1024,
    messages: [{ role: 'user', content: prompt }],
  });
 
  return message.content[0].text;
}

The critical instruction is "reads from the data — tell the story of what happened." Without it, Claude defaults to listing information rather than interpreting it. With it, you get openings like "This week saw a focused push on the authentication layer, with four related PRs landing back to back" — which is the kind of context that makes a report worth reading.

Posting to Slack and Wiring Everything Together

// src/slack.js
export async function postToSlack(text) {
  const response = await fetch(process.env.SLACK_WEBHOOK_URL, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ text, unfurl_links: false }),
  });
 
  if (!response.ok) {
    throw new Error(`Slack post failed: ${response.status}`);
  }
}
// src/index.js
import 'dotenv/config';
import { getWeeklyGitHubData } from './github.js';
import { getWeeklyLinearData } from './linear.js';
import { generateWeeklyReport } from './report.js';
import { postToSlack } from './slack.js';
 
async function main() {
  console.log('Generating weekly report...');
 
  try {
    const [githubData, linearIssues] = await Promise.all([
      getWeeklyGitHubData(),
      getWeeklyLinearData(),
    ]);
 
    console.log(`GitHub: ${githubData.mergedPRs.length} PRs, ${githubData.commitCount} commits`);
    console.log(`Linear: ${linearIssues.length} issues completed`);
 
    const report = await generateWeeklyReport(githubData, linearIssues);
    await postToSlack(report);
 
    console.log('✅ Weekly report posted');
  } catch (error) {
    console.error('❌ Error:', error.message);
    process.exit(1);
  }
}
 
main();

Scheduling with GitHub Actions

Create .github/workflows/weekly-report.yml:

name: Weekly Progress Report
 
on:
  schedule:
    # Every Friday at 08:00 UTC (5 PM JST)
    - cron: '0 8 * * 5'
  workflow_dispatch: # Allow manual runs during development
 
jobs:
  report:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
 
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
 
      - run: npm ci
 
      - name: Generate and post weekly report
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          LINEAR_API_KEY: ${{ secrets.LINEAR_API_KEY }}
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
          GITHUB_OWNER: ${{ github.repository_owner }}
          GITHUB_REPO: ${{ github.event.repository.name }}
        run: node src/index.js

The workflow_dispatch trigger is worth including — it lets you run the workflow manually from GitHub's UI without waiting for Friday. This is how you'll test it the first time.

Notes From Running This in Production

After three months of running this system, a few things stood out.

Claude's output never feels copy-paste. Even with identical data structures, each report reads slightly differently. This is a meaningful improvement over templates — team members actually read them.

Not using Linear? The getWeeklyLinearData function can be replaced with Jira (REST API), GitHub Issues, or any project management tool. The return format just needs to match what the report generator expects.

Cost is negligible. Using claude-sonnet-4-6 once per week costs a few cents per month at current pricing. Extended Thinking isn't necessary here — the standard model produces reports that are genuinely good.

The best way to start is to run it manually with workflow_dispatch, see what Claude generates with your actual data, and adjust the prompt from there. The first report is usually surprisingly usable.

Share

Thank You for Reading

Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

API & SDK2026-07-05
Fable 5 Is Back Worldwide and Sonnet 5 Is the Default — Where Each of the Three Models Belongs in a Solo Automation Stack
With Fable 5 redeployed worldwide and Sonnet 5 now the default, solo automation suddenly has three capable top-tier models to reach for. Instead of ranking them, this piece assigns each a role and captures that in a policy object with a fallback ladder and run-level logging.
API & SDK2026-07-02
Introductory Pricing Has an End Date — Effective-Dated Cost Forecasts for the Sonnet 5 Price Step
Claude Sonnet 5's introductory $2/$10 pricing ends on 2026-08-31 and reverts to $3/$15. A static price map will quietly understate your September forecast by a third. Here is an effective-dated price table and forecast design that absorbs the step.
API & SDK2026-06-28
Did That Post Actually Go Through? Safely Retrying an Interrupted MCP Write Without Double-Executing
When an MCP write tool call is interrupted by a dropped connection, you can't tell whether the server ran it. Here's why naive retries cause double-execution, and a working wrapper that uses idempotency keys and a reconcile read to retry safely — with examples from an unattended pipeline.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →