npsp-trigger-framework-extension

Use when extending the NPSP Trigger-Driven Trigger Management (TDTM) framework with custom Apex handler classes — covering class authorship, DmlWrapper return patterns, Trigger_Handler__c registration, load order, recursion guards, and test isolation. NOT for standard Apex triggers outside of NPSP, general trigger-handler framework design, or Nonprofit Cloud (NPC) which replaced NPSP in new orgs.

Best use case

npsp-trigger-framework-extension is best used when you need a repeatable AI agent workflow instead of a one-off prompt.

Use when extending the NPSP Trigger-Driven Trigger Management (TDTM) framework with custom Apex handler classes — covering class authorship, DmlWrapper return patterns, Trigger_Handler__c registration, load order, recursion guards, and test isolation. NOT for standard Apex triggers outside of NPSP, general trigger-handler framework design, or Nonprofit Cloud (NPC) which replaced NPSP in new orgs.

Teams using npsp-trigger-framework-extension should expect a more consistent output, faster repeated execution, less prompt rewriting, better workflow continuity with your supporting tools.

When to use this skill

  • You want a reusable workflow that can be run more than once with consistent structure.
  • You already have the supporting tools or dependencies needed by this skill.

When not to use this skill

  • You only need a quick one-off answer and do not need a reusable workflow.
  • You cannot install or maintain the underlying files, dependencies, or repository context.

Installation

Claude Code / Cursor / Codex

$curl -o ~/.claude/skills/npsp-trigger-framework-extension/SKILL.md --create-dirs "https://raw.githubusercontent.com/PranavNagrecha/AwesomeSalesforceSkills/main/skills/apex/npsp-trigger-framework-extension/SKILL.md"

Manual Installation

  1. Download SKILL.md from GitHub
  2. Place it in .claude/skills/npsp-trigger-framework-extension/SKILL.md inside your project
  3. Restart your AI agent — it will auto-discover the skill

How npsp-trigger-framework-extension Compares

Feature / Agentnpsp-trigger-framework-extensionStandard Approach
Platform SupportNot specifiedLimited / Varies
Context Awareness High Baseline
Installation ComplexityUnknownN/A

Frequently Asked Questions

What does this skill do?

Use when extending the NPSP Trigger-Driven Trigger Management (TDTM) framework with custom Apex handler classes — covering class authorship, DmlWrapper return patterns, Trigger_Handler__c registration, load order, recursion guards, and test isolation. NOT for standard Apex triggers outside of NPSP, general trigger-handler framework design, or Nonprofit Cloud (NPC) which replaced NPSP in new orgs.

Where can I find the source code?

You can find the source code on GitHub using the link provided at the top of the page.

SKILL.md Source

# NPSP Trigger Framework Extension (TDTM)

This skill activates when a practitioner needs to add custom Apex logic that participates in NPSP's Trigger-Driven Trigger Management (TDTM) pipeline — the built-in trigger framework used by the Nonprofit Success Pack managed package. It covers the complete lifecycle: authoring a handler class, returning results via DmlWrapper instead of issuing direct DML, registering the handler via Trigger_Handler__c records, controlling execution order, guarding against recursion with static state, and isolating tests from the packaged handler chain.

---

## Before Starting

Gather this context before working on anything in this domain:

- Confirm the org has NPSP installed and identify the namespace prefix in use (`npsp` for production orgs, potentially different in sandboxes cloned from scratch orgs).
- Determine which sObject the new handler targets and which trigger actions are required (BeforeInsert, AfterInsert, AfterUpdate, etc.).
- List existing custom Trigger_Handler__c records on the same object — use the highest `npsp__Load_Order__c` value among them as the baseline for your new handler's load order to avoid silent ordering conflicts.
- Confirm whether this is a managed-package deployment context or an unmanaged metadata deployment. The `npsp__Owned_by_Namespace__c` field behavior differs between contexts.
- The most common wrong assumption practitioners make: assuming TDTM works like a standard Apex trigger handler, where you can issue DML directly and return void. TDTM requires all DML to be batched into a `DmlWrapper` return value — direct DML inside `run()` causes double-trigger recursion and governor limit problems.

---

## Core Concepts

