How to Automate Engineering Team Updates with n8n

By The LightCube Labs team

The Problem

Weekly engineering updates are valuable and hated in equal measure. Someone has to scroll through merged pull requests, open Jira, copy ticket keys, guess at velocity, and write it all up on a Friday afternoon. The result is late, inconsistent, and usually missing half the work. Stakeholders then ask for status in meetings, which costs even more time. This guide shows how to automate the whole recap with n8n. A scheduled workflow queries GitHub for merged pull requests, queries Jira for stories resolved this week, calculates velocity metrics, and posts a Block Kit formatted summary to Slack.

What You Will Need

How the n8n Workflow Fits Together

This n8n workflow assembles a Friday engineering recap from two systems of record: GitHub for code and Jira for planned work. It asks each system for the week's finished items, merges the answers in a Code node, and posts a formatted message to Slack. Nobody has to write a status update, and the n8n workflow never forgets.

The n8n canvas contains seven nodes: a Schedule Trigger, a Set node for dates, two HTTP Request nodes, two Code nodes, and a Slack node. Date filtering is handled by the APIs themselves, which is why n8n stays lightweight and the workflow stays fast.

Step-by-Step Workflow Setup

1 Schedule the Friday recap in n8n

A weekly update works best when it lands just before the weekend, while the week is still fresh. A Schedule Trigger set to Friday at 16:00 gives people time to read it and gives the team a natural moment to reflect.

Add an n8n Schedule Trigger node and configure:

Node: Schedule Trigger
Trigger Interval: Weeks
Weeks Between Triggers: 1
Trigger on Weekdays: Friday
Trigger at Hour: 16
Trigger at Minute: 0

Check that the workflow timezone is correct in n8n settings, because a self-hosted server in UTC would otherwise post the recap at the wrong local hour.

Expression: {{ $now.toFormat("cccc HH:mm") }}
Sample trigger output:
{
  "timestamp": "2026-11-06T16:00:00.000-08:00",
  "Day of week": "Friday"
}

If your team works a four day week, move the trigger to Thursday and adjust the date range in the next step.

2 Calculate the week date range in n8n

Both APIs need the start of the week, so compute it once in an Edit Fields (Set) node and reuse it. Luxon, the date library inside n8n, treats Monday as the first day of the week, which matches most engineering teams.

Add an n8n Edit Fields (Set) node and configure:

Node: Edit Fields (Set)
Mode: Manual Mapping
  weekStart (String): {{ $now.startOf("week").toFormat("yyyy-MM-dd") }}
  weekEnd (String): {{ $now.endOf("week").toFormat("yyyy-MM-dd") }}
  weekLabel (String): {{ $now.startOf("week").toFormat("LLL d") }} to {{ $now.endOf("week").toFormat("LLL d") }}

Computing dates in one node means there is a single place to fix if you ever change the reporting window, for example to a rolling seven days with $now.minus({ days: 7 }).

Expression: {{ $now.startOf("week").toFormat("yyyy-MM-dd") }}
Sample output:
{
  "weekStart": "2026-11-02",
  "weekEnd": "2026-11-08",
  "weekLabel": "Nov 2 to Nov 8"
}

Run the node by itself on a Wednesday and confirm that weekStart is the Monday of the same week.

3 Search merged pull requests on GitHub in n8n

The GitHub search API can find pull requests by state and date in one call. An HTTP Request node sends a query using the is:pr is:merged merged:>= qualifiers, so GitHub does the filtering instead of n8n.

Add an n8n HTTP Request node and configure:

Node: HTTP Request
Method: GET
URL: https://api.github.com/search/issues
Authentication: Generic Credential Type > Header Auth
  Name: Authorization
  Value: Bearer ghp_yourFineGrainedToken
Send Headers: On
  Accept: application/vnd.github+json
Send Query Parameters: On
  q: org:your-org is:pr is:merged merged:>={{ $json.weekStart }}
  sort: updated
  order: desc
  per_page: 100

The org: qualifier searches all repositories in your organization; use repo:your-org/your-repo for a single one. n8n URL encodes the query parameter for you, including the greater-than sign. A token with read access to pull requests is enough.

Expression: merged:>={{ $json.weekStart }}
Sample response:
{
  "total_count": 23,
  "items": [
    {
      "number": 1482,
      "title": "Add retry logic to webhook delivery",
      "html_url": "https://github.com/your-org/api/pull/1482",
      "user": { "login": "mchen" },
      "pull_request": { "merged_at": "2026-11-04T18:22:10Z" }
    }
  ]
}

The search API returns at most 1000 results and 100 per page. For a very busy organization, enable Pagination in the node options and follow the page parameter. Search is also limited to 30 requests per minute, which one weekly call never approaches.

