Skip to content

KernDX Framework — AI Code Generation Context ​

This is the KernDX AI rules file — load it into your AI assistant before writing KernDX code.

Where it lives: docs/AI Agent Instructions.md — in the KernDX repo, the release/source download, and the docs site. Nothing to generate or install; it is just this file.

How to use it: an assistant that has merely seen this file once won't reliably apply it — put its full contents where the assistant always reads them. Copy it into the agent rules file your tool auto-loads at your project root (e.g. AGENTS.md, which Claude Code, Codex, Cursor and others read), or paste it into your tool's system prompt / project-knowledge. Tip: if you track KernDX in git, symlink or reference docs/AI Agent Instructions.md from that rules file so a git pull keeps it current automatically.

Namespace: kern. API version: 67.0.

Audience: subscribers writing code that uses KernDX in their own org. If you're instead working inside this repository (modifying the kern source itself or extending the framework), AGENTS.md at the repo root is the entry point.

STOP — Before Generating Code ​

Collect these from the user before writing any code:

  1. Namespace — e.g., kern. Required for all framework class references.
  2. Class prefix convention — e.g., PRJ_, ACME_, or none.
  3. Org alias — only if deployment commands are needed.

If provided upfront, proceed immediately.

Schema rule — If custom object fields are unknown, ask the user. Do not guess field API names.


CRITICAL — Namespace Prefix Rule ​

Every framework class reference MUST be prefixed with the namespace. Without it, code will NOT compile. This is the single most common AI generation error. Verify EVERY line of generated code.

Decision rule — apply this to every class reference:

  1. Does docs/reference/apex/<ClassName>.md exist in this repo? → It is a KernDX framework class. Add the kern. prefix.
  2. Is the class defined inside YOUR subscriber repository? → No prefix.

This rule replaces any hardcoded list of framework classes (the kern namespace has 95+ global types and inner classes; any hardcoded list has gaps). The auto-generated reference under docs/reference/apex/ is the authoritative inventory — if a class is documented there, it ships in the kern package.

Inner-class and interface members of framework classes also require the prefix (e.g. kern.IF_Trigger.BeforeInsert, kern.UTIL_AsyncChain.ChainStep, kern.TST_Mock.MockBuilder). The rule applies to the outermost class name; nested types inherit the same namespace.

Your own subscriber classes do NOT get the namespace prefix. Adding the prefix to your own classes causes Invalid type errors.

apex
// WRONG — missing prefix on framework classes → "Invalid type: TRG_Base"
public class TRG_SetDefaults extends TRG_Base implements IF_Trigger.BeforeInsert { }
QRY_Builder.selectFrom(Account.SObjectType).toList();

// WRONG — adding prefix to YOUR OWN subscriber class → "Invalid type: kern.API_CreateOrder"
kern.API_Dispatcher.processInboundService(kern.API_CreateOrder.class.getName());

// RIGHT — prefix ONLY on framework classes; subscriber classes are bare
public with sharing class TRG_SetDefaults extends kern.TRG_Base implements kern.IF_Trigger.BeforeInsert { }
kern.QRY_Builder.selectFrom(Account.SObjectType).toList();
kern.API_Dispatcher.processInboundService(API_CreateOrder.class.getName());

Self-check after generating: For every class name in the generated code, run the decision rule. Framework classes (those documented under docs/reference/apex/) without the kern. prefix → add it. Subscriber classes with the prefix → remove it. Both errors cause deployment failure.

Always follow every rule in this document. Do not use alternative patterns (fflib, AT4DX, etc.).


Coding Standards ​

Formatting ​

Apex: Tabs (size 3), Allman bracing, 180-char line limit.

apex
public with sharing class MyClass
{
	public void myMethod()
	{
		if(condition)
		{
			// code
		}
	}
}

Line wrap (>180 chars): paren on new line, args indented:

apex
List<Account> accounts = (List<Account>)kern.QRY_Builder.selectFrom
(
	Account.SObjectType
)
	.fields(new List<SObjectField>{ Account.Name, Account.Description })
	.toList();

Never break the line between a kern.-qualified class and its first method call. The Apex parser then reads kern as a variable and the class fails to compile with Variable does not exist: kern. Keep the class and its first call on one line; wrap on later calls in the chain:

apex
// WRONG — line break after the namespaced class → "Variable does not exist: kern"
IpResponse response = (IpResponse) kern.UTIL_HttpClient
	.get('Echo', '/')
	.deserialize(IpResponse.class);

// RIGHT — the first call stays on the class's line; wrap afterwards
IpResponse response = (IpResponse) kern.UTIL_HttpClient.get('Echo', '/')
	.deserialize(IpResponse.class);

JavaScript (LWC): Tabs, single quotes, semicolons, Allman bracing, camelCase, JSDoc required on LWC component classes (@description, @author, @date) and methods with parameters (@param {type} name).

Source files come in pairs — every .cls needs its .cls-meta.xml ​

Salesforce deploys an Apex class only when its *-meta.xml descriptor sits beside it. A .cls written without one is silently skipped — the deploy reports No local changes to deploy and the class never lands. Whenever you create a class file (.cls), also create its paired .cls-meta.xml descriptor:

xml
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>67.0</apiVersion>
    <status>Active</status>
</ApexClass>

This pairing applies to every metadata type, each with its own descriptor — Apex triggers use *.trigger-meta.xml, LWC use *.js-meta.xml, and so on. The exact shape differs by type, so when in doubt copy the *-meta.xml descriptor of an existing file of the same kind in the project.

Naming Conventions ​

PrefixUseExample
SEL_*Object selectorsSEL_Cases
QRY_*Query infrastructureQRY_Builder
DML_*DML operationsDML_Builder
TRG_*Trigger handlers/dispatcherTRG_SetDefaults
IF_*InterfacesIF_Trigger
API_*Web servicesAPI_SendEmail
REST_*REST endpointsREST_Echo
DTO_*Data Transfer ObjectsDTO_JsonBase
CTRL_*LWC controllersCTRL_ScheduledJob
FLOW_*Flow invocablesFLOW_CreateAccount
SVC_*Service facadesSVC_Onboarding
SCHED_*SchedulablesSCHED_PurgeRecords
BATCH_*Batch jobsBATCH_Process
UTIL_*UtilitiesUTIL_String
LOG_*LoggingLOG_Builder
MAP_*In-memory indexingMAP_SObject
TST_*Test utilitiesTST_Builder
*_TESTTest classesSEL_Cases_TEST

ApexDoc (Required) ​

Class-level (blank line between each tag):

apex
/**
 * @description Handler for Foobar defaults.
 *
 * @see TRG_SetFoobarDefaults_TEST
 *
 * @author <your-author-tag>
 *
 * @group Trigger Actions
 *
 * @date January 2026
 */

Tags in order: @description, @see, @author, @group, @date, @since, @example

Method-level (REQUIRED on EVERY method — including constructors and overrides like getFields(), configure(), onSuccess()):@description, @param (each), @return (if non-void), @throws, @since (if global)

Do NOT skip ApexDoc on overridden methods. Every method needs at minimum @description. Constructors also need @description.

@since: Required on ALL global members (classes, methods, properties). Format: @since 1.0. Not required on public/private. Subscribers rarely need @since — only on REST_* classes (the only subscriber class type that requires global).