### TDTM_Runnable Contract

Every NPSP custom trigger handler must extend `npsp.TDTM_Runnable` and override the `run()` method with this exact signature:

```apex
public override npsp.TDTM_Runnable.DmlWrapper run(
    List<SObject> newlist,
    List<SObject> oldlist,
    npsp.TDTM_Runnable.Action triggerAction,
    Schema.DescribeSObjectResult objResult
) {
    npsp.TDTM_Runnable.DmlWrapper wrapper = new npsp.TDTM_Runnable.DmlWrapper();
    // Add records to wrapper.objectsToInsert, objectsToUpdate, objectsToDelete
    return wrapper;
}
```

NPSP's dispatcher calls all registered handlers in load order and accumulates each handler's DmlWrapper into a single, batched DML operation after all handlers have run. This design ensures that custom handler DML participates in the same transaction as package DML and respects the single-trigger-per-object Salesforce best practice enforced by NPSP.

The `triggerAction` parameter maps to `npsp.TDTM_Runnable.Action` enum values: `BeforeInsert`, `BeforeUpdate`, `BeforeDelete`, `AfterInsert`, `AfterUpdate`, `AfterDelete`, `AfterUndelete`.

### Trigger_Handler__c Registration

A custom handler is invisible to NPSP until a corresponding `npsp__Trigger_Handler__c` record exists. The critical fields are:

| Field | Purpose | Notes |
|---|---|---|
| `npsp__Class__c` | Fully-qualified class name | Do not include `npsp.` prefix — this is your class name only |
| `npsp__Object__c` | API name of the sObject | e.g. `Contact`, `npe01__OppPayment__c` |
| `npsp__Trigger_Action__c` | Semicolon-delimited list of actions | e.g. `AfterInsert;AfterUpdate` |
| `npsp__Load_Order__c` | Integer execution order | Use values higher than packaged handlers; leave gaps of 10+ between custom handlers |
| `npsp__Owned_by_Namespace__c` | Protects the record from NPSP upgrades | Set to your namespace or leave blank — **never** set to `npsp` unless you intend the package to manage it |
| `npsp__Active__c` | Toggles the handler on/off | Default true; useful for debugging |

Packaged NPSP handlers typically use load orders in the 1–50 range. Start custom handlers at 100 or higher to ensure they run after packaged logic has established relationship state.

### Recursion Guard with Static State

NPSP does not expose a public recursion flag API for custom code. Custom handlers cannot add flags to the internal `TDTM_ProcessControl.flag` enum. The correct recursion guard is a static `Set<Id>` declared in the handler class:

```apex
private static Set<Id> processedIds = new Set<Id>();
```

Check and populate `processedIds` at the start of each relevant record iteration inside `run()`. Reset it only in test setup, never in production flow.

### Test Isolation via setTdtmConfig

NPSP's test isolation requirement is non-obvious: test classes must call `npsp.TDTM_Global_API.setTdtmConfig()` to replace the full packaged handler chain with a minimal or custom-only chain. Do **not** call `getTdtmConfig()` before `setTdtmConfig()` — as of the versions covered by this skill, `getTdtmConfig()` populates a static cache, and when `setTdtmConfig()` then tries to override it, the cache entry for your custom handler is dropped, causing it to silently never run during tests. The correct pattern is to pass a pre-built list directly to `setTdtmConfig()`.

---

## Common Patterns

### Pattern 1: Custom Handler Reacting to Opportunity Closure

**When to use:** When you need to create, update, or stamp related records whenever an Opportunity moves to Closed Won — after NPSP's own payment and rollup handlers have already fired.

**How it works:**
1. Set `npsp__Load_Order__c` to a value above the highest packaged handler on Opportunity (check the NPSP Setup tab or query `npsp__Trigger_Handler__c` for Opportunity records).
2. In `run()`, filter `newlist` where `StageName == 'Closed Won'` and `triggerAction == npsp.TDTM_Runnable.Action.AfterUpdate`.
3. Build the related records and add them to `wrapper.objectsToInsert`.
4. Return the wrapper; NPSP batches the insert after all handlers complete.

