Custom Add-On API Object Reference.
Table of Contents
- Overview
- Quick Start
- Add-On Declaration Model
- Module Contract
- Runtime Context Model
- Runtime API Model
- Script Model
- Job and Task Models
- Metadata Models
- Core Enums Available in SDK
- How to Use the SDK
- Calling and Running Custom Add-Ons
- Debugging Custom Add-Ons
- Deployment and Distribution
- Best Practices
- Common Pitfalls
- Related Articles
Overview ⇧
This page is a practical reference for developers who want to build and run Custom SFDMU Add-On modules. It documents:
- the Add-On declaration model in
export.jsonandaddons.json; - the SDK object model and interfaces exposed to custom modules;
- execution lifecycle and event behavior;
- how to create, debug, package, and run custom add-ons.
This document is detailed on purpose and assumes you are a programmer, but new to SFDMU Add-On internals.
Quick Start ⇧
If you need the shortest path from zero to a running add-on, use this flow:
- Create a template:
node scripts/create-custom-addon-template.js my-addon - Open:
custom-addon-sdk/custom-modules/my-addon - Install and compile:
npm installnpm run compile - Register the module in
export.jsonusingmodule. - Run SFDMU and verify the add-on logs.
Example export.json snippet:
{
"addons": [
{
"event": "onBefore",
"module": "./custom-addon-sdk/custom-modules/my-addon/dist/index.js"
}
]
}
Notes:
argsis optional and can be omitted when your module does not need parameters.- At least one locator must exist:
moduleorpath. - Relative paths are resolved from the directory where
export.jsonis located.
Add-On Declaration Model ⇧
AddOnManifestDefinition ⇧
Custom add-ons are declared as manifest entries in:
- script-level arrays inside
export.json; - object-level arrays inside
export.json; - optional external
addons.jsonfile in the run folder.
Each entry is normalized into the runtime Add-On manifest object.
| Property | Type | Required | Description |
|---|---|---|---|
event |
string |
Yes | Add-on event, for example onBeforeUpdate. |
module |
string |
No* | Preferred locator (package name or file path). |
path |
string |
No* | Legacy locator, still supported. |
description |
string |
No | Human-readable module description shown in context. |
args |
Record<string, unknown> |
No | Arbitrary config object passed to module methods. |
command |
string |
No | Command scope filter. Defaults to sfdmu:run. |
excluded |
boolean |
No | Skips this entry when true. |
objects |
string[] |
No | Optional object filter for global declarations. |
objectName |
string |
No | Single-object shortcut. |
* At least one locator must be present: module or path.
Property cross-links (Manifest -> Runtime):
| Manifest Property | Used In Runtime/SDK | Main Documentation |
|---|---|---|
event |
context.eventName, Event Values |
Supported Add-On API Events |
module |
Module Resolution Behavior, ISfdmuRunCustomAddonModule | Custom Add-On API |
path |
Locator Rules (module vs path) |
Custom Add-On API |
description |
context.description |
Add-On API Overview |
args |
onExecute(context, args), onInit(context, args) |
Custom Add-On API |
objects |
context.objectName, task.sObjectName |
ScriptObject |
objectName |
context.objectName |
ScriptObject |
command |
Command filter in add-on loader pipeline | Running the Plugin |
excluded |
Entry is skipped before invocation | Add-On API Overview |
Example of a complete manifest entry (object-level):
{
"beforeUpdateAddons": [
{
"event": "onBeforeUpdate",
"command": "sfdmu:run",
"module": "./custom-addon-sdk/custom-modules/my-addon/dist/index.js",
"path": "./custom-addon-sdk/custom-modules/my-addon/dist/index.js",
"description": "Normalize Account fields before update",
"objects": ["Account"],
"objectName": "Account",
"excluded": false,
"args": {
"trimFields": ["Name", "BillingCity"],
"schemaVersion": 1
}
}
]
}
How it works:
moduleis used as the effective locator.argsis passed toonExecute(context, args)as-is.objectsandobjectNameallow strict targeting.excluded: truedisables the add-on without removing configuration.
Locator Rules (module vs path)
⇧
modulehas priority when bothmoduleandpathare provided.modulecan be:- package name (
my-sfdmu-addon); - relative file path (
./custom-addon-sdk/custom-modules/demo/dist/index.js); - absolute file path (
C:/.../index.js).
- package name (
pathis supported for backward compatibility with older configs.
Example with both fields present:
{
"addons": [
{
"event": "onBefore",
"module": "./custom-addon-sdk/custom-modules/demo/dist/index.js",
"path": "./legacy/path/that-will-be-ignored.js"
}
]
}
How it works:
- runtime normalizes locator fields and uses the
modulevalue; pathremains legacy-compatible but does not overridemodule.
Module Resolution Behavior ⇧
At runtime, SFDMU resolves custom add-ons with this strategy:
- Try dynamic ESM import for the requested locator.
- Try alternate extension (
.ts<->.js) when relevant. - For package-like locators, try global
node_modulesresolution. - Fallback to CommonJS
require.
This is why both ESM and CommonJS add-ons are supported.
Examples:
{
"addons": [
{
"event": "onBefore",
"module": "my-company-sfdmu-addon"
},
{
"event": "onAfter",
"module": "./custom-addon-sdk/custom-modules/demo/index.ts"
},
{
"event": "onBeforeUpdate",
"module": "./custom-addon-sdk/custom-modules/demo/dist/index.js"
}
]
}
How it works:
- package specifier tries local install first, then global module locations;
.ts/.jsfile paths can be resolved as alternative entry points;- if ESM import fails, loader can fallback to CommonJS
require.
Event Values ⇧
Supported event names:
onBeforeonAfteronDataRetrievedonBeforeUpdateonAfterUpdatefilterRecordsAddons
For event timing, use: Supported Add-On API Events
Example event usage patterns:
{
"beforeAddons": [{ "module": "./addons/global-before.js" }],
"dataRetrievedAddons": [{ "module": "./addons/global-data-retrieved.js" }],
"afterAddons": [{ "module": "./addons/global-after.js" }],
"objects": [
{
"query": "SELECT Id, Name FROM Account",
"operation": "Upsert",
"beforeAddons": [{ "module": "./addons/object-before.js" }],
"filterRecordsAddons": [{ "module": "./addons/filter.js" }],
"beforeUpdateAddons": [{ "module": "./addons/before-update.js" }],
"afterUpdateAddons": [{ "module": "./addons/after-update.js" }],
"afterAddons": [{ "module": "./addons/object-after.js" }]
}
]
}
How it works:
- global arrays fire at script-level lifecycle points;
- object arrays fire in object processing loops;
- choose event by data availability and intended mutation moment.
Event-to-data quick links:
onBefore-> script-level setup: ISfdmuRunCustomAddonScriptonDataRetrieved-> full dataset phase: ISfdmuRunCustomAddonApiServicefilterRecordsAddons-> pre-update filtering: ISFdmuRunCustomAddonTask.tempRecordsonBeforeUpdate-> final mutation point: ISfdmuRunCustomAddonProcessedDataonAfterUpdate-> post-update integration: ISFdmuRunCustomAddonTask
Module Contract ⇧
ISfdmuRunCustomAddonModule ⇧
Every custom module implements this interface.
| Member | Type | Required | Description |
|---|---|---|---|
runtime |
ISfdmuRunCustomAddonRuntime |
Yes | Runtime bridge to script, API service, logging, and helpers. |
onExecute(context, args) |
Promise<ISfdmuRunCustomAddonResult> |
Yes | Main execution entry point when an event fires. |
onInit(context, args) |
Promise<ISfdmuRunCustomAddonResult> |
No | One-time initialization hook after script load and before job execution. |
Minimal implementation:
import type {
ISfdmuRunCustomAddonContext,
ISfdmuRunCustomAddonModule,
ISfdmuRunCustomAddonResult,
ISfdmuRunCustomAddonRuntime
} from 'sfdmu-addon-sdk';
export default class MyAddon implements ISfdmuRunCustomAddonModule {
public runtime: ISfdmuRunCustomAddonRuntime;
public constructor(runtime: ISfdmuRunCustomAddonRuntime) {
this.runtime = runtime;
}
public async onInit(
context: ISfdmuRunCustomAddonContext,
args: Record<string, unknown>
): Promise<ISfdmuRunCustomAddonResult> {
this.runtime.service.log(this, `Init ${context.moduleDisplayName}`, 'INFO');
this.runtime.service.log(this, args, 'JSON');
return { cancel: false };
}
public async onExecute(
context: ISfdmuRunCustomAddonContext,
args: Record<string, unknown>
): Promise<ISfdmuRunCustomAddonResult> {
this.runtime.service.log(this, `Execute ${context.eventName}`, 'INFO');
this.runtime.service.log(this, args, 'JSON');
return { cancel: false };
}
}
ISfdmuRunCustomAddonResult ⇧
| Property | Type | Description |
|---|---|---|
cancel |
boolean |
When true, stops the current SFDMU job after add-on execution. |
Example of controlled abort:
public async onExecute(
context: ISfdmuRunCustomAddonContext,
args: Record<string, unknown>
): Promise<ISfdmuRunCustomAddonResult> {
const enabled = Boolean(args['enabled']);
if (!enabled) {
this.runtime.service.log(this, 'Add-on disabled by args.enabled=false', 'WARNING');
return { cancel: false };
}
const safetyFlag = Boolean(args['confirmDestructiveAction']);
if (!safetyFlag) {
this.runtime.service.log(this, 'Missing safety flag. Aborting by add-on policy.', 'ERROR');
return { cancel: true };
}
return { cancel: false };
}
How it works:
- return
{ cancel: true }only for explicit stop conditions; - use log messages so operators can understand why job was aborted.
Runtime Context Model ⇧
ISfdmuRunCustomAddonContext ⇧
Context is passed to onInit and onExecute.
| Property | Type | Description |
|---|---|---|
eventName |
string |
Triggered event name, for example onBeforeUpdate. |
moduleDisplayName |
string |
Runtime name with prefix, for example custom:my-addon. |
objectName |
string |
API name of the object currently being processed. |
objectDisplayName |
string |
User-facing object display label. |
description |
string |
Description from the add-on declaration. |
objectSetIndex |
number | undefined |
Zero-based object set index in multi-set runs. |
passNumber |
number | undefined |
Zero-based pass index for multi-pass events. |
isFirstPass |
boolean | undefined |
true only for the first pass in current object set. |
Example context-aware behavior:
public async onExecute(
context: ISfdmuRunCustomAddonContext,
args: Record<string, unknown>
): Promise<ISfdmuRunCustomAddonResult> {
this.runtime.service.log(this, `event=${context.eventName}`, 'INFO');
this.runtime.service.log(this, `object=${context.objectName}`, 'INFO');
this.runtime.service.log(this, `set=${String(context.objectSetIndex ?? 0)}`, 'INFO');
this.runtime.service.log(this, `pass=${String(context.passNumber ?? 0)}`, 'INFO');
if (context.isFirstPass) {
this.runtime.service.log(this, 'Executing first-pass initialization logic', 'INFO');
}
return { cancel: false };
}
How it works:
- same module can react differently depending on event/object/pass;
- pass metadata is useful in multi-pass update pipelines.
Runtime API Model ⇧
ISfdmuRunCustomAddonRuntime ⇧
This runtime is exposed as this.runtime in your module.
Paths and Script ⇧
| Member | Type | Description |
|---|---|---|
basePath |
string |
Base directory of current run. |
sourcePath |
string |
Runtime source folder for current object-set scope. |
targetPath |
string |
Runtime target folder for current object-set scope. |
getScript() |
ISfdmuRunCustomAddonScript |
Returns the active script model (export.json in runtime form). |
Example:
const script = this.runtime.getScript();
this.runtime.service.log(this, `basePath=${this.runtime.basePath}`, 'INFO');
this.runtime.service.log(this, `sourcePath=${this.runtime.sourcePath}`, 'INFO');
this.runtime.service.log(this, `targetPath=${this.runtime.targetPath}`, 'INFO');
this.runtime.service.log(this, `objectCount=${String(script.getAllObjects().length)}`, 'INFO');
How it works:
- runtime paths point to the current run scope;
getScript()gives access to live script config and helper methods.
Logging Helpers ⇧
| Method | Description |
|---|---|
logFormattedInfo |
Formatted info logging. |
logFormattedInfoVerbose |
Verbose info logging. |
logFormattedWarning |
Warning logging. |
logFormattedError |
Error logging. |
logFormatted |
Generic formatted logging with explicit type. |
logAddonExecutionStarted |
Start marker for execution logs. |
logAddonExecutionFinished |
End marker for execution logs. |
Example:
this.runtime.logAddonExecutionStarted(this);
this.runtime.logFormattedInfo(this, 'Addon started for object %s', context.objectName);
this.runtime.logFormattedInfoVerbose(this, 'args=%s', JSON.stringify(args));
this.runtime.logFormattedWarning(this, 'Running in validation mode only');
this.runtime.logAddonExecutionFinished(this);
How it works:
- formatted logs support token replacement;
- start/finish markers improve traceability in long runs.
Data Helpers ⇧
| Method | Description |
|---|---|
validateSupportedEvents |
Validates current event against allowed events list. |
queryMultiAsync |
Runs multiple SOQL queries and returns combined records. |
createFieldInQueries |
Builds chunked SOQL IN queries safely. |
updateTargetRecordsAsync |
Applies Insert/Update/Upsert/Delete style operations to target side. |
transferContentVersions |
Transfers ContentVersion payloads and metadata. |
Example:
const queries = this.runtime.createFieldInQueries(
['Id', 'Name'],
'Id',
'Account',
['001AAA', '001AAB', '001AAC']
);
const sourceRows = await this.runtime.queryMultiAsync(true, queries);
const toUpdate = sourceRows.map((row) => ({
...row,
Name: String(row['Name'] ?? '').trim(),
}));
await this.runtime.updateTargetRecordsAsync('Account', OPERATION.Update, toUpdate);
How it works:
createFieldInQueriesavoids oversizedINclauses;queryMultiAsyncreturns merged results from generated queries;updateTargetRecordsAsyncapplies DML/engine logic via runtime.
ISfdmuRunCustomAddonApiService ⇧
This service is exposed as this.runtime.service.
| Method | Returns | Description |
|---|---|---|
log(module, message, type, ...tokens) |
void |
General-purpose logging. Supports string/object payloads. |
getProcessedData(context) |
ISfdmuRunCustomAddonProcessedData |
Live data for the current processing step. |
getPluginRunInfo() |
ISfdmuRunCustomAddonCommandRunInfo |
Run-level command and plugin metadata. |
getPluginJob() |
ISFdmuRunCustomAddonJob |
Current migration job model. |
getPluginTask(context) |
ISFdmuRunCustomAddonTask | null |
Current object task by context. |
Example:
const runInfo = this.runtime.service.getPluginRunInfo();
this.runtime.service.log(this, `command=${runInfo.pinfo.commandString}`, 'INFO');
this.runtime.service.log(this, `source=${runInfo.sourceUsername}`, 'INFO');
this.runtime.service.log(this, `target=${runInfo.targetUsername}`, 'INFO');
const task = this.runtime.service.getPluginTask(context);
if (task) {
this.runtime.service.log(this, `task=${task.sObjectName}`, 'INFO');
const processed = this.runtime.service.getProcessedData(context);
this.runtime.service.log(this, `toInsert=${processed.recordsToInsert.length}`, 'INFO');
}
How it works:
- run info is ideal for diagnostics and environment-aware logic;
- task + processed data expose live migration state for mutation.
Notes:
getProcessedData()is most useful in update/filter-oriented events.- In events where no task is active, the service can return empty/default structures.
Script Model ⇧
ISfdmuRunCustomAddonScript ⇧
Represents the active runtime view of export.json with runtime links.
Main groups of properties:
- orgs and selected source/target orgs (
orgs,sourceOrg,targetOrg); - object configuration (
objects,getAllObjects(),addObjectToFirstSet()); - object structure helpers (
objectSets,objectsMap); - runtime tuning and CSV options (
bulkThreshold,csvFileDelimiter,csvFileEncoding, etc.); - object selection and safety controls (
excludedObjects,groupQuery,canModify); - global add-on arrays (
beforeAddons,afterAddons,dataRetrievedAddons); - job link (
job), object-set runtime position (objectSetIndex), and mapping helper (sourceTargetFieldMapping).
Example:
const script = this.runtime.getScript();
if (script.csvFileDelimiter === ';') {
this.runtime.service.log(this, 'Semicolon CSV mode detected', 'INFO');
}
const allObjects = script.getAllObjects();
const hasAccount = allObjects.some((obj) => obj.name === 'Account');
this.runtime.service.log(this, `hasAccount=${String(hasAccount)}`, 'INFO');
How it works:
- script-level options allow add-on behavior to align with runtime configuration;
getAllObjects()gives full object scope beyond current task.
Property cross-links (Script -> Related Models):
| Script Property | Related Model | Main Documentation |
|---|---|---|
orgs / sourceOrg / targetOrg |
ISfdmuRunCustomAddonScriptOrg | ScriptOrg Object |
objects |
ISfdmuRunCustomAddonScriptObject | ScriptObject |
beforeAddons / afterAddons / dataRetrievedAddons |
AddOnManifestDefinition | Script Object |
job |
ISFdmuRunCustomAddonJob | Add-On API Overview |
csvFileDelimiter / csvFileEncoding |
Runtime CSV behavior | Script Object |
excludedObjects / groupQuery / canModify |
Runtime object scope and org-modification controls | Script Object |
objectSets / objectsMap / sourceTargetFieldMapping |
Runtime structure and mapping helpers | Script Object |
ISfdmuRunCustomAddonScriptObject ⇧
Represents one object-level migration config plus runtime markers.
Important fields:
- operation and delete flags (
operation,deleteOldData,deleteFromSource, etc.); - mapping and transform config (
fieldMapping,useFieldMapping,useValuesMapping); - API mode and delete-order controls (
alwaysUseRestApi,alwaysUseBulkApi,alwaysUseBulkApiToUpdateRecords,respectOrderByOnDeleteRecords); - query scope controls (
useQueryAll,queryAllTarget,skipExistingRecords); - event arrays (
beforeAddons,afterAddons,beforeUpdateAddons,afterUpdateAddons,filterRecordsAddons); - query/filter controls (
query,deleteQuery,sourceRecordsFilter,targetRecordsFilter); - runtime identity (
name,objectName,isAutoAdded); - external-id and exclusion helpers (
originalExternalId,originalExternalIdIsEmpty,excludedFieldsFromUpdate); - runtime metadata links (
processAllSource,processAllTarget,isFromOriginalScript,sourceSObjectDescribe,targetSObjectDescribe,isExtraObject).
Example:
const task = this.runtime.service.getPluginTask(context);
if (!task) {
return { cancel: false };
}
const scriptObject = task.scriptObject;
this.runtime.service.log(this, `operation=${String(scriptObject.operation)}`, 'INFO');
this.runtime.service.log(this, `externalId=${String(scriptObject.externalId ?? '')}`, 'INFO');
this.runtime.service.log(this, `useValuesMapping=${String(scriptObject.useValuesMapping ?? false)}`, 'INFO');
How it works:
scriptObjectreflects current object-level migration behavior;- useful for branching logic by operation or mapping mode.
Related property links in main docs:
operation-> ScriptObject.operationquery/deleteQuery-> ScriptObject.query/deleteQueryuseFieldMapping/fieldMapping-> Fields MappinguseValuesMapping-> Values MappingupdateWithMockData/mockFields-> Data AnonymizationrespectOrderByOnDeleteRecords-> ScriptObject.respectOrderByOnDeleteRecordsalwaysUseBulkApiToUpdateRecords-> ScriptObject.alwaysUseBulkApiToUpdateRecordsalwaysUseRestApi-> ScriptObject.alwaysUseRestApialwaysUseBulkApi-> ScriptObject.alwaysUseBulkApi- delete flags -> Delete Source, Delete by Hierarchy
ISfdmuRunCustomAddonScriptOrg ⇧
Represents resolved source/target org info in runtime.
| Property | Description |
|---|---|
name |
Alias/name from script context. |
orgUserName |
Effective org username/alias used for connection. |
orgId |
Salesforce org Id resolved for the connected org. |
instanceUrl |
Optional instance URL for token-based auth. |
accessToken |
Optional access token for token-based auth. |
media |
Runtime media type (Org or File). |
isSource |
True when this org is used as source in the current run. |
orgDescribe |
Runtime org metadata map keyed by object API name. |
isPersonAccountEnabled |
True when Person Accounts are available in this org. |
organizationType |
Organization type returned by Salesforce. |
isSandbox |
True when Salesforce reports this org as sandbox. |
isScratch |
True when current org is detected as scratch org. |
connectionData |
Runtime connection payload with instanceUrl, token, apiVersion, proxyUrl. |
isConnected |
True when access token is available for this org entry. |
isDescribed |
True when org metadata was already described. |
objectNamesList |
Runtime list of described object API names. |
isProduction |
True when org is production according to Salesforce metadata. |
isDeveloper |
True when org is Developer Edition. |
instanceDomain |
Runtime domain extracted from instanceUrl. |
isFileMedia |
True for csvfile mode. |
isOrgMedia |
True for org mode. |
Example:
const script = this.runtime.getScript();
const sourceOrg = script.sourceOrg;
const targetOrg = script.targetOrg;
this.runtime.service.log(this, `source=${String(sourceOrg?.name ?? '')}`, 'INFO');
this.runtime.service.log(this, `target=${String(targetOrg?.name ?? '')}`, 'INFO');
this.runtime.service.log(this, `sourceIsFile=${String(sourceOrg?.isFileMedia ?? false)}`, 'INFO');
How it works:
- org metadata allows different logic for org-to-org vs csvfile scenarios.
ISfdmuRunCustomAddonScriptAddonManifestDefinition ⇧
The SDK view of add-on declaration as seen from object/script models.
| Property | Description |
|---|---|
path |
Legacy locator path. |
module |
Preferred module locator (path or package). |
description |
User-facing description. |
excluded |
Skip switch. |
args |
Raw argument object passed to module methods. |
Example:
const script = this.runtime.getScript();
const before = script.beforeAddons ?? [];
before.forEach((addon) => {
this.runtime.service.log(this, `module=${String(addon.module ?? addon.path ?? '')}`, 'INFO');
this.runtime.service.log(this, `excluded=${String(addon.excluded)}`, 'INFO');
});
How it works:
- you can inspect declared add-ons to implement validation/reporting logic.
ISfdmuRunCustomAddonScriptMappingItem ⇧
Field mapping item.
| Property | Description |
|---|---|
targetObject |
Target object name for cross-object mapping. |
sourceField |
Source field API name. |
targetField |
Target field API name. |
Example:
const task = this.runtime.service.getPluginTask(context);
const mappings = task?.scriptObject.fieldMapping ?? [];
for (const mapItem of mappings) {
this.runtime.service.log(
this,
`${mapItem.sourceField} -> ${mapItem.targetObject}.${mapItem.targetField}`,
'INFO'
);
}
How it works:
- mapping items explain how source fields are projected to target schema.
ISfdmuRunCustomAddonScriptMockField ⇧
Mock/anonymization field rule item.
| Property | Description |
|---|---|
name |
Field API name. |
excludeNames |
Excluded field names. |
pattern |
Mock generation pattern. |
locale |
Optional casual locale key, for example ru_RU. |
excludedRegex |
Regex exclusion rule. |
includedRegex |
Regex inclusion rule. |
Example:
const task = this.runtime.service.getPluginTask(context);
const mockFields = task?.scriptObject.mockFields ?? [];
mockFields.forEach((field) => {
this.runtime.service.log(
this,
`mock field=${field.name} pattern=${field.pattern} locale=${String(field.locale ?? '')}`,
'INFO'
);
});
How it works:
- this metadata is useful when add-on logic must coordinate with anonymization rules.
Job and Task Models ⇧
ISFdmuRunCustomAddonJob ⇧
Represents the active migration job.
| Member | Description |
|---|---|
tasks |
Array of all object tasks in current job. |
getTaskByFieldPath(fieldPath) |
Resolves a task by lookup path and returns { task, field }. |
Example:
const job = this.runtime.service.getPluginJob();
this.runtime.service.log(this, `taskCount=${job.tasks.length}`, 'INFO');
const resolved = job.getTaskByFieldPath('Account.Owner.Manager.Name');
if (resolved.task) {
this.runtime.service.log(
this,
`fieldPath mapped to task=${resolved.task.sObjectName}, field=${resolved.field}`,
'INFO'
);
}
How it works:
taskslets you inspect the whole job graph;getTaskByFieldPathhelps locate lookup-driven dependencies.
ISFdmuRunCustomAddonTask ⇧
Represents one object task in current job pipeline.
Useful properties:
- task identity and operation (
sObjectName,targetSObjectName,operation); - source/target maps (
sourceToTargetRecordMap,sourceToTargetFieldNameMap); - task data (
sourceTaskData,targetTaskData,sourceData,targetData); - current working sets (
processedData,tempRecords,fieldsInQuery,fieldsToUpdate); - helpers (
mapRecords(records)for values mapping).
Example:
const task = this.runtime.service.getPluginTask(context);
if (!task) {
return { cancel: false };
}
this.runtime.service.log(this, `sObject=${task.sObjectName}`, 'INFO');
this.runtime.service.log(this, `operation=${String(task.operation)}`, 'INFO');
this.runtime.service.log(this, `updateMode=${task.updateMode}`, 'INFO');
if (context.eventName === 'filterRecordsAddons') {
task.tempRecords = task.tempRecords.filter((record) => String(record['Name'] ?? '').length > 0);
}
How it works:
tempRecordsis designed for filtering transformations in filter events;- operation/update mode allow precise event-phase logic.
Property flow cross-links:
task.scriptObject-> ISfdmuRunCustomAddonScriptObjecttask.processedData-> ISfdmuRunCustomAddonProcessedDatatask.sourceData/task.targetData-> ISfdmuRunCustomAddonTaskDatatask.operation-> OPERATION enumtask.data.fieldsInQueryMap/processedData.fields-> ISfdmuRunCustomAddonSFieldDescribe
ISfdmuRunCustomAddonTaskData ⇧
Represents source-side or target-side data state for the task.
| Property | Description |
|---|---|
mediaType |
Org or File media type. |
isSource |
True for source-side task data. |
idRecordsMap |
Record Id to record map. |
extIdRecordsMap |
ExternalId value to record Id map. |
records |
Records array. |
Example:
const task = this.runtime.service.getPluginTask(context);
if (!task) {
return { cancel: false };
}
const sourceCount = task.sourceData.records.length;
const targetCount = task.targetData.records.length;
this.runtime.service.log(this, `source=${sourceCount}, target=${targetCount}`, 'INFO');
const knownId = [...task.sourceData.idRecordsMap.keys()][0];
if (knownId) {
const row = task.sourceData.idRecordsMap.get(knownId);
this.runtime.service.log(this, `sampleSourceId=${knownId}`, 'INFO');
this.runtime.service.log(this, row ?? {}, 'JSON');
}
How it works:
- maps provide fast lookups by Id/externalId without scanning full arrays.
ISfdmuRunCustomAddonProcessedData ⇧
Live dataset for current update/filter cycle.
| Property | Description |
|---|---|
fieldNames |
API names of fields in current update scope. |
fields |
Field describe objects for current update scope. |
recordsToInsert |
Mutable array of records scheduled for insert. |
recordsToUpdate |
Mutable array of records scheduled for update. |
Example:
const processed = this.runtime.service.getProcessedData(context);
processed.recordsToInsert.forEach((record) => {
record['Migration_Source__c'] = 'SFDMU';
});
processed.recordsToUpdate.forEach((record) => {
record['Last_Migrated_By__c'] = 'CustomAddon';
});
this.runtime.service.log(
this,
`insert=${processed.recordsToInsert.length}, update=${processed.recordsToUpdate.length}`,
'INFO'
);
How it works:
- this is the most direct place to mutate records before DML in update events.
See mutation timing in:
Metadata Models ⇧
ISfdmuRunCustomAddonSFieldDescribe ⇧
Describe metadata for one Salesforce field.
Includes:
- identity (
objectName,name,label,type); - write capabilities (
creatable,updateable,readonly); - relationship metadata (
lookup,referencedObjectType,isMasterDetail, polymorphic flags); - technical flags (
custom,calculated,autoNumber,unique, etc.).
Example:
const task = this.runtime.service.getPluginTask(context);
if (!task) {
return { cancel: false };
}
for (const field of task.processedData.fields) {
if (!field.updateable) {
this.runtime.service.log(this, `Skip non-updateable field ${field.name}`, 'INFO');
}
if (field.lookup) {
this.runtime.service.log(this, `Lookup field ${field.name} -> ${field.referencedObjectType}`, 'INFO');
}
}
How it works:
- field describe metadata helps enforce safe transformation rules.
Run and Plugin Metadata ⇧
ISfdmuRunCustomAddonCommandRunInfo ⇧
| Property | Description |
|---|---|
sourceUsername |
Value from --sourceusername. |
targetUsername |
Value from --targetusername. |
apiVersion |
Effective API version used for run. |
basePath |
Runtime base path. |
pinfo |
Plugin metadata object. |
Example:
const run = this.runtime.service.getPluginRunInfo();
this.runtime.service.log(this, `apiVersion=${run.apiVersion}`, 'INFO');
this.runtime.service.log(this, `basePath=${run.basePath}`, 'INFO');
this.runtime.service.log(this, `source=${run.sourceUsername}`, 'INFO');
this.runtime.service.log(this, `target=${run.targetUsername}`, 'INFO');
How it works:
- use this object for environment-dependent behavior and diagnostics.
ISfdmuRunCustomAddonPluginInfo ⇧
| Property | Description |
|---|---|
pluginName |
Usually sfdmu. |
commandName |
Usually run. |
version |
Running plugin version. |
path |
Installation path of plugin package. |
commandString |
Full command line string. |
argv |
Parsed argument array. |
runAddOnApiInfo |
Add-On API version metadata. |
Example:
const run = this.runtime.service.getPluginRunInfo();
const plugin = run.pinfo;
this.runtime.service.log(this, `plugin=${plugin.pluginName}`, 'INFO');
this.runtime.service.log(this, `version=${plugin.version}`, 'INFO');
this.runtime.service.log(this, `command=${plugin.commandString}`, 'INFO');
this.runtime.service.log(this, `addonApi=${plugin.runAddOnApiInfo.version}`, 'INFO');
How it works:
- use plugin metadata to guard features by API version.
Core Enums Available in SDK ⇧
OPERATION ⇧
Supported values:
InsertUpdateUpsertReadonlyDeleteDeleteSourceDeleteHierarchyHardDeleteUnknown
Example:
const task = this.runtime.service.getPluginTask(context);
switch (task?.operation) {
case OPERATION.Insert:
this.runtime.service.log(this, 'Insert mode', 'INFO');
break;
case OPERATION.Upsert:
this.runtime.service.log(this, 'Upsert mode', 'INFO');
break;
case OPERATION.Update:
this.runtime.service.log(this, 'Update mode', 'INFO');
break;
default:
this.runtime.service.log(this, 'Other operation mode', 'INFO');
break;
}
How it works:
- enum-based branching is safer than string comparisons.
DATA_MEDIA_TYPE ⇧
OrgFile
Example:
const task = this.runtime.service.getPluginTask(context);
if (task?.sourceData.mediaType === DATA_MEDIA_TYPE.File) {
this.runtime.service.log(this, 'Source is csvfile media', 'INFO');
}
if (task?.targetData.mediaType === DATA_MEDIA_TYPE.Org) {
this.runtime.service.log(this, 'Target is org media', 'INFO');
}
How it works:
- media checks are important for add-ons that mix CSV and org behavior.
DATA_CACHE_TYPES ⇧
InMemoryCleanFileCacheFileCache
Example:
const script = this.runtime.getScript();
if (script.binaryDataCache === DATA_CACHE_TYPES.FileCache) {
this.runtime.service.log(this, 'Binary cache uses persistent files', 'INFO');
}
How it works:
- cache mode helps explain runtime performance and I/O behavior in logs.
How to Use the SDK ⇧
Install SDK in Your Add-On Project ⇧
npm install <absolute-path-to>/custom-addon-sdk
Then import types:
import type { ISfdmuRunCustomAddonModule } from 'sfdmu-addon-sdk';
Example with full imports:
import type {
ISfdmuRunCustomAddonContext,
ISfdmuRunCustomAddonModule,
ISfdmuRunCustomAddonResult,
ISfdmuRunCustomAddonRuntime,
OPERATION,
} from 'sfdmu-addon-sdk';
How it works:
- use
import typefor interface-only imports and cleaner build output.
Recommended Project Layout ⇧
my-addon/
|- index.ts
|- package.json
|- tsconfig.json
|- resources/
|- messages.md
|- dist/
|- index.js
Example package.json:
{
"name": "my-sfdmu-addon",
"version": "1.0.0",
"type": "module",
"main": "./dist/index.js",
"types": "./index.ts",
"scripts": {
"compile": "tsc -p . --pretty",
"build": "npm run compile"
}
}
How it works:
mainpoints to compiled runtime entry;typespoints to source typings for editor support.
Create from Template ⇧
From repository root:
node scripts/create-custom-addon-template.js <addon-name>
Template location:
custom-addon-sdk/custom-modules/<addon-name>
Example generated entry file (short form):
export default class MyAddon implements ISfdmuRunCustomAddonModule {
public runtime: ISfdmuRunCustomAddonRuntime;
public constructor(runtime: ISfdmuRunCustomAddonRuntime) {
this.runtime = runtime;
}
public async onExecute(
context: ISfdmuRunCustomAddonContext,
args: Record<string, unknown>
): Promise<ISfdmuRunCustomAddonResult> {
this.runtime.service.log(this, 'Hello from template', 'INFO');
this.runtime.service.log(this, args, 'JSON');
return { cancel: false };
}
}
How it works:
- template gives you a valid contract with runtime already injected.
Compile and Build ⇧
Standalone add-on:
npm install
npm run compile
npm run build
Compile with entire repository:
npm run compile
npm run build
Example tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "./dist",
"rootDir": ".",
"sourceMap": true,
"strict": true
},
"include": ["./index.ts"]
}
How it works:
NodeNextkeeps compatibility with runtime loader for modern module formats.
Calling and Running Custom Add-Ons ⇧
Global-Level Registration (export.json)
⇧
{
"beforeAddons": [
{
"module": "./custom-addon-sdk/custom-modules/demo/dist/index.js",
"args": {
"mode": "dry-check"
}
}
]
}
How it works:
- this hook runs once per job before object processing starts;
- useful for setup, script validation, and global feature toggles.
Object-Level Registration (export.json)
⇧
{
"objects": [
{
"query": "SELECT Id, Name FROM Account",
"operation": "Upsert",
"beforeUpdateAddons": [
{
"module": "./custom-addon-sdk/custom-modules/demo/dist/index.js",
"args": {
"fieldName": "Name"
}
}
]
}
]
}
How it works:
- this hook runs in the context of a specific object task;
- best for record-level transformations tied to that object.
External Manifest (addons.json)
⇧
{
"addons": [
{
"event": "onBefore",
"command": "sfdmu:run",
"module": "./custom-addon-sdk/custom-modules/demo/dist/index.js",
"objects": ["Account"],
"args": {
"enabled": true
}
}
]
}
How it works:
- external manifests are useful when you want reusable add-on packages across projects;
- you can keep
export.jsonfocused on data migration config and move add-on catalog toaddons.json.
Notes:
commandis optional; default scope issfdmu:run.objectscan be used to narrow execution without duplicating declarations.- If both
moduleandpathare absent, SFDMU logs a warning and skips that entry.
Debugging Custom Add-Ons ⇧
Debug from TypeScript Source ⇧
For fast development feedback, point module to the .ts file:
{
"addons": [
{
"event": "onBefore",
"module": "./custom-addon-sdk/custom-modules/demo/index.ts"
}
]
}
Run in debug mode with the cross-platform runner:
./sfdmu-run-debug.cmd --sourceusername source@mail.com --targetusername target@mail.com --path .
Example debug-oriented logging:
this.runtime.service.log(this, 'Debug marker: before map step', 'INFO');
const data = this.runtime.service.getProcessedData(context);
this.runtime.service.log(this, `recordsToInsert=${data.recordsToInsert.length}`, 'INFO');
How it works:
- these logs help verify that breakpoints and event timing align with expectations.
Attach Debugger ⇧
- Start plugin with the universal debug runner
./sfdmu-run-debug.cmd. - Attach your IDE debugger to Node process.
- Set breakpoints in add-on
index.ts. - Execute run; breakpoints should hit when event is fired.
Example launch command (Windows):
sfdmu-run-debug.cmd --sourceusername DEMO-SOURCE --targetusername DEMO-TARGET --path ".\\path\\to\\migration-folder"
How it works:
- inspector-enabled run allows IDE attachment before add-on methods execute.
Debug Checklist ⇧
- Confirm event name in config matches expected hook.
- Confirm module path is resolved from
export.jsondirectory. - Confirm add-on is not
excluded. - Confirm return object is
{ cancel: false }unless intentional stop. - Confirm logs appear through
runtime.service.log(...).
Deployment and Distribution ⇧
Local File Deployment ⇧
Best for development and internal automation:
- compile add-on to
dist/index.js; - reference local file path via
module.
Example:
{
"addons": [
{
"event": "onBefore",
"module": "C:/addons/my-addon/dist/index.js"
}
]
}
How it works:
- this approach is fastest for internal projects and CI with fixed workspace paths.
NPM Package Deployment ⇧
Best for versioned team distribution:
- set package name/version;
- build package;
- publish to npm (or private registry);
- install where SFDMU runs;
- reference package name via
module.
Example:
npm install my-company-sfdmu-addon
{
"addons": [
{
"event": "onAfter",
"module": "my-company-sfdmu-addon"
}
]
}
How it works:
- package-based deployment is better for semantic versioning and reuse across teams.
Message Bundles ⇧
Custom add-ons can provide local message keys in:
resources/messages.md
Resolution is isolated per add-on module, so local keys do not leak between modules.
Example resources/messages.md:
# hello_world
Hello world from custom add-on!
# records_count
Processed records: %s
Example usage:
this.runtime.logFormattedInfo(this, 'hello_world');
this.runtime.logFormattedInfo(this, 'records_count', String(42));
How it works:
- keep message keys stable and local to the module for safer upgrades.
Best Practices ⇧
- Keep business logic deterministic and idempotent where possible.
- Treat context/script metadata as configuration; mutate only when required.
- Prefer editing
recordsToInsertandrecordsToUpdatein update-phase events. - Use
filterRecordsAddons+tempRecordsfor filtering scenarios. - Avoid unbounded SOQL loops; prefer
createFieldInQueries. - Log decision points clearly for troubleshooting.
- Keep
argsstable and versioned (for exampleschemaVersionargument). - Fail fast on invalid configuration and return
cancel: trueonly intentionally.
Example pattern (idempotent and schema-aware):
const schemaVersion = Number(args['schemaVersion'] ?? 1);
if (schemaVersion !== 1) {
this.runtime.service.log(this, `Unsupported schemaVersion=${String(schemaVersion)}`, 'ERROR');
return { cancel: true };
}
const processed = this.runtime.service.getProcessedData(context);
for (const record of processed.recordsToInsert) {
if (!record['MigrationTag__c']) {
record['MigrationTag__c'] = 'my-addon-v1';
}
}
How it works:
- version checks protect contract changes;
- idempotent writes avoid unstable repeated transformations.
Common Pitfalls ⇧
- Registering add-on under a wrong event and expecting unavailable data context.
- Using only
pathin new configs instead ofmodule. - Forgetting to compile add-on before running in non-dev mode.
- Assuming all events run once; object-level events run per object and can be multi-pass.
- Depending on shared mutable state between separate add-on instances.
Example of a problematic pattern and fix:
// Problematic: module-global mutable state shared across invocations.
let globalCounter = 0;
// Better: derive state from context or task per invocation.
const task = this.runtime.service.getPluginTask(context);
const currentCount = task?.processedData.recordsToInsert.length ?? 0;
this.runtime.service.log(this, `currentInsertBatch=${String(currentCount)}`, 'INFO');
How it works:
- avoid hidden state between events; use runtime-provided context/task data instead.