DEV Community

Alex Kane
Alex Kane

Posted on

n8n for NonProfitTech SaaS Vendors: 5 Automations for IRS Form 990, UBIT, FASB ASC 958, State Charitable Registration, and Donor Privacy Compliance

NonProfitTech SaaS vendors process donor data, distribute fundraising software to 40+ states, and enable federal grant expenditure tracking — which means your product sits at the intersection of IRS charitable reporting, state charitable solicitation law, federal Uniform Guidance, and donor privacy regulation.

If your platform solicits donations or processes them on behalf of a nonprofit client whose state registration has lapsed, each solicitation is a separate violation under the state AG charitable solicitation statute — not a one-time paperwork lapse. That's the fastest compliance clock in the sector.

Below are 5 production-ready n8n workflows for the compliance surface area that creates the most liability for NonProfitTech SaaS vendors. Free to use.


Who needs this

  • Fundraising SaaS vendors (online donation pages, peer-to-peer, event registration)
  • Donor CRM vendors (holding donor PII, generating CCPA/GDPR requests)
  • Grant management SaaS vendors (federal award tracking, Uniform Guidance audit trails)
  • Nonprofit management platform vendors (990 deadline visibility for client orgs)
  • Volunteer/impact measurement SaaS (secondary regulatory exposure through client nonprofit obligations)

Workflow 1 — IRS Form 990 Filing Deadline Monitor (26 USC §6033)

Regulatory basis: 26 USC §6033; Treas. Reg. §1.6033-2

Penalty: $20/day up to $10,500 (§6652(c)(1)(A)); $100/day for large organizations (§6652(c)(1)(B))

Clock: Due 15th day of 5th month after fiscal year end; automatic 6-month extension available

Form type selection matters: 990-N (e-Postcard) for gross receipts ≤ $50K; 990-EZ for gross receipts ≤ $200K; full 990 above that. Persistent failure to file triggers automatic revocation of tax-exempt status under §6033(j).

This workflow queries your nonprofit client registry daily, calculates days-to-deadline for all form types, and routes critical (≤14 days) and approaching (≤45 days) deadlines to your compliance alert channel.