**Why not direct DML:** Issuing `insert` inside `run()` fires that object's trigger chain immediately and within the same call stack, risking recursive TDTM dispatch and consuming governor limits before the rest of the handler chain has run.

### Pattern 2: Conditional Handler Toggle via Active Flag

**When to use:** During rollout, debugging, or environment-specific deployments where the handler should be off in certain sandboxes.

**How it works:** The `npsp__Active__c` field on `npsp__Trigger_Handler__c` acts as a runtime toggle. A scratch org or sandbox deployment script can set it to false for that environment without code changes. Test classes that call `setTdtmConfig()` with a handler list control activation programmatically.

---

## Decision Guidance

| Situation | Recommended Approach | Reason |
|---|---|---|
| Need logic after NPSP payment creation on Opportunity | Register handler on Opportunity AfterUpdate at load order 100+ | Ensures NPSP payment handler (typically order 1-10) has already fired |
| Need to create related records from handler logic | Add to DmlWrapper.objectsToInsert | Avoids re-entrant TDTM dispatch from direct DML |
| Need to prevent double-processing in a batch context | Static Set<Id> recursion guard | TDTM_ProcessControl enum is package-private; static Set is the only extensible option |
| Handler silently not firing in tests | Use setTdtmConfig() with explicit handler list | getTdtmConfig() cache bug causes silent drops |
| Handler deleted after NPSP upgrade | Set npsp__Owned_by_Namespace__c to non-npsp value | Package upgrade routine only deletes records owned by 'npsp' namespace |
| Org migrating from NPSP to Nonprofit Cloud (NPC) | Evaluate NPC trigger extensibility approach | TDTM does not exist in NPC; this skill does not apply |

---

## Recommended Workflow

Step-by-step instructions for an AI agent or practitioner working on this task:

1. **Gather context** — Confirm NPSP is installed, identify the target sObject and trigger actions, query `npsp__Trigger_Handler__c` to find the highest existing load order for that object, and note any existing custom handlers.
2. **Author the handler class** — Extend `npsp.TDTM_Runnable`, override `run()` with the correct four-parameter signature, add a static `Set<Id>` recursion guard, and batch all related-record DML into `DmlWrapper` (never direct DML).
3. **Register the handler** — Create the `npsp__Trigger_Handler__c` record with `npsp__Class__c`, `npsp__Object__c`, `npsp__Trigger_Action__c` (semicolon-delimited), `npsp__Load_Order__c` (start at 100+), and `npsp__Owned_by_Namespace__c` set to your org namespace or a custom value — never `npsp`.
4. **Write isolated tests** — Call `npsp.TDTM_Global_API.setTdtmConfig()` first with an explicit handler list containing only your custom handler; do **not** call `getTdtmConfig()` beforehand. Use `System.runAs` where context matters and assert on DML outcomes, not on internal state.
5. **Validate execution order** — In a sandbox, verify the handler fires by inserting/updating the target records and checking logs or results. Confirm load order does not conflict with packaged handlers by reviewing the full `npsp__Trigger_Handler__c` list.
6. **Deploy** — Include the Apex class, test class, and the `npsp__Trigger_Handler__c` record as part of the deployment package. Verify `npsp__Owned_by_Namespace__c` is set to protect the record from upgrade deletion.
7. **Post-deploy check** — After the next NPSP upgrade cycle, confirm the handler record still exists and is still active. Set up a monitoring query or validation script to catch silent deletions.

---

## Review Checklist

Run through these before marking work in this area complete:

- [ ] Handler class extends `npsp.TDTM_Runnable` and overrides `run()` with the exact four-parameter signature
- [ ] All related-record DML is added to `DmlWrapper` fields — no direct `insert`, `update`, or `delete` calls inside `run()`
- [ ] `npsp__Trigger_Handler__c` record has `npsp__Owned_by_Namespace__c` set to a non-`npsp` value
- [ ] `npsp__Load_Order__c` is set above the highest packaged handler for that object (confirm by querying existing records)
- [ ] Test class uses `npsp.TDTM_Global_API.setTdtmConfig()` without a prior `getTdtmConfig()` call
- [ ] Static recursion guard (`Set<Id>`) is in place for any handler that could be triggered by its own DML wrapper output
- [ ] Handler is verified in sandbox before production deployment