4 Query resolved Jira issues with JQL in n8n

A second n8n HTTP Request node asks Jira for everything that moved to Done during the current week. JQL has built-in date functions, so no date arithmetic is needed on the n8n side.

Add an n8n HTTP Request node and configure:

Node: HTTP Request
Method: POST
URL: https://your-company.atlassian.net/rest/api/3/search/jql
Authentication: Generic Credential Type > Basic Auth
  User: you@your-company.com
  Password: (Atlassian API token)
Send Body: On
Body Content Type: JSON
Specify Body: Using JSON
{
  "jql": "project = ENG AND status changed to Done DURING (startOfWeek(), endOfWeek()) ORDER BY resolved DESC",
  "fields": ["summary", "assignee", "issuetype", "customfield_10016"],
  "maxResults": 100
}

The function startOfWeek() follows the calendar settings of the Jira user who owns the token, so if your week starts on Sunday, use startOfWeek(+1) and endOfWeek(+1) to anchor to Monday. The field customfield_10016 is the story points field on many Jira Cloud sites; confirm the ID in your own instance.

Expression: "jql": "project = ENG AND resolved >= {{ $('Edit Fields').item.json.weekStart }}"
Sample response:
{
  "issues": [
    {
      "key": "ENG-912",
      "fields": {
        "summary": "Webhook retries with backoff",
        "assignee": { "displayName": "Mei Chen" },
        "issuetype": { "name": "Story" },
        "customfield_10016": 5
      }
    }
  ]
}

If you prefer explicit dates, replace the DURING clause with the resolved qualifier shown in the expression above, which uses the weekStart value from the Set node.

5 Compile PRs, stories and velocity in n8n

An n8n Code node reads both responses and builds the report data. It groups pull requests by author, stories by assignee, and calculates simple velocity numbers: items finished and story points completed.

Add an n8n Code node and configure:

Node: Code (Run Once for All Items)
Language: JavaScript

const prs = $('GitHub PRs').first().json.items || [];
const issues = $('Jira Done').first().json.issues || [];
const byAuthor = {}, byAssignee = {};
for (const p of prs) {
  const a = p.user.login;
  (byAuthor[a] = byAuthor[a] || []).push({ title: p.title, url: p.html_url });
}
let points = 0;
for (const i of issues) {
  const name = i.fields.assignee ? i.fields.assignee.displayName : 'Unassigned';
  (byAssignee[name] = byAssignee[name] || []).push({ key: i.key, summary: i.fields.summary });
  points += i.fields.customfield_10016 || 0;
}
const metrics = {
  prs: prs.length, stories: issues.length, points,
  contributors: Object.keys(byAuthor).length,
  prsPerContributor: Number((prs.length / (Object.keys(byAuthor).length || 1)).toFixed(1))
};
return [{ json: { byAuthor, byAssignee, metrics } }];

Rename the two HTTP Request nodes to GitHub PRs and Jira Done in n8n so these references resolve. Referencing earlier nodes by name keeps this Code node independent of the order of the data flow. Unassigned stories are collected under their own label instead of being dropped. Velocity here means throughput for the week: stories closed and points delivered, which you can compare week to week.

Expression: {{ $json.metrics.prs }} PRs, {{ $json.metrics.points }} points
Sample output:
{
  "byAuthor": { "mchen": [{ "title": "Add retry logic to webhook delivery", "url": "https://github.com/your-org/api/pull/1482" }] },
  "byAssignee": { "Mei Chen": [{ "key": "ENG-912", "summary": "Webhook retries with backoff" }] },
  "metrics": { "prs": 23, "stories": 14, "points": 41, "contributors": 7, "prsPerContributor": 3.3 }
}

To compare against last week, store the metrics object in workflow static data and print the difference in the next step.

6 Format the message as Slack Block Kit in n8n

Plain text gets long and hard to scan. A second n8n Code node builds Slack Block Kit JSON, with a header, a metrics row of side by side fields, and one section per list. Slack limits a section to 3000 characters, so the code truncates long lists.

Add an n8n Code node and configure:

Node: Code (Run Once for All Items)
Language: JavaScript

const d = $input.first().json;
const label = $('Edit Fields').first().json.weekLabel;
const lines = (obj, fmt) => Object.entries(obj).map(([who, arr]) =>
  '*' + who + '*\n' + arr.slice(0, 6).map(fmt).join('\n')).join('\n\n').slice(0, 2900);