{"name":"IRS Form 990 Filing Deadline Monitor","nodes":[{"id":"n1","name":"Schedule Trigger","type":"n8n-nodes-base.scheduleTrigger","typeVersion":1.1,"position":[240,300],"parameters":{"rule":{"interval":[{"field":"days","daysInterval":1}]}}},{"id":"n2","name":"Load Nonprofit Client Registry","type":"n8n-nodes-base.postgres","typeVersion":2.4,"position":[460,300],"parameters":{"operation":"executeQuery","query":"SELECT org_id, org_name, ein, fiscal_year_end, form_type, tax_year, filing_deadline, extension_deadline, filed_date, vendor_account_id FROM nonprofit_clients WHERE active = true AND filed_date IS NULL ORDER BY filing_deadline ASC"}},{"id":"n3","name":"Calculate Days to Deadline","type":"n8n-nodes-base.code","typeVersion":2,"position":[680,300],"parameters":{"jsCode":"const today = new Date(); const items = []; for (const item of $input.all()) { const d = item.json; const deadline = new Date(d.filing_deadline); const extDeadline = new Date(d.extension_deadline); const daysToFiling = Math.ceil((deadline - today) / 86400000); const daysToExt = Math.ceil((extDeadline - today) / 86400000); const formType = d.form_type; const grossThreshold = formType === '990-N' ? 50000 : formType === '990-EZ' ? 200000 : null; items.push({ json: { ...d, days_to_filing_deadline: daysToFiling, days_to_extension_deadline: daysToExt, is_critical: daysToFiling <= 14 && daysToFiling > 0, is_approaching: daysToFiling > 14 && daysToFiling <= 45, gross_receipts_threshold: grossThreshold } }); } return items;"}},{"id":"n4","name":"Filter Critical Deadlines","type":"n8n-nodes-base.filter","typeVersion":2,"position":[900,200],"parameters":{"conditions":{"options":{"combineOperation":"or"},"conditions":[{"leftValue":"={{ $json.is_critical }}","operator":{"type":"boolean","operation":"true"}},{"leftValue":"={{ $json.is_approaching }}","operator":{"type":"boolean","operation":"true"}}]}}},{"id":"n5","name":"Build 990 Alert Payload","type":"n8n-nodes-base.code","typeVersion":2,"position":[1120,200],"parameters":{"jsCode":"return $input.all().map(item => { const d = item.json; const urgency = d.is_critical ? 'CRITICAL' : 'APPROACHING'; const irsUrl = 'https://www.irs.gov/charities-non-profits/annual-reporting-requirements-for-exempt-organizations'; return { json: { alert_type: '990_FILING_DEADLINE', urgency, org_id: d.org_id, org_name: d.org_name, ein: d.ein, form_type: d.form_type, tax_year: d.tax_year, fiscal_year_end: d.fiscal_year_end, filing_deadline: d.filing_deadline, extension_deadline: d.extension_deadline, days_to_filing: d.days_to_filing_deadline, days_to_extension: d.days_to_extension_deadline, regulatory_citation: '26 USC \u00a76033; Treas. Reg. \u00a71.6033-2', penalty_risk: d.form_type === '990' ? '$20/day up to $10,500 (\u00a76652(c)(1)(A)); $100/day for large orgs (\u00a76652(c)(1)(B))' : '$20/day', vendor_action_required: 'Notify account holder, surface 990 preparation checklist in dashboard', irs_reference: irsUrl } }; });"}},{"id":"n6","name":"Send Compliance Alert","type":"n8n-nodes-base.slack","typeVersion":2.1,"position":[1340,200],"parameters":{"channel":"#compliance-alerts","text":"={{ $json.urgency }}: {{ $json.org_name }} (EIN: {{ $json.ein }}) \u2014 {{ $json.form_type }} for tax year {{ $json.tax_year }} due {{ $json.filing_deadline }} ({{ $json.days_to_filing }} days). Extension: {{ $json.extension_deadline }}. Penalty: {{ $json.penalty_risk }}. Action: {{ $json.vendor_action_required }}"}}],"connections":{"Schedule Trigger":{"main":[[{"node":"Load Nonprofit Client Registry","type":"main","index":0}]]},"Load Nonprofit Client Registry":{"main":[[{"node":"Calculate Days to Deadline","type":"main","index":0}]]},"Calculate Days to Deadline":{"main":[[{"node":"Filter Critical Deadlines","type":"main","index":0}]]},"Filter Critical Deadlines":{"main":[[{"node":"Build 990 Alert Payload","type":"main","index":0}]]},"Build 990 Alert Payload":{"main":[[{"node":"Send Compliance Alert","type":"main","index":0}]]}},"settings":{"executionOrder":"v1"}}
Enter fullscreen mode Exit fullscreen mode

Workflow 2 — State Charitable Registration Renewal Tracker (40+ States)

Regulatory basis: Model Charitable Solicitation Act; individual state AG charitable solicitation statutes

Fastest clock: EXPIRED = IMMEDIATE — each solicitation to residents of an unregistered state is a separate violation

Self-hosting angle: State AG investigations into charitable fraud subpoena cloud vendors — your transaction logs, donor data, and platform audit trails become evidence even when you're not the nonprofit

40+ states require charitable solicitation registration. Expiration is not a grace-period event — it's a hard cutoff. This workflow monitors registrations across all states for your client orgs, enforces solicitation blocks in your platform for expired states, and queues renewal tasks.

The solicitation_hold_required flag triggers a DB update that blocks email/donation solicitations to residents of the expired-registration state directly in your vendor platform — not just an alert.