---

## Salesforce-Specific Gotchas

Non-obvious platform behaviors that cause real production problems:

1. **NPSP Upgrade Silently Deletes Unprotected Custom Handler Records** — If `npsp__Owned_by_Namespace__c` is blank or set to `npsp`, the NPSP package upgrade routine treats the record as package-owned and may delete it. The handler disappears silently with no deploy error or warning. Always set this field to your org's namespace or a custom sentinel value.
2. **getTdtmConfig() Cache Bug Drops Custom Handlers in Tests** — Calling `getTdtmConfig()` before `setTdtmConfig()` in a test causes a static cache to be populated. When `setTdtmConfig()` then tries to register the custom handler, the cache entry for that class is already set to the packaged state, causing the custom handler to be silently skipped during the test run. Tests pass but the handler is never actually tested.
3. **Direct DML Inside run() Triggers Recursive TDTM Dispatch** — Any `insert`, `update`, or `delete` statement inside `run()` fires that object's full trigger pipeline again, including all NPSP handlers. This doubles governor limit consumption, risks an infinite recursion in some configurations, and violates NPSP's design contract. Use `DmlWrapper` exclusively.

---

## Output Artifacts

| Artifact | Description |
|---|---|
| Custom TDTM handler class | Apex class extending npsp.TDTM_Runnable, ready to deploy |
| Trigger_Handler__c record | Registration record for the custom handler, protected from upgrade deletion |
| Test class | Isolated test using setTdtmConfig() with assertions on DML outcomes |

---

## Related Skills

- apex/trigger-framework — general trigger handler framework design for orgs not using NPSP TDTM

Related Skills

industries-api-extensions

8
from PranavNagrecha/AwesomeSalesforceSkills

Use this skill when integrating with Salesforce Industries-specific API layers — Insurance Policy Business Connect API, Communications Cloud TM Forum Open APIs (TMF679, TMF680, etc.), Energy and Utilities Update Asset Status API, and Service Process Studio Connect APIs. Trigger keywords: Insurance policy issuance API, endorsement API, TMF679, Communications Cloud REST API, Update Asset Status, Service Process API, InsurancePolicy Connect API, sfiEnergy, industry-specific REST endpoint. NOT for standard Salesforce REST API, SOAP API, Bulk API, or platform event integration unrelated to an industry vertical.

record-triggered-flow-patterns

8
from PranavNagrecha/AwesomeSalesforceSkills

Use when designing or reviewing Salesforce record-triggered Flows, especially before-save vs after-save behavior, entry criteria, recursion avoidance, and when to escalate to Apex. Triggers: 'before save vs after save', '$Record__Prior', 'record-triggered flow', 'order of execution', 'flow recursion'. NOT for screen-flow UX or pure bulkification work when the trigger model is already correct.

flow-migration-from-trigger

8
from PranavNagrecha/AwesomeSalesforceSkills

Decide whether an existing Apex trigger should be rewritten as a Flow, and execute the migration safely. Covers the decision criteria (complexity, ownership, performance), side-by-side rollout, test-coverage parity, and the inverse case (recognize when Flow should stay Apex). NOT for migrating Process Builder / Workflow Rule to Flow (use those migration skills). NOT for brand-new automation decisions (use automation-selection.md).

flow-action-framework

8
from PranavNagrecha/AwesomeSalesforceSkills

Use when designing or troubleshooting Salesforce Flow actions in Flow Builder: standard and core actions, the Apex action element for @InvocableMethod classes, how list-shaped inputs and outputs map at the Flow–Apex boundary, subflows, and choosing between declarative actions versus custom Apex versus packaged invocables. Triggers: 'Flow Apex action', 'add Apex to Flow', 'InvocableMethod in Flow', 'Flow action palette', 'map Flow variables to Apex invocable inputs'. NOT for authoring or testing Apex @InvocableMethod bodies (use the Apex invocable-methods skill), External Services or HTTP callout registration (use flow-external-services), OmniStudio action packs, or LWC screen component local actions.

vscode-salesforce-extensions

8
from PranavNagrecha/AwesomeSalesforceSkills

