Custom Add-On API Object Reference.


Table of Contents



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.json and addons.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:

  1. Create a template: node scripts/create-custom-addon-template.js my-addon
  2. Open: custom-addon-sdk/custom-modules/my-addon
  3. Install and compile: npm install npm run compile
  4. Register the module in export.json using module.
  5. 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:
  • args is optional and can be omitted when your module does not need parameters.
  • At least one locator must exist: module or path.
  • Relative paths are resolved from the directory where export.json is 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.json file 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:

  • module is used as the effective locator.
  • args is passed to onExecute(context, args) as-is.
  • objects and objectName allow strict targeting.
  • excluded: true disables the add-on without removing configuration.

Locator Rules (module vs path)

  • module has priority when both module and path are provided.
  • module can be:
    • package name (my-sfdmu-addon);
    • relative file path (./custom-addon-sdk/custom-modules/demo/dist/index.js);
    • absolute file path (C:/.../index.js).
  • path is 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 module value;
  • path remains legacy-compatible but does not override module.

Module Resolution Behavior

At runtime, SFDMU resolves custom add-ons with this strategy:

  1. Try dynamic ESM import for the requested locator.
  2. Try alternate extension (.ts <-> .js) when relevant.
  3. For package-like locators, try global node_modules resolution.
  4. 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/.js file paths can be resolved as alternative entry points;
  • if ESM import fails, loader can fallback to CommonJS require.

Event Values

Supported event names:

  • onBefore
  • onAfter
  • onDataRetrieved
  • onBeforeUpdate
  • onAfterUpdate
  • filterRecordsAddons

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:

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:

  • createFieldInQueries avoids oversized IN clauses;
  • queryMultiAsync returns merged results from generated queries;
  • updateTargetRecordsAsync applies 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:

  • scriptObject reflects current object-level migration behavior;
  • useful for branching logic by operation or mapping mode.

Related property links in main docs:

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:

  • tasks lets you inspect the whole job graph;
  • getTaskByFieldPath helps 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:

  • tempRecords is designed for filtering transformations in filter events;
  • operation/update mode allow precise event-phase logic.

Property flow cross-links:

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:

  • Insert
  • Update
  • Upsert
  • Readonly
  • Delete
  • DeleteSource
  • DeleteHierarchy
  • HardDelete
  • Unknown

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

  • Org
  • File

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

  • InMemory
  • CleanFileCache
  • FileCache

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 type for 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:

  • main points to compiled runtime entry;
  • types points 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:

  • NodeNext keeps 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.json focused on data migration config and move add-on catalog to addons.json.
Notes:
  • command is optional; default scope is sfdmu:run.
  • objects can be used to narrow execution without duplicating declarations.
  • If both module and path are 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

  1. Start plugin with the universal debug runner ./sfdmu-run-debug.cmd.
  2. Attach your IDE debugger to Node process.
  3. Set breakpoints in add-on index.ts.
  4. 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.json directory.
  • 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:

  1. set package name/version;
  2. build package;
  3. publish to npm (or private registry);
  4. install where SFDMU runs;
  5. 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

  1. Keep business logic deterministic and idempotent where possible.
  2. Treat context/script metadata as configuration; mutate only when required.
  3. Prefer editing recordsToInsert and recordsToUpdate in update-phase events.
  4. Use filterRecordsAddons + tempRecords for filtering scenarios.
  5. Avoid unbounded SOQL loops; prefer createFieldInQueries.
  6. Log decision points clearly for troubleshooting.
  7. Keep args stable and versioned (for example schemaVersion argument).
  8. Fail fast on invalid configuration and return cancel: true only 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

  1. Registering add-on under a wrong event and expecting unavailable data context.
  2. Using only path in new configs instead of module.
  3. Forgetting to compile add-on before running in non-dev mode.
  4. Assuming all events run once; object-level events run per object and can be multi-pass.
  5. 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.

Related Articles

Last updated on 21st Feb 2026