{"name":"State Charitable Registration Renewal Tracker (40+ States)","nodes":[{"id":"n1","name":"Daily Trigger","type":"n8n-nodes-base.scheduleTrigger","typeVersion":1.1,"position":[240,300],"parameters":{"rule":{"interval":[{"field":"days","daysInterval":1}]}}},{"id":"n2","name":"Query Registration Expirations","type":"n8n-nodes-base.postgres","typeVersion":2.4,"position":[460,300],"parameters":{"operation":"executeQuery","query":"SELECT cr.org_id, cr.state_code, cr.state_name, cr.registration_number, cr.expiration_date, cr.registration_type, cr.renewal_fee_usd, cr.filing_agent, cr.penalty_per_solicitation, o.org_name, o.ein, o.vendor_account_id FROM charitable_registrations cr JOIN nonprofit_clients o ON cr.org_id = o.org_id WHERE cr.active = true AND cr.expiration_date IS NOT NULL AND cr.expiration_date <= CURRENT_DATE + INTERVAL '60 days' ORDER BY cr.expiration_date ASC"}},{"id":"n3","name":"Classify Registration Urgency","type":"n8n-nodes-base.code","typeVersion":2,"position":[680,300],"parameters":{"jsCode":"const today = new Date(); const FASTEST_CLOCK_STATES = ['CA','NY','PA','IL','FL','TX','WA','MA','NJ','MD','CT','VA','OH','GA','MN']; return $input.all().map(item => { const d = item.json; const expDate = new Date(d.expiration_date); const daysLeft = Math.ceil((expDate - today) / 86400000); const isFastestClock = FASTEST_CLOCK_STATES.includes(d.state_code); const status = daysLeft <= 0 ? 'EXPIRED' : daysLeft <= 7 ? 'CRITICAL' : daysLeft <= 21 ? 'URGENT' : 'APPROACHING'; return { json: { ...d, days_until_expiration: daysLeft, urgency_status: status, fastest_clock_state: isFastestClock, penalty_note: daysLeft <= 0 ? `EXPIRED \u2014 each solicitation to ${d.state_code} residents is a separate violation: ${d.penalty_per_solicitation || 'see state AG schedule'}` : `${daysLeft} days remaining`, regulatory_cite: 'Model Charitable Solicitation Act; see state AG charitable solicitation statutes', solicitation_hold_required: daysLeft <= 0 } }; });"}},{"id":"n4","name":"Flag Expired Registrations","type":"n8n-nodes-base.filter","typeVersion":2,"position":[900,200],"parameters":{"conditions":{"conditions":[{"leftValue":"={{ $json.solicitation_hold_required }}","operator":{"type":"boolean","operation":"true"}}]}}},{"id":"n5","name":"Enforce Solicitation Hold","type":"n8n-nodes-base.postgres","typeVersion":2.4,"position":[1120,200],"parameters":{"operation":"executeQuery","query":"UPDATE vendor_accounts SET solicitation_blocked_states = array_append(solicitation_blocked_states, '{{ $json.state_code }}'), block_reason = 'charitable_registration_expired', block_timestamp = NOW() WHERE vendor_account_id = '{{ $json.vendor_account_id }}' AND NOT ('{{ $json.state_code }}' = ANY(solicitation_blocked_states))"}},{"id":"n6","name":"Queue Renewal Task","type":"n8n-nodes-base.postgres","typeVersion":2.4,"position":[900,400],"parameters":{"operation":"executeQuery","query":"INSERT INTO compliance_tasks (org_id, task_type, state_code, due_date, renewal_fee_usd, filing_agent, priority, created_at) VALUES ('{{ $json.org_id }}', 'charitable_registration_renewal', '{{ $json.state_code }}', '{{ $json.expiration_date }}', {{ $json.renewal_fee_usd || 0 }}, '{{ $json.filing_agent }}', '{{ $json.urgency_status }}', NOW()) ON CONFLICT (org_id, task_type, state_code) DO UPDATE SET priority = EXCLUDED.priority, updated_at = NOW()"}}],"connections":{"Daily Trigger":{"main":[[{"node":"Query Registration Expirations","type":"main","index":0}]]},"Query Registration Expirations":{"main":[[{"node":"Classify Registration Urgency","type":"main","index":0}]]},"Classify Registration Urgency":{"main":[[{"node":"Flag Expired Registrations","type":"main","index":0},{"node":"Queue Renewal Task","type":"main","index":0}]]},"Flag Expired Registrations":{"main":[[{"node":"Enforce Solicitation Hold","type":"main","index":0}]]}},"settings":{"executionOrder":"v1"}}
Enter fullscreen mode Exit fullscreen mode