Use this skill when setting up, configuring, or troubleshooting the Salesforce Extensions for VS Code: Apex Language Server activation, deploy-on-save behavior, Apex debugging, org authorization, and workspace project structure. NOT for Salesforce CLI command reference (use sf-cli-and-sfdx-essentials), scratch org definition files (use scratch-org-management), or CI/CD pipeline configuration.

npsp-data-model

8
from PranavNagrecha/AwesomeSalesforceSkills

Use this skill when working with NPSP (Nonprofit Success Pack) objects, namespace prefixes, GAU allocations, recurring donation objects, relationship and affiliation objects, or the NPSP data dictionary. Trigger keywords: npe01__, npe03__, npe4__, npe5__, npsp__, OppPayment__c, Recurring_Donation__c, Allocation__c, General_Accounting_Unit__c, NPSP data model. NOT for standard Salesforce data model, Financial Services Cloud data model, or Program Management Module (PMM) data model.

data-extension-design

8
from PranavNagrecha/AwesomeSalesforceSkills

Use this skill when designing, creating, or troubleshooting Marketing Cloud Data Extensions — including sendable vs. non-sendable DE selection, primary key composition, data retention configuration, Send Relationship mapping, and performance indexing. Trigger keywords: data extension, sendable DE, send relationship, DE primary key, data retention, Marketing Cloud data model, DE columns, subscriber key mapping. NOT for CRM (Sales/Service Cloud) custom object design, Marketing Cloud Connect object sync configuration, or Contact Builder attribute group architecture beyond simple relationship type selection.

npsp-vs-nonprofit-cloud-decision

8
from PranavNagrecha/AwesomeSalesforceSkills

Use this skill when an organization must decide whether to stay on NPSP (Nonprofit Success Pack) or move to Nonprofit Cloud (NPC), evaluate the timeline for that move, and understand what the migration entails at an architectural level. Trigger keywords: NPSP vs Nonprofit Cloud, upgrade NPSP, migrate to NPC, nonprofit platform decision, NPSP end of life, Nonprofit Cloud vs NPSP comparison. NOT for implementation — does not cover post-decision NPC build, NPSP customization, or data migration execution.

nonprofit-cloud-vs-npsp-migration

8
from PranavNagrecha/AwesomeSalesforceSkills

Nonprofit Cloud vs NPSP decision and migration: choose NPSP (managed package) or Nonprofit Cloud (native), plan data migration, Account Model differences, Program Management, fundraising. NOT for nonprofit accounting (use revenue-cloud-foundation). NOT for generic Sales Cloud setup (use sales-cloud-core-setup).

integration-framework-design

8
from PranavNagrecha/AwesomeSalesforceSkills

Use when designing a reusable integration layer in Salesforce that serves multiple external APIs through a shared callout infrastructure. Triggers: 'how to design a reusable integration layer in Salesforce', 'architect an Apex callout framework for multiple APIs', 'create a centralized error handling pattern for integrations', 'service interface pattern for external APIs', 'factory pattern for dynamic API resolution', 'centralized callout dispatcher'. NOT for individual API implementation (use apex/callouts-and-http-integrations), NOT for Named Credential setup (use integration/named-credentials-setup), NOT for async callout patterns (use apex/continuation-callouts).

trigger-framework

8
from PranavNagrecha/AwesomeSalesforceSkills

Use when writing, reviewing, or designing Apex triggers. Triggers: 'trigger', 'trigger handler', 'trigger framework', 'recursion', 'before insert', 'after update', 'one trigger per object'. NOT for Flow-based automation — use admin/flow-for-admins for declarative automation decisions.

trigger-and-flow-coexistence

8
from PranavNagrecha/AwesomeSalesforceSkills

Governance patterns for orgs where Apex triggers and record-triggered Flows both run on the same object. Covers field-write conflict prevention, single-entry-point consolidation, recursion guards across automation types, and automation inventory documentation. Use when inheriting a mixed-automation org, adding a Flow to an object that already has triggers, or resolving silent field-overwrite bugs. NOT for order-of-execution mechanics (use order-of-execution-deep-dive). NOT for trigger handler framework design (use trigger-framework). NOT for Flow-only design patterns (use record-triggered-flow-patterns).