Data Workspaces with AI Coding Agents
AI coding agents such as Claude Code can generate complete SFDMU data workspace configurations from natural-language descriptions, then trigger data exports and imports using sfdx-hardis commands.
This page documents:
- The
/hardis-dataClaude Code skill definition (ready to copy) - The complete SFDMU export.json reference the skill relies on
- How to wire data workspaces into deployment actions
The /hardis-data Skill
Installation
Create .claude/skills/hardis-data/SKILL.md in your project with the following content:
---
name: hardis-data
description: Generate, edit, and run SFDMU data workspaces for Salesforce data migration - create export.json configs, trigger imports/exports, set up data anonymization, and wire data actions into deployment pipelines. Use when the user asks to create a data workspace, export/import data, anonymize fields, configure SFDMU, manage CSV data, or add a data deployment action. Use this skill even if the user says "create data workspace", "export accounts", "import test data", "anonymize contacts", "set up SFDMU", "add data step to deployment", "migrate data", "seed scratch org data", or "delete old records".
argument-hint: "[description of data workspace or operation]"
allowed-tools: Bash, Read, Write, Edit, Glob, Grep, AskUserQuestion
user-invocable: true
---
You are a Salesforce data migration expert. You help users create, edit, and run SFDMU
data workspaces managed by sfdx-hardis.
## CRITICAL: External ID Resolution
**Every** object with an `Upsert`, `Update`, or `Readonly` operation MUST have a verified `externalId`.
Never guess or blindly default to `Name`. Follow this procedure for each object:
### Step 1: Look for a declared External ID field
**Option A - Local metadata (preferred when available):**
Search for `.field-meta.xml` files of the object in the local project and look for
`<externalId>true</externalId>`:
```bash
grep -rl "<externalId>true</externalId>" force-app/**/objects/<ObjectName>/fields/ 2>/dev/null
```
**Option B - Tooling API on target org (when local metadata is incomplete or absent):**
```bash
sf data query --query "SELECT QualifiedApiName FROM FieldDefinition WHERE EntityDefinition.QualifiedApiName = '<ObjectName>' AND IsExternalId = true" --target-org <orgAlias> --json
```
If either method returns one or more External ID fields, use the most appropriate one.
**Never use an AutoNumber field as externalId** - AutoNumber values are generated independently
per org and will not match between source and target, causing every record to be treated as new.
### Step 2: No External ID field exists - build a composite key
If no declared External ID field is found, you must identify a combination of fields whose
values are **unique across all records** in the table. Candidates: `Name`, `DeveloperName`,
`Email`, `Username`, lookup relationships combined with a name field, etc.
**Verify uniqueness by querying the org:**
```bash
sf data query --query "SELECT <Field1>, <Field2>, COUNT(Id) cnt FROM <ObjectName> GROUP BY <Field1>, <Field2> HAVING COUNT(Id) > 1" --target-org <orgAlias> --json
```
- If the query returns **zero rows**, the combination is unique - use it as a composite
external ID with semicolons: `"externalId": "Field1;Field2"`
- If the query returns rows, those fields have duplicates - try a different/wider
combination and re-check.
- For standard objects with well-known unique fields, you may skip the query:
- `User` -> `Username`
- `RecordType` -> `DeveloperName;SobjectType` (or `DeveloperName;NamespacePrefix;SobjectType`)
- `Profile` -> `Name`
- `Group` -> `DeveloperName`
- `BusinessProcess` -> `Name;SobjectType`
### Step 3: Same-org Update or Delete operations
When the operation is `Update` or `Delete`/`DeleteSource` targeting the **same org** the data
was exported from, `"externalId": "Id"` is acceptable - the Salesforce record ID is the
primary key.
### Summary
| Scenario | externalId to use |
|-------------------------------------------------|--------------------------------------------|
| Object has a declared External ID field | That field (e.g. `External_Key__c`) |
| Standard object with well-known unique field | The known field (e.g. `Username` for User) |
| No External ID, combination of fields is unique | Composite key: `"Field1;Field2"` |
| Same-org Update/Delete only | `"Id"` |
**Never use `Name` as external ID without first confirming it is either a declared External ID
or genuinely unique for that object in the org.**
**Never use an AutoNumber field as externalId** - AutoNumber values are generated per org and
will differ between source and target, making matching impossible.
## Data Workspace Structure
Workspaces live under `scripts/data/<WorkspaceName>/` and contain:
- `export.json` - the SFDMU configuration file (see full reference below)
- `*.csv` - exported data files (one per object, auto-generated)
- `logs/` - execution logs (auto-created)
## Creating a Data Workspace
1. Create the folder: `scripts/data/<PascalCaseName>/`
2. Write `export.json` inside it following the specification below
3. The workspace is immediately usable for export/import
## Running Data Operations
```bash
# Export data from an org to CSV files
sf hardis:org:data:export --agent --path scripts/data/<WorkspaceName> --target-org <orgAlias>
# Import CSV data into an org
sf hardis:org:data:import --agent --path scripts/data/<WorkspaceName> --target-org <orgAlias>
# Delete records from an org
sf hardis:org:data:delete --agent --path scripts/data/<WorkspaceName> --target-org <orgAlias>
```
## Wiring Data into Deployment Actions
To run a data import automatically during deployments, create a deployment action:
```bash
sf hardis:project:action:create --agent \
--scope <project|branch|pr> --when <pre-deploy|post-deploy> \
--type data --label "<description>" --sfdmu-project <WorkspaceName> \
[--pr-id <id>] [--context process-deployment-only]
```
Or add it directly in the YAML config:
```yaml
# config/.sfdx-hardis.yml (project scope) or scripts/actions/.sfdx-hardis.<prId>.yml
commandsPostDeploy:
- id: importMyData
label: Import reference data
type: data
parameters:
sfdmuProject: MyWorkspaceName
context: process-deployment-only
```
## Auto-Import in Scratch Orgs
To auto-import data when creating scratch orgs, add to `config/.sfdx-hardis.yml`:
```yaml
dataPackages:
- dataPath: scripts/data/<WorkspaceName>
importInScratchOrgs: true
```
## export.json Reference
### Root-Level Properties
| Property | Type | Default | Description |
|--------------------------------|-------------------|------------|-------------------------------------------------------|
| `sfdxHardisLabel` | string | - | Display name for the workspace (sfdx-hardis specific) |
| `sfdxHardisDescription` | string | - | Description (sfdx-hardis specific) |
| `objects` | ScriptObject[] | - | Array of object migration definitions (required) |
| `objectSets` | ScriptObjectSet[] | - | Groups of objects processed as sub-jobs |
| `allOrNone` | boolean | false | Abort if any record fails |
| `bulkApiVersion` | string | "2.0" | "1.0" or "2.0" |
| `bulkThreshold` | integer | 200 | Record count triggering Bulk API for CRUD |
| `concurrencyMode` | string | "Parallel" | "Serial" or "Parallel" (Bulk API V1) |
| `apiVersion` | string | auto | Salesforce API version (e.g. "62.0") |
| `simulationMode` | boolean | false | Preview mode without updating target |
| `promptOnMissingParentObjects` | boolean | true | Prompt on missing parents |
| `promptOnIssuesInCSVFiles` | boolean | true | Prompt on CSV issues |
| `createTargetCSVFiles` | boolean | true | Generate target CSV reports |
| `excludeIdsFromCSVFiles` | boolean | false | Omit ID columns from CSV |
| `keepObjectOrderWhileExecute` | boolean | false | Preserve array order (false = smart order) |
| `bulkApiV1BatchSize` | integer | 9500 | Batch size for Bulk API V1 |
| `queryBulkApiThreshold` | integer | 30000 | Record count triggering Bulk API for queries |
| `restApiBatchSize` | integer | - | REST API batch size |
| `parallelBulkJobs` | integer | 1 | Concurrent Bulk API jobs |
| `parallelRestJobs` | integer | 1 | Concurrent REST API jobs |
| `allowFieldTruncation` | boolean | false | Truncate values to metadata lengths |
| `csvFileDelimiter` | string | "," | CSV delimiter |
| `csvFileEncoding` | string | "utf8" | CSV encoding |
| `csvInsertNulls` | boolean | true | Null handling for CSV |
| `csvUseEuropeanDateFormat` | boolean | false | Day-first date parsing |
| `binaryDataCache` | string | "InMemory" | "InMemory", "CleanFileCache", "FileCache" |
| `sourceRecordsCache` | string | "InMemory" | "InMemory", "CleanFileCache", "FileCache" |
| `pollingIntervalMs` | integer | 5000 | Bulk API polling interval |
| `runnableInProduction` | boolean | false | Required true for delete in production |
| `beforeAddons` | AddonDef[] | - | Add-ons before migration |
| `afterAddons` | AddonDef[] | - | Add-ons after migration |
| `dataRetrievedAddons` | AddonDef[] | - | Add-ons after retrieval |
| `excludedObjects` | string[] | - | Global object exclusion list |
### ScriptObject Properties (each entry in `objects`)
| Property | Type | Default | Description |
|-----------------------------------|---------------|--------------|------------------------------------------------------------------------------------------------|
| `query` | string | **required** | SOQL query (supports `SELECT all FROM Object` shorthand) |
| `operation` | string | "Readonly" | Operation type (see values below) |
| `externalId` | string | "Name" | External ID field for matching - **must be resolved using the procedure above, never guessed** |
| `master` | boolean | true | Master object in hierarchy |
| `deleteOldData` | boolean | false | Delete old target records first |
| `deleteFromSource` | boolean | false | Delete from source org |
| `deleteByHierarchy` | boolean | false | Cascading hierarchical deletion |
| `deleteQuery` | string | - | SOQL WHERE for old-data deletion |
| `hardDelete` | boolean | false | Bypass Recycle Bin |
| `excluded` | boolean | false | Exclude object from migration |
| `excludedFields` | string[] | - | Fields to omit |
| `excludedFromUpdateFields` | string[] | - | Fields retrieved but not updated |
| `skipExistingRecords` | boolean | false | Skip updates for existing records |
| `skipRecordsComparison` | boolean | false | Skip source/target comparison |
| `useQueryAll` | boolean | false | Include deleted/archived records |
| `queryAllTarget` | boolean | false | Query all target records |
| `useSourceCSVFile` | boolean | false | Use CSV as data source |
| `sourceRecordsFilter` | string | - | AlaSQL filter on source records |
| `targetRecordsFilter` | string | - | AlaSQL filter on target records |
| `useFieldMapping` | boolean | false | Enable field mapping |
| `fieldMapping` | MappingItem[] | - | Field mapping definitions |
| `useValuesMapping` | boolean | false | Enable ValueMapping.csv |
| `updateWithMockData` | boolean | false | Enable data anonymization |
| `mockFields` | MockField[] | - | Anonymization field definitions |
| `bulkApiV1BatchSize` | integer | - | Object-level batch size |
| `restApiBatchSize` | integer | - | Object-level REST batch size |
| `parallelBulkJobs` | integer | 1 | Object-level parallel bulk jobs |
| `parallelRestJobs` | integer | 1 | Object-level parallel REST jobs |
| `alwaysUseRestApi` | boolean | false | Force REST API |
| `alwaysUseBulkApi` | boolean | false | Force Bulk API |
| `alwaysUseBulkApiToUpdateRecords` | boolean | false | Force Bulk API for DML only |
| `beforeAddons` | AddonDef[] | - | Add-ons before this object |
| `afterAddons` | AddonDef[] | - | Add-ons after this object |
| `filterRecordsAddons` | AddonDef[] | - | Record filter add-ons |
**Operation values:** `Insert`, `Update`, `Upsert`, `Readonly`, `Delete`, `HardDelete`, `DeleteSource`, `DeleteHierarchy`
### MockField Properties (data anonymization)
| Property | Type | Description |
|-----------------|----------|------------------------------------------------|
| `name` | string | Field to anonymize (or `"all"` for all fields) |
| `pattern` | string | Fake data type (see patterns below) |
| `locale` | string | Locale code (e.g. `"en_US"`, `"fr_FR"`) |
| `excludedRegex` | string | JS regex to exclude values |
| `includedRegex` | string | JS regex to include values |
| `excludeNames` | string[] | Fields to skip when name is `"all"` |
**Pattern values:** `country`, `city`, `street`, `address`, `zip`, `name`, `full_name`, `username`, `first_name`, `last_name`, `email`, `sentence`, `title`, `text`, `word`, `ip`, `domain`, `url`, `integer`, `date`, `time`, `year`, `ids`
**Special functions:**
- `c_seq_number('prefix', startNum, increment)` - sequential numbers
- `c_seq_date('YYYY-MM-DD', step)` - sequential dates (steps: `d`, `m`, `y`, `-d`, `s`, `ms`)
- `c_set_value('value')` - fixed constant
### MappingItem Properties (field mapping)
| Property | Type | Description |
|----------------|--------|--------------------------------------|
| `targetObject` | string | Destination object (first item only) |
| `sourceField` | string | Source field name |
| `targetField` | string | Target field name |
### Composite External ID
Use semicolons for composite keys: `"externalId": "Field1;Field2"`
### Polymorphic Lookups
Use `$` syntax: `ParentId$Account`, `WhatId$Opportunity`, `WhoId$Contact`
### Field Multiselect Keywords (in SOQL)
`all`, `custom_true`, `custom_false`, `readonly_true`, `readonly_false`, `createable_true`, `updateable_true`, `lookup_true`, `lookup_false`, `type_string`, `type_boolean`, etc.
### core:RecordsTransform Add-On
```json
{
"module": "core:RecordsTransform",
"args": {
"fields": [
{ "alias": "varName", "sourceObject": "Account", "sourceField": "Name" }
],
"transformations": [
{ "targetObject": "Contact", "targetField": "Description", "formula": "'Account: ' + formula.varName" }
]
}
}
```
### core:ExportFiles Add-On
```json
{
"module": "core:ExportFiles",
"args": { "operation": "Upsert", "externalId": "Title", "deleteOldData": true }
}
```
## Examples
### Simple reference data (upsert by Name)
```json
{
"sfdxHardisLabel": "Reference Data",
"sfdxHardisDescription": "Upsert core reference data",
"objects": [
{ "query": "SELECT Id, Name FROM RecordType WHERE IsActive = true", "operation": "Readonly", "externalId": "DeveloperName;SobjectType" },
{ "query": "SELECT all FROM Account WHERE Type = 'Reference'", "operation": "Upsert", "externalId": "Name" },
{ "query": "SELECT all FROM Contact", "operation": "Upsert", "externalId": "Email", "master": false }
],
"promptOnMissingParentObjects": false,
"promptOnIssuesInCSVFiles": false
}
```
### Anonymize contacts
```json
{
"sfdxHardisLabel": "Anonymize Contacts",
"sfdxHardisDescription": "Anonymize contact emails and names",
"objects": [
{
"query": "SELECT Id, FirstName, LastName, Email, Phone FROM Contact",
"operation": "Update",
"externalId": "Id",
"updateWithMockData": true,
"mockFields": [
{ "name": "FirstName", "pattern": "first_name" },
{ "name": "LastName", "pattern": "last_name" },
{ "name": "Email", "pattern": "email" },
{ "name": "Phone", "pattern": "integer" }
]
}
],
"allOrNone": true
}
```
### Delete records
```json
{
"sfdxHardisLabel": "Clean Test Data",
"sfdxHardisDescription": "Delete test records from org",
"runnableInProduction": false,
"objects": [
{ "query": "SELECT Id FROM Case WHERE Subject LIKE 'TEST%'", "operation": "DeleteSource", "deleteFromSource": true },
{ "query": "SELECT Id FROM Account WHERE Name LIKE 'TEST%'", "operation": "DeleteSource", "deleteFromSource": true }
]
}
```
### CPQ configuration (complex, multi-object)
```json
{
"sfdxHardisLabel": "CPQ Configuration",
"sfdxHardisDescription": "Export/import Salesforce CPQ configuration objects",
"allOrNone": true,
"concurrencyMode": "Serial",
"bulkApiVersion": "1.0",
"promptOnMissingParentObjects": false,
"promptOnIssuesInCSVFiles": false,
"objects": [
{ "query": "SELECT all FROM Product2", "operation": "Upsert", "externalId": "Name" },
{ "query": "SELECT all FROM SBQQ__ProductFeature__c", "operation": "Upsert", "externalId": "Name" },
{ "query": "SELECT all FROM SBQQ__ProductOption__c", "operation": "Upsert", "externalId": "Name" },
{ "query": "SELECT all FROM Pricebook2", "operation": "Upsert", "externalId": "Name" },
{ "query": "SELECT all FROM PricebookEntry", "operation": "Upsert", "externalId": "Name" },
{ "query": "SELECT all FROM SBQQ__ProductRule__c", "operation": "Upsert", "externalId": "Name" },
{ "query": "SELECT all FROM SBQQ__PriceRule__c", "operation": "Upsert", "externalId": "Name" },
{ "query": "SELECT all FROM SBQQ__DiscountSchedule__c", "operation": "Upsert", "externalId": "Name" },
{ "query": "SELECT Name FROM Account", "operation": "Readonly", "externalId": "Name" }
]
}
```
### With field mapping and RecordsTransform
```json
{
"sfdxHardisLabel": "Cross-Org Migration",
"sfdxHardisDescription": "Migrate accounts with field remapping between orgs",
"objects": [
{
"query": "SELECT Id, Name, BillingCity, Industry FROM Account",
"operation": "Upsert",
"externalId": "Name",
"useFieldMapping": true,
"fieldMapping": [
{ "targetObject": "Account" },
{ "sourceField": "BillingCity", "targetField": "ShippingCity" }
]
}
],
"dataRetrievedAddons": [
{
"module": "core:RecordsTransform",
"args": {
"fields": [
{ "alias": "acctName", "sourceObject": "Account", "sourceField": "Name" }
],
"transformations": [
{ "targetObject": "Account", "targetField": "Description", "formula": "'Migrated: ' + formula.acctName" }
]
}
}
]
}
```
## Design Guidelines
When generating an export.json:
1. **Always include** `sfdxHardisLabel` and `sfdxHardisDescription` at the root
2. **Always set** `promptOnMissingParentObjects: false` and `promptOnIssuesInCSVFiles: false` for CI/CD compatibility
3. **Order objects** so parent objects come before children (e.g. Account before Contact)
4. **Include Readonly objects** for reference data that must exist but should not be modified (RecordType, User, etc.)
5. **Use `SELECT all`** when all fields are needed; list specific fields when only a subset is required or for performance
6. **Always resolve externalId** using the "External ID Resolution" procedure above - search local metadata or query the Tooling API for declared External ID fields first, fall back to a verified-unique composite key, never blindly use `Name`
7. **Use `master: false`** on child objects that depend on parent relationships
8. **For production safety**: never set `runnableInProduction: true` unless explicitly asked; always default to `false`
9. **For delete workspaces**: use `operation: "DeleteSource"` with `deleteFromSource: true`; keep these in separate workspaces from upsert workspaces
10. **Folder naming**: use PascalCase for workspace folder names (e.g. `ReferenceData`, `CPQConfig`, `ContactAnonymize`)
$ARGUMENTS
Quick Start
1. Generate a workspace with natural language
With the skill installed, ask your coding agent:
"Create a data workspace to export Account, Contact, and Opportunity records from my org, using Email as the external ID for Contact and Name for everything else"
The agent will create scripts/data/AccountContactOpportunity/export.json with the appropriate SFDMU configuration.
2. Export data
sf hardis:org:data:export --agent --path scripts/data/AccountContactOpportunity --target-org myOrg
This runs SFDMU to export records from the org into CSV files in the workspace folder.
3. Import data
sf hardis:org:data:import --agent --path scripts/data/AccountContactOpportunity --target-org targetOrg
This reads the CSV files and upserts them into the target org.
4. Delete records
sf hardis:org:data:delete --agent --path scripts/data/CleanTestData --target-org targetOrg
Requires deleteFromSource: true or operation: "DeleteSource" in the workspace's export.json.
Data Workspace Folder Structure
scripts/data/
MyWorkspace/
export.json - SFDMU configuration
Account.csv - exported records (auto-generated)
Contact.csv
source/ - source CSV snapshots (auto-created)
target/ - target CSV reports (auto-created)
logs/ - execution logs (auto-created)
Integrating Data Workspaces with Deployment Actions
Data imports can be wired into the CI/CD pipeline as deployment actions - steps that run before or after metadata deployment.
Using the CLI
# Add a post-deploy data import for a pull request
sf hardis:project:action:create --agent \
--scope pr --pr-id 123 --when post-deploy \
--type data --label "Import email templates" --sfdmu-project EmailTemplates
# Add a pre-deploy data import at project level
sf hardis:project:action:create --agent \
--scope project --when pre-deploy \
--type data --label "Load reference data" --sfdmu-project ReferenceData
Using YAML configuration directly
Pull Request scope (scripts/actions/.sfdx-hardis.123.yml):
commandsPostDeploy:
- id: importEmailTemplates
label: Import email templates
type: data
parameters:
sfdmuProject: EmailTemplates
context: process-deployment-only
Project scope (config/.sfdx-hardis.yml):
commandsPostDeploy:
- id: loadReferenceData
label: Load reference data
type: data
parameters:
sfdmuProject: ReferenceData
context: all
Branch scope (config/branches/.sfdx-hardis.integration.yml):
commandsPreDeploy:
- id: cleanTestRecords
label: Clean test records before deploy
type: data
parameters:
sfdmuProject: CleanTestData
context: process-deployment-only
Context values
| Value | Description |
|---|---|
all |
Run on every deployment (check and process) |
check-deployment-only |
Run only during validation/simulation |
process-deployment-only |
Run only during actual deployment |
Auto-import in scratch orgs
Add to config/.sfdx-hardis.yml:
dataPackages:
- dataPath: scripts/data/ReferenceData
importInScratchOrgs: true
- dataPath: scripts/data/TestData
importInScratchOrgs: true
importInSandboxOrgs: false
These workspaces are automatically imported after metadata deployment during scratch org creation (sf hardis:scratch:create).
Complete export.json Reference
Root-Level (Script) Properties
| Property | Type | Default | Description |
|---|---|---|---|
sfdxHardisLabel |
string | - | Display name (sfdx-hardis) |
sfdxHardisDescription |
string | - | Description (sfdx-hardis) |
objects |
ScriptObject[] | - | Object definitions (required unless objectSets used) |
objectSets |
ScriptObjectSet[] | - | Groups of objects as sub-jobs |
orgs |
ScriptOrg[] | - | Org credentials (omit to use CLI auth) |
allOrNone |
boolean | false | Abort if any record fails; REST API rolls back |
allowFieldTruncation |
boolean | false | Truncate values to metadata-defined lengths |
alwaysUseRestApiToUpdateRecords |
boolean | false | Force REST API, bypassing bulkThreshold |
apiVersion |
string | auto | API version (e.g. "62.0") |
bulkApiV1BatchSize |
integer | 9500 | Records per Bulk API V1 batch |
bulkApiVersion |
string | "2.0" | "1.0" or "2.0" |
bulkThreshold |
integer | 200 | Record count triggering Bulk API |
concurrencyMode |
string | "Parallel" | "Serial" or "Parallel" |
createTargetCSVFiles |
boolean | true | Generate target CSV reports |
csvAlwaysQuoted |
boolean | true | Quote all CSV values |
csvFileDelimiter |
string | "," | CSV delimiter |
csvFileEncoding |
string | "utf8" | utf8, utf16le, latin1, ascii, base64 |
csvInsertNulls |
boolean | true | Null handling for CSV |
csvUseEuropeanDateFormat |
boolean | false | Day-first date parsing |
csvUseUtf8Bom |
boolean | true | UTF-8 BOM in CSV output |
csvWriteUpperCaseHeaders |
boolean | false | Uppercase CSV headers |
excludeIdsFromCSVFiles |
boolean | false | Omit ID/lookup columns from CSV |
excludedObjects |
string[] | - | Global object exclusion list |
importCSVFilesAsIs |
boolean | false | Skip CSV validation/repair |
keepObjectOrderWhileExecute |
boolean | false | Preserve array order vs. smart order |
parallelBinaryDownloads |
integer | 20 | Concurrent binary transfers |
parallelBulkJobs |
integer | 1 | Concurrent Bulk API jobs |
parallelRestJobs |
integer | 1 | Concurrent REST API jobs |
pollingIntervalMs |
integer | 5000 | Bulk API poll interval (ms) |
pollingQueryTimeoutMs |
integer | 240000 | SOQL query timeout (ms) |
promptOnIssuesInCSVFiles |
boolean | true | Prompt on CSV issues |
promptOnMissingParentObjects |
boolean | true | Prompt on missing parents |
proxyUrl |
string | - | Proxy server URL |
queryBulkApiThreshold |
integer | 30000 | Record count triggering Bulk API for queries |
restApiBatchSize |
integer | - | REST API batch size |
simulationMode |
boolean | false | Dry run without persisting |
sourceRecordsCache |
string | "InMemory" | "InMemory", "CleanFileCache", "FileCache" |
binaryDataCache |
string | "InMemory" | "InMemory", "CleanFileCache", "FileCache" |
useSeparatedCSVFiles |
boolean | false | Separate CSV dirs per object set |
validateCSVFilesOnly |
boolean | false | Validate CSV without running migration |
runnableInProduction |
boolean | false | Required true for production deletes (sfdx-hardis) |
beforeAddons |
AddonDef[] | - | Add-ons before entire migration |
afterAddons |
AddonDef[] | - | Add-ons after entire migration |
dataRetrievedAddons |
AddonDef[] | - | Add-ons after data retrieval |
ScriptObject Properties
| Property | Type | Default | Description |
|---|---|---|---|
query |
string | required | SOQL query string |
operation |
string | "Readonly" | See operation values below |
externalId |
string | "Name" | External ID field for matching - must be resolved using the procedure above, never guessed |
master |
boolean | true | Master object in hierarchy |
excluded |
boolean | false | Exclude from migration |
excludedFields |
string[] | - | Fields omitted from migration |
excludedFromUpdateFields |
string[] | - | Retrieved but not updated |
deleteOldData |
boolean | false | Delete old target records first |
deleteFromSource |
boolean | false | Delete from source org |
deleteByHierarchy |
boolean | false | Cascading hierarchical delete |
deleteQuery |
string | - | SOQL for old-data selection |
hardDelete |
boolean | false | Bypass Recycle Bin |
skipExistingRecords |
boolean | false | Skip existing record updates |
skipRecordsComparison |
boolean | false | Skip source/target comparison |
useQueryAll |
boolean | false | Include deleted/archived records |
queryAllTarget |
boolean | false | Query all target records |
useSourceCSVFile |
boolean | false | Use CSV as data source |
sourceRecordsFilter |
string | - | AlaSQL filter on source |
targetRecordsFilter |
string | - | AlaSQL filter on target |
useFieldMapping |
boolean | false | Enable field mapping |
fieldMapping |
MappingItem[] | - | Field mappings |
useValuesMapping |
boolean | false | Enable ValueMapping.csv |
updateWithMockData |
boolean | false | Enable anonymization |
mockFields |
MockField[] | - | Anonymization definitions |
bulkApiV1BatchSize |
integer | - | Object-level Bulk batch size |
restApiBatchSize |
integer | - | Object-level REST batch size |
parallelBulkJobs |
integer | 1 | Parallel Bulk jobs |
parallelRestJobs |
integer | 1 | Parallel REST jobs |
alwaysUseRestApi |
boolean | false | Force REST API |
alwaysUseBulkApi |
boolean | false | Force Bulk API |
alwaysUseBulkApiToUpdateRecords |
boolean | false | Force Bulk API for DML |
respectOrderByOnDeleteRecords |
boolean | false | Force REST single-record delete |
beforeAddons |
AddonDef[] | - | Add-ons before object |
afterAddons |
AddonDef[] | - | Add-ons after object |
beforeUpdateAddons |
AddonDef[] | - | Add-ons before update batch |
afterUpdateAddons |
AddonDef[] | - | Add-ons after update batch |
filterRecordsAddons |
AddonDef[] | - | Record filter add-ons |
Operation Values
| Operation | Description |
|---|---|
Insert |
Create new records only |
Update |
Update existing records only |
Upsert |
Insert or update based on external ID match |
Readonly |
Retrieve without modification (reference data) |
Delete |
Remove from target org |
HardDelete |
Delete bypassing Recycle Bin |
DeleteSource |
Delete from source org |
DeleteHierarchy |
Cascading hierarchical deletion |
MockField Properties
| Property | Type | Description |
|---|---|---|
name |
string | Field to anonymize, or "all" for all fields |
pattern |
string | Fake data type |
locale |
string | Locale code (e.g. "en_US", "fr_FR") |
excludedRegex |
string | JS regex to exclude values from anonymization |
includedRegex |
string | JS regex to include values |
excludeNames |
string[] | Fields to skip when name is "all" |
Available patterns: country, city, street, address, zip, name, full_name, username, first_name, last_name, email, sentence, title, text, word, ip, domain, url, integer, date, time, year, ids
Special pattern functions:
| Function | Description | Example |
|---|---|---|
c_seq_number('prefix', start, step) |
Sequential numbers | c_seq_number('ACC-', 1000, 1) |
c_seq_date('YYYY-MM-DD', step) |
Sequential dates (steps: d, m, y, -d, s, ms) | c_seq_date('2024-01-01', 'd') |
c_set_value('value') |
Fixed constant value | c_set_value('ANONYMIZED') |
MappingItem Properties
First item must include targetObject. Subsequent items map fields:
{
"useFieldMapping": true,
"fieldMapping": [
{ "targetObject": "Contact" },
{ "sourceField": "AccountId", "targetField": "Account__c" },
{ "sourceField": "ExternalID__c", "targetField": "External_ID__c" }
]
}
ScriptObjectSet
Groups objects into sub-jobs processed sequentially after the main objects array:
{
"objectSets": [
{
"objects": [
{ "query": "SELECT Id, Body, ParentId$Account FROM Attachment", "operation": "Insert" }
]
}
]
}
Composite External ID Keys
Use semicolons to create composite keys:
"externalId": "DeveloperName;NamespacePrefix;SobjectType"
Polymorphic Lookup Fields
Use $ to bind a polymorphic field to a specific parent object:
ParentId$Account - Attachment/Note ParentId to Account
ParentId$Case - FeedItem ParentId to Case
WhatId$Opportunity - Activity WhatId to Opportunity
WhoId$Contact - Activity WhoId to Contact
A polymorphic field can only bind to one parent type at a time.
SOQL Field Multiselect Keywords
Use these keywords in SELECT instead of listing individual fields:
| Keyword | Description |
|---|---|
all |
All object fields |
custom_true / custom_false |
Custom vs standard fields |
readonly_true / readonly_false |
Non-modifiable vs modifiable fields |
createable_true / createable_false |
By creation capability |
updateable_true / updateable_false |
By update capability |
lookup_true / lookup_false |
Relationship vs non-relationship fields |
type_string, type_boolean, etc. |
By specific field data type |
Example: SELECT Id, Name, custom_true, lookup_false FROM Account
Target Records Filter (AlaSQL syntax)
| Syntax | Meaning |
|---|---|
"FieldName" |
Field is not null |
"NOT FieldName" |
Field is null |
"Field1 AND Field2" |
Both conditions |
"Field1 = Field2" |
Field comparison |
core:RecordsTransform Add-On
Transform records using JavaScript formulas after retrieval:
{
"module": "core:RecordsTransform",
"args": {
"fields": [
{
"alias": "acctName",
"sourceObject": "Account",
"sourceField": "Name",
"lookupSource": "source",
"isConstant": false
}
],
"transformations": [
{
"targetObject": "Contact",
"targetField": "Description",
"formula": "'Account: ' + formula.acctName"
}
]
}
}
Field properties: alias (required), sourceObject (required), sourceField (required), valueSource ("source"/"target"), lookupSource ("source"/"target"), isConstant (boolean), lookupExpression (JS), includeFields (string[])
Transformation properties: targetObject (required), targetField (required), formula (required, JS), expressions (JS array, pre-formula)
core:ExportFiles Add-On
Migrate ContentVersion, Attachment, and Note files:
{
"query": "SELECT all FROM Account",
"operation": "Upsert",
"externalId": "Name",
"afterAddons": [
{
"module": "core:ExportFiles",
"args": {
"operation": "Upsert",
"externalId": "Title",
"deleteOldData": false,
"sourceWhere": "FileType = 'PDF'",
"maxFileSize": 38797312
}
}
]
}
Complete Examples
Example 1: Simple reference data
{
"sfdxHardisLabel": "Reference Data",
"sfdxHardisDescription": "Core reference data for scratch orgs and sandboxes",
"promptOnMissingParentObjects": false,
"promptOnIssuesInCSVFiles": false,
"objects": [
{
"query": "SELECT Id FROM RecordType WHERE IsActive = true",
"operation": "Readonly",
"externalId": "DeveloperName;NamespacePrefix;SobjectType"
},
{
"query": "SELECT Id FROM User WHERE IsActive = true",
"operation": "Readonly",
"externalId": "Username"
},
{
"query": "SELECT all FROM Account WHERE Type = 'Customer'",
"operation": "Upsert",
"externalId": "Name"
},
{
"query": "SELECT all FROM Contact",
"operation": "Upsert",
"externalId": "Email",
"master": false
}
]
}
Example 2: Data anonymization
{
"sfdxHardisLabel": "Anonymize PII",
"sfdxHardisDescription": "Anonymize personal data in contacts and accounts",
"allOrNone": true,
"objects": [
{
"query": "SELECT Id, Name, Phone, Website FROM Account",
"operation": "Update",
"externalId": "Id",
"updateWithMockData": true,
"mockFields": [
{ "name": "Name", "pattern": "name" },
{ "name": "Phone", "pattern": "integer" },
{ "name": "Website", "pattern": "url" }
]
},
{
"query": "SELECT Id, FirstName, LastName, Email, Phone, MailingStreet, MailingCity FROM Contact",
"operation": "Update",
"externalId": "Id",
"updateWithMockData": true,
"mockFields": [
{ "name": "FirstName", "pattern": "first_name" },
{ "name": "LastName", "pattern": "last_name" },
{ "name": "Email", "pattern": "email" },
{ "name": "Phone", "pattern": "integer" },
{ "name": "MailingStreet", "pattern": "street" },
{ "name": "MailingCity", "pattern": "city" }
]
}
]
}
Example 3: Delete old records
{
"sfdxHardisLabel": "Clean Test Data",
"sfdxHardisDescription": "Remove test records created during QA",
"runnableInProduction": false,
"objects": [
{
"query": "SELECT Id FROM Case WHERE Subject LIKE 'TEST%'",
"operation": "DeleteSource",
"deleteFromSource": true
},
{
"query": "SELECT Id FROM Opportunity WHERE Name LIKE 'TEST%'",
"operation": "DeleteSource",
"deleteFromSource": true
},
{
"query": "SELECT Id FROM Account WHERE Name LIKE 'TEST%'",
"operation": "DeleteSource",
"deleteFromSource": true
}
]
}
Example 4: CPQ configuration (complex multi-object)
{
"sfdxHardisLabel": "CPQ Configuration",
"sfdxHardisDescription": "Export/import Salesforce CPQ configuration objects",
"allOrNone": true,
"concurrencyMode": "Serial",
"bulkApiVersion": "1.0",
"promptOnMissingParentObjects": false,
"promptOnIssuesInCSVFiles": false,
"objects": [
{ "query": "SELECT all FROM SBQQ__PricingGuidance__c", "operation": "Upsert", "externalId": "Name" },
{ "query": "SELECT all FROM Product2", "operation": "Upsert", "externalId": "Name" },
{ "query": "SELECT all FROM SBQQ__ProductFeature__c", "operation": "Upsert", "externalId": "Name" },
{ "query": "SELECT all FROM SBQQ__ProductOption__c", "operation": "Upsert", "externalId": "Name" },
{ "query": "SELECT all FROM Pricebook2", "operation": "Upsert", "externalId": "Name" },
{ "query": "SELECT all FROM PricebookEntry", "operation": "Upsert", "externalId": "Name" },
{ "query": "SELECT all FROM SBQQ__ProductRule__c", "operation": "Upsert", "externalId": "Name" },
{ "query": "SELECT all FROM SBQQ__ConfigurationRule__c", "operation": "Upsert", "externalId": "Name" },
{ "query": "SELECT all FROM SBQQ__PriceRule__c", "operation": "Upsert", "externalId": "Name" },
{ "query": "SELECT all FROM SBQQ__DiscountSchedule__c", "operation": "Upsert", "externalId": "Name" },
{ "query": "SELECT all FROM SBQQ__DiscountCategory__c", "operation": "Upsert", "externalId": "Name" },
{ "query": "SELECT all FROM SBQQ__ErrorCondition__c", "operation": "Upsert", "externalId": "Name" },
{ "query": "SELECT all FROM SBQQ__PriceCondition__c", "operation": "Upsert", "externalId": "Name" },
{ "query": "SELECT all FROM SBQQ__PriceAction__c", "operation": "Upsert", "externalId": "Name" },
{ "query": "SELECT all FROM SBQQ__ProductAction__c", "operation": "Upsert", "externalId": "Name" },
{ "query": "SELECT all FROM SBQQ__SummaryVariable__c", "operation": "Upsert", "externalId": "Name" },
{ "query": "SELECT all FROM SBQQ__LookupQuery__c", "operation": "Upsert", "externalId": "Name" },
{ "query": "SELECT Name FROM Account", "operation": "Readonly", "externalId": "Name" },
{ "query": "SELECT Name FROM Order", "operation": "Readonly", "externalId": "Name" }
]
}
Example 5: Cross-org migration with field mapping and transformations
{
"sfdxHardisLabel": "Cross-Org Account Migration",
"sfdxHardisDescription": "Migrate accounts between orgs with different field structures",
"excludeIdsFromCSVFiles": true,
"promptOnMissingParentObjects": false,
"promptOnIssuesInCSVFiles": false,
"objects": [
{
"query": "SELECT Id FROM RecordType WHERE IsActive = true",
"operation": "Readonly",
"externalId": "DeveloperName;SobjectType"
},
{
"query": "SELECT Id, Name, BillingStreet, BillingCity, BillingState, Phone, Industry FROM Account",
"operation": "Upsert",
"externalId": "Name",
"useFieldMapping": true,
"fieldMapping": [
{ "targetObject": "Account" },
{ "sourceField": "BillingStreet", "targetField": "ShippingStreet" },
{ "sourceField": "BillingCity", "targetField": "ShippingCity" },
{ "sourceField": "BillingState", "targetField": "ShippingState" }
]
},
{
"query": "SELECT Id, FirstName, LastName, Email, AccountId, Title FROM Contact",
"operation": "Upsert",
"externalId": "Email",
"master": false
}
],
"dataRetrievedAddons": [
{
"module": "core:RecordsTransform",
"args": {
"fields": [
{ "alias": "acctName", "sourceObject": "Account", "sourceField": "Name" },
{ "alias": "acctIndustry", "sourceObject": "Account", "sourceField": "Industry" }
],
"transformations": [
{
"targetObject": "Account",
"targetField": "Description",
"formula": "'Migrated from source. Industry: ' + (formula.acctIndustry || 'N/A')"
}
]
}
}
]
}
Example 6: Files and attachments migration
{
"sfdxHardisLabel": "Account Files",
"sfdxHardisDescription": "Migrate accounts with their attached files",
"promptOnMissingParentObjects": false,
"promptOnIssuesInCSVFiles": false,
"objects": [
{
"query": "SELECT all FROM Account WHERE CreatedDate > LAST_N_DAYS:365",
"operation": "Upsert",
"externalId": "Name",
"afterAddons": [
{
"module": "core:ExportFiles",
"args": {
"operation": "Upsert",
"externalId": "Title",
"deleteOldData": false
}
}
]
}
]
}
Example 7: Conga configuration
{
"sfdxHardisLabel": "Conga Configuration",
"sfdxHardisDescription": "Export/import Conga templates, queries, email templates and solutions",
"objects": [
{ "query": "SELECT all FROM APXTConga4__Conga_Template__c", "operation": "Upsert", "externalId": "APXTConga4__Key__c" },
{ "query": "SELECT all FROM APXTConga4__Conga_Merge_Query__c", "operation": "Upsert", "externalId": "APXTConga4__Key__c" },
{ "query": "SELECT all FROM APXTConga4__Conga_Email_Template__c", "operation": "Upsert", "externalId": "APXTConga4__Key__c" },
{ "query": "SELECT all FROM APXTConga4__Conga_Solution__c", "operation": "Upsert", "externalId": "Name" }
]
}
Troubleshooting
| Issue | Solution |
|---|---|
sfdmu plugin not found |
Install it: sf plugins install sfdmu |
Cannot modify production org |
Set sfdmuCanModify in .sfdx-hardis.yml or SFDMU_CAN_MODIFY env var |
export.json contains deletion info |
Delete workspaces and import workspaces must be separate; use sf hardis:org:data:delete for deletion workspaces |
Missing parent records |
Add parent objects as Readonly entries before child objects in the objects array |
External ID mismatch |
Verify the externalId field exists and is populated in both source and target |
Bulk API timeout |
Reduce bulkApiV1BatchSize, switch to concurrencyMode: "Serial", or use alwaysUseRestApi: true |
CSV encoding issues |
Set csvFileEncoding and csvUseUtf8Bom in export.json |
See Also
- SFDMU Documentation - complete SFDMU reference
- Deployment actions - all deployment action types
- Using sfdx-hardis with AI Coding Agents - other agent skills
- AI Agents Overview - all agent-compatible commands
hardis:org:data:exportcommand referencehardis:org:data:importcommand referencehardis:org:data:deletecommand referencehardis:org:configure:datacommand reference- sfdx-hardis for CPQ
- sfdx-hardis for Conga