Workflow 3 — UBIT Revenue Classification Engine (26 USC §511-514)

Regulatory basis: 26 USC §511-514; IRS Publication 598

Form 990-T threshold: $1,000 of UBIT income triggers filing requirement

Tax rate: Corporate rate (21%) or graduated rates on net unrelated business taxable income

Unrelated Business Income Tax applies when a nonprofit earns income from a trade or business (1) regularly carried on (2) not substantially related to exempt purpose. Three-part test implemented in workflow logic.

For SaaS vendors: if your platform surfaces advertising revenue, facilitates debt-financed property transactions, or enables revenue from controlled entities, the UBIT classification engine needs to run on each transaction — not as a year-end batch.

{"name":"UBIT Revenue Classification Engine (26 USC \u00a7511-514)","nodes":[{"id":"n1","name":"Webhook Trigger","type":"n8n-nodes-base.webhook","typeVersion":1.1,"position":[240,300],"parameters":{"path":"ubit-classify","httpMethod":"POST"}},{"id":"n2","name":"Extract Revenue Event","type":"n8n-nodes-base.set","typeVersion":3.3,"position":[460,300],"parameters":{"assignments":{"assignments":[{"id":"a1","name":"org_id","value":"={{ $json.body.org_id }}","type":"string"},{"id":"a2","name":"revenue_source","value":"={{ $json.body.revenue_source }}","type":"string"},{"id":"a3","name":"amount_usd","value":"={{ $json.body.amount_usd }}","type":"number"},{"id":"a4","name":"activity_description","value":"={{ $json.body.activity_description }}","type":"string"},{"id":"a5","name":"payer_type","value":"={{ $json.body.payer_type }}","type":"string"},{"id":"a6","name":"transaction_date","value":"={{ $json.body.transaction_date }}","type":"string"}]}}},{"id":"n3","name":"Apply UBIT Classification Logic","type":"n8n-nodes-base.code","typeVersion":2,"position":[680,300],"parameters":{"jsCode":"const d = $input.first().json; const EXEMPT_REVENUE_TYPES = ['member_dues','program_service','charitable_contribution','government_grant','investment_income_passive','rental_passive']; const UBIT_INDICATORS = ['advertising','debt_financed_property','controlled_entity_payment','unrelated_trade_business']; const THREE_TESTS = { tradeOrBusiness: !EXEMPT_REVENUE_TYPES.includes(d.revenue_source), regularlyCarriedOn: true, notSubstantiallyRelated: UBIT_INDICATORS.some(i => d.activity_description?.toLowerCase().includes(i) || d.revenue_source?.includes(i)) }; const isUBIT = THREE_TESTS.tradeOrBusiness && THREE_TESTS.regularlyCarriedOn && THREE_TESTS.notSubstantiallyRelated; const EXCLUSIONS = ['royalties_passive','rental_exempt','convenience_exception']; const hasExclusion = EXCLUSIONS.some(e => d.revenue_source?.includes(e)); return [{ json: { ...d, ubit_classification: isUBIT && !hasExclusion ? 'UNRELATED_BUSINESS_INCOME' : 'EXEMPT_INCOME', is_ubit: isUBIT && !hasExclusion, three_part_test: THREE_TESTS, exclusion_applies: hasExclusion, regulatory_cite: '26 USC \u00a7511-514; IRS Pub 598', form_990_t_required_threshold: 1000, action: isUBIT && !hasExclusion ? 'Flag for Form 990-T; accrue federal tax at 21% corporate rate or graduated rates' : 'No UBIT; classify as exempt revenue' } }];"}},{"id":"n4","name":"Accumulate UBIT Ledger","type":"n8n-nodes-base.postgres","typeVersion":2.4,"position":[900,300],"parameters":{"operation":"executeQuery","query":"INSERT INTO ubit_ledger (org_id, transaction_date, revenue_source, amount_usd, classification, is_ubit, exclusion_applies, regulatory_cite, created_at) VALUES ('{{ $json.org_id }}', '{{ $json.transaction_date }}', '{{ $json.revenue_source }}', {{ $json.amount_usd }}, '{{ $json.ubit_classification }}', {{ $json.is_ubit }}, {{ $json.exclusion_applies }}, '{{ $json.regulatory_cite }}', NOW())"}},{"id":"n5","name":"Check 990-T Threshold","type":"n8n-nodes-base.postgres","typeVersion":2.4,"position":[1120,300],"parameters":{"operation":"executeQuery","query":"SELECT SUM(amount_usd) as ytd_ubit_total FROM ubit_ledger WHERE org_id = '{{ $json.org_id }}' AND EXTRACT(YEAR FROM transaction_date) = EXTRACT(YEAR FROM CURRENT_DATE) AND is_ubit = true"}},{"id":"n6","name":"Alert if 990-T Required","type":"n8n-nodes-base.if","typeVersion":2,"position":[1340,300],"parameters":{"conditions":{"conditions":[{"leftValue":"={{ $json.ytd_ubit_total }}","operator":{"type":"number","operation":"gte"},"rightValue":1000}]}}},{"id":"n7","name":"Create 990-T Filing Task","type":"n8n-nodes-base.postgres","typeVersion":2.4,"position":[1560,200],"parameters":{"operation":"executeQuery","query":"INSERT INTO compliance_tasks (org_id, task_type, priority, details, created_at) VALUES ('{{ $json.org_id }}', 'form_990_t_required', 'HIGH', 'YTD UBIT income exceeds $1,000 threshold under 26 USC \u00a7511 \u2014 Form 990-T due 15th day of 5th month after fiscal year end', NOW()) ON CONFLICT (org_id, task_type) DO UPDATE SET priority = 'HIGH', updated_at = NOW()"}}],"connections":{"Webhook Trigger":{"main":[[{"node":"Extract Revenue Event","type":"main","index":0}]]},"Extract Revenue Event":{"main":[[{"node":"Apply UBIT Classification Logic","type":"main","index":0}]]},"Apply UBIT Classification Logic":{"main":[[{"node":"Accumulate UBIT Ledger","type":"main","index":0}]]},"Accumulate UBIT Ledger":{"main":[[{"node":"Check 990-T Threshold","type":"main","index":0}]]},"Check 990-T Threshold":{"main":[[{"node":"Alert if 990-T Required","type":"main","index":0}]]},"Alert if 990-T Required":{"main":[[{"node":"Create 990-T Filing Task","type":"main","index":0}],[]]}},"settings":{"executionOrder":"v1"}}
Enter fullscreen mode Exit fullscreen mode