const blocks = [
  { type: 'header', text: { type: 'plain_text', text: 'Engineering update: ' + label } },
  { type: 'section', fields: [
    { type: 'mrkdwn', text: '*PRs merged*\n' + d.metrics.prs },
    { type: 'mrkdwn', text: '*Stories done*\n' + d.metrics.stories },
    { type: 'mrkdwn', text: '*Story points*\n' + d.metrics.points },
    { type: 'mrkdwn', text: '*PRs per contributor*\n' + d.metrics.prsPerContributor } ] },
  { type: 'divider' },
  { type: 'section', text: { type: 'mrkdwn', text: '*Merged pull requests*\n' +
    lines(d.byAuthor, p => '<' + p.url + '|' + p.title + '>') } },
  { type: 'section', text: { type: 'mrkdwn', text: '*Completed stories*\n' +
    lines(d.byAssignee, s => s.key + ' ' + s.summary) } }
];
return [{ json: { blocks, text: 'Engineering update: ' + label } }];

Slack mrkdwn links use the form angle bracket, URL, pipe, label. The fields array in a section renders two columns, which suits short metrics. The top level text value is a fallback for notifications and screen readers.

Expression: {{ JSON.stringify({ blocks: $json.blocks }) }}
Sample blocks:
{
  "blocks": [
    { "type": "header", "text": { "type": "plain_text", "text": "Engineering update: Nov 2 to Nov 8" } },
    { "type": "section", "fields": [
      { "type": "mrkdwn", "text": "*PRs merged*\n23" },
      { "type": "mrkdwn", "text": "*Stories done*\n14" } ] }
  ]
}

You can design the layout in the Slack Block Kit Builder and paste its output as a template, then replace values with the variables above.

7 Post the update to #engineering in n8n

The last step is an n8n Slack node set to send Block Kit JSON instead of simple text. This is the only node that talks to Slack, so a formatting error shows up here and nowhere else.

Add an n8n Slack node and configure:

Node: Slack
Resource: Message
Operation: Send
Send Message To: Channel
Channel: #engineering
Message Type: Blocks
Blocks: {{ JSON.stringify({ blocks: $json.blocks }) }}
Options > Text (fallback): {{ $json.text }}

The Slack app needs the chat:write scope and a membership in the channel. If the post fails with invalid_blocks, paste the JSON into the Block Kit Builder to see which block is malformed.

Expression: {{ $json.text }}
Sample Slack response:
{
  "ok": true,
  "channel": "C02ENGINEER",
  "ts": "1793995200.000200"
}

Pin the message with a recurring reminder in the channel so the Friday update becomes a team ritual instead of background noise.

Extending This n8n Workflow

After the first few Fridays, improve the n8n workflow where it hurts most. Add a comparison against the previous week by saving metrics in workflow static data. Add a Slack thread reply with the five largest pull requests, using the additions and deletions fields from the pulls API. Add a section for open pull requests older than five days, with a second search query using the is:open qualifier, to surface review bottlenecks.

If you manage several teams, put the project key and channel in a Set node, and let n8n loop over a list of teams so each team receives its own recap from the same n8n workflow.

Troubleshooting

GitHub search returns zero pull requests

Check the query string in the node output. A merged qualifier needs the greater-than sign encoded, which n8n does when you use the Send Query Parameters option, but not when you paste the whole query into the URL. Also confirm the token can read the repositories in the org qualifier.

Jira returns a 410 or an unknown endpoint error

Atlassian has been retiring the older search endpoint for Jira Cloud. Use the /rest/api/3/search/jql path shown above and a POST body. For Jira Server or Data Center, use /rest/api/2/search with the same JQL instead.

Slack says invalid_blocks

The usual cause is a section text longer than 3000 characters or an empty mrkdwn field. The Code node truncates at 2900 characters, but a team with very long PR titles may need a smaller slice. Test the generated JSON in the Block Kit Builder to find the exact block at fault.

Free n8n Workflow Resources

Get started with the free workflow pack on the LightCube Labs templates page.

Production-Ready Workflow Packs

Get this workflow and more, with setup guides, editable configurations, and a shared failure-alert system.

View the Starter ($29) and Pro ($99) Packs

Frequently Asked Questions

Why use GitHub search instead of listing pulls per repository?

Search covers every repository in the organization with one request and filters by merge date on the server. Listing pulls requires one call per repository and returns closed but unmerged items you would have to filter out in n8n.

What counts as velocity in this report?

Velocity is throughput: stories moved to Done, story points on those stories, and pull requests merged per contributor. It is a trend indicator for the team, not a performance score for individuals, so label it that way in the channel.

How do I report on more than one Jira project?

Change the JQL to project in (ENG, PLAT, MOBILE). If you also want separate sections per project, add the project key to the fields list and group by it in the compile Code node.

Can the update go to Microsoft Teams or email instead?

Yes. The compile Code node produces neutral data. Replace the Block Kit node with a Code node that builds HTML or an adaptive card, and swap the final node for Send Email or an HTTP Request to a Teams webhook.