{
  "id": "0cfAN67Kpkgyt6Hp",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "Generate and send freelance invoices from an Airtable project tracker",
  "tags": [],
  "nodes": [
    {
      "id": "e440b307-bfdb-4b63-9e93-6ca51c8d420e",
      "name": "Every day",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        976,
        352
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "triggerAtHour": 8
            }
          ]
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "ff2b1e7a-453e-4ecd-b64f-bd39bd8a9baf",
      "name": "Template Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        192,
        -112
      ],
      "parameters": {
        "color": 4,
        "width": 680,
        "height": 1104,
        "content": "## \ud83e\uddfe Generate and Send Freelance Invoices from an Airtable Project Tracker\n\n## Who's it for\nFreelancers and small agencies tracking client projects and billable work in Airtable who want invoices generated and emailed automatically once a project is marked ready to invoice, instead of manually building an invoice in a separate tool every time.\n\n## How it works\nOnce a day, the workflow checks Airtable for projects marked ready to invoice that haven't been billed yet. Each one is checked for the fields an invoice actually needs, client email, project name, and either an hourly rate with hours or a fixed fee. Records missing something are flagged in Airtable instead of generating a broken invoice. Complete records get their total calculated (including tax if applicable), a PDF invoice is generated and emailed straight to the client, and the record is marked invoiced so it's never billed twice.\n\n## How to set up\n1. Connect your Airtable credentials and point the workflow at your project base and table.\n2. Connect your Gmail credentials to the send node.\n3. Get an API key from your HTML-to-PDF provider.\n4. Make sure your Airtable table has the fields listed in the sticky note near the fetch step.\n5. Open the Set Configuration node and fill in your base ID, table name, tax percentage, business name, and PDF API details.\n6. Set your preferred run time in the Schedule Trigger.\n7. Activate the workflow.\n\n## Requirements\n- Airtable account with a project tracker base\n- Gmail account\n- An HTML-to-PDF API\n\n## How to customize\nEdit the invoice HTML template in Build Invoice HTML to match your branding. Adjust the required-fields check in Are Required Fields Complete? if your invoices need additional mandatory info."
      },
      "typeVersion": 1
    },
    {
      "id": "53b9b4c3-8d75-4f1f-b911-e582e8a21d19",
      "name": "Section - Trigger, Config, Fetch",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        928,
        128
      ],
      "parameters": {
        "color": 7,
        "width": 728,
        "height": 448,
        "content": "## 1\ufe0f\u20e3 2\ufe0f\u20e3 3\ufe0f\u20e3 Trigger + Configuration + Fetch\nRuns daily, pulls every project marked ready to invoice and not yet billed. Required fields: status, invoiced, client_email, client_name, project_name, hourly_rate, billable_hours, fixed_fee."
      },
      "typeVersion": 1
    },
    {
      "id": "2ec56a3c-1357-4a50-8965-4ddd7402c5d2",
      "name": "Section - Validate",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1680,
        160
      ],
      "parameters": {
        "color": 7,
        "width": 500,
        "height": 396,
        "content": "## 4\ufe0f\u20e3 Validate Before Generating\nOnly complete records get an invoice. Missing client_email, project_name, or a rate/hours (or a fixed_fee) sends the record to the needs-info branch instead of generating a broken invoice."
      },
      "typeVersion": 1
    },
    {
      "id": "bddff099-af2c-4a5d-b73a-96dadd4aedf1",
      "name": "Section - Needs Info Branch",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2208,
        592
      ],
      "parameters": {
        "color": 7,
        "width": 384,
        "height": 368,
        "content": "## \u274c If Incomplete\nFlags the record in Airtable so you can fix it manually, nothing gets invoiced with missing information."
      },
      "typeVersion": 1
    },
    {
      "id": "57bd8e3f-dea3-4b4b-a4ad-f9bdd2c37198",
      "name": "Section - Calculate and Build",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2192,
        32
      ],
      "parameters": {
        "color": 7,
        "width": 640,
        "height": 420,
        "content": "## 5\ufe0f\u20e3 6\ufe0f\u20e3 Calculate + Build Invoice\nCalculates the subtotal (fixed fee or rate \u00d7 hours), adds tax, then builds the invoice HTML. Edit the template here for your branding."
      },
      "typeVersion": 1
    },
    {
      "id": "015d9f19-4e2e-407b-a415-12834e602b70",
      "name": "Section - Generate and Send",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2848,
        112
      ],
      "parameters": {
        "color": 7,
        "width": 720,
        "height": 420,
        "content": "## 7\ufe0f\u20e3 8\ufe0f\u20e3 Generate PDF + Send\nGenerates the PDF, attaches it directly to the email (no separate hosting/upload step needed since Gmail accepts binary attachments), then marks the project invoiced."
      },
      "typeVersion": 1
    },
    {
      "id": "ad1f69ea-7c53-4e8d-a03f-ab588367dde1",
      "name": "Flag Airtable - Needs Info",
      "type": "n8n-nodes-base.airtable",
      "position": [
        2320,
        768
      ],
      "parameters": {
        "base": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $('Set Configuration').item.json.airtable_base_id }}"
        },
        "table": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $('Set Configuration').item.json.airtable_table_name }}"
        },
        "columns": {
          "value": {
            "id": "={{ $json.id }}",
            "status": "needs_info"
          },
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "id"
          ]
        },
        "options": {},
        "operation": "update"
      },
      "credentials": {
        "airtableTokenApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "2558e99f-7858-492c-962a-93cd6f5e6234",
      "name": "Calculate Invoice Total",
      "type": "n8n-nodes-base.code",
      "position": [
        2304,
        240
      ],
      "parameters": {
        "jsCode": "const item = $json;\nconst config = $('Set Configuration').first().json;\n\nconst hourlyRate = item.hourly_rate ? Number(item.hourly_rate) : null;\nconst hours = item.billable_hours ? Number(item.billable_hours) : null;\nconst fixedFee = item.fixed_fee ? Number(item.fixed_fee) : null;\n\nconst subtotal = fixedFee != null ? fixedFee : (hourlyRate * hours);\nconst taxPercent = Number(config.tax_percent) || 0;\nconst taxAmount = Math.round((subtotal * taxPercent / 100) * 100) / 100;\nconst totalAmount = Math.round((subtotal + taxAmount) * 100) / 100;\n\nreturn [{\n  json: {\n    ...item,\n    subtotal,\n    tax_amount: taxAmount,\n    total_amount: totalAmount,\n    invoice_date: new Date().toISOString().slice(0, 10)\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "c1588d5f-3b70-4453-8b21-d169e5110605",
      "name": "Build Invoice HTML",
      "type": "n8n-nodes-base.set",
      "position": [
        2560,
        240
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "html-1",
              "name": "invoice_html",
              "type": "string",
              "value": "=<html><body style=\"font-family: Arial, sans-serif; padding: 24px;\"><h2>{{ $('Set Configuration').item.json.business_name }}</h2><p><strong>Invoice for:</strong> {{ $json.client_name }} ({{ $json.client_email }})</p><p><strong>Project:</strong> {{ $json.project_name }}</p><p><strong>Date:</strong> {{ $json.invoice_date }}</p><table border=\"1\" cellpadding=\"8\" cellspacing=\"0\" style=\"border-collapse: collapse; width: 100%; margin-top: 16px;\"><tr><th>Description</th><th>Amount</th></tr><tr><td>{{ $json.project_name }}</td><td>\u20b9{{ $json.subtotal }}</td></tr><tr><td>Tax</td><td>\u20b9{{ $json.tax_amount }}</td></tr><tr><td><strong>Total</strong></td><td><strong>\u20b9{{ $json.total_amount }}</strong></td></tr></table></body></html>"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "58a0bf48-c43f-4e02-bdc8-17efd8ee7a35",
      "name": "Generate Invoice PDF",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        2912,
        336
      ],
      "parameters": {
        "url": "={{ $('Set Configuration').item.json.pdf_api_url }}",
        "method": "POST",
        "options": {
          "response": {
            "response": {
              "responseFormat": "file"
            }
          }
        },
        "jsonBody": "={{ JSON.stringify({ html: $json.invoice_html }) }}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "headerParameters": {
          "parameters": [
            {
              "name": "CLIENT-API-KEY",
              "value": "={{ $('Set Configuration').item.json.pdf_api_key }}"
            },
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "6fc5a471-b049-4a69-9dbc-ce17b9a1bc02",
      "name": "Send Invoice Email",
      "type": "n8n-nodes-base.gmail",
      "position": [
        3120,
        336
      ],
      "parameters": {
        "sendTo": "={{ $('Calculate Invoice Total').item.json.client_email }}",
        "message": "=Hi {{ $('Calculate Invoice Total').item.json.client_name }},\n\nPlease find attached the invoice for {{ $('Calculate Invoice Total').item.json.project_name }}. Total due: \u20b9{{ $('Calculate Invoice Total').item.json.total_amount }}.\n\nThanks,\n{{ $('Set Configuration').item.json.business_name }}",
        "options": {
          "attachmentsUi": {
            "attachmentsBinary": [
              {}
            ]
          }
        },
        "subject": "=Invoice for {{ $('Calculate Invoice Total').item.json.project_name }}"
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "b4b35fa4-0005-49a8-86b2-5b09c2e9b4b9",
      "name": "Update Airtable - Mark Invoiced",
      "type": "n8n-nodes-base.airtable",
      "position": [
        3360,
        336
      ],
      "parameters": {
        "base": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $('Set Configuration').item.json.airtable_base_id }}"
        },
        "table": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $('Set Configuration').item.json.airtable_table_name }}"
        },
        "columns": {
          "value": {
            "id": "={{ $('Calculate Invoice Total').item.json.id }}",
            "invoiced": true,
            "invoice_date": "={{ $('Calculate Invoice Total').item.json.invoice_date }}",
            "invoice_amount": "={{ $('Calculate Invoice Total').item.json.total_amount }}"
          },
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "id"
          ]
        },
        "options": {},
        "operation": "update"
      },
      "credentials": {
        "airtableTokenApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "795528a6-4e2f-4c68-ae86-e90caa45ab0a",
      "name": "Set Configuration",
      "type": "n8n-nodes-base.set",
      "position": [
        1200,
        352
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "86442326-9f82-497a-88ca-ca06a3d4cc10",
              "name": "business_name",
              "type": "string",
              "value": "REPLACE_WITH_BUSINESS_NAME"
            },
            {
              "id": "c15e4953-066b-4e4a-a226-9636fcf2255e",
              "name": "airtable_base_id",
              "type": "string",
              "value": "REPLACE_WITH_AIRTABLE_BASE_ID"
            },
            {
              "id": "1c4152d8-661d-4f03-b26a-1ea4e01772c6",
              "name": "airtable_table_name",
              "type": "string",
              "value": "Projects"
            },
            {
              "id": "d992eabc-3f1f-4d67-92d3-4c63f26e6d95",
              "name": "tax_percent",
              "type": "string",
              "value": "18"
            },
            {
              "id": "020159fe-9f02-4a8e-b9dc-90774323f4da",
              "name": "pdf_api_url",
              "type": "string",
              "value": "REPLACE_WITH_PDF_API_ENDPOINT"
            },
            {
              "id": "f99a3b85-f624-449a-a7ef-c86b83dd4828",
              "name": "pdf_api_key",
              "type": "string",
              "value": "REPLACE_WITH_PDF_API_KEY"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "8217b8be-63cf-43cc-a56d-9a4f1d4559b5",
      "name": "Fetch Ready-to-Invoice Projects",
      "type": "n8n-nodes-base.airtable",
      "position": [
        1424,
        352
      ],
      "parameters": {
        "base": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $('Set Configuration').item.json.airtable_base_id }}"
        },
        "table": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $('Set Configuration').item.json.airtable_table_name }}"
        },
        "options": {},
        "operation": "search",
        "filterByFormula": "AND({status} = 'ready_to_invoice', {invoiced} = FALSE())"
      },
      "credentials": {
        "airtableTokenApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "54c97bef-ae32-477b-8a94-e41565503f07",
      "name": "Are Required Fields Complete?",
      "type": "n8n-nodes-base.if",
      "position": [
        1808,
        352
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "22f9e7c7-bf8f-4767-818b-ae546c9071d6",
              "operator": {
                "type": "string",
                "operation": "notEquals"
              },
              "leftValue": "={{ $json.client_email }}",
              "rightValue": ""
            },
            {
              "id": "c8c14831-ae40-44a8-a3d8-c4e3fbf27c30",
              "operator": {
                "type": "string",
                "operation": "notEquals"
              },
              "leftValue": "={{ $json.project_name }}",
              "rightValue": ""
            },
            {
              "id": "a876ce21-73e7-41ac-89a0-c545677a61e0",
              "operator": {
                "name": "filter.operator.equals",
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ !!$json.fixed_fee || (!!$json.hourly_rate && !!$json.billable_hours) }}",
              "rightValue": "true"
            }
          ]
        }
      },
      "typeVersion": 2.3
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "availableInMCP": false,
    "executionOrder": "v1"
  },
  "versionId": "ec6c8083-f676-4e1a-add9-b3bd9b2a2eb7",
  "nodeGroups": [],
  "connections": {
    "Every day": {
      "main": [
        [
          {
            "node": "Set Configuration",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set Configuration": {
      "main": [
        [
          {
            "node": "Fetch Ready-to-Invoice Projects",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Invoice HTML": {
      "main": [
        [
          {
            "node": "Generate Invoice PDF",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send Invoice Email": {
      "main": [
        [
          {
            "node": "Update Airtable - Mark Invoiced",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate Invoice PDF": {
      "main": [
        [
          {
            "node": "Send Invoice Email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Calculate Invoice Total": {
      "main": [
        [
          {
            "node": "Build Invoice HTML",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Are Required Fields Complete?": {
      "main": [
        [
          {
            "node": "Calculate Invoice Total",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Flag Airtable - Needs Info",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Ready-to-Invoice Projects": {
      "main": [
        [
          {
            "node": "Are Required Fields Complete?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}