47 min readDeterministic automation

Implementation Guide: Trigger invoicing milestones based on project phase completion

Step-by-step implementation guide for deploying AI to trigger invoicing milestones based on project phase completion for Professional Services clients.

Hardware Procurement

Automation Server VM (for self-hosted n8n)

Microsoft Azure / AWS / MSP Private CloudAzure B2s (2 vCPU, 4 GB RAM, 32 GB SSD) or AWS t3.mediumQty: 1

$30–$60/month MSP cost / $150–$300/month suggested resale as 'Managed Automation Platform'

Hosts the self-hosted n8n Community Edition instance that serves as the automation middleware, processing webhook events from the PSA and issuing API calls to the accounting platform. This VM is managed by the MSP on behalf of the client.

Dual Monitor (optional, for project managers)

DellDell P2423D (24-inch QHD IPS)Qty: 2

$250 per unit MSP cost / $325 suggested resale

Recommended for project managers who need to review project phase dashboards alongside invoicing queues simultaneously. Not strictly required but improves workflow efficiency during the transition period.

Software Procurement

Scoro Standard (PSA / Project Management)

ScoroStandardQty: per-seat SaaS, annual billing, 5-user minimum

$26/user/month (annual) direct cost; MSP partner pricing ~$19–$22/user/month. Suggested resale: $35–$45/user/month. For 10 users: ~$220–$260/month MSP cost, $350–$450/month client price.

Primary PSA platform for project phase tracking, milestone definition, time tracking, and resource management. Scoro's project phases serve as the trigger source for the automation—when a phase status changes to 'Completed', a webhook fires to initiate the invoicing workflow.

BigTime Advanced (Alternative PSA)

BigTime SoftwareBigTime AdvancedQty: per-seat SaaS, monthly or annual billing

$35/user/month direct; MSP partner pricing ~$28–$30/user/month. For 10 users: ~$280–$300/month MSP cost. Suggested resale: $40–$50/user/month.

Alternative PSA platform, especially strong for accounting firms, legal practices, and government contractors. Supports flexible milestone-based billing natively. Use instead of Scoro when the client already has BigTime or needs DCAA-compliant timekeeping.

n8n Community Edition (Automation Middleware — Primary)

n8n GmbHCommunity EditionQty: Self-hosted, unlimited executions

$0 software cost; only infrastructure cost ($30–$60/month VM). MSP charges $150–$300/month as managed automation service. Margin: 75–90%.

Core automation engine. Receives webhook events from the PSA when project phases are completed, evaluates billing rules (milestone type, amount, client, approval status), and generates invoices in QuickBooks Online or Xero via API. Self-hosted by the MSP for maximum control and margin.

Zapier Team Plan (Alternative Middleware)

ZapierTeam PlanQty: 2,000 tasks/month

$103.50/month (Team plan, 2,000 tasks). Suggested resale: $150–$175/month. Margin: 30–40%.

Alternative automation middleware for MSPs that prefer a fully managed no-code platform over self-hosted n8n. Easier to set up but higher ongoing cost and less control. Best for very small deployments or when the MSP lacks Docker/VM management experience.

Make Pro Plan (Alternative Middleware)

Make (formerly Integromat)Pro PlanQty: 10,000–40,000 operations/month

$9–$29/month (Pro plan). Suggested resale: $50–$75/month bundled with management. Margin: 60%+.

Alternative automation middleware with a powerful visual workflow builder. More affordable than Zapier, supports complex branching logic. Good middle ground between n8n self-hosted and Zapier. License type: SaaS, usage-based (operations/month).

QuickBooks Online Plus (Accounting)

IntuitQuickBooks Online Plus

$60/month direct. MSP may resell via Intuit partner program at modest markup ($70–$80/month).

Primary accounting platform for invoice creation and management. The automation middleware calls the QuickBooks API to create invoices with the correct line items, amounts, due dates, and client details when a milestone is triggered.

$42/month direct. Suggested resale: $55–$65/month.

Alternative accounting platform, especially popular with international firms and consultancies. Full REST API with webhook support for invoice creation automation.

$15/user/month via CSP. Suggested resale: $25–$35/user/month. For 3 power users: $45/month MSP cost, $75–$105/month client price.

Best alternative middleware for clients already invested in Microsoft 365 and Dynamics 365. Can trigger on SharePoint list changes, Dataverse events, or custom connectors to PSA platforms. License type: per-user SaaS, monthly.

Pricing is quote-based, typically $150–$300/month depending on volume.

Specialized real-time sync connector between ConnectWise PSA and QuickBooks Online or Xero. Use only when the client runs ConnectWise PSA—eliminates need for custom middleware for the PSA-to-accounting sync specifically.