@example: Must use ```apex code fences.

All properties and constants require @description. Bidirectional @see between main class and test class.

Critical Rules ​

RuleCorrectWrong
No inline SOQLkern.QRY_Builder.selectFrom(...) or new SEL_Cases()[SELECT ...]
No System.debugkern.LOG_Builder.build().info('msg').emitAt('Class.method')System.debug(...)
No LightningElementextends ComponentBuilder('notification')extends LightningElement
No fflib layering pattern in subscriber codekern.QRY_Builder, kern.DML_Builder, extends kern.SEL_Base, extends kern.TRG_Basefflib_SObjectSelector, fflib_SObjectDomain, fflib_Application, fflib_SObjectUnitOfWork, newQueryFactory(). The framework's own UTIL_SObjectDescribe + DML_Transaction utilities are BSD-licensed from the same lineage (see NOTICES.md); subscriber code goes through kern.* not fflib_*.
No abbreviationsaccount, messageacc, msg
No Given/When/ThenSelf-documenting code// Given, // When
No test cleanupSF resets static state between testsCleanup code in tests
No hardcoded field namesfield.getDescribe().getName()String literal field names
No section separatorsClean code//==== banners
No inline DML in testskern.TST_Builder.of(SObjectType).build()insert new Account()
Use Assert.*Assert.isTrue(result, 'msg')System.assert(...)
Declare sharingAlways with sharing; inherited sharing only for SCHED_*Missing sharing
No unnecessary globalpublic everywhere; global only for REST_* and SCHED_*global on handlers/selectors/services/flows

Sharing & Access by Class Type (mandatory) ​

Subscriber code uses with sharing everywhere. The inherited sharing you may see in framework source is a managed package internal pattern — it does NOT apply to subscriber orgs.

Class TypeSharingAccess
TRG_* handlerswith sharingpublic
SEL_* selectorswith sharing (subscriber) / inherited sharing (framework)public
API_* Inbound/Outboundwith sharingpublic
REST_* routingwith sharingglobal — SF requires for @RestResource
SVC_* serviceswith sharingpublic
FLOW_* invocableswith sharingpublic
SCHED_* schedulablesinherited sharingglobal — framework uses Type.newInstance() cross-namespace
UTIL_* utilitieswith sharingpublic
*_TEST test classes(none)private

Sharing rule: Always with sharing. Never without sharing unless explicitly justified.

Access rule: Always public. Exceptions: REST_* classes (global required for @RestResource) and SCHED_* classes (global required for cross-namespace Type.newInstance()).

PMD ​

Cyclomatic complexity ≤15/method, ≤50/class. Parameters ≤5. Nested ifs ≤4. Suppress: @SuppressWarnings('PMD.RuleName').


Selector Framework (SEL_Base) ​

Extend kern.SEL_Base. Define default fields. Custom methods use query property (NOT QRY_Builder.selectFrom()).

apex
/**
 * @description Selector for the Case SObject.
 *
 * @see SEL_Cases_TEST
 */
public with sharing class SEL_Cases extends kern.SEL_Base  // MUST be "with sharing" in subscriber code
{
	/** @description Constructs the Case selector. */
	public SEL_Cases() { super(Case.SObjectType); }

	/** @description Returns the default fields for Case queries. */
	public override List<SObjectField> getFields()
	{
		return new List<SObjectField>{ Case.Id, Case.Subject, Case.Status };
	}

	/** @description Returns related field paths for Case queries. */
	public override List<String> getFieldPaths()
	{
		return new List<String>{ 'Account.Name', 'Owner.Name' };
	}

	/** @description Finds open cases ordered by creation date. */
	public List<Case> findOpen()
	{
		return query.condition(Case.IsClosed).equals(false)
			.orderBy(Case.CreatedDate).descending().toList();
	}
}

Inherited methods: findById(Id) / findById(Set<Id>) | findByIdOrThrow(Id/Set<Id>) | findByField(field, value/Set) | findFirstByField(field, value) | findByFields(Map) / findFirstByFields(Map) | query (property → QRY_Builder.Builder) | toList() / getFirst() / getRandomItem() | count() / exists() | toQueryLocator()


Query Builder (QRY_Builder) ​

SEL_* for reusable queries; QRY_Builder directly for one-off. Prefer List<SObjectField> for compile-time safety.

apex
List<Account> accounts = kern.QRY_Builder.selectFrom(Account.SObjectType)
	.fields(new List<SObjectField>{ Account.Name, Account.Industry })
	.condition(Account.Industry).equals('Tech')
	.orderBy(Account.Name).ascending()
	.withLimit(100)
	.toList();

Fields: .fields(List<SObjectField/String>) | .addFields() | .addField() | .relatedField(String) | .relatedFields(List<String>) | .selectAllFields() | .fieldSet() | .subselect(Builder, String)

Conditions: .condition(field) | .andCondition(field) | .orCondition(field) | .addCondition(QRY_Condition.Evaluable)

Operators: .equals() | .notEquals() | .greaterThan() | .greaterThanOrEquals() | .lessThan() | .lessThanOrEquals() | .isNull() | .isNotNull() | .isIn(Set/List/Builder) | .notInSet(Set/List/List<SObject>) | .notIn(Builder) | .contains() | .startsWith() | .endsWith() | .includes() | .excludes()

Ordering: .orderBy(field) → .ascending() / .descending() / .nullsFirst() / .nullsLast() | .orderBy(field, sortDescending) | .orderBy(field, sortDescending, nullsLast)

Aggregates: .groupBy(field) | .sum() / .avg() / .min() / .max() / .count(String) / .countDistinct() | Aliased multi-aggregate overloads: .sum(field, alias) / .avg(field, alias) / .min(field, alias) / .max(field, alias) / .count(field, alias) / .countDistinct(field, alias) (several aggregates in one query; read back via AggregateRow.get(alias); alias must be a bare identifier) | .orderByCount(field, sortDescending) (ORDER BY COUNT for top-N groups with .withLimit()) | .havingSumOf() / .havingCount() etc. | Terminal: .toAggregateList() → List<QRY_Builder.AggregateRow> (NOT AggregateResult)

Limit/Cache/Scope: .withLimit() | .withOffset() | .withCache(seconds) | .usingScope(QRY_Builder.Scope) | .forUpdate() | .allRows()

Security: .withUserMode() / .withSystemMode() | .stripInaccessible() | .withSharing() / .bypassSharing() | .withoutSecurity()

Default access mode: USER_MODE (CRUD, FLS, sharing enforced). Controlled by the UserModeQueries_Enabled feature flag (ships IsEnabledByDefault__c = true). Call .withSystemMode() per-query to bypass, or subclass kern.SEL_Base and override systemModeRequired() to return true to pin a selector's queries to SYSTEM_MODE regardless of the flag.

Terminal: .toList() | .getFirst() | .toQueryLocator() | .toCursor() | .toAggregateList() | .getFirstAggregate() | .getPage(page, size) → QueryPage | .getRandomItem() / .getRandomItems(n) | .count() | .exists() | .asMap() | .asMapById(field) | .asMapByString(field) | .asGroupedMapById(field) | .asGroupedMapByString(field) | .asIdSet() | .asIdList() | .asValueSet(field) | .asStringSet(field) | .toSoql()

Semi-join Subqueries ​

IMPORTANT: Do NOT call terminal methods on subqueries:

apex
// CORRECT
kern.QRY_Builder.Builder subquery = kern.QRY_Builder.selectFrom(Contact.SObjectType)
	.fields(new List<String>{ 'AccountId' })
	.condition(Contact.Email).isNotNull();

List<Account> accounts = kern.QRY_Builder.selectFrom(Account.SObjectType)
	.condition(Account.Id).isIn(subquery)
	.toList();

Compound Conditions ​

apex
QRY_Condition.OrCondition statusGroup = new kern.QRY_Condition.OrCondition();
statusGroup.add(new kern.QRY_Condition.FieldCondition(Account.Status__c, kern.QRY_Condition.Operator.EQUALS, 'Active'));
statusGroup.add(new kern.QRY_Condition.FieldCondition(Account.Status__c, kern.QRY_Condition.Operator.EQUALS, 'Pending'));

List<Account> accounts = kern.QRY_Builder.selectFrom(Account.SObjectType)
	.condition(Account.Industry).equals('Tech')
	.addCondition(statusGroup)
	.toList();

LIKE-contains leaf (field-only constructor + contains(), wildcards handled for you): new kern.QRY_Condition.FieldCondition(Account.Name).contains('Acme') → Name LIKE '%Acme%'

LIKE-contains leaf for text a person typed (containsLiteral() escapes %, _ and \ so the term matches exactly as typed): new kern.QRY_Condition.FieldCondition(Account.Name).containsLiteral(searchBoxValue). Use it for every search box, filter or report parameter; contains() keeps the wildcards and is for patterns you wrote yourself.

Date Literals ​

apex
.condition(Account.CreatedDate).equals(new kern.QRY_Condition.DateLiteral().lastNDays(30))
.condition(Case.CreatedDate).equals(new kern.QRY_Condition.DateLiteral().today())

Methods: .today() / .yesterday() / .tomorrow() | .lastNDays(n) / .nextNDays(n) | .thisUnit(UnitOfTime) | .last(UnitOfTime) / .next(UnitOfTime) | .last(n, UnitOfTime) / .next(n, UnitOfTime)


DML Builder ​

apex
kern.DML_Builder.TransactionResult result = kern.DML_Builder.newTransaction()
	.doInsert(account)
	.doInsert(contact, Contact.AccountId, account)
	.doInsert(opportunity, Opportunity.AccountId, account)
	.execute();

Operations: .doInsert(SObject/List) | .doInsert(child, relField, parent) | .doUpdate() | .doDelete() | .doUpsert() | .doUpsert(records, externalId) | .doUndelete()

Config: .allowPartial() | .withUserMode() / .withSystemMode() | .bypassSharing() | .suppressLogging() | .suppressExceptions()

Result: isSuccess() | getInsertedIds() | getErrors() | getSuccessCount() | getFailureCount()

Async: .async() → kern.DML_Builder.DML_AsyncBuilder — a type switch, not a config method. The async builder exposes only .withBatchSize(Integer) and .execute(), and its execute() returns void, not a TransactionResult. Chain .async() last; none of the Config methods above exist on it. Three dials carry across the switch: .allowPartial(), the access mode (.withUserMode() / .withSystemMode()), and .bypassSharing(). .suppressLogging() / .suppressExceptions() do not — the async leg always logs and rethrows a DmlException.

Record sharing on the async leg: enforced unless you opt out. The async job cannot inherit your class's sharing declaration (the work runs inside the framework's own job processor), so the framework pins the sharing context explicitly: the write runs with record sharing enforced by default, and .bypassSharing() is the deliberate opt-out that carries across the switch. Under the default USER_MODE the access mode itself also enforces sharing, so the dial only changes system-mode writes. The one remaining sync/async difference: a synchronous chain called without a bypass inherits the sharing context of the class that calls .execute(), while the async leg always enforces unless bypassed.

apex
kern.DML_Builder.newTransaction().doInsert(accounts).async().withBatchSize(50).execute();   // returns void

Default access mode: USER_MODE (CRUD, FLS, sharing enforced). Controlled by the UserModeDml_Enabled feature flag (ships IsEnabledByDefault__c = true). Call .withSystemMode() on the transaction to bypass (e.g. writes to framework-owned audit objects).

Parent must appear earlier in chain. Framework auto-sets lookup IDs.


Trigger Framework ​

Trigger File ​

apex
trigger TRG_Account on Account (before insert, before update, after insert, after update)
{
	new kern.TRG_Dispatcher().run();
}

Only declare events needed by configured TriggerAction__mdt handlers.

Trigger Action Class ​

apex
public with sharing class TRG_SetAccountDefaults extends kern.TRG_Base implements kern.IF_Trigger.BeforeInsert
{
	/** @description Default description text. */
	private static final String DEFAULT_DESCRIPTION = 'New Account';

	/**
	 * @description Sets default Description on Account records when blank.
	 *
	 * @param newRecords The list of new Account records.
	 */
	public void beforeInsert(List<SObject> newRecords)
	{
		for(SObject record : newRecords)
		{
			Account account = (Account)record;

			if(String.isBlank(account.Description))
			{
				account.Description = DEFAULT_DESCRIPTION;
			}
		}
	}
}

Rules: Extract magic strings to named constants with @description. ApexDoc on every method including overrides.

IF_Trigger Interfaces ​

These are interface contracts — code will NOT compile if signatures differ. Copy exactly.

InterfaceMethod Signature
kern.IF_Trigger.BeforeInsertvoid beforeInsert(List<SObject> newRecords)
kern.IF_Trigger.AfterInsertvoid afterInsert(List<SObject> newRecords)
kern.IF_Trigger.BeforeUpdatevoid beforeUpdate(List<SObject> newRecords, List<SObject> oldRecords)
kern.IF_Trigger.AfterUpdatevoid afterUpdate(List<SObject> newRecords, List<SObject> oldRecords)
kern.IF_Trigger.BeforeDeletevoid beforeDelete(List<SObject> oldRecords)
kern.IF_Trigger.AfterDeletevoid afterDelete(List<SObject> oldRecords)
kern.IF_Trigger.AfterUndeletevoid afterUndelete(List<SObject> newRecords)

Do NOT use triggerNew or omit the parameter. Iterate the method parameter (e.g., for(SObject record : newRecords)).

Update/delete handlers — pair old and new from the oldRecords parameter, NOT triggerOldMap. The framework hands you the prior-state records as the oldRecords argument (the dispatcher calls beforeUpdate(Trigger.new, Trigger.old)). Pair them by Id with a map built from that parameter:

apex
public with sharing class TRG_StampIndustryChange extends kern.TRG_Base implements kern.IF_Trigger.BeforeUpdate
{
	/** @description Stamp applied to Description when Industry changes. */
	private static final String INDUSTRY_CHANGED_STAMP = 'Industry updated';

	/**
	 * @description Stamps Description when an Account's Industry changes.
	 *
	 * @param newRecords The Accounts with their proposed values.
	 * @param oldRecords The same Accounts with their values prior to the update.
	 */
	public void beforeUpdate(List<SObject> newRecords, List<SObject> oldRecords)
	{
		Map<Id, SObject> oldById = new Map<Id, SObject>(oldRecords);

		for(SObject record : newRecords)
		{
			Account newAccount = (Account)record;
			Account oldAccount = (Account)oldById.get(newAccount.Id);

			if(oldAccount != null && newAccount.Industry != oldAccount.Industry)
			{
				newAccount.Description = INDUSTRY_CHANGED_STAMP;
			}
		}
	}
}

(Pairing by positional index — newRecords[i] vs oldRecords[i] — also works; the two lists are aligned.) The triggerNew / triggerOld / triggerOldMap accessors on kern.TRG_Base are convenience getters that read live Trigger.* context and are null outside a running trigger — an action that reads them cannot be unit-tested by direct handler invocation (the test pattern below), so pair from the method parameters instead.

Custom Metadata Configuration (REQUIRED) ​

A trigger handler will NOT execute without these records. Always generate them alongside the handler class.

Two records needed per handler: a TriggerSetting (one per SObject) and a TriggerAction (one per handler + event).

File naming: kern__TriggerSetting.{ObjectName}.md-meta.xml and kern__TriggerAction.{ActionName}_{Event}.md-meta.xml

TriggerSetting — one per SObject (skip if already exists). File: kern__TriggerSetting.{ObjectName}.md-meta.xml

FieldValue
kern__SObjectType__cObject API name (e.g., Account)
kern__BypassExecution__cfalse

Optional on TriggerSetting (omit unless needed):

FieldDefaultPurpose
kern__ObjectApiNameOverride__c—Registers an entity SObjectType__c cannot reference — Change Data Capture events (AccountChangeEvent). Wins at dispatch; at least one of the two must be set.
kern__ApplyMasking__ctrueRuns the masking pass before insert/update on this object. Uncheck to opt one object out.
kern__BypassFeatureFlag__c—Skip ALL actions on this object while the named FeatureFlag__mdt is on.
kern__RequiredFeatureFlag__c—Run actions on this object ONLY while the named FeatureFlag__mdt is on.
kern__EnablePerformanceLogging__ctrueTrack execution time for every action on this object.
kern__PerformanceThresholdMs__c—Log actions slower than this (ms). Overrides the global default.

TriggerAction — one per handler + event. File: kern__TriggerAction.{ActionName}_{Event}.md-meta.xml

FieldValue
kern__ApexClassName__cHandler class name (no namespace prefix)
kern__Event__cBefore Insert / Before Update / After Insert / After Update / Before Delete / After Delete / After Undelete
kern__Order__cExecution sequence (lower first; use 100, 200, etc.)
kern__TriggerSetting__cDeveloperName of the TriggerSetting record
kern__Description__cHuman-readable description (a validation rule requires it)

Optional on TriggerAction (omit unless needed):

FieldDefaultPurpose
kern__FlowName__c—Runs an active auto-launched flow instead of an Apex class. Set exactly one of ApexClassName__c / FlowName__c — a validation rule blocks both or neither. The flow needs an in/out record variable (plus input-only recordPrior for update events).
kern__BypassExecution__cfalseKill switch for this one action.
kern__BypassFeatureFlag__c—Skip this action while the named FeatureFlag__mdt is on.
kern__RequiredFeatureFlag__c—Run this action ONLY while the named FeatureFlag__mdt is on.
kern__AllowRecursion__ctrueRe-entrancy guard, NOT a once-per-transaction guard. Uncheck to skip the action while an earlier run of it is still in progress (its own DML re-firing the same trigger). It still runs for separate, later operations in the same transaction.
kern__AllowNonSelfInitiated__ctrueUncheck to skip the action when another trigger, not the original DML, caused this execution.
kern__EntryCriteriaFormula__c—Formula gate — the action runs only for records where it evaluates true.
kern__EntryCriteriaContextClassName__c—Class implementing kern.UTIL_FormulaFilter.INT_SObjectFormulaEvaluationContext. Required for CUSTOM objects only — Account, Campaign, Case, Contact, Event, Lead, Opportunity, Task and User resolve automatically.
kern__FailureAction__cLogAndContinueLogAndContinue or BlockDml when the action throws.
kern__PerformanceThresholdMs__c—Per-action override of the threshold (ms).
kern__ForcePerformanceLogging__cfalseLog this action's timing even when the object-level switch is off.
kern__SuppressPerformanceLogging__cfalseNever log this action's timing.

Copy-paste XML reference (TriggerAction — required fields only; add optional fields from the table above as needed):

xml
<?xml version="1.0" encoding="UTF-8"?>
<CustomMetadata xmlns="http://soap.sforce.com/2006/04/metadata" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
    <label>Set Account Defaults - Before Insert</label>
    <protected>false</protected>
    <values><field>kern__ApexClassName__c</field><value xsi:type="xsd:string">TRG_SetAccountDefaults</value></values>
    <values><field>kern__Event__c</field><value xsi:type="xsd:string">Before Insert</value></values>
    <values><field>kern__Order__c</field><value xsi:type="xsd:double">100.0</value></values>
    <values><field>kern__TriggerSetting__c</field><value xsi:type="xsd:string">Account</value></values>
    <values><field>kern__Description__c</field><value xsi:type="xsd:string">Sets default Description when blank on Account insert</value></values>
</CustomMetadata>

TriggerSetting and ApiSetting use the same XML structure — only the field names differ (see tables above and below).

Flow as a Trigger Action ​

Subscribers can register an auto-launched Flow as a trigger action by setting kern__FlowName__c on a TriggerAction__mdt row and leaving kern__ApexClassName__c blank. The framework dispatches via its built-in flow runner and the flow inherits ordering, bypass, recursion control, performance monitoring, audit logging, and feature-flag gating. No subscriber Apex is required — registration is metadata-only.

Configure via a TriggerAction__mdt record with these flow-specific fields:

FieldRequiredDefaultAllowed values
kern__FlowName__cYes (XOR with ApexClassName__c)—Bare flow API name (no namespace prefix — the framework resolves cross-namespace)
kern__ApexClassName__cLeave blank for flow rows—The MutuallyExclusiveTarget validation rule rejects rows with both FlowName__c and ApexClassName__c populated, and rows with neither populated.
kern__FailureAction__cNoLogAndContinueLogAndContinue (logs error, DML proceeds — for orchestration), BlockDml (calls record.addError, blocks save — for validation). Applies to Apex and flow actions alike.

The registered flow declares variable record (input + output, type matching the trigger object) and — for update contexts — variable recordPrior (input only). Variable type must match TriggerSetting__mdt.SObjectType__c exactly. LogSetting__c.EnableFlowActionLogging__c (Off / ErrorsOnly (default) / AlwaysOn) controls audit volume.

Mock harness for tests: kern.TST_InvokeFlowMock.forFlow(name).succeed().withOutputRecord(record).register() short-circuits Flow.Interview at test time. Helpers: .fail(message).register(), .assertInvoked(name, count), .assertNotInvoked(name), .getLastInputRecord(name), .getLastInputPriorRecord(name), .clear(). Mocks test framework orchestration, not flow logic — pair with one deployed-flow integration test per flow.

Post-Trigger Actions ​

Runs after every TriggerAction__mdt on every touched object in a dispatch has completed. Transaction-scoped, not per-record — it receives no records. The class implements kern.IF_Trigger.PostAction and does not extend kern.TRG_Base; registration is metadata-only via PostTriggerAction__mdt (no Event__c field — post-actions are not bound to an event).

TypeMember
kern.IF_Trigger.PostActionvoid execute(kern.IF_Trigger.PostActionContext context)
kern.IF_Trigger.PostActionEntryCriteriaBoolean shouldRun(kern.IF_Trigger.PostActionContext context) — optional gate; false skips the row
kern.IF_Trigger.PostActionContextSet<SObjectType> touchedSObjectTypes — read-only, never null, may be empty. The entire context.

Synchronous DML is contractually banned. The framework compares Limits.getDmlStatements() across execute() and throws a PostActionDmlException (framework-internal — you cannot catch it by name), always rethrown regardless of FailureAction__c. Compute, or enqueue async (System.enqueueJob / @future / System.schedule) instead.

It fires once per timing per DML operation, not once per transaction. Each trigger timing is a separate top-level dispatch, so an insert against a handler registered on both before- and after-insert fires post-actions twice, and N top-level DML statements multiply that again. Write them idempotent or gate on touchedSObjectTypes. They do not fire at all when the object is bypassed or when a trigger action threw.

Registration record — kern__PostTriggerAction.{Name}.md-meta.xml. Fields: kern__ApexClassName__c (the implementing class) | kern__Order__c (ascending) | kern__TriggerSetting__c (when set, the row runs only if that setting's object is in touchedSObjectTypes; omit for cross-object) | kern__EntryCriteriaContextClassName__c | kern__BypassExecution__c (false) | kern__RequiredFeatureFlag__c / kern__BypassFeatureFlag__c | kern__FailureAction__c (LogAndContinue) | kern__PerformanceThresholdMs__c | kern__ForcePerformanceLogging__c (false) | kern__Description__c. Gate order, cheapest first: bypass → feature flags → trigger-setting scope → entry criteria.

apex
public with sharing class TRG_EnqueueAccountSync implements kern.IF_Trigger.PostAction
{
	/**
	 * @description Enqueues one downstream sync job per dispatch when Account triggers participated.
	 *
	 * @param context The touched-type context for this dispatch.
	 */
	public void execute(kern.IF_Trigger.PostActionContext context)
	{
		if(!context.touchedSObjectTypes.contains(Account.SObjectType))
		{
			return;
		}

		System.enqueueJob(new AccountSyncQueueable());
	}
}

Bypass ​

Object: kern.TRG_Base.bypass(SObjectType) / .isBypassed(SObjectType) / .clearBypass(SObjectType) / .clearAllBypasses() — each single-argument call also takes a String object API name Action: .bypassAction(String) / .isActionBypassed(String) / .clearActionBypass(String) / .clearAllActionBypasses() — names match case-insensitively Either surface: .bypass(String, BypassType) / .isBypassed(String, BypassType) / .clearBypass(String, BypassType) / .clearAllBypasses(BypassType) — one call site for both sets, routed by kern.TRG_Base.BypassType.OBJECT_NAME or .CLASS_NAME. Parse a string into the enum with kern.TRG_Base.resolveBypassType(String). Declarative: TriggerSetting__mdt.BypassExecution__c Audit: every bypass*() / clear*Bypass() call emits a LogEntryEvent__e with category BypassEvent — the runtime audit trail is always on. Set a free-text reason for the whole transaction via kern.TRG_Base.setBypassReason(String) and it attaches to every bypass emission that follows. (The underlying BypassAction enum and resolveBypassAction(String) resolver are framework-internal — public, not global — and drive the declarative FLOW_BypassTrigger invocable; subscribers do not call them directly.)

The object and action sets are independent — clearAllBypasses() does not clear action bypasses, and vice versa.

Trigger Test Pattern ​

apex
@SuppressWarnings('PMD.ApexUnitTestClassShouldHaveRunAs')
@IsTest(SeeAllData=false IsParallel=true)
private class TRG_SetAccountDefaults_TEST
{
	@IsTest
	private static void shouldSetDefaultWhenDescriptionBlank()
	{
		Account record = (Account)kern.TST_Builder.of(Account.SObjectType)
			.withoutInsertion()
			.build();

		new TRG_SetAccountDefaults().beforeInsert(new List<SObject>{ record });

		Assert.isNotNull(record.Description, 'Default should be set');
	}
}

HTTP Client (UTIL_HttpClient) ​

Static factories only — never new UTIL_HttpClient().

apex
kern.UTIL_HttpClient.post('PaymentGateway', '/charges').body(chargeRequest).send();
kern.UTIL_HttpClient.get('CRM', '/accounts/{id}').pathParam('id', accountId).asMap();
kern.UTIL_HttpClient.post('EmailService', '/send').body(payload).timeout(30000)
	.withRetry(3).withCircuitBreaker().onFailure(kern.UTIL_HttpClient.FailureAction.LOG_FAILURE)
	.withTriggeringRecord(record.Id).send();

Factories: .get() / .post() / .put() / .del() / .patch() / .useHandler(Type)

Builder: .body() | .header() | .headers() | .queryParam() | .pathParam() | .timeout() | .withRetry(n) | .withExponentialBackoff(n, base) | .retryOn(Set<Integer>) | .onFailure(FailureAction) | .withCircuitBreaker() | .withTriggeringRecord(Id) | .withCorrelationId() | .skipLogging() | .withParameter() | .withParameters()

Terminal: .send() → HttpResponse | .asString() | .asMap() | .deserialize(Type) | .invoke()


Outbound Web Services (API_Outbound) ​

apex
public with sharing class API_SendNotification extends kern.API_Outbound
{
	@TestVisible
	private static final String PARAM_RECIPIENT = 'recipient';

	private static final String ERROR_NO_EMAIL = 'Record does not have an email address';

	public override void configure()
	{
		super.configure();
		requestPayload = new DTO_Request();
		responsePayload = new DTO_Response();
		defaultMockBody = '{"messageId": "msg-12345"}';
	}

	public override Set<String> getRequiredInputs()
	{
		Set<String> requiredInputs = super.getRequiredInputs();
		requiredInputs.add(PARAM_RECIPIENT);
		return requiredInputs;
	}

	public override List<String> getValidationErrors()
	{
		List<String> errors = new List<String>();
		Contact record = (Contact)new SEL_Contacts().findById(apiCall.kern__TriggeringRecordId__c);

		if(String.isBlank(record.Email))
		{
			errors.add(ERROR_NO_EMAIL);
		}

		return errors;
	}

	@JsonAccess(Serializable='always' Deserializable='always')
	public class DTO_Request extends kern.DTO_JsonBase
	{
		/** @description The recipient address. */
		private String recipient;

		public override void populate(Id recordId, kern.DTO_NameValues params)
		{
			super.populate(recordId, params);
			Contact record = (Contact)new SEL_Contacts().findById(recordId);
			this.recipient = record.Email;
		}
	}

	@JsonAccess(Serializable='always' Deserializable='always')
	public class DTO_Response extends kern.DTO_JsonBase
	{
		/** @description The message ID from the gateway. */
		private String messageId;
	}
}

Rules: with sharing | Default POST for outbound (override getHttpMethod() if different) | super.configure() first | super.getRequiredInputs() and add | @TestVisible param constants | defaultMockBody in configure | DTO props private + ApexDoc | @JsonAccess on all DTOs | Extend kern.DTO_JsonBase | super.populate() first

Triggering record: the inherited apiCall property (kern__ApiCall__c, populated before getValidationErrors() runs) carries the triggering record id. Note the field is namespaced from your org: apiCall.kern__TriggeringRecordId__c, not apiCall.TriggeringRecordId__c. Other inherited members you can read: setting | request | response | requestPayload | responsePayload | requiresTriggeringRecord | baseUrl | defaultMockBody | result | errorText | correlationId | inputs. Your selector must declare every field you then read — findById selects only the selector's declared fields, so a SEL_Contacts that omits Contact.Email throws SObjectException: SObject row was retrieved via SOQL without querying the requested field.

API Custom Metadata Configuration (REQUIRED for both Inbound and Outbound) ​

An API service will NOT execute without an ApiSetting__mdt record. Always generate it alongside the API class.

File naming: kern__ApiSetting.{ServiceName}.md-meta.xml

ApiSetting fields (same XML wrapper as trigger metadata):

FieldOutboundInbound
kern__ClassName__cHandler class name (no namespace)Same
kern__Direction__cOutboundInbound
kern__EndpointPath__cAPI path (e.g., /notifications)REST route pattern (e.g., /v1/person/*)
kern__IsActive__ctruetrue
kern__MockingEnabled__ctrue (enables test mocking)Same
kern__LogIssues__ctruetrue
kern__Priority__c100.0100.0
kern__ApiCredential__cNamed Credential DeveloperName—

DeveloperName (filename) should match class without API_ prefix (e.g., kern__ApiSetting.SendNotification).

Copy-paste XML reference (ApiSetting — outbound).

xml
<?xml version="1.0" encoding="UTF-8"?>
<CustomMetadata xmlns="http://soap.sforce.com/2006/04/metadata" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
    <label>Send Notification</label>
    <protected>false</protected>
    <values><field>kern__ClassName__c</field><value xsi:type="xsd:string">API_SendNotification</value></values>
    <values><field>kern__Direction__c</field><value xsi:type="xsd:string">Outbound</value></values>
    <values><field>kern__EndpointPath__c</field><value xsi:type="xsd:string">/notifications</value></values>
    <values><field>kern__IsActive__c</field><value xsi:type="xsd:boolean">true</value></values>
    <values><field>kern__MockingEnabled__c</field><value xsi:type="xsd:boolean">true</value></values>
    <values><field>kern__LogIssues__c</field><value xsi:type="xsd:boolean">true</value></values>
    <values><field>kern__Priority__c</field><value xsi:type="xsd:double">100.0</value></values>
</CustomMetadata>

Copy-paste XML reference (ApiSetting — inbound).

xml
<?xml version="1.0" encoding="UTF-8"?>
<CustomMetadata xmlns="http://soap.sforce.com/2006/04/metadata" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
    <label>Get Person</label>
    <protected>false</protected>
    <values><field>kern__ClassName__c</field><value xsi:type="xsd:string">API_PersonRetrieve</value></values>
    <values><field>kern__Direction__c</field><value xsi:type="xsd:string">Inbound</value></values>
    <values><field>kern__EndpointPath__c</field><value xsi:type="xsd:string">/v1/person/*</value></values>
    <values><field>kern__IsActive__c</field><value xsi:type="xsd:boolean">true</value></values>
    <values><field>kern__LogIssues__c</field><value xsi:type="xsd:boolean">true</value></values>
    <values><field>kern__Priority__c</field><value xsi:type="xsd:double">100.0</value></values>
</CustomMetadata>

Outbound Test Pattern ​

apex
@SuppressWarnings('PMD.ApexUnitTestClassShouldHaveRunAs')
@IsTest(SeeAllData=false IsParallel=true)
private class API_SendNotification_TEST
{
	private static final String SERVICE_NAME = API_SendNotification.class.getName();

	@IsTest
	private static void shouldCompleteSuccessfully()
	{
		Contact record = (Contact)kern.TST_Builder.of(Contact.SObjectType)
			.withOverride(Contact.Email, 'test@example.com').build();
		Map<String, String> parameters = new Map<String, String>
		{
			API_SendNotification.PARAM_RECIPIENT => 'recipient@example.com'
		};
		kern.API_OutboundTestHelper.assertCallSuccessful(SERVICE_NAME, record.Id, parameters);
	}
	// Abort: build record missing required data → assertCallAborted(SERVICE_NAME, record.Id, parameters)

	@IsTest
	private static void shouldFailWhenApiReturnsError()
	{
		kern.API_MockFactory.registerErrorMock(SERVICE_NAME);
		Contact record = (Contact)kern.TST_Builder.of(Contact.SObjectType)
			.withOverride(Contact.Email, 'test@example.com').build();
		kern__ApiCall__c apiCall = kern.TST_Factory.newOutboundApiCall(SERVICE_NAME, record.Id);
		kern.API_OutboundTestHelper.assertCallFailed(new List<kern__ApiCall__c>{ apiCall });
	}
}

API_MockFactory ​

apex
kern.API_MockFactory.forService(SERVICE_NAME).body('{"id":"123"}').statusCode(200).register();
kern.API_MockFactory.registerErrorMock(SERVICE_NAME);
kern.API_MockFactory.wasCalled(SERVICE_NAME);

Inbound Web Services ​

Two-class: REST_* (routing, global with sharing) + API_* (logic, public with sharing, extends kern.API_Inbound).

REST Routing Class ​

apex
@RestResource(UrlMapping='/v1/contact-form/*')
global with sharing class REST_ContactForm
{
	@HttpPost
	global static void submitForm()
	{
		kern.API_Dispatcher.processInboundService(API_ContactFormSubmit.class.getName());
	}
}

API Handler ​

public with sharing class API_* extends kern.API_Inbound. Same pattern as Outbound (configure(), getValidationErrors(), DTOs extend kern.DTO_JsonBase) plus:

  • onSuccess() — DML via inherited doInsert()/doUpdate()/doDelete() (NOT kern.DML_Builder)
  • updateResponseDTO() — runs after commit, populate response DTO from result.isSuccess
  • Both Request and Response DTOs: @JsonAccess(Serializable='always' Deserializable='always'), public props
  • No manual JSON.serialize() — framework serializes DTOs

Inbound Test Pattern ​

apex
kern.API_InboundTestHelper.assertCallSuccessful(SERVICE_NAME, request);
kern.API_InboundTestHelper.assertCallAborted(SERVICE_NAME, new DTO_Request(), 'error message');

REST routing test (separate class, suppress PMD.ApexUnitTestClassShouldHaveAsserts):

apex
kern.API_InboundTestHelper.setupRestContext(request);
Test.startTest();
REST_ContactForm.submitForm();
Test.stopTest();
kern.SEL_ApiCall.assertServiceCompleted(API_ContactFormSubmit.class.getName());

Flow Invocables (FLOW_*) ​

apex
public with sharing class FLOW_CreateAccount
{
	@InvocableMethod(Category='Account Management' Label='Create Account' Description='Creates an Account.')
	public static List<DTO_Response> execute(List<DTO_Request> requests)
	{
		if(requests == null || requests.size() != 1)
		{
			throw new IllegalArgumentException('FLOW_CreateAccount expects a single request');
		}
		DTO_Request request = requests.iterator().next();

		Account account = new Account(Name = request.accountName);
		kern.DML_Builder.TransactionResult result = kern.DML_Builder.newTransaction().doInsert(account).execute();

		DTO_Response response = new DTO_Response();
		response.accountId = account.Id;
		response.success = result.isSuccess();
		return new List<DTO_Response>{ response };
	}

	public class DTO_Request
	{
		@InvocableVariable(Label='Account Name' Description='The name for the new Account' Required=true)
		public String accountName;
	}

	public class DTO_Response
	{
		@InvocableVariable(Label='Account ID' Description='The ID of the created Account')
		public Id accountId;

		@InvocableVariable(Label='Success' Description='Whether the operation succeeded')
		public Boolean success;
	}
}

Rules: public with sharing on class + DTOs | guard requests.size() == 1 with a platform-native IllegalArgumentException | @InvocableVariable with Label/Description/Required | @InvocableMethod with Category/Label/Description | kern.DML_Builder for DML | Method takes List<DTO_Request>, returns void or List<DTO_Response>

Flow Test Pattern ​

apex
@SuppressWarnings('PMD.ApexUnitTestClassShouldHaveRunAs')
@IsTest(SeeAllData=false IsParallel=true)
private class FLOW_CreateAccount_TEST
{
	@IsTest
	private static void shouldCreateAccount()
	{
		FLOW_CreateAccount.DTO_Request request = new FLOW_CreateAccount.DTO_Request();
		request.accountName = 'Test Account';

		Test.startTest();
		List<FLOW_CreateAccount.DTO_Response> responses =
			FLOW_CreateAccount.execute(new List<FLOW_CreateAccount.DTO_Request>{ request });
		Test.stopTest();

		Assert.isTrue(responses[0].success, 'Should succeed');
	}

	@IsTest
	private static void shouldThrowWhenNoRequests()
	{
		try
		{
			FLOW_CreateAccount.execute(new List<FLOW_CreateAccount.DTO_Request>());
			Assert.fail('Should throw IllegalArgumentException');
		}
		catch(Exception error)
		{
			Assert.isInstanceOfType(error, IllegalArgumentException.class, 'Incorrect Exception Type');
		}
	}
}

Logging (LOG_Builder) ​

Never System.debug(). Use platform event logging:

apex
kern.LOG_Builder.build().error(e).emitAt('MyClass.myMethod');
kern.LOG_Builder.build().info('Done').emitAt('MyClass.myMethod');
kern.LOG_Builder.build().warn('Threshold exceeded').emitAt('MyClass.myMethod');

// Long form
kern.LOG_Builder.build()
	.error('Payment failed')
	.at('PaymentService.charge')
	.forRecord(paymentId)
	.withContext('amount', payment.Amount__c)
	.emit();

// Scoped
kern.LOG_Builder.LogScope scope = kern.LOG_Builder.scope();
kern.LOG_Builder.build().info('Step 1').emitAt('MyClass.step');
scope.close();

Levels: .error(Exception/String) | .warn(String) | .info(String) | .debug(String) (also List<String>) Chain: .at(String) | .forRecord(Id) | .withContext(key, value) | .withSummary(String)Terminal: .emitAt(String) (shorthand) | .emit()


Testing ​

Structure ​

@IsTest(SeeAllData=false IsParallel=true) private class MyClass_TEST — Constants → @IsTest methods → helpers → inner classes.

TST_Builder ​

apex
Account account = (Account)kern.TST_Builder.of(Account.SObjectType).build();                           // inserted
Account inMemory = (Account)kern.TST_Builder.of(Account.SObjectType).withoutInsertion().build();        // in-memory
Account mockId = (Account)kern.TST_Builder.of(Account.SObjectType).withoutInsertion(true).build();      // mock ID
Account custom = (Account)kern.TST_Builder.of(Account.SObjectType)
	.withOverride(Account.Industry, 'Tech').build();                                                     // override
List<SObject> bulk = kern.TST_Builder.of(Account.SObjectType).withCount(5).buildList();                  // bulk
Account parent = (Account)kern.TST_Builder.of(Account.SObjectType)
	.withChildren(Contact.SObjectType, 3).build();                                                       // children

TST_Mock (DML-free) ​

apex
Account mock = (Account)kern.TST_Mock.of(Account.SObjectType)
	.withOverride(Account.Name, 'Test').build();
// Selector queries now return mock data

Negative-path: kern.TST_Mock.throwsException(SObjectType[, Exception/String]) makes any query against the type throw -- use to exercise catch blocks without mocking at a different layer. Exception fires before record path when both are registered; cleared by kern.TST_Mock.clear() / kern.TST_Mock.clear(SObjectType).

apex
kern.TST_Mock.throwsException(Account.SObjectType, new QueryException('Simulated failure'));
try
{
	new SEL_Accounts().findById(someAccountId);
	Assert.fail('Expected QueryException');
}
catch(QueryException error)
{
	Assert.areEqual('Simulated failure', error.getMessage(), 'Caught the simulated failure');
}

TST_Factory ​

kern.TST_Factory.newFeatureFlag(flagName) | .newOutboundApiCall(service, recordId[, params]) | .newInboundApiCall(service) | .newUser(profileName)

For trigger-action tests, invoke the handler directly on in-memory records built with kern.TST_Builder.of(SObjectType).withoutInsertion().build() (see the Trigger Test Pattern above). For custom-validation tests, deploy the rule as Custom Metadata, then assert with kern.UTIL_ValidationTestHelper.assertRuleFails(record, ruleName) / .assertRulePasses(record, ruleName).

Exception Testing ​

apex
try
{
	methodThatShouldThrow();
	Assert.fail('Should throw IllegalArgumentException');
}
catch(Exception error)
{
	Assert.isInstanceOfType(error, IllegalArgumentException.class, 'Incorrect Exception Type');
}

Never use Boolean exceptionThrown.


Lightning Web Components ​

Never LightningElement. Always ComponentBuilder.

⚠ CRITICAL: The import MUST use the package namespace, not c/:

javascript
WRONG: import { ComponentBuilder } from 'c/componentBuilder';     // WILL NOT RESOLVE
RIGHT: import { ComponentBuilder } from 'kern/componentBuilder';  // Correct namespace
javascript
import { ComponentBuilder } from 'kern/componentBuilder';  // MUST be kern/, not c/

export default class MyComponent extends ComponentBuilder('notification', 'controller')
{
	async handleSave()
	{
		try
		{
			await this.callControllerMethod('saveRecord', { account: this.account });
			this.showSuccessToast('Saved');
		}
		catch(error)
		{
			this.showErrorToast(error);
		}
	}
}

Modules: notification (showSuccess/Error/Warning/InfoToast) | controller (callControllerMethod, handleWireResponse) | navigation (redirectToRecordPage, generateRecordPageURL) | flow-navigation (dispatchFlowNext/Back/FinishEvent) | lightning-message (addMessageChannelSubscription, publishLightningMessage) | all

Base (always): dispatchCustomEvent() | consoleLog() | consoleError() | isLoading

HTML: Lightning base components only (not raw HTML). Meta: apiVersion 67.0, isExposed true.

Jest Test Pattern ​

javascript
jest.mock('kern/componentBuilder', () => ({
	ComponentBuilder: jest.fn().mockImplementation(() =>
	{
		const { LightningElement } = require('lwc');
		return class extends LightningElement
		{
			showSuccessToast = jest.fn();
			showErrorToast = jest.fn();
			callControllerMethod = jest.fn().mockResolvedValue({});
		};
	})
}), { virtual: true });

DTOs ​

Extend kern.DTO_JsonBase + @JsonAccess(Serializable='always' Deserializable='always').

  • Standalone (separate file, exposed to UI): @AuraEnabled public props
  • Nested (inside API class): private props + ApexDoc
  • Always super.populate() first in populate() overrides
  • Use SEL_* in populate() — no inline SOQL

Framework Utilities ​

kern.UTIL_Exceptions (IllegalState/Configuration/NotFoundException) | kern.UTIL_Cache | kern.UTIL_CircuitBreaker | kern.UTIL_Retry | kern.UTIL_FeatureFlag.isEnabled(flagName) | kern.UTIL_TypeResolver | kern.UTIL_String | kern.UTIL_Date | kern.UTIL_List | kern.UTIL_Map | kern.UTIL_SObject | kern.UTIL_Email | kern.UTIL_Random | kern.UTIL_SObjectDescribe (request-cached describe wrapper — getDescribe(sObjectType) or getDescribe(String objectApiName) — BOTH return a kern.UTIL_SObjectDescribe instance (the wrapper itself, NOT a native Schema.DescribeSObjectResult); the String overload returns null for an unknown/blank name, resolving an object API name to the wrapper WITHOUT raw Schema.getGlobalDescribe(). On the returned wrapper call .getField(name) (namespace-aware by default), .getFieldDescribe(name) returns cached DescribeFieldResult with isUpdateable/isCreateable; static getCachedFieldDescribe(SObjectField) / getCachedFieldName(SObjectField) for token-keyed lookups; never use raw Schema.getGlobalDescribe() / fields.getMap() from package code — namespace handling is the trap that bites)

The roster above is a selection, not the whole surface — other pillars in this document cover kern.UTIL_AsyncChain, kern.UTIL_HttpClient, kern.UTIL_ValidationRule, kern.UTIL_System, kern.UTIL_Limits and more. The reference-page rule at the top of this document is the authority. Some UTIL_* classes read like general-purpose helpers but are framework-internal and will not compile from a subscriber org: UTIL_Set, UTIL_Comparators, UTIL_Crypto, UTIL_CronExpression, UTIL_NumberRange, UTIL_Boolean, UTIL_Character. Check for a docs/reference/apex/<ClassName>.md page before reaching for any UTIL_* name you have not seen used here. There is no UTIL_Number or UTIL_Security in any version.

Framework-shipped Feature Flags ​

Ten pre-built FeatureFlag__mdt records ship with the package. Query via kern.UTIL_FeatureFlag.isEnabled('FlagDeveloperName'); flip values by deploying an override record that sets IsEnabledByDefault__c (and, if strategies are attached, ResultOnNoMatch__c).

FlagDefaultPurpose
UserModeQueries_EnabledtrueDrives the default AccessLevel on kern.QRY_Builder / kern.SEL_Base. When true queries run USER_MODE (CRUD+FLS+sharing enforced); when false they fall back to SYSTEM_MODE. Emergency kill switch only.
UserModeDml_EnabledtrueSame for kern.DML_Builder writes (insert/update/delete/upsert/undelete).
MaskingFramework_EnabledtrueMaster switch for the data-masking framework (MaskingRule__mdt + MaskingTarget__mdt).
AsyncChaintrueMaster kill switch for kern.UTIL_AsyncChain. When false new chain executions abort immediately.
DisableAllAPIsfalseDisables both inbound and outbound calls through the web-services framework.
DisableAllInboundAPIsfalseDisables inbound kern.API_Inbound routing only.
DisableAllOutboundAPIsfalseDisables outbound kern.API_Outbound calls only.
MockAllAPIsfalseReturns mock responses for every outbound call (tests + dry runs).
MockAllInboundAPIsfalseReturns mock responses for every inbound call.
TestFeatureFlagtrueExample / smoke-test flag. Safe to leave as-is.

Data Masking (declarative, no subscriber code) ​

Sensitive-data redaction is declarative — subscribers configure MaskingRule__mdt + MaskingTarget__mdt records, there is no subscriber-written Apex class. The framework runs masking as a before-insert / before-update pre-step on the trigger dispatcher for package-owned objects (ApiCall__c, ApiIssue__c, AsyncChainExecution__c, LogEntryEvent__e / LogEntry__c).

Ship set: 18 MaskingRule__mdt records (3 active by default — MaskSecretKeys JSON-key redaction, MaskPaymentCard Luhn-validated card redaction, and MaskCreditCard, the original card rule that MaskPaymentCard replaces, kept for compatibility; 15 inactive templates for SSN, email, phone, IBAN, SWIFT, Medicare, JWT, AWS keys, postal address, free text, IPv4, etc.). 12 MaskingTarget__mdt wildcard records wire the active rules onto all four logged objects; where the payment-card and credit-card rules are wired to the same object, the payment-card rule does the work.

Mode picklist values on MaskingRule__mdt.Mode__c: Regex, JsonKey, ExactMatch, CreditCard. Optional short-circuit fields: MinInputLength__c (skip rule for shorter values) and ApplicableFieldTypes__c (semicolon-delimited System.DisplayType names — e.g., STRING;TEXTAREA;ENCRYPTEDSTRING). Failure handling: MaskingRule__mdt.FailureAction__c = LogAndContinue / WriteFailureMarker / BlockDml. Kill switch: FeatureFlag.MaskingFramework_Enabled (default true). Per-object opt-out: TriggerSetting__mdt.ApplyMasking__c. Performance logging (opt-in): LogSetting__c.EnableMaskerPerformanceLogging__c = true with LogSetting__c.MaskerPerformanceThresholdMs__c (default 100 ms) emits LogEntryEvent__e for batches exceeding the threshold.

Subscribers extend the default ship set by deploying additional MaskingRule__mdt + MaskingTarget__mdt records; no Apex needed.


Async Chain Orchestration ​

kern.UTIL_AsyncChain — sequences steps across Queueable transactions with shared state, error handling, and progress tracking.

apex
kern.UTIL_AsyncChain.newChain('DataMigration')
	.then(new LoadDataStep())
	.then(new TransformDataStep(), true)
	.withInitialContext('batchSize', 200)
	.onError(new NotifyAdminStep())
	.execute();

IF_Chain.Step interface: Implement kern.UTIL_AsyncChain.StepResult work(kern.UTIL_AsyncChain.ChainContext context) or extend kern.UTIL_AsyncChain.ChainStep (abstract, adds the stepName field and the isIdempotent() declaration). Never declare a constructor on a step class — the framework recreates every step by class name in each hop, and from inside the package only the implicit constructor of a class that declares none (or a global constructor on a global class) is reachable; a step with its own public constructor fails on its first hop. Set stepName on the instance before handing it to the builder (MyStep step = new MyStep(); step.stepName = 'Load accounts'; ...then(step)); the builder records it at build time.

apex
public with sharing class LoadDataStep extends kern.UTIL_AsyncChain.ChainStep
{
	public override kern.UTIL_AsyncChain.StepResult work(kern.UTIL_AsyncChain.ChainContext context)
	{
		List<Account> accounts = new SEL_Accounts().toList();
		context.put('accountCount', accounts.size());
		return kern.UTIL_AsyncChain.succeeded('Loaded ' + accounts.size() + ' accounts');
	}
}

ChainBuilder: .then(step)/.then(step, continueOnError)/.withInitialContext(key, value)/.withMaxSteps(n)/.withMaxContextSize(n)/.withDelayMinutes(n)/.withDeduplicationKey(key)/.allowRestart()/.withAsyncOptions(opts)/.onError(step)/.onComplete(step)/.execute()/.execute(correlationId)Operator re-drive declarations (both default off — the Chain Monitor's retry/restart actions stay unavailable until the author declares them; neither changes how the chain runs): public override Boolean isIdempotent() { return true; } on a ChainStep (an override, never a constructor; declares the step safe to re-run, gating retry-from-failed-step) / .allowRestart() on the builder (declares the whole chain safe to restart from step 1 and captures the as-built context snapshot for the restart to replay) ChainContext: .put(key, value)/.get(key)/.getAs(key, Type)/.has(key)/.getChainExecutionId()/.getCorrelationId()/.getPreviousStepResult()/.getCurrentStepIndex()/.idempotencyKey()/.idempotencyKey(recordId)/.idempotencyKey(grain)/.getAttempt()/.isFinalAttempt()/.isFinalAttempt(error)StepResult: kern.UTIL_AsyncChain.succeeded()/.succeeded(msg)/.succeeded(msg, data)/.failed(msg)/.failed(exception)/.failedPermanently(msg)/.failedPermanently(exception) — properties: success, message, data, error, permanentFailureApiStep: Wraps kern.API_Outbound handler as a chain step:

apex
new kern.UTIL_AsyncChain.ApiStep(API_SendEmail.class)
	.credential('Gateway')
	.withParameter(API_SendEmail.PARAM_RECIPIENT, 'test@example.com')
	.withParameterFrom('subject', 'emailSubject')
	.triggeringRecordFrom('recordId')
	.withRetry(3, 30).retryOn(new Set<Integer>{500, 503})

Status: kern.UTIL_AsyncChain.getStatus(chainExecutionId) → Map<String, Object>


Schedulable Framework ​

Subscriber schedulable classes must be global inherited sharing extending kern.SCHED_Base.

apex
@SuppressWarnings('PMD.AvoidGlobalModifier')
global inherited sharing class SCHED_PurgeOldRecords extends kern.SCHED_Base
{
	public override List<kern.DTO_ScheduledParameterDefinition> getParameterDefinitions()
	{
		return new List<kern.DTO_ScheduledParameterDefinition>
		{
			kern.DTO_ScheduledParameterDefinition.of('objectName').required(),
			kern.DTO_ScheduledParameterDefinition.of('batchSize').asNumeric().withDefault('2000')
		};
	}

	public void execute(SchedulableContext context)
	{
		String objectName = getTextParameter('objectName');
		Integer batchSize = getNumericParameter('batchSize');
	}
}

SCHED_Base accessors: getTextParameter(name)/getNumericParameter(name)/getBooleanParameter(name)DTO_ScheduledParameterDefinition: .of(name) → .required()/.asNumeric()/.asBoolean()/.withDefault(value)/.withDescription(text)


Common Agent Mistakes (beyond Critical Rules table above) ​

#WrongRight
1DTO missing @JsonAccess@JsonAccess(Serializable='always' Deserializable='always')
2Missing super.configure()Always call super.configure() first
3Missing super.populate()Always call super.populate() first
4Missing super.getRequiredInputs()Call super and add to returned set
5.toList() on semi-join subqueryPass Builder directly to .isIn()/.notIn()
6new UTIL_HttpClient()kern.UTIL_HttpClient.post('Cred', '/path')
7Boolean exceptionThrownAssert.fail + Assert.isInstanceOfType
8Namespace missing in subscriberkern.QRY_Builder, kern.SEL_Base, kern.TRG_Base
9inherited sharing in subscriber codewith sharing — except SCHED_* which uses inherited sharing
10global on non-REST/SCHED classespublic everywhere — only REST_* and SCHED_* need global
11Manual JSON.serialize in responseUse updateResponseDTO() — framework serializes
12[SELECT ...] in test classesUse kern.QRY_Builder or SEL_* even in tests
13Missing @SuppressWarnings on tests@SuppressWarnings('PMD.ApexUnitTestClassShouldHaveRunAs') on test classes
14import from 'c/componentBuilder'import from 'kern/componentBuilder' — must use package namespace
15Line break after kern.Class in a chainKeep the first .method() on the class's line — a break compiles kern as a variable (Variable does not exist: kern)
16.cls written without its .cls-meta.xmlAlways create the paired *-meta.xml — a lone .cls is silently skipped (No local changes to deploy)
17triggerOldMap.get(id) to pair old/new in an actionBuild new Map<Id, SObject>(oldRecords) from the parameter — triggerOldMap is null outside live trigger context (NullPointerException in direct-invocation tests)
18Schema.getGlobalDescribe().get(name) to resolve an objectkern.UTIL_SObjectDescribe d = kern.UTIL_SObjectDescribe.getDescribe(name) — returns the wrapper (type kern.UTIL_SObjectDescribe, NOT Schema.DescribeSObjectResult), null for an unknown name; then d.getField(name)

KernDX vs fflib ​

ConceptfflibKernDX
Selectorsextends fflib_SObjectSelectorextends kern.SEL_Base
QueriesDatabase.query(newQueryFactory().toSOQL())kern.QRY_Builder.selectFrom().toList()
DMLfflib_SObjectUnitOfWorkkern.DML_Builder.newTransaction().execute()
Triggersfflib_SObjectDomainextends kern.TRG_Base implements kern.IF_Trigger.*
LoggingSystem.debug()kern.LOG_Builder.build().error(e).emitAt()