AutomationFlowsEmail & Gmail › Auto-generate Developer Invoices & Compliance Reminders with Jira and Gmail

Auto-generate Developer Invoices & Compliance Reminders with Jira and Gmail

ByRahul Joshi @rahul08 on n8n.io

This automation streamlines developer billing and compliance tracking by integrating Jira, Gmail, and n8n into a single intelligent workflow. It fetches all project issues from Jira, calculates logged hours per team member, identifies missing time entries, and automatically…

Event trigger★★★★☆ complexity15 nodesJiraGmail
Email & Gmail Trigger: Event Nodes: 15 Complexity: ★★★★☆ Added:

This workflow corresponds to n8n.io template #9837 — we link there as the canonical source.

The workflow JSON

Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →

Download .json
{
  "id": "VyFk3RnfdN7deOBG",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "Time Tracking & Billing Automation:",
  "tags": [],
  "nodes": [
    {
      "id": "e62e8f45-3b5b-4a1d-a120-b0c07ebcc178",
      "name": "When clicking \u2018Execute workflow\u2019",
      "type": "n8n-nodes-base.manualTrigger",
      "position": [
        -128,
        0
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "fadb2658-c02d-4819-b6eb-12114cc58127",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1120,
        -352
      ],
      "parameters": {
        "width": 256,
        "height": 336,
        "content": "Combine Reminder & Invoice Data Streams\n\nAction: Merges reminder and invoice outputs for unified processing.\nDescription:\n\nCombines both branches (missing hours and invoice data).\n\nEnsures all team members get relevant communication.\n\nKeeps all data intact for the next reconciliation step."
      },
      "typeVersion": 1
    },
    {
      "id": "4b5a9488-f7cd-436b-afaf-f698c756d933",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        736,
        -368
      ],
      "parameters": {
        "width": 288,
        "height": 336,
        "content": "Identify Issues with Missing Time Logs\n\nAction: Detects issues where time wasn\u2019t logged.\nDescription:\n\nScans each user\u2019s issues to find zero-hour entries.\n\nGenerates HTML reminder messages listing missing issues.\n\nCreates reminders only for users with actual missing logs.\n\nEnsures timely and accurate billing compliance."
      },
      "typeVersion": 1
    },
    {
      "id": "174d0eec-6cce-4d0c-8b38-045b67f12f76",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        368,
        -368
      ],
      "parameters": {
        "width": 288,
        "height": 336,
        "content": "Aggregate Hours by Team Member\n\nAction: Groups Jira issues by each assignee and totals their hours.\nDescription:\n\nCalculates total logged hours per user.\n\nKeeps issue details like summary, status, and sprint.\n\nHandles missing data (unassigned or null time entries).\n\nProduces structured user-level data for invoices and reminders."
      },
      "typeVersion": 1
    },
    {
      "id": "13457e31-0e46-48e0-88a1-91610a40b646",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        80,
        176
      ],
      "parameters": {
        "width": 304,
        "height": 368,
        "content": "Fetch All Project Issues with Time Data\n\nAction: Retrieves all Jira issues with time tracking info.\nDescription:\n\nUses Jira API to fetch every issue with getAll and returnAll.\n\nCollects essential fields like keys, summaries, time spent, and assignee.\n\nProvides project, sprint, and priority details.\n\nServes as the base dataset for billing and reporting."
      },
      "typeVersion": 1
    },
    {
      "id": "edcde860-f8c8-4229-be41-c4c026be9a46",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        720,
        560
      ],
      "parameters": {
        "width": 304,
        "height": 400,
        "content": "Generate Invoice Summary with Text Attachment\n\nAction: Creates detailed text-based invoices per user.\nDescription:\n\nCalculates billing using a default hourly rate ($50/hour).\n\nLists each issue with hours and status.\n\nExports a plain-text invoice as .txt (base64 encoded).\n\nSkips users with zero hours to avoid empty invoices.\n\nSends structured JSON + binary data for email delivery"
      },
      "typeVersion": 1
    },
    {
      "id": "d2450d54-f3e7-4732-bccd-2bc0e29b8238",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1760,
        208
      ],
      "parameters": {
        "width": 272,
        "height": 384,
        "content": "Send Invoices & Reminders to Team\n\nAction: Sends personalized emails with invoices and reminders.\nDescription:\n\nSends to each developer\u2019s email dynamically.\n\nIncludes name, project, and total hours in the message.\n\nAttaches generated invoice files.\n\nCan be extended to CC managers or finance.\n\nFinal delivery step for automated billing communication."
      },
      "typeVersion": 1
    },
    {
      "id": "559bf371-f7b3-477f-82d9-a022ed54856f",
      "name": "Sticky Note6",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1424,
        -384
      ],
      "parameters": {
        "width": 288,
        "height": 368,
        "content": "Reconcile JSON & Binary Attachments\n\nAction: Aligns JSON data with corresponding invoice files.\nDescription:\n\nMatches items using assignee name as identifier.\n\nFixes cases where JSON or binary is missing.\n\nGuarantees each record includes both email info and invoice.\n\nPrevents send failures and ensures complete data merging."
      },
      "typeVersion": 1
    },
    {
      "id": "0da5f2fa-32f6-4c7e-85a0-474bf3550f4e",
      "name": " Fetch All Project Issues with Time Data",
      "type": "n8n-nodes-base.jira",
      "position": [
        176,
        0
      ],
      "parameters": {
        "options": {},
        "operation": "getAll",
        "returnAll": true
      },
      "credentials": {
        "jiraSoftwareCloudApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "76eca7bd-8b7d-4119-a18f-81ae5c142653",
      "name": "Aggregate Hours by Team Member",
      "type": "n8n-nodes-base.code",
      "position": [
        448,
        0
      ],
      "parameters": {
        "jsCode": "// Get all Jira issues from input\nconst items = $input.all().map(i => i.json);\n\n// Initialize grouped output\nconst grouped = {};\n\n// Iterate over each issue\nfor (const issue of items) {\n  const f = issue.fields || {};\n\n  const assigneeName = f.assignee?.displayName || 'Unassigned';\n  const email = f.assignee?.emailAddress || 'user@example.com';\n  const projectName = f.project?.name || 'Unknown Project';\n  const sprint = f.customfield_10020?.[0]?.name || 'No Sprint';\n  const timeSpentSeconds = f.timespent || 0;\n  const timeSpentHours = (timeSpentSeconds / 3600).toFixed(2);\n\n  if (!grouped[assigneeName]) {\n    grouped[assigneeName] = {\n      assignee: assigneeName,\n      email,\n      project_name: projectName,\n      total_hours: 0,\n      issues: []\n    };\n  }\n\n  grouped[assigneeName].total_hours += parseFloat(timeSpentHours);\n  grouped[assigneeName].issues.push({\n    issue_key: issue.key,\n    summary: f.summary,\n    timespent_hours: timeSpentHours,\n    status: f.status?.name || 'Unknown',\n    priority: f.priority?.name || 'None',\n    sprint,\n  });\n}\n\n// Convert grouped object into array format\nconst output = Object.values(grouped).map(user => ({\n  json: {\n    assignee: user.assignee,\n    email: user.email,\n    project_name: user.project_name,\n    total_hours: user.total_hours.toFixed(2),\n    issues: user.issues,\n  }\n}));\n\nreturn output;\n"
      },
      "typeVersion": 2,
      "alwaysOutputData": false
    },
    {
      "id": "3ef99004-e0bd-4346-8ac0-6e83a88260f0",
      "name": "Identify Issues with Missing Time Logs",
      "type": "n8n-nodes-base.code",
      "position": [
        832,
        0
      ],
      "parameters": {
        "jsCode": "const users = $input.all().map(i => i.json);\n\nconst reminders = [];\n\nfor (const user of users) {\n  const missing = user.issues.filter(i => parseFloat(i.timespent_hours) === 0);\n\n  if (missing.length > 0) {\n    const table = missing.map(i => \n      `<tr>\n        <td>${i.issue_key}</td>\n        <td>${i.summary}</td>\n        <td>${i.status}</td>\n        <td>${i.priority}</td>\n        <td>${i.sprint}</td>\n      </tr>`\n    ).join('');\n\n    reminders.push({\n      json: {\n        assignee: user.assignee,\n        email: user.email,\n        missing_count: missing.length,\n        project_name: user.project_name,\n        html_message: `\n          <p>Hi ${user.assignee},</p>\n          <p>Our system found ${missing.length} issues where no time is logged:</p>\n          <table border=\"1\" cellspacing=\"0\" cellpadding=\"5\">\n            <tr><th>Issue Key</th><th>Summary</th><th>Status</th><th>Priority</th><th>Sprint</th></tr>\n            ${table}\n          </table>\n          <p>Please log your hours for these issues before EOD to ensure accurate billing.</p>\n          <p>Thank you,<br>Automation Bot \ud83e\udd16</p>\n        `\n      }\n    });\n  }\n}\n\nreturn reminders;\n"
      },
      "typeVersion": 2
    },
    {
      "id": "db74c4fe-3bf1-45f9-a971-64ffc74695df",
      "name": "Generate Invoice Summary with Text Attachment",
      "type": "n8n-nodes-base.code",
      "position": [
        832,
        384
      ],
      "parameters": {
        "jsCode": "// Combined Invoice Generator + Text File Export\n\nconst users = $input.all().map(i => i.json);\nconst RATE = 50; // hourly rate\nconst results = [];\n\nfor (const user of users) {\n  const totalHours = parseFloat(user.total_hours || 0);\n  if (totalHours === 0) continue;\n\n  const totalAmount = totalHours * RATE;\n\n  // Build the plain text table\n  const issueLines = user.issues.map(i =>\n    `${i.issue_key.padEnd(10)} | ${i.summary.padEnd(30)} | ${i.timespent_hours} hrs | ${i.status}`\n  ).join('\\n');\n\n  // Final plain-text invoice content\n  const text = `\nYOUR_AWS_SECRET_KEY_HERE===\nINVOICE SUMMARY \u2014 ${user.project_name}\nYOUR_AWS_SECRET_KEY_HERE===\n\nAssignee: ${user.assignee}\nEmail: ${user.email}\nRate: $${RATE}/hour\n\n-------------------------------------------\nIssues\n-------------------------------------------\n${issueLines}\n\n-------------------------------------------\nTotal Hours: ${totalHours.toFixed(2)}\nTotal Amount: $${totalAmount.toFixed(2)}\n-------------------------------------------\n\nGenerated automatically by Techdome Billing Automation Bot.\n`;\n\n  // Push JSON + Binary for email attachment\n  results.push({\n    json: {\n      assignee: user.assignee,\n      email: user.email,\n      project_name: user.project_name,\n      total_hours: totalHours.toFixed(2),\n      total_amount: totalAmount.toFixed(2)\n    },\n    binary: {\n      data: {\n        fileName: `Invoice_${user.assignee}.txt`,\n        mimeType: 'text/plain',\n        data: Buffer.from(text, 'utf8').toString('base64')\n      }\n    }\n  });\n}\n\nreturn results;\n"
      },
      "typeVersion": 2
    },
    {
      "id": "064ef92a-f13c-4926-8526-a1eb398fa3db",
      "name": "Combine Reminder & Invoice Data Streams",
      "type": "n8n-nodes-base.merge",
      "position": [
        1216,
        16
      ],
      "parameters": {},
      "typeVersion": 3.2
    },
    {
      "id": "d3aec18e-05de-4ca9-8b19-263695b105b7",
      "name": "Reconcile JSON & Binary Attachments",
      "type": "n8n-nodes-base.code",
      "position": [
        1488,
        16
      ],
      "parameters": {
        "jsCode": "// Combine JSON + Binary after Merge node\n// Safe for n8n v1.112.6 and later\n\nconst combined = [];\n\n// Get all incoming items\nconst items = $input.all();\n\n// We might have duplicates or partials, so we merge smartly\nfor (const item of items) {\n  const json = item.json || {};\n  const binary = item.binary || {};\n\n  // Case 1: this item already has both\n  if (json.email && binary.data) {\n    combined.push({ json, binary });\n    continue;\n  }\n\n  // Case 2: only has JSON\n  if (json.email && !binary.data) {\n    // Try to find its binary partner\n    const match = items.find(\n      (i) => i.binary && i.binary.data && i.json?.assignee === json.assignee\n    );\n\n    if (match) {\n      combined.push({ json, binary: match.binary });\n    } else {\n      combined.push({ json }); // fallback\n    }\n    continue;\n  }\n\n  // Case 3: only has Binary\n  if (binary.data && !json.email) {\n    // Try to find its JSON partner\n    const match = items.find(\n      (i) => i.json && i.json.assignee && i.binary === undefined\n    );\n    if (match) {\n      combined.push({ json: match.json, binary });\n    } else {\n      combined.push({ binary });\n    }\n    continue;\n  }\n}\n\nreturn combined;\n"
      },
      "typeVersion": 2
    },
    {
      "id": "94b59a38-4e45-47dc-8c3d-d133ddd07455",
      "name": "Send Invoices & Reminders to Team",
      "type": "n8n-nodes-base.gmail",
      "position": [
        1808,
        16
      ],
      "parameters": {
        "sendTo": "={{ $json.email }}",
        "message": "=Hi {{$json[\"assignee\"]}},<br><br>\nPlease find attached your invoice summary for <strong>{{$json[\"project_name\"]}}</strong>.<br><br>\n\ud83d\udcca <b>Total Hours:</b> {{ $('Aggregate Hours by Team Member').item.json.total_hours }}<br>\nRegards,<br>\nTechdome Billing Automation Bot\n",
        "options": {
          "attachmentsUi": {
            "attachmentsBinary": [
              {}
            ]
          }
        },
        "subject": "={{ $json.project_name }}"
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.1
    }
  ],
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "c7c9ac45-c34e-4b9e-a15d-0aea4da1f6d6",
  "connections": {
    "Aggregate Hours by Team Member": {
      "main": [
        [
          {
            "node": "Identify Issues with Missing Time Logs",
            "type": "main",
            "index": 0
          },
          {
            "node": "Generate Invoice Summary with Text Attachment",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send Invoices & Reminders to Team": {
      "main": [
        []
      ]
    },
    "Reconcile JSON & Binary Attachments": {
      "main": [
        [
          {
            "node": "Send Invoices & Reminders to Team",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "When clicking \u2018Execute workflow\u2019": {
      "main": [
        [
          {
            "node": " Fetch All Project Issues with Time Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Identify Issues with Missing Time Logs": {
      "main": [
        [
          {
            "node": "Combine Reminder & Invoice Data Streams",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Combine Reminder & Invoice Data Streams": {
      "main": [
        [
          {
            "node": "Reconcile JSON & Binary Attachments",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    " Fetch All Project Issues with Time Data": {
      "main": [
        [
          {
            "node": "Aggregate Hours by Team Member",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate Invoice Summary with Text Attachment": {
      "main": [
        [
          {
            "node": "Combine Reminder & Invoice Data Streams",
            "type": "main",
            "index": 1
          }
        ]
      ]
    }
  }
}

Credentials you'll need

Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.

Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

This automation streamlines developer billing and compliance tracking by integrating Jira, Gmail, and n8n into a single intelligent workflow. It fetches all project issues from Jira, calculates logged hours per team member, identifies missing time entries, and automatically…

Source: https://n8n.io/workflows/9837/ — original creator credit. Request a take-down →

More Email & Gmail workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

Email & Gmail

This workflow is an end-to-end AI video generation, review, and delivery pipeline. It takes a plain-text creative prompt via webhook, generates a video using an AI model, continuously checks job statu

Slack, Error Trigger, HTTP Request +2
Email & Gmail

Monitor_security_advisories. Uses manualTrigger, rssFeedRead, jira, n8nTrainingCustomerDatastore. Event-driven trigger; 17 nodes.

RSS Feed Read, Jira, N8N Training Customer Datastore +1
Email & Gmail

This n8n workflow automates the monitoring and notification of Palo Alto Networks security advisories. It is triggered manually from within the n8n UI or scheduled to run daily at midnight using the S

RSS Feed Read, Jira, N8N Training Customer Datastore +1
Email & Gmail

This workflow automates team capacity monitoring using Jira data to identify over-allocated team members and alert managers instantly. It ensures proactive workload management by fetching active issue

Jira, Google Sheets, Gmail
Email & Gmail

Automated workflow that creates Jira issues directly from Google Forms. The flow validates and normalizes the data, creates the Jira issue, writes the key back to the Google Sheet, and sends a Gmail n

Google Sheets Trigger, Jira, Google Sheets +1