$0 (Let's Encrypt auto-renewal via Certbot) or included in Cloudflare free tier

Required to secure the webhook endpoint that receives events from the PSA platform. All webhook URLs must use HTTPS for production use.

Prerequisites

  • Client has an active PSA or project management platform (Scoro, BigTime, Kantata, ConnectWise PSA, or HaloPSA) with admin-level access available, OR client has approved procurement of one of these platforms
  • Client has an active accounting platform (QuickBooks Online Plus or higher, or Xero Standard or higher) with API access enabled and admin credentials available
  • Client has defined project templates with clearly delineated phases/milestones, including milestone names, expected deliverables, and billing amounts per milestone
  • Client has documented billing terms for at least their top 5 project types, including: milestone payment percentages, payment terms (Net 15/30/60), and any approval gates required before invoicing
  • Client has a modern web browser (Chrome 90+, Edge 90+, Firefox 90+) on all workstations that will access the PSA and accounting platforms
  • Reliable internet connectivity (25+ Mbps symmetric minimum) at all client office locations
  • Microsoft 365 or Google Workspace accounts for all users who will interact with the system (for SSO and notification delivery)
  • Client has designated a Project Operations Lead (typically a senior PM or operations manager) who will own the business rules and serve as the primary point of contact during implementation
  • Client finance team has confirmed the chart of accounts, income categories, and tax codes to be used for automated invoices
  • For clients subject to ASC 606: written confirmation from the client's accountant or CFO on how revenue recognition should differ from billing events, and whether the automation should create the invoice in draft or approved status
  • MSP has provisioned or has access to a VM or VPS (2 vCPU, 4 GB RAM, 40 GB SSD minimum) for hosting n8n, OR has an active Zapier Team / Make Pro subscription
  • MSP technician assigned to the project has experience with REST APIs, webhook configuration, and at least one of: n8n, Zapier, Make, or Power Automate
  • Client has provided a list of all active projects that will be migrated to the new milestone billing workflow, including current phase status and outstanding unbilled milestones

Installation Steps

...

Step 1: Discovery & Business Process Mapping

Conduct a 2–3 hour discovery workshop with the client's project managers, finance team, and operations lead. Map the current invoicing workflow from project kickoff to payment receipt. Document all project types, phase structures, billing terms, approval gates, and exception handling (e.g., scope changes, partial completions, cancellations). Output a formal Business Process Document (BPD) and Automation Rules Specification.

Note

Use a whiteboard or Miro board to visualize the as-is and to-be workflows. Pay special attention to: (1) Who currently decides when to invoice? (2) How are milestone amounts determined—fixed at contract signing or variable? (3) Are there approval gates between phase completion and invoicing? (4) How are credits, refunds, and scope changes handled? This step is critical—automation failures almost always trace back to incomplete process mapping, not technical issues.

Step 2: Configure PSA Platform — Project Templates & Milestones

In the client's PSA platform (Scoro, BigTime, or equivalent), create or update project templates with standardized phase structures that will serve as automation triggers. Each phase must have: a unique phase name, a billing type (milestone/fixed/T&M), a milestone amount or percentage, and a status field with defined values (Not Started, In Progress, Complete, On Hold). Enable webhook or API notification capabilities on the PSA platform.

  • Scoro: Navigate to Settings > Site Settings > Integrations > Webhooks
  • Add a new webhook with the following configuration:
  • Event: Project Phase Status Changed
  • URL: https://your-n8n-domain.com/webhook/scoro-phase-complete
  • Method: POST
  • Content-Type: application/json
  • Secret: [generate a 32-character random string and store in password manager]
  • BigTime: Navigate to My Company > Integrations > API Settings
  • Enable REST API access
  • Generate an API key and store securely
  • Note: BigTime uses polling rather than webhooks, so n8n will poll the API on a schedule
Note

If the client does not have standardized project templates, this step will take significantly longer (add 1–2 weeks). Work with the operations lead to standardize at least the top 3–5 project types before proceeding. For Scoro, the webhook configuration is straightforward. For BigTime and some other PSAs that lack native webhooks, you will need to implement a polling pattern in n8n (check for status changes every 5–15 minutes).

Step 3: Configure Accounting Platform — Invoice Templates & API Access

In QuickBooks Online (or Xero), create invoice templates that match the client's branding and include all required fields for milestone invoices. Configure API access by creating an app in the developer portal, obtaining OAuth 2.0 credentials, and setting up the necessary scopes for invoice creation.

plaintext
# QuickBooks Online API Setup:
# 1. Go to https://developer.intuit.com/app/developer/qbo/docs/get-started
# 2. Create a new app (select 'Accounting' as the scope)
# 3. In the app settings, configure:
#    - Redirect URI: https://your-n8n-domain.com/rest/oauth2-credential/callback
#    - Scopes: com.intuit.quickbooks.accounting (read/write)
# 4. Note the Client ID and Client Secret
# 5. Store credentials in the MSP's password vault

# Xero API Setup:
# 1. Go to https://developer.xero.com/app/manage
# 2. Create a new app (Web App type)
# 3. Configure:
#    - Redirect URI: https://your-n8n-domain.com/rest/oauth2-credential/callback
#    - Scopes: accounting.transactions, accounting.contacts
# 4. Note the Client ID and Client Secret
Note

QuickBooks OAuth 2.0 tokens expire every 60 minutes and refresh tokens expire after 100 days of non-use. The n8n QuickBooks node handles token refresh automatically, but if the automation is paused for more than 100 days, you will need to re-authorize. For Xero, tokens expire after 30 minutes with a 60-day refresh token window. Set a calendar reminder to verify token health monthly.

Step 4: Deploy n8n Automation Server (Self-Hosted)

Provision and configure the n8n server that will serve as the automation middleware. This server receives webhook events from the PSA, evaluates business rules, and creates invoices in the accounting platform. Deploy using Docker Compose on the provisioned VM for easy management and updates.

bash
# SSH into the provisioned VM
ssh adminuser@your-n8n-server-ip

# Update system packages
sudo apt update && sudo apt upgrade -y

# Install Docker and Docker Compose
sudo apt install -y docker.io docker-compose-v2
sudo systemctl enable docker
sudo systemctl start docker
sudo usermod -aG docker $USER

# Create n8n directory structure
sudo mkdir -p /opt/n8n/data
sudo mkdir -p /opt/n8n/config
cd /opt/n8n

# Create Docker Compose file
cat > docker-compose.yml << 'EOF'
version: '3.8'
services:
  n8n:
    image: n8nio/n8n:latest
    restart: always
    ports:
      - '5678:5678'
    environment:
      - N8N_HOST=your-n8n-domain.com
      - N8N_PORT=5678
      - N8N_PROTOCOL=https
      - WEBHOOK_URL=https://your-n8n-domain.com/
      - N8N_BASIC_AUTH_ACTIVE=true
      - N8N_BASIC_AUTH_USER=msp-admin
      - N8N_BASIC_AUTH_PASSWORD=<generate-strong-password-here>
      - DB_TYPE=postgresdb
      - DB_POSTGRESDB_HOST=postgres
      - DB_POSTGRESDB_PORT=5432
      - DB_POSTGRESDB_DATABASE=n8n
      - DB_POSTGRESDB_USER=n8n
      - DB_POSTGRESDB_PASSWORD=<generate-db-password-here>
      - N8N_ENCRYPTION_KEY=<generate-32-char-encryption-key>
      - GENERIC_TIMEZONE=America/New_York
      - N8N_LOG_LEVEL=info
      - N8N_DIAGNOSTICS_ENABLED=false
    volumes:
      - ./data:/home/node/.n8n
  postgres:
    image: postgres:16-alpine
    restart: always
    environment:
      - POSTGRES_USER=n8n
      - POSTGRES_PASSWORD=<same-db-password-as-above>
      - POSTGRES_DB=n8n
    volumes:
      - ./postgres-data:/var/lib/postgresql/data
EOF

# Start n8n
sudo docker compose up -d

# Verify containers are running
sudo docker compose ps

# Check n8n logs
sudo docker compose logs -f n8n
Note

Replace all placeholder values (<generate-...>) with actual strong passwords/keys. Store all credentials in the MSP's password vault (e.g., IT Glue, Hudu, Bitwarden). The N8N_ENCRYPTION_KEY is critical—it encrypts stored credentials. If lost, all credential configurations must be re-entered. Back up this key separately. For production, place an Nginx reverse proxy or Cloudflare Tunnel in front of n8n to handle SSL termination. Do NOT expose port 5678 directly to the internet without HTTPS.

Step 5: Configure SSL and Reverse Proxy for n8n

Set up Nginx as a reverse proxy with Let's Encrypt SSL certificates to secure the n8n webhook endpoint. All webhook URLs must use HTTPS in production. Alternatively, use Cloudflare Tunnel for zero-trust access without opening inbound ports.

bash
# Install Nginx and Certbot
sudo apt install -y nginx certbot python3-certbot-nginx

# Create Nginx configuration
sudo cat > /etc/nginx/sites-available/n8n << 'EOF'
server {
    listen 80;
    server_name your-n8n-domain.com;

    location / {
        proxy_pass http://localhost:5678;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_cache_bypass $http_upgrade;
        chunked_transfer_encoding off;
        proxy_buffering off;
    }
}
EOF

# Enable the site
sudo ln -s /etc/nginx/sites-available/n8n /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

# Obtain SSL certificate
sudo certbot --nginx -d your-n8n-domain.com --non-interactive --agree-tos -m msp-alerts@yourdomain.com

# Verify auto-renewal
sudo certbot renew --dry-run
Note

Ensure DNS A record for your-n8n-domain.com points to the VM's public IP before running certbot. Let's Encrypt certificates auto-renew every 90 days via the certbot systemd timer. Alternative: Use Cloudflare Tunnel (cloudflared) to avoid opening any inbound ports—this is more secure but adds Cloudflare as a dependency. For Azure-hosted VMs, also configure the Network Security Group to allow inbound TCP 80 and 443 only.

Step 6: Build the Core Milestone-to-Invoice Automation Workflow in n8n

Create the primary n8n workflow that receives webhook events from the PSA, validates the event data against business rules, looks up client and milestone billing details, and creates a draft invoice in the accounting platform. This is the heart of the automation.

1
Access n8n web interface at https://your-n8n-domain.com
2
Login with the basic auth credentials configured in Step 4
3
Create a new workflow named: 'Milestone Invoice Trigger - [ClientName]'
4
The workflow JSON is provided in the custom_ai_components section below
5
Import it via: Workflow menu > Import from File > paste the JSON
Note

The workflow should always create invoices in DRAFT status initially, not as sent invoices. This provides a safety net—the finance team reviews drafts before sending. After 2–4 weeks of clean operation, the client may choose to auto-send invoices for standard milestones while keeping drafts for non-standard amounts. See the custom_ai_components section for the complete workflow specification.

Step 7: Configure PSA Webhook to n8n Connection

Register the n8n webhook URL in the PSA platform so that phase completion events are automatically sent to the automation engine. Test the webhook connection with a sample event to verify connectivity and payload format.

  • For Scoro: Go to Settings > Site Settings > Integrations > Webhooks
  • Add webhook — Name: n8n Milestone Invoice Trigger | URL: https://your-n8n-domain.com/webhook/scoro-phase-complete | Events: project.modified (or project_phase.status_changed if available) | Secret: <the-secret-from-step-2>
  • Save and click 'Test' to send a sample payload
  • For BigTime (polling-based): In n8n, the workflow uses a Schedule Trigger node instead of a Webhook node
  • Configure Schedule Trigger to poll the BigTime API every 10 minutes: Schedule Trigger > Rule: Every 10 minutes
  • HTTP Request node: GET https://iq.bigtime.net/BigtimeData/api/v2/project/{id}/phases with Headers: X-Auth-Token: <bigtime-api-key>, X-Auth-Realm: <bigtime-firm-id>
Note

After registering the webhook, change a test project's phase to 'Complete' in the PSA and verify that n8n receives the event. Check n8n's execution log to see the raw payload. Common issues: (1) Webhook URL typo—double-check the domain and path. (2) Firewall blocking—ensure the VM accepts inbound HTTPS. (3) Secret mismatch—verify the HMAC secret matches between PSA and n8n. For BigTime's polling approach, implement a state store (n8n's static data or a simple database table) to track which phase completions have already been processed, preventing duplicate invoices.

Step 8: Configure QuickBooks Online Integration in n8n

Set up the QuickBooks Online credential in n8n using OAuth 2.0, and configure the invoice creation node with the correct customer mapping, line items, and tax settings. Test with a sample invoice creation.

  • In n8n: Go to Credentials > Add Credential > QuickBooks
  • Enter: Client ID: <from Step 3>, Client Secret: <from Step 3>, Environment: Production
  • Click 'Connect' — this opens the Intuit OAuth consent screen
  • Authorize the connection for the client's QBO company
  • Test the connection: Create a simple test workflow with a Manual Trigger > QuickBooks node
  • Operation: Get All Customers — Execute and verify you see the client's customer list
  • For Xero: Go to Credentials > Add Credential > Xero
  • Enter Client ID and Client Secret from Step 3
  • Click 'Connect' and authorize
  • Select the correct Xero Organization if multiple exist
Note

The OAuth connection is company-specific. If the client has multiple QBO companies (e.g., separate entities), you need a separate credential per company. QuickBooks sandbox is available for testing—create a sandbox company at developer.intuit.com to test invoice creation without affecting production data. Always test in sandbox first before connecting to the live company.

Step 9: Build Client and Project Mapping Table

Create a mapping configuration that links PSA project phases to accounting platform customer IDs, invoice line items, amounts, and payment terms. This mapping table is the bridge between the two systems and encodes the client's billing rules.

Option A: n8n Static Data mapping (Function node) / Option B: Google Sheets config structure
javascript
# In n8n, create a static data configuration or use a Google Sheet / Airtable as a lightweight config database
# The mapping table should contain:

# Option A: Use n8n Static Data (simplest for <50 projects)
# In the workflow's Function node, define the mapping:

const milestoneConfig = {
  'project-template-web-design': {
    phases: {
      'Discovery': { pct: 25, qbo_item_id: '1', payment_terms: 'Net 30' },
      'Design': { pct: 25, qbo_item_id: '1', payment_terms: 'Net 30' },
      'Development': { pct: 35, qbo_item_id: '1', payment_terms: 'Net 30' },
      'Launch': { pct: 15, qbo_item_id: '1', payment_terms: 'Net 15' }
    }
  },
  'project-template-consulting': {
    phases: {
      'Assessment': { pct: 30, qbo_item_id: '2', payment_terms: 'Net 30' },
      'Strategy': { pct: 30, qbo_item_id: '2', payment_terms: 'Net 30' },
      'Implementation': { pct: 40, qbo_item_id: '2', payment_terms: 'Net 30' }
    }
  }
};

# Option B: Use Google Sheets as config (better for >50 projects, client-editable)
# Create a Google Sheet with columns:
# ProjectTemplate | PhaseName | BillingPct | QBO_ItemID | PaymentTerms | RequiresApproval
# Share with the n8n service account
Note

The mapping table is the most client-specific part of the implementation. Spend time with the finance team to get this right. Common issues: (1) Project contract amounts in the PSA not matching the mapping percentages—build a validation check. (2) Customer name mismatches between PSA and QBO—use customer IDs, not names, for matching. (3) Tax handling—some milestones may be taxable, others not. Include tax code in the mapping.

For clients requiring a human review before invoices are sent, add an approval workflow step. When a milestone is triggered, the system creates a draft invoice and sends a Slack or Teams notification (or email) to the designated approver. The approver clicks 'Approve' to finalize and send the invoice, or 'Reject' to flag for review.

  • In n8n, add a Slack or Microsoft Teams node after the invoice draft creation
  • Slack notification configuration — Channel: #billing-approvals
  • Message template fields: Project ({{$json.projectName}}), Client ({{$json.clientName}}), Phase Completed ({{$json.phaseName}}), Invoice Amount (${{$json.invoiceAmount}}), QBO Draft Invoice # ({{$json.qboInvoiceNumber}})
  • Include a review link: https://your-qbo-url.com/invoice/{{$json.qboInvoiceId}}
  • React with ✅ to approve or ❌ to flag for review
  • For email-based approval: Use n8n's Email (IMAP) node with a unique subject line, or use a simple webhook-based approve/reject URL pair
Note

The approval gate adds 1–2 business days to the invoicing cycle but provides critical safety, especially during the first 1–3 months of automation. For SOX-compliant clients, an approval gate is mandatory—the person completing the project phase cannot be the same person approving the invoice. For smaller firms, the approval step can be removed after the system proves reliable (typically after 20–30 successful automated invoices).

Step 11: Configure Monitoring, Logging, and Error Handling

Set up error notification workflows in n8n so the MSP is immediately alerted if any automation execution fails. Configure execution logging for audit trail purposes and set up health check monitoring on the n8n server.

1
In n8n, create a new workflow: 'Error Handler - Invoice Automation'
2
Add Error Trigger node (catches all workflow errors)
3
Add Slack/Email node to notify MSP — Channel: #msp-alerts, Message: '🚨 Invoice automation failed: {{$json.workflow.name}} - {{$json.execution.error.message}}'
4
Activate the error workflow
5
Set up server health monitoring — Option A: UptimeRobot (free tier), Monitor URL: https://your-n8n-domain.com/healthz, Check interval: 5 minutes, Alert via: Email + Slack
Option B: Simple cron-based health check script deployed on the VM
bash
cat > /opt/n8n/healthcheck.sh << 'EOF'
#!/bin/bash
HTTP_CODE=$(curl -s -o /dev/null -w '%{http_code}' https://your-n8n-domain.com/healthz)
if [ "$HTTP_CODE" != "200" ]; then
  curl -X POST -H 'Content-type: application/json' \
    --data '{"text":"🚨 n8n server is DOWN (HTTP $HTTP_CODE)"}' \
    https://hooks.slack.com/services/YOUR/SLACK/WEBHOOK
fi
EOF
chmod +x /opt/n8n/healthcheck.sh
(crontab -l 2>/dev/null; echo '*/5 * * * * /opt/n8n/healthcheck.sh') | crontab -
Note

n8n stores execution history by default for 336 hours (14 days). For audit compliance (especially ASC 606 and SOX), increase this by setting EXECUTIONS_DATA_MAX_AGE=8760 (1 year) in the Docker environment. Monitor disk usage—execution logs can grow quickly with high-volume workflows. Set up log rotation or export logs to an external system monthly. Always test the error notification by deliberately triggering a failure (e.g., temporarily break the QBO OAuth token).

Step 12: Data Migration — Import Active Projects and Outstanding Milestones

For clients with existing in-flight projects, import the current project data, phase statuses, and outstanding milestone billing amounts into the PSA platform. Configure the automation to recognize which milestones have already been billed versus those pending.

Export project data, import into PSA platform via CSV or BigTime API, and flag already-billed milestones
bash
# Export current project data from existing systems (spreadsheets, old PM tool, etc.)
# Create a CSV with columns:
# ProjectName, ClientName, QBO_CustomerID, ProjectTemplate, PhaseNumber, PhaseName, Status, MilestoneAmount, AlreadyBilled (Y/N)

# Import into PSA platform:
# Scoro: Settings > Data Import > Projects (CSV upload)
# BigTime: Use the BigTime API for bulk project creation:
# POST https://iq.bigtime.net/BigtimeData/api/v2/project
# Content-Type: application/json
# X-Auth-Token: <api-key>
# Body: { "ProjectNm": "...", "ClientId": "...", ... }

# Mark already-billed milestones to prevent double-invoicing:
# In the n8n mapping table or PSA custom fields, flag phases where AlreadyBilled = Y
# The automation workflow checks this flag before creating an invoice
Note

This is the highest-risk step for errors. Double-invoicing a client is extremely damaging to the relationship. Approach: (1) Import all projects with the automation INACTIVE. (2) Have the finance team review and verify all project statuses and billing flags. (3) Only activate the automation after written sign-off from the client's finance lead. For the first 2 weeks after go-live, run the automation in parallel with the manual process as a safety check.

Step 13: User Training and Go-Live

Conduct training sessions for three user groups: (1) Project Managers — how phase completion triggers work and what they need to do differently, (2) Finance Team — how to review and approve draft invoices, handle exceptions, and reconcile, (3) Operations Lead — how to manage the mapping table, add new project templates, and troubleshoot. Then activate the automation in production.

1
Session 1 (30 min): Project Managers — Demo: completing a phase in the PSA and seeing the draft invoice appear in QBO. Key rule: only mark phase 'Complete' when deliverables are truly done. Exception handling: what to do for partial completions, scope changes.
2
Session 2 (45 min): Finance Team — Demo: reviewing draft invoices in QBO, approving/sending. Reconciliation: how automated invoices appear in the invoice register. Error handling: what to do if an invoice has wrong amount, wrong client, etc. ASC 606 considerations: billing vs. revenue recognition timing.
3
Session 3 (45 min): Operations Lead + MSP Technician — Demo: n8n workflow overview, how to read execution logs. How to add a new project template to the mapping table. How to pause automation (e.g., for system maintenance). Escalation process: when to call the MSP.
Note

Record all training sessions (Teams/Zoom recording) and provide as reference material. Create a one-page quick reference card for project managers: 'How Milestone Billing Automation Works — What You Need to Know.' The parallel run period (2 weeks) is critical. During this time, every automated invoice should be cross-checked against what would have been created manually. Any discrepancies must be resolved before ending the parallel run.

Custom AI Components

Milestone Phase Completion Webhook Handler

Type: workflow n8n workflow node that receives incoming webhook events from the PSA platform when a project phase status changes. It validates the webhook signature, extracts the relevant data (project ID, phase name, new status, client ID), and passes it to the next processing stage. Handles both real-time webhooks (Scoro) and polling-based detection (BigTime).

Implementation

For Scoro (Real-Time Webhook)

  • Node Type: Webhook
  • HTTP Method: POST
  • Path: /scoro-phase-complete
  • Authentication: Header Auth
  • Header Name: X-Webhook-Secret
  • Header Value: {{$credentials.scoroWebhookSecret}}
  • Response Mode: Last Node
  • Response Code: 200
Output Fields Mapping — Code Node after Webhook (Scoro)
javascript
const payload = $input.first().json;

// Validate the event type
if (!payload || !payload.data || !payload.data.project_id) {
  throw new Error('Invalid webhook payload: missing project_id');
}

// Extract relevant fields from Scoro webhook
const output = {
  source: 'scoro',
  eventType: payload.event || 'project.modified',
  projectId: payload.data.project_id,
  projectName: payload.data.project_name || '',
  phaseName: payload.data.phase_name || payload.data.activity_name || '',
  phaseStatus: payload.data.phase_status || payload.data.status || '',
  clientId: payload.data.company_id || '',
  clientName: payload.data.company_name || '',
  modifiedBy: payload.data.modified_by || '',
  modifiedAt: payload.data.modified_date || new Date().toISOString(),
  rawPayload: JSON.stringify(payload)
};

// Only proceed if the status indicates completion
const completionStatuses = ['completed', 'complete', 'done', 'finished', 'closed'];
if (!completionStatuses.includes(output.phaseStatus.toLowerCase())) {
  // Not a completion event — stop processing
  return [];
}

return [output];

For BigTime (Polling-Based)

  • Node Type: Schedule Trigger
  • Rule: Every 10 Minutes
HTTP Request Node — BigTime phase polling
http
GET https://iq.bigtime.net/BigtimeData/api/v2/project/phases?UpdatedAfter={{$now.minus(15, 'minutes').toISO()}}
X-Auth-Token: {{$credentials.bigtimeApiKey}}
X-Auth-Realm: {{$credentials.bigtimeFirmId}}
Content-Type: application/json
Code Node — Deduplication logic for BigTime polling
javascript
const items = $input.all();
const processedKey = 'processedPhaseCompletions';
const staticData = $getWorkflowStaticData('global');

if (!staticData[processedKey]) {
  staticData[processedKey] = {};
}

const newCompletions = [];

for (const item of items) {
  const phase = item.json;
  const uniqueKey = `${phase.ProjectId}-${phase.PhaseId}-${phase.Status}`;
  
  if (phase.Status === 'Completed' && !staticData[processedKey][uniqueKey]) {
    staticData[processedKey][uniqueKey] = new Date().toISOString();
    newCompletions.push({
      source: 'bigtime',
      eventType: 'phase.completed',
      projectId: phase.ProjectId,
      projectName: phase.ProjectNm,
      phaseName: phase.PhaseNm,
      phaseStatus: 'completed',
      clientId: phase.ClientId,
      clientName: phase.ClientNm,
      modifiedAt: phase.UpdatedDt,
      rawPayload: JSON.stringify(phase)
    });
  }
}

// Prune entries older than 30 days to prevent memory bloat
const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString();
for (const [key, timestamp] of Object.entries(staticData[processedKey])) {
  if (timestamp < thirtyDaysAgo) {
    delete staticData[processedKey][key];
  }
}

return newCompletions;

Billing Rules Engine

Type: workflow n8n Code node that evaluates the completed milestone against the client's billing configuration to determine: (1) whether an invoice should be created, (2) the invoice amount, (3) the line item details, (4) payment terms, and (5) whether approval is required. This is the deterministic 'brain' of the automation that encodes all business rules.

Implementation:

n8n Billing Rules Engine — Code Node
javascript
// Billing Rules Engine
// Receives: phase completion event from webhook handler
// Outputs: invoice specification or skip signal

const event = $input.first().json;

// ===== CONFIGURATION: BILLING RULES =====
// In production, load this from Google Sheets, Airtable, or a database
// For initial deployment, define inline and update as needed

const billingConfig = {
  projectTemplates: {
    'web-design': {
      defaultPaymentTerms: 'Net 30',
      requiresApproval: false,
      phases: {
        'Discovery & Research': {
          billingPercentage: 25,
          qboItemId: '1',  // Service item ID in QuickBooks
          qboItemName: 'Web Design Services',
          description: 'Discovery & Research phase - Web Design project',
          taxable: true,
          requiresApproval: false
        },
        'Design & Wireframes': {
          billingPercentage: 25,
          qboItemId: '1',
          qboItemName: 'Web Design Services',
          description: 'Design & Wireframes phase - Web Design project',
          taxable: true,
          requiresApproval: false
        },
        'Development & Build': {
          billingPercentage: 35,
          qboItemId: '1',
          qboItemName: 'Web Design Services',
          description: 'Development & Build phase - Web Design project',
          taxable: true,
          requiresApproval: true  // Largest milestone, require review
        },
        'Launch & Handoff': {
          billingPercentage: 15,
          qboItemId: '1',
          qboItemName: 'Web Design Services',
          description: 'Launch & Handoff phase - Web Design project',
          taxable: true,
          requiresApproval: false
        }
      }
    },
    'consulting-engagement': {
      defaultPaymentTerms: 'Net 30',
      requiresApproval: true,
      phases: {
        'Assessment': {
          billingPercentage: 30,
          qboItemId: '2',
          qboItemName: 'Consulting Services',
          description: 'Assessment phase - Consulting engagement',
          taxable: false,
          requiresApproval: true
        },
        'Strategy Development': {
          billingPercentage: 30,
          qboItemId: '2',
          qboItemName: 'Consulting Services',
          description: 'Strategy Development phase - Consulting engagement',
          taxable: false,
          requiresApproval: true
        },
        'Implementation Support': {
          billingPercentage: 40,
          qboItemId: '2',
          qboItemName: 'Consulting Services',
          description: 'Implementation Support phase - Consulting engagement',
          taxable: false,
          requiresApproval: true
        }
      }
    }
  },
  
  // Client-to-QBO customer mapping
  // Key: PSA client ID, Value: QBO customer ID
  clientMapping: {
    'psa-client-001': { qboCustomerId: '123', qboCustomerName: 'Acme Corp' },
    'psa-client-002': { qboCustomerId: '456', qboCustomerName: 'Globex Inc' },
    // Add more client mappings as needed
  },
  
  // Project-to-template mapping (links specific projects to templates)
  // Key: PSA project ID, Value: { templateKey, contractAmount }
  projectMapping: {
    'proj-001': { templateKey: 'web-design', contractAmount: 20000 },
    'proj-002': { templateKey: 'consulting-engagement', contractAmount: 50000 },
    // Add more project mappings as needed
  }
};

// ===== RULES ENGINE =====

// 1. Look up the project configuration
const projectConfig = billingConfig.projectMapping[event.projectId];
if (!projectConfig) {
  return [{
    json: {
      action: 'SKIP',
      reason: `Project ${event.projectId} (${event.projectName}) not found in billing configuration. May be a non-billable project or needs to be added to the mapping.`,
      event: event,
      severity: 'WARNING'
    }
  }];
}

// 2. Look up the template and phase
const template = billingConfig.projectTemplates[projectConfig.templateKey];
if (!template) {
  return [{
    json: {
      action: 'ERROR',
      reason: `Template '${projectConfig.templateKey}' not found in billing configuration.`,
      event: event,
      severity: 'ERROR'
    }
  }];
}

const phaseConfig = template.phases[event.phaseName];
if (!phaseConfig) {
  return [{
    json: {
      action: 'SKIP',
      reason: `Phase '${event.phaseName}' not found in template '${projectConfig.templateKey}'. May be a non-billable phase.`,
      event: event,
      severity: 'INFO'
    }
  }];
}

// 3. Look up the client in accounting
const clientConfig = billingConfig.clientMapping[event.clientId];
if (!clientConfig) {
  return [{
    json: {
      action: 'ERROR',
      reason: `Client ${event.clientId} (${event.clientName}) not found in QBO customer mapping. Must be added before invoicing.`,
      event: event,
      severity: 'ERROR'
    }
  }];
}

// 4. Calculate invoice amount
const invoiceAmount = (projectConfig.contractAmount * phaseConfig.billingPercentage) / 100;

// 5. Determine if approval is needed
const requiresApproval = template.requiresApproval || phaseConfig.requiresApproval;

// 6. Build the invoice specification
const invoiceSpec = {
  action: 'CREATE_INVOICE',
  requiresApproval: requiresApproval,
  invoice: {
    customerId: clientConfig.qboCustomerId,
    customerName: clientConfig.qboCustomerName,
    invoiceDate: new Date().toISOString().split('T')[0],
    dueDate: calculateDueDate(template.defaultPaymentTerms),
    paymentTerms: template.defaultPaymentTerms,
    lineItems: [
      {
        itemId: phaseConfig.qboItemId,
        itemName: phaseConfig.qboItemName,
        description: `${phaseConfig.description}\nProject: ${event.projectName}\nPhase completed: ${event.modifiedAt}`,
        quantity: 1,
        unitPrice: invoiceAmount,
        taxable: phaseConfig.taxable
      }
    ],
    memo: `Auto-generated from milestone completion. Project: ${event.projectName}, Phase: ${event.phaseName}, Completed by: ${event.modifiedBy}`,
    customFields: {
      projectId: event.projectId,
      projectName: event.projectName,
      phaseName: event.phaseName,
      automationId: `auto-${Date.now()}`
    }
  },
  auditTrail: {
    triggeredAt: new Date().toISOString(),
    triggeredBy: 'milestone-automation',
    sourceEvent: event,
    billingRule: {
      template: projectConfig.templateKey,
      phase: event.phaseName,
      percentage: phaseConfig.billingPercentage,
      contractAmount: projectConfig.contractAmount,
      calculatedAmount: invoiceAmount
    }
  }
};

return [{ json: invoiceSpec }];

// ===== HELPER FUNCTIONS =====

function calculateDueDate(terms) {
  const today = new Date();
  const daysMap = {
    'Net 15': 15,
    'Net 30': 30,
    'Net 45': 45,
    'Net 60': 60,
    'Due on Receipt': 0
  };
  const days = daysMap[terms] || 30;
  today.setDate(today.getDate() + days);
  return today.toISOString().split('T')[0];
}

Integration Notes

  • After this Code node, add an IF node that routes based on the action field: CREATE_INVOICE → proceed to invoice creation; SKIP → log and end (optionally notify via Slack for WARNINGs); ERROR → trigger error notification to MSP and client ops lead
  • The requiresApproval field routes to either the direct invoice creation path or the approval gate path
  • The auditTrail object should be stored (e.g., in a Google Sheet or database) for compliance reporting

QuickBooks Invoice Creator

Type: integration n8n workflow segment that takes the invoice specification from the Billing Rules Engine and creates a draft invoice in QuickBooks Online via the QBO API. Handles line item creation, customer association, payment terms, and memo fields. Returns the created invoice ID and number for downstream notification.

HTTP Request Node: Create QBO Invoice

  • Method: POST
  • URL: https://quickbooks.api.intuit.com/v3/company/{{$credentials.qboCompanyId}}/invoice
  • Authentication: OAuth 2.0 (QBO credential configured in Step 8)
  • Headers: Content-Type: application/json
  • Headers: Accept: application/json
Body (JSON Expression)
json
# HTTP Request Node POST to QBO Invoice API

{
  "Line": [
    {
      "Amount": {{$json.invoice.lineItems[0].unitPrice}},
      "DetailType": "SalesItemLineDetail",
      "SalesItemLineDetail": {
        "ItemRef": {
          "value": "{{$json.invoice.lineItems[0].itemId}}"
        },
        "UnitPrice": {{$json.invoice.lineItems[0].unitPrice}},
        "Qty": 1,
        "TaxCodeRef": {
          "value": "{{$json.invoice.lineItems[0].taxable ? 'TAX' : 'NON'}}"
        }
      },
      "Description": "{{$json.invoice.lineItems[0].description}}"
    }
  ],
  "CustomerRef": {
    "value": "{{$json.invoice.customerId}}"
  },
  "TxnDate": "{{$json.invoice.invoiceDate}}",
  "DueDate": "{{$json.invoice.dueDate}}",
  "PrivateNote": "{{$json.invoice.memo}}",
  "CustomerMemo": {
    "value": "Thank you for your business. This invoice was generated upon completion of project milestone: {{$json.invoice.customFields.phaseName}}."
  },
  "SalesTermRef": {
    "value": "{{$json.invoice.paymentTerms === 'Net 30' ? '3' : $json.invoice.paymentTerms === 'Net 15' ? '2' : $json.invoice.paymentTerms === 'Net 60' ? '5' : '3'}}"
  },
  "EmailStatus": "NotSet"
}
Note

EmailStatus: NotSet creates the invoice as a DRAFT — it will NOT be emailed to the client automatically. To auto-send, change to EmailStatus: NeedToSend and add BillEmail object.

Warning

The SalesTermRef.value IDs (2, 3, 5) are QBO default term IDs — verify these match the client's QBO instance by querying GET /v3/company/{id}/query?query=SELECT * FROM Term. Tax code references ('TAX', 'NON') are QBO defaults; verify or replace with the client's actual tax code IDs.

Response Processing (Code Node after HTTP Request)

n8n Code Node
javascript
// parse QBO API response and build result object

const response = $input.first().json;
const invoiceSpec = $('Billing Rules Engine').first().json;

const result = {
  success: true,
  qboInvoiceId: response.Invoice.Id,
  qboInvoiceNumber: response.Invoice.DocNumber,
  invoiceAmount: response.Invoice.TotalAmt,
  customerName: response.Invoice.CustomerRef.name,
  dueDate: response.Invoice.DueDate,
  qboInvoiceUrl: `https://app.qbo.intuit.com/app/invoice?txnId=${response.Invoice.Id}`,
  requiresApproval: invoiceSpec.requiresApproval,
  auditTrail: invoiceSpec.auditTrail,
  createdAt: new Date().toISOString()
};

return [{ json: result }];

For Xero (Alternative)

  • Method: POST
  • URL: https://api.xero.com/api.xro/2.0/Invoices
  • Headers: Xero-Tenant-Id: {{$credentials.xeroTenantId}}
Body (JSON) — HTTP Request Node POST to Xero Invoices API
json
{
  "Invoices": [{
    "Type": "ACCREC",
    "Contact": { "ContactID": "{{$json.invoice.customerId}}" },
    "Date": "{{$json.invoice.invoiceDate}}",
    "DueDate": "{{$json.invoice.dueDate}}",
    "Status": "DRAFT",
    "LineItems": [{
      "Description": "{{$json.invoice.lineItems[0].description}}",
      "Quantity": 1,
      "UnitAmount": {{$json.invoice.lineItems[0].unitPrice}},
      "AccountCode": "200",
      "TaxType": "{{$json.invoice.lineItems[0].taxable ? 'OUTPUT' : 'NONE'}}"
    }],
    "Reference": "{{$json.invoice.customFields.automationId}}"
  }]
}

Notification and Audit Logger

Type: workflow n8n workflow segment that sends notifications to relevant stakeholders after an invoice is created (or after an error occurs) and logs all automation events to a persistent audit trail for compliance purposes (ASC 606, SOX). Supports Slack, Microsoft Teams, and email notifications.

Part 1: Stakeholder Notification

Slack Notification Node Configuration

  • Node Type: Slack
  • Resource: Message
  • Operation: Send
  • Channel: #billing-notifications (for successful invoices) OR #billing-approvals (if approval required)

Slack Message Text (Expression)

{{$json.requiresApproval ? '🔔 *Invoice Requires Approval*' : '✅ *Milestone Invoice Created*'}} *Project:* {{$json.auditTrail.sourceEvent.projectName}} *Client:* {{$json.customerName}} *Phase Completed:* {{$json.auditTrail.sourceEvent.phaseName}} *Invoice Amount:* ${{$json.invoiceAmount.toLocaleString('en-US', {minimumFractionDigits: 2})}} *Invoice #:* {{$json.qboInvoiceNumber}} *Due Date:* {{$json.dueDate}} *Status:* Draft — {{$json.requiresApproval ? 'Awaiting approval before sending to client' : 'Ready for review and sending'}} <{{$json.qboInvoiceUrl}}|📄 Open Invoice in QuickBooks>
Sonnet 4.6

Microsoft Teams Notification (Alternative)

  • Node Type: Microsoft Teams
  • Resource: Chat Message or Channel Message
  • Channel: Billing Automation
  • Message: (same content as Slack, formatted with Teams markdown)

Email Notification (Fallback)

  • Node Type: Send Email (SMTP)
  • To: finance-team@clientdomain.com
  • Subject: Milestone Invoice #{{$json.qboInvoiceNumber}} Created - {{$json.customerName}} - ${{$json.invoiceAmount}}
  • Body: HTML version of the notification content above

Part 2: Audit Trail Logger

Log every automation event to a Google Sheet (simple) or PostgreSQL table (robust) for compliance.

Option A: Google Sheets Audit Log

  • Node Type: Google Sheets
  • Operation: Append Row
  • Sheet: 'Invoice Automation Audit Log'
n8n Code Node — Format audit row for Google Sheets append
javascript
// Code node to format audit row
const event = $input.first().json;

return [{
  json: {
    Timestamp: new Date().toISOString(),
    EventType: 'INVOICE_CREATED',
    ProjectId: event.auditTrail.sourceEvent.projectId,
    ProjectName: event.auditTrail.sourceEvent.projectName,
    ClientName: event.customerName,
    PhaseName: event.auditTrail.sourceEvent.phaseName,
    BillingTemplate: event.auditTrail.billingRule.template,
    BillingPercentage: event.auditTrail.billingRule.percentage,
    ContractAmount: event.auditTrail.billingRule.contractAmount,
    InvoiceAmount: event.invoiceAmount,
    QBOInvoiceId: event.qboInvoiceId,
    QBOInvoiceNumber: event.qboInvoiceNumber,
    DueDate: event.dueDate,
    RequiredApproval: event.requiresApproval,
    TriggeredBy: event.auditTrail.triggeredBy,
    CompletedBy: event.auditTrail.sourceEvent.modifiedBy,
    AutomationId: event.auditTrail.billingRule ? 'auto-' + event.qboInvoiceId : '',
    Status: 'DRAFT_CREATED'
  }
}];

Option B: PostgreSQL Audit Log (for higher volume / SOX compliance)

PostgreSQL
sql
-- Create audit table and indexes (run once during setup)

-- Create audit table (run once during setup)
CREATE TABLE IF NOT EXISTS invoice_automation_audit (
  id SERIAL PRIMARY KEY,
  timestamp TIMESTAMPTZ DEFAULT NOW(),
  event_type VARCHAR(50) NOT NULL,
  project_id VARCHAR(100),
  project_name VARCHAR(255),
  client_name VARCHAR(255),
  phase_name VARCHAR(255),
  billing_template VARCHAR(100),
  billing_percentage DECIMAL(5,2),
  contract_amount DECIMAL(12,2),
  invoice_amount DECIMAL(12,2),
  qbo_invoice_id VARCHAR(50),
  qbo_invoice_number VARCHAR(50),
  due_date DATE,
  requires_approval BOOLEAN,
  triggered_by VARCHAR(100),
  completed_by VARCHAR(255),
  status VARCHAR(50),
  raw_event JSONB
);

CREATE INDEX idx_audit_project ON invoice_automation_audit(project_id);
CREATE INDEX idx_audit_timestamp ON invoice_automation_audit(timestamp);
CREATE INDEX idx_audit_client ON invoice_automation_audit(client_name);
  • Node Type: Postgres
  • Operation: Insert
  • Table: invoice_automation_audit
  • Map all fields from the Code node output above

Part 3: Error Notification (for failed executions)

  • Node Type: Slack (connected to Error Trigger workflow)

Slack Error Notification Message

🚨 *Invoice Automation Error* *Workflow:* {{$json.workflow.name}} *Error:* {{$json.execution.error.message}} *Time:* {{$json.execution.startedAt}} *Execution ID:* {{$json.execution.id}} <https://your-n8n-domain.com/workflow/{{$json.workflow.id}}/executions/{{$json.execution.id}}|🔍 View Failed Execution in n8n> cc @msp-oncall
Sonnet 4.6

Duplicate Invoice Prevention Guard

Type: workflow A critical safety component that prevents the same milestone from generating multiple invoices. This can happen due to webhook retries, PSA status toggling (complete → in progress → complete), or system errors. The guard checks for existing invoices before creating a new one. Implementation:

Duplicate Invoice Prevention — n8n Code Node

  • Place this node AFTER the Billing Rules Engine and BEFORE the QuickBooks Invoice Creator.
Duplicate Invoice Prevention Guard — n8n Code Node
javascript
// Duplicate Invoice Prevention Guard
// Checks multiple sources to ensure this milestone hasn't already been invoiced

const invoiceSpec = $input.first().json;

if (invoiceSpec.action !== 'CREATE_INVOICE') {
  // Not an invoice creation — pass through
  return [$input.first()];
}

const projectId = invoiceSpec.auditTrail.sourceEvent.projectId;
const phaseName = invoiceSpec.auditTrail.sourceEvent.phaseName;
const uniqueKey = `${projectId}::${phaseName}`;

// Check 1: n8n Static Data (in-memory dedup)
const staticData = $getWorkflowStaticData('global');
if (!staticData.invoicedMilestones) {
  staticData.invoicedMilestones = {};
}

if (staticData.invoicedMilestones[uniqueKey]) {
  const previousInvoice = staticData.invoicedMilestones[uniqueKey];
  return [{
    json: {
      action: 'SKIP',
      reason: `DUPLICATE PREVENTED: Milestone '${phaseName}' for project '${projectId}' was already invoiced on ${previousInvoice.date}. QBO Invoice #: ${previousInvoice.invoiceNumber}`,
      severity: 'WARNING',
      previousInvoice: previousInvoice
    }
  }];
}

// Check 2: Query QuickBooks for existing invoice with this project+phase reference
// This is a secondary check in case static data was lost (server restart, etc.)
// We use the PrivateNote field which contains our automation memo
// This check is performed by a subsequent HTTP Request node — flag it here

const output = {
  ...invoiceSpec,
  _dedup: {
    uniqueKey: uniqueKey,
    searchQuery: `SELECT * FROM Invoice WHERE PrivateNote LIKE '%${projectId}%' AND PrivateNote LIKE '%${phaseName}%'`,
    passed_memory_check: true
  }
};

return [{ json: output }];

Secondary QBO Query Check (HTTP Request Node after Code Node)

  • Method: GET
  • URL: https://quickbooks.api.intuit.com/v3/company/{{$credentials.qboCompanyId}}/query?query=SELECT * FROM Invoice WHERE PrivateNote LIKE '%{{$json.auditTrail.sourceEvent.projectId}}%' AND PrivateNote LIKE '%{{encodeURIComponent($json.auditTrail.sourceEvent.phaseName)}}%'
  • Authentication: OAuth 2.0 (QBO credential)

Post-QBO-Check Code Node

Post-QBO-Check Code Node
javascript
const qboResponse = $input.first().json;
const invoiceSpec = $('Duplicate Guard Code').first().json;

const existingInvoices = qboResponse?.QueryResponse?.Invoice || [];

if (existingInvoices.length > 0) {
  // Invoice already exists in QBO — duplicate!
  const existing = existingInvoices[0];
  return [{
    json: {
      action: 'SKIP',
      reason: `DUPLICATE PREVENTED (QBO check): Invoice #${existing.DocNumber} already exists for this milestone. Amount: $${existing.TotalAmt}. Created: ${existing.MetaData.CreateTime}`,
      severity: 'WARNING',
      existingInvoice: {
        id: existing.Id,
        number: existing.DocNumber,
        amount: existing.TotalAmt
      }
    }
  }];
}

// No duplicate found — proceed with invoice creation
// Also store in static data for future in-memory checks
const staticData = $getWorkflowStaticData('global');
if (!staticData.invoicedMilestones) {
  staticData.invoicedMilestones = {};
}
// Note: We'll update this AFTER successful creation in the post-creation node

return [{ json: invoiceSpec }];

Post-Creation: Update Dedup Store

  • After the QBO Invoice Creator succeeds, add this Code Node:
Post-Creation: Update Dedup Store
javascript
const result = $input.first().json;
const invoiceSpec = $('Billing Rules Engine').first().json;

const staticData = $getWorkflowStaticData('global');
if (!staticData.invoicedMilestones) {
  staticData.invoicedMilestones = {};
}

const uniqueKey = `${invoiceSpec.auditTrail.sourceEvent.projectId}::${invoiceSpec.auditTrail.sourceEvent.phaseName}`;
staticData.invoicedMilestones[uniqueKey] = {
  invoiceId: result.qboInvoiceId,
  invoiceNumber: result.qboInvoiceNumber,
  amount: result.invoiceAmount,
  date: new Date().toISOString()
};

return [$input.first()];

Testing & Validation

  • TEST 1 — Webhook Connectivity: In the PSA platform (Scoro/BigTime), change a test project's phase from 'In Progress' to 'Complete'. Verify within 60 seconds (Scoro webhook) or 15 minutes (BigTime polling) that the n8n execution log shows a successful trigger with the correct project ID, phase name, and client ID in the payload.
  • TEST 2 — Billing Rules Engine Accuracy: Create 5 test scenarios covering different project templates and phases. For each, manually calculate the expected invoice amount (contract amount × billing percentage). Run each through the automation and verify the Billing Rules Engine output matches the manual calculation exactly (to the penny).
  • TEST 3 — QuickBooks Invoice Creation: Trigger a milestone completion for a mapped test project. Verify that a DRAFT invoice appears in QuickBooks Online with: (a) correct customer name, (b) correct line item description, (c) correct amount, (d) correct due date based on payment terms, (e) correct memo containing project and phase details. Cross-check all fields against the billing rules configuration.
  • TEST 4 — Duplicate Prevention: Trigger the same milestone completion event twice (e.g., toggle a phase status back and forth, or resend the webhook manually). Verify that only ONE invoice is created. Check the n8n execution log to confirm the second attempt was caught by the Duplicate Invoice Prevention Guard with a clear 'DUPLICATE PREVENTED' message.
  • TEST 5 — Unknown Project/Phase Handling: Trigger a phase completion for a project that is NOT in the billing configuration mapping table. Verify that no invoice is created, the workflow logs a SKIP event with an appropriate warning message, and a notification is sent to the MSP/ops lead alerting them to add the mapping.
  • TEST 6 — Error Notification: Temporarily invalidate the QuickBooks OAuth token (e.g., revoke access in the QBO developer portal). Trigger a milestone completion. Verify that: (a) the workflow fails gracefully without crashing n8n, (b) the error trigger workflow fires, (c) a Slack/email notification is received by the MSP within 5 minutes with the error details and execution link.
  • TEST 7 — Approval Gate: For a milestone configured with requiresApproval=true, trigger a phase completion. Verify that: (a) a draft invoice is created in QBO, (b) a notification is sent to the #billing-approvals channel with a link to the invoice, (c) the invoice remains in Draft status until manually approved and sent by the finance team.
  • TEST 8 — Audit Trail Completeness: After running tests 1–7, check the audit log (Google Sheet or PostgreSQL table). Verify that every automation event is logged with all required fields: timestamp, project ID, client name, phase name, invoice amount, QBO invoice number, triggered-by, and status. Verify that SKIP and ERROR events are also logged, not just successful invoice creations.
  • TEST 9 — End-to-End Full Cycle: Execute a complete project lifecycle in the PSA: create a project using a standard template, complete Phase 1 → verify invoice 1 is created, complete Phase 2 → verify invoice 2 is created, complete Phase 3 → verify invoice 3 is created. Confirm total invoiced amount equals the contract amount. Confirm all three invoices appear in QBO and the audit log.
  • TEST 10 — Performance and Reliability: If the client has >20 active projects, simulate 5 near-simultaneous phase completions (within a 1-minute window). Verify that all 5 invoices are created correctly without duplicates, errors, or missed triggers. Check the n8n server CPU and memory utilization during the burst to confirm it stays within acceptable limits (CPU <80%, RAM <90%).
  • TEST 11 — Parallel Run Validation: During the 2-week parallel run period, compare every automated invoice against the manually-created invoice for the same milestone. Document any discrepancies in amount, customer mapping, payment terms, line item descriptions, or tax codes. Zero discrepancies over 10+ invoices required before ending the parallel run.

Client Handoff

Client Handoff Checklist

Training Sessions Delivered (with recordings provided):

1
Project Manager Training (30 min): How to properly mark phases as complete, what triggers an invoice, how to handle partial completions and scope changes, what NOT to do (e.g., don't toggle phase status for testing).
2
Finance Team Training (45 min): How to review draft invoices in QuickBooks, how to approve and send, how to handle invoice corrections or credits, how the audit trail works for ASC 606 compliance, monthly reconciliation process.
3
Operations Lead Training (45 min): How to add new project templates and client mappings to the billing configuration, how to read n8n execution logs, how to pause/resume the automation, when and how to escalate to the MSP.

Documentation Delivered:

  • System Architecture Diagram: Visual showing PSA → n8n → QuickBooks flow with all connection points
  • Billing Rules Reference Document: Complete table of all project templates, phases, billing percentages, payment terms, and approval requirements
  • Client-to-QBO Customer Mapping Table: Current mapping of all PSA clients to QBO customer IDs
  • Runbook: Adding a New Project Template: Step-by-step guide for the operations lead to add new billing rules
  • Runbook: Adding a New Client Mapping: Step-by-step guide for mapping a new client
  • Troubleshooting Guide: Common issues (webhook failures, QBO token expiry, duplicate detection, missing mappings) with resolution steps
  • Escalation Contact Sheet: MSP support email, phone, SLA response times, and emergency procedures

Success Criteria Reviewed with Client:

Credentials and Access Handed Over:

  • n8n admin URL and credentials (stored in client's password manager)
  • QBO API app credentials (stored in MSP's vault, accessible to client upon request)
  • Webhook secret keys (stored in MSP's vault)
  • All credentials also documented in MSP's IT documentation platform (IT Glue/Hudu)

Sign-Off:

  • Client operations lead signs off on successful parallel run completion
  • Client finance lead signs off on invoice accuracy and compliance requirements met
  • MSP transitions from project mode to managed service mode with agreed SLA

Maintenance

Ongoing Maintenance Responsibilities

Weekly (MSP Automated):

  • n8n Health Check: Automated monitoring via UptimeRobot or equivalent confirms n8n server is responding (HTTPS 200 on /healthz endpoint). Alert threshold: 2 consecutive failures (10 minutes of downtime). Automated restart via Docker Compose watchdog if container crashes.
  • Execution Log Review: Automated script checks for failed executions in the past 7 days. Any failures trigger a Slack alert to the MSP team for investigation.

Monthly (MSP Manual — 1–2 hours):

  • Execution Log Audit: Review all n8n executions from the past month. Verify zero unresolved errors. Check for any SKIP events that indicate missing project/client mappings that should be added.
  • OAuth Token Health: Verify QuickBooks Online and any other OAuth connections are still active. QBO refresh tokens expire after 100 days of inactivity—if the automation has been running regularly, this is not an issue, but verify.
  • Disk Space Check: Verify the VM has >20% free disk space. Execution logs and PostgreSQL data can grow. Prune old execution data if needed: n8n Settings > Executions > adjust retention period.
  • Billing Reconciliation Support: Provide the client's finance team with the monthly audit log export for reconciliation against QBO records and revenue recognition schedules.

Server Patching: Apply OS security updates to the n8n VM. Update n8n Docker image to the latest stable version (test in staging first if available).

Update n8n Docker image to the latest stable version
bash
cd /opt/n8n && docker compose pull && docker compose up -d

Quarterly (MSP + Client — 2–3 hours):

  • Quarterly Business Review (QBR): Review automation performance metrics: number of invoices auto-generated, error rate, average time from phase completion to invoice creation, any client feedback. Identify optimization opportunities (e.g., new project templates to add, approval workflow adjustments, new integrations).
  • Billing Rules Update: Review and update the billing configuration with the client's operations lead. Add any new project templates, update client mappings, adjust billing percentages as needed.
  • Security Review: Rotate API keys and webhook secrets. Review n8n user access. Verify SSL certificate auto-renewal is working.
  • Compliance Check: For ASC 606 clients, review the audit trail with the client's accountant/CFO to ensure billing events are being properly documented for revenue recognition purposes.

Annual:

  • Platform Version Upgrade: Major version upgrades for n8n (test thoroughly before applying). Review PSA and QBO API changelog for breaking changes.
  • Disaster Recovery Test: Restore n8n from backup to a test environment and verify all workflows execute correctly. Verify that the PostgreSQL backup contains complete audit trail data.
  • Contract and SLA Review: Review the managed service agreement, adjust pricing if scope has changed, and renew.

SLA Considerations:

  • Severity 1 (Automation completely down — no invoices being generated): 4-hour response, 8-hour resolution. MSP restarts n8n, checks webhook connectivity, verifies QBO OAuth.
  • Severity 2 (Partial failure — some invoices failing, others succeeding): 8-hour response, 24-hour resolution. MSP investigates specific failure pattern (usually a mapping issue or API rate limit).
  • Severity 3 (Cosmetic/non-urgent — invoice has wrong description, notification not firing): Next business day response, 3 business day resolution.

Backup Strategy:

  • n8n workflow exports: Automated weekly export of all workflows as JSON to the MSP's backup storage (S3/Azure Blob/Google Cloud Storage).
  • PostgreSQL database: Daily automated backup via pg_dump, retained for 90 days.
  • Configuration files (docker-compose.yml, nginx config): Stored in MSP's Git repository.
  • Encryption key and credentials: Stored in MSP's password vault with break-glass access procedures.

Escalation Path:

1
Client operations lead attempts resolution using the Troubleshooting Guide
2
Client contacts MSP helpdesk (email/phone/ticket)
3
MSP L1 technician checks n8n execution logs and server health
4
MSP L2 engineer investigates API/integration issues
5
MSP escalates to n8n community forums or vendor support if platform bug suspected
6
For QBO API issues, MSP contacts Intuit Developer Support

Alternatives

Native PSA Milestone Billing (No Middleware)

Zapier Team Plan (Fully Managed SaaS Middleware)

Replace self-hosted n8n with Zapier's Team plan as the automation middleware. Zapier offers a fully managed, no-code platform with 7,000+ app connectors, built-in error handling, and a user-friendly interface. The MSP builds and manages Zaps (automation workflows) that connect the PSA to QuickBooks/Xero.

Microsoft Power Automate (Microsoft Ecosystem)

For clients already invested in Microsoft 365 and potentially Dynamics 365, use Power Automate Premium as the automation middleware. Power Automate integrates natively with SharePoint, Teams, Dynamics 365 Project Operations, and can connect to QuickBooks/Xero via premium connectors.

Dynamics 365 Project Operations (Full Enterprise Stack)

Deploy Microsoft Dynamics 365 Project Operations as the end-to-end solution, replacing both the PSA and middleware layers. D365 Project Operations natively supports milestone-based billing with automated invoice scheduling, built-in approval workflows, and direct integration with Dynamics 365 Finance or external ERPs.

Custom Application (Low-Code Platform: Retool or AppSmith)

Build a custom milestone billing dashboard using a low-code platform like Retool or AppSmith that connects directly to the PSA and accounting APIs. This approach gives the client a purpose-built UI for managing billing rules, reviewing pending invoices, and triggering invoice creation—all without the overhead of a full PSA deployment.

Want early access to the full toolkit?