Workflow 4 — Uniform Guidance 2 CFR Part 200 Grant Audit Trail

Regulatory basis: 2 CFR Part 200 (Uniform Administrative Requirements, Cost Principles, and Audit Requirements for Federal Awards)

Single Audit threshold: $750,000 federal expenditures in a fiscal year (§200.501)

Auditor access: §200.336 — Inspectors General, HHS, OMB, and other federal agencies retain right of access to records, including records held by SaaS vendors

When a nonprofit client exceeds $750K in federal expenditures, a Single Audit is required within 9 months of fiscal year end. The audit covers your platform's transaction records. This workflow creates an immutable SHA-256-hashed audit ledger, validates cost category classification against allowable cost principles (§200.403), and triggers Single Audit threshold alerts with automatic auditor_access_required flags on grant records.

{"name":"Uniform Guidance 2 CFR Part 200 Grant Audit Trail","nodes":[{"id":"n1","name":"Webhook: Grant Transaction","type":"n8n-nodes-base.webhook","typeVersion":1.1,"position":[240,300],"parameters":{"path":"grant-transaction","httpMethod":"POST"}},{"id":"n2","name":"Validate Grant Data","type":"n8n-nodes-base.code","typeVersion":2,"position":[460,300],"parameters":{"jsCode":"const d = $input.first().json.body; const REQUIRED = ['grant_id','federal_award_id','cfda_number','expenditure_amount','cost_category','period_of_performance_start','period_of_performance_end','fiscal_year']; const missing = REQUIRED.filter(f => !d[f]); if (missing.length > 0) { return [{ json: { error: true, missing_fields: missing, message: '2 CFR \u00a7200.302 financial management system requires: ' + missing.join(', ') } }]; } const ALLOWABLE_COST_CATEGORIES = ['direct_personnel','direct_fringe','direct_travel','direct_equipment','direct_supplies','direct_contractual','direct_other','indirect_facilities','indirect_admin']; const costValid = ALLOWABLE_COST_CATEGORIES.includes(d.cost_category); return [{ json: { ...d, cost_category_valid: costValid, error: false, validation_cite: '2 CFR \u00a7200.302 (financial management); 2 CFR \u00a7200.403 (allowable costs)' } }];"}},{"id":"n3","name":"Record to Immutable Audit Ledger","type":"n8n-nodes-base.postgres","typeVersion":2.4,"position":[680,300],"parameters":{"operation":"executeQuery","query":"INSERT INTO federal_grant_audit_ledger (grant_id, federal_award_id, cfda_number, expenditure_amount, cost_category, cost_category_valid, period_start, period_end, fiscal_year, transaction_timestamp, created_by, immutable_hash) VALUES ('{{ $json.grant_id }}', '{{ $json.federal_award_id }}', '{{ $json.cfda_number }}', {{ $json.expenditure_amount }}, '{{ $json.cost_category }}', {{ $json.cost_category_valid }}, '{{ $json.period_of_performance_start }}', '{{ $json.period_of_performance_end }}', {{ $json.fiscal_year }}, NOW(), '{{ $json.submitted_by }}', encode(sha256(('{{ $json.grant_id }}'||'{{ $json.federal_award_id }}'||{{ $json.expenditure_amount }}||NOW()::text)::bytea), 'hex'))"}},{"id":"n4","name":"Check Single Audit Threshold","type":"n8n-nodes-base.postgres","typeVersion":2.4,"position":[900,300],"parameters":{"operation":"executeQuery","query":"SELECT SUM(expenditure_amount) as total_federal_expenditures FROM federal_grant_audit_ledger WHERE grant_id IN (SELECT grant_id FROM federal_grants WHERE org_id = '{{ $json.org_id }}') AND fiscal_year = {{ $json.fiscal_year }}"}},{"id":"n5","name":"Flag Single Audit Requirement","type":"n8n-nodes-base.if","typeVersion":2,"position":[1120,300],"parameters":{"conditions":{"conditions":[{"leftValue":"={{ $json.total_federal_expenditures }}","operator":{"type":"number","operation":"gte"},"rightValue":750000}]}}},{"id":"n6","name":"Create Single Audit Alert","type":"n8n-nodes-base.slack","typeVersion":2.1,"position":[1340,200],"parameters":{"channel":"#compliance-alerts","text":"SINGLE AUDIT THRESHOLD REACHED: Org {{ $json.org_id }} \u2014 federal expenditures {{ $json.total_federal_expenditures }} USD in FY {{ $json.fiscal_year }} exceed $750,000 threshold. Single Audit required under 2 CFR \u00a7200.501. Auditor engagement deadline: 9 months after fiscal year end per 2 CFR \u00a7200.512. Vendor must provide \u00a7200.336 auditor access to records."}},{"id":"n7","name":"Enforce \u00a7200.336 Auditor Access Flag","type":"n8n-nodes-base.postgres","typeVersion":2.4,"position":[1340,400],"parameters":{"operation":"executeQuery","query":"UPDATE federal_grants SET single_audit_required = true, auditor_access_required = true, access_cite = '2 CFR \u00a7200.336 \u2014 HHS/OMB/Inspectors General retain right of access to records', updated_at = NOW() WHERE org_id = '{{ $json.org_id }}' AND fiscal_year = {{ $json.fiscal_year }}"}}],"connections":{"Webhook: Grant Transaction":{"main":[[{"node":"Validate Grant Data","type":"main","index":0}]]},"Validate Grant Data":{"main":[[{"node":"Record to Immutable Audit Ledger","type":"main","index":0}]]},"Record to Immutable Audit Ledger":{"main":[[{"node":"Check Single Audit Threshold","type":"main","index":0}]]},"Check Single Audit Threshold":{"main":[[{"node":"Flag Single Audit Requirement","type":"main","index":0}]]},"Flag Single Audit Requirement":{"main":[[{"node":"Create Single Audit Alert","type":"main","index":0}],[{"node":"Enforce \u00a7200.336 Auditor Access Flag","type":"main","index":0}]]}},"settings":{"executionOrder":"v1"}}
Enter fullscreen mode Exit fullscreen mode

Workflow 5 — Donor PII Privacy Request Processor (CCPA/GDPR + IRS §6104 Name Exemption)

Regulatory basis: CCPA (Cal. Civ. Code §1798.100-§1798.199); GDPR Art. 17/15/20; IRS §6104(b) Schedule B donor name exemption

Response deadline: 30 days (GDPR); 45 days (CCPA, extendable to 90)

Critical interaction: IRS §6104 requires Form 990 public disclosure — but Schedule B donor names are EXEMPT. Your deletion/access responses must not inadvertently disclose donor identities through 990 public record lookups

Fundraising platforms process donor PII from California, EU, and other regulated jurisdictions. CCPA deletion requests must be completed within 45 days. This workflow handles deletion queuing (with §6104 safe harbor check before erasure), access/portability data compilation, and opt-out-of-sale flagging — all jurisdiction-aware.

{"name":"Donor PII Privacy Request Processor (CCPA/GDPR + IRS \u00a76104 Exemption)","nodes":[{"id":"n1","name":"Webhook: Privacy Request","type":"n8n-nodes-base.webhook","typeVersion":1.1,"position":[240,300],"parameters":{"path":"donor-privacy-request","httpMethod":"POST"}},{"id":"n2","name":"Parse Privacy Request","type":"n8n-nodes-base.code","typeVersion":2,"position":[460,300],"parameters":{"jsCode":"const d = $input.first().json.body; const VALID_REQUEST_TYPES = ['deletion','access','portability','opt_out_sale','correction']; const JURISDICTIONS = { CA: 'CCPA', EU: 'GDPR_EU', UK: 'UK_GDPR', CO: 'CPA_COLORADO', CT: 'CTDPA', VA: 'VCDPA' }; const jurisdiction = JURISDICTIONS[d.donor_state_code] || (d.donor_country !== 'US' ? 'GDPR_EU' : 'NO_APPLICABLE_LAW'); const requestType = d.request_type?.toLowerCase().replace(/[^a-z_]/g,''); if (!VALID_REQUEST_TYPES.includes(requestType)) { return [{ json: { error: true, message: 'Invalid request type: ' + requestType } }]; } return [{ json: { ...d, jurisdiction, request_type: requestType, irs_6104_name_exemption_applies: true, exemption_note: 'IRS \u00a76104(b) requires Form 990 public disclosure but Schedule B donor names are exempt \u2014 do NOT disclose donor identity in public records compliance response', deadline_days: jurisdiction.includes('GDPR') ? 30 : jurisdiction === 'CCPA' ? 45 : 45, regulatory_cite: jurisdiction.includes('GDPR') ? 'GDPR Art.17 (erasure); Art.15 (access); Art.20 (portability)' : jurisdiction === 'CCPA' ? 'Cal. Civ. Code \u00a71798.100-1798.199' : 'Applicable state privacy law', created_at: new Date().toISOString() } }];"}},{"id":"n3","name":"Log Privacy Request","type":"n8n-nodes-base.postgres","typeVersion":2.4,"position":[680,300],"parameters":{"operation":"executeQuery","query":"INSERT INTO donor_privacy_requests (request_id, org_id, donor_id, request_type, jurisdiction, deadline_days, deadline_date, irs_6104_exemption_noted, status, created_at) VALUES (gen_random_uuid(), '{{ $json.org_id }}', '{{ $json.donor_id }}', '{{ $json.request_type }}', '{{ $json.jurisdiction }}', {{ $json.deadline_days }}, CURRENT_DATE + {{ $json.deadline_days }}, true, 'pending', NOW()) RETURNING request_id"}},{"id":"n4","name":"Route by Request Type","type":"n8n-nodes-base.switch","typeVersion":3,"position":[900,300],"parameters":{"rules":{"rules":[{"outputIndex":0,"conditions":{"conditions":[{"leftValue":"={{ $json.request_type }}","operator":{"type":"string","operation":"equals"},"rightValue":"deletion"}]}},{"outputIndex":1,"conditions":{"conditions":[{"leftValue":"={{ $json.request_type }}","operator":{"type":"string","operation":"equals"},"rightValue":"access"}]}},{"outputIndex":2,"conditions":{"conditions":[{"leftValue":"={{ $json.request_type }}","operator":{"type":"string","operation":"equals"},"rightValue":"opt_out_sale"}]}}]}}},{"id":"n5","name":"Queue Deletion \u2014 IRS \u00a76104 Safe Harbor Check","type":"n8n-nodes-base.postgres","typeVersion":2.4,"position":[1120,160],"parameters":{"operation":"executeQuery","query":"INSERT INTO deletion_tasks (org_id, donor_id, request_id, safe_harbor_check_required, check_note, created_at) VALUES ('{{ $json.org_id }}', '{{ $json.donor_id }}', '{{ $json.request_id }}', true, 'Verify donor record not on Schedule B (IRS \u00a76104 \u2014 names exempt from disclosure but must confirm no IRS-required retention obligation)', NOW())"}},{"id":"n6","name":"Compile Donor Data Report","type":"n8n-nodes-base.postgres","typeVersion":2.4,"position":[1120,300],"parameters":{"operation":"executeQuery","query":"SELECT donor_id, first_name, last_name, email, phone, address_city, address_state, donation_history_summary, communication_preferences, created_at, last_active FROM donor_records WHERE donor_id = '{{ $json.donor_id }}' AND org_id = '{{ $json.org_id }}'"}},{"id":"n7","name":"Set Opt-Out Flag","type":"n8n-nodes-base.postgres","typeVersion":2.4,"position":[1120,440],"parameters":{"operation":"executeQuery","query":"UPDATE donor_records SET sale_opt_out = true, sale_opt_out_date = NOW(), sale_opt_out_jurisdiction = '{{ $json.jurisdiction }}' WHERE donor_id = '{{ $json.donor_id }}' AND org_id = '{{ $json.org_id }}'"}}],"connections":{"Webhook: Privacy Request":{"main":[[{"node":"Parse Privacy Request","type":"main","index":0}]]},"Parse Privacy Request":{"main":[[{"node":"Log Privacy Request","type":"main","index":0}]]},"Log Privacy Request":{"main":[[{"node":"Route by Request Type","type":"main","index":0}]]},"Route by Request Type":{"main":[[{"node":"Queue Deletion \u2014 IRS \u00a76104 Safe Harbor Check","type":"main","index":0}],[{"node":"Compile Donor Data Report","type":"main","index":0}],[{"node":"Set Opt-Out Flag","type":"main","index":0}]]}},"settings":{"executionOrder":"v1"}}
Enter fullscreen mode Exit fullscreen mode

Implementation notes

State charitable registration is the fastest operational clock. Unlike most compliance deadlines that give you days or weeks to respond, an expired charitable registration means every outgoing solicitation email your platform sends on a client's behalf is a separate violation — before you've even been notified. Implement the solicitation block enforcement in Workflow 2 before any other piece here.

Federal auditor access (2 CFR §200.336) is often overlooked by SaaS vendors. Your client signed a grant agreement with a federal agency. That agency (and its Inspector General) retains audit access rights that extend to data held in third-party systems — including yours. Design your grant data storage with this in mind.

IRS §6104 and donor privacy interact unexpectedly. Form 990 is a public document. Schedule B (major donor names) is not. When processing a CCPA access or deletion request, do not cross-reference donor identity against publicly filed 990 data — that could constitute a §6104 violation in the other direction.


All 5 workflows above are available individually and as part of the Compliance Automation Bundle at stripeai.gumroad.com — ready-to-import n8n JSON with setup documentation.

Published by FlowKit — n8n automation templates for compliance-sensitive SaaS verticals.

Top comments (0)