AI-Assisted
By Richard C, AIL Studio
1. The combination is the unit of review
A country can be allowed and a region can be allowed, yet the pair can be outside the intended target UI configuration. Checking each column against a global list misses that relationship. Record type matters too: a value available for one type need not be available for another.
This is a pre-load configuration/business-conformance review, not source-versus-target reconciliation. An import path may accept values that the UI would not offer together. A finding here is a reason to review the mapping, not proof Salesforce will reject a CSV row.
2. Freeze the intended target configuration
Example data and schema throughout: Market__c and Region__c are illustrative custom Account fields, not built-in Salesforce fields. This tutorial handles one confirmed dependency: a non-blank single-select controlling picklist and a single-select dependent picklist. It does not support multi-select fields, checkbox controllers or multiple dependency levels.
Identify your real field API names, intended target record types and appropriate read permissions. Confirm the dependency in Setup or Object Info: inspect object dependentFields and field controllingFields. Salesforce's UI API guide warns that empty controllerValues does not establish independence: field-level security can hide the controller. Stop and resolve access or configuration uncertainty rather than treating missing metadata as permission.
Using an already-approved authenticated API client, save the dependent field response for each intended target record type. The relative request is:
GET /ui-api/object-info/{objectApiName}/picklist-values/{recordTypeId}/{fieldApiName} Record the target org/environment, retrieval date, API version used, object, fields, record type and access scope alongside the snapshot. Do not substitute the reader's default record type for every row. Resolve the target type the migration intends for that row.
3. Keep API values, not translated labels
The Picklist Value reference distinguishes API value, display label and integer controller indexes in validFor. The picklist adapter documentation explains record-type scope and translated labels; value remains the untranslated API name.
Keep source-to-target business mappings explicit. Do not silently trim, uppercase or relabel input. Target record-type IDs are org-specific: map logical aliases to real target IDs, not copied source IDs. A saved response covers one field and one record type, not all org values.
For the offline input, wrap each saved per-field response body under its logical type alias. Do not pass a whole batch response directly. Keep a separate alias-to-target-ID mapping with the snapshot so reviewers can trace which response was selected.
Example data: Business and Partner below are invented type aliases. These reduced UI API-shaped fragments are not real API responses. Save as metadata.json:
{ "Business": { "controllerValues": {"EMEA": 0, "AMER": 1}, "values": [ {"label": "United Kingdom", "value": "UK", "validFor": [0]}, {"label": "United States", "value": "US", "validFor": [1]} ] }, "Partner": { "controllerValues": {"EMEA": 0, "AMER": 1}, "values": [ {"label": "United Kingdom", "value": "UK", "validFor": [0]} ] } } 4. Run a local CSV preflight
Example data: save these six invented rows as input.csv. row_key is only an output locator.
row_key,record_type,Market__c,Region__c A01,Business,EMEA,UK A02,Business,EMEA,US A03,Partner,AMER,US A04,Business,APAC,UK A05,Business,EMEA, A06,Unknown,EMEA,UK Save this complete Python 3 standard-library checker as preflight.py. It has no network or authentication code, needs no pip packages and never writes to Salesforce. Adapt the column constants only after confirming your real schema. It preserves exact input strings.
import csv import json import sys COLUMNS = ["row_key", "record_type", "Market__c", "Region__c"] def checked_metadata(item): if not isinstance(item, dict): raise ValueError("metadata must be an object") controllers = item.get("controllerValues") values = item.get("values") if not isinstance(controllers, dict) or not controllers: raise ValueError("controllerValues missing, empty or inaccessible") if any(not isinstance(k, str) or not k or type(v) is not int or v < 0 for k, v in controllers.items()): raise ValueError("bad controller index") if len(set(controllers.values())) != len(controllers): raise ValueError("duplicate controller index") if not isinstance(values, list): raise ValueError("values missing") lookup = {} for value in values: if not isinstance(value, dict): raise ValueError("bad value object") api = value.get("value") valid = value.get("validFor") if not isinstance(api, str) or not api or api in lookup: raise ValueError("missing or duplicate dependent API value") if not isinstance(valid, list) or any( type(i) is not int or i not in controllers.values() for i in valid): raise ValueError("missing or malformed validFor") lookup[api] = valid return controllers, lookup def main(): if len(sys.argv) != 3: raise ValueError("usage: python3 preflight.py metadata.json input.csv") with open(sys.argv[1], encoding="utf-8-sig") as stream: metadata = json.load(stream) if not isinstance(metadata, dict): raise ValueError("metadata root must be a type-alias object") # Validate each type once; malformed known types never auto-pass. prepared = {} for alias, item in metadata.items(): try: prepared[alias] = checked_metadata(item) except ValueError: prepared[alias] = None with open(sys.argv[2], newline="", encoding="utf-8-sig") as stream: reader = csv.DictReader(stream) if (reader.fieldnames is None or len(reader.fieldnames) != len(set(reader.fieldnames)) or not set(COLUMNS).issubset(reader.fieldnames)): raise ValueError("missing or duplicate CSV headers") findings = [] for row in reader: if None in row or any(row[k] is None for k in COLUMNS): raise ValueError("malformed CSV row") alias, controller, dependent = (row[k] for k in COLUMNS[1:]) if alias not in prepared: status = "UNKNOWN_RECORD_TYPE" elif prepared[alias] is None: status = "METADATA_REVIEW" elif controller == "" or dependent == "": status = "REVIEW_BLANK" else: controllers, values = prepared[alias] index = controllers.get(controller) if index is None: status = "UNKNOWN_CONTROLLER" elif dependent not in values: status = "VALUE_NOT_AVAILABLE_FOR_TYPE" elif index not in values[dependent]: status = "COMBINATION_NOT_AVAILABLE" else: status = "CONFIG_MATCH" findings.append([row[k] for k in COLUMNS] + [status]) writer = csv.writer(sys.stdout, lineterminator="\n") writer.writerow(COLUMNS + ["status"]) writer.writerows(findings) if __name__ == "__main__": try: main() except (OSError, ValueError, csv.Error) as error: print("ERROR: " + str(error), file=sys.stderr) sys.exit(2) Run locally:
python3 preflight.py metadata.json input.csv Optionally redirect stdout to a local findings file. An unreadable file, malformed JSON or missing CSV headers produces an explicit stderr error and exit code 2, without findings. A known type with incomplete or malformed metadata produces METADATA_REVIEW; duplicate dependent API values are rejected rather than overwritten. An empty controller map is also review-only in this confirmed-dependent scope.
For usable metadata, priority is unknown type, blanks, unknown controller, unavailable dependent value, unavailable combination, then match. Metadata safety checks come before value checks for malformed known types. Controller index zero is valid: the script tests is None, not truthiness. A well-formed empty validFor list allows no controller for this dependent-field check.
5. Read the result, then resolve deliberately
Example data — expected findings:
row_key,record_type,Market__c,Region__c,status A01,Business,EMEA,UK,CONFIG_MATCH A02,Business,EMEA,US,COMBINATION_NOT_AVAILABLE A03,Partner,AMER,US,VALUE_NOT_AVAILABLE_FOR_TYPE A04,Business,APAC,UK,UNKNOWN_CONTROLLER A05,Business,EMEA,,REVIEW_BLANK A06,Unknown,EMEA,UK,UNKNOWN_RECORD_TYPE A01 matches the configured pair, including controller index 0. A02 has a known dependent value but the wrong controller index. A03's dependent value is absent for its type. A04 has an unknown controller; A05 needs blank-value review; A06 has no saved type entry. That is exactly one configuration match and five rows to review, not five demonstrated Salesforce rejections.
The accompanying local fixture tests also replace A01's dependent input with the display label United Kingdom; it becomes VALUE_NOT_AVAILABLE_FOR_TYPE, not a match. Missing files, malformed metadata and broken CSV headers are negative cases, not successful preflights.
Have the business owner compare the source mapping with the intended target configuration. Resolve which is wrong before altering either. Do not blanket-enable values, disable restrictions, change record types, mass-update records or automatically rewrite the CSV. Keep the snapshot and findings together; re-fetch and re-run when target metadata changes.
Assign each review finding an owner and retain the mapping decision with the load plan. If the intended target type is uncertain, resolve that uncertainty first; testing the row against a convenient different type would answer the wrong question.
6. What this does not prove
This snapshot describes UI-available combinations for the reading user's access, not full server validation. Separately review required fields, validation rules, flows/triggers, FLS, import-path behavior, restricted/unrestricted picklist enforcement and historical inactive values. Use approved tooling for separate sandbox tests. Blank values are review cases, not automatically invalid.
No real org execution was performed here. The executed tests use only the labelled example fixtures. A configuration match is not a migration certification or assurance that an import will succeed.
For a separate post-load check, I make RecordBridge, which compares Account, Contact and Opportunity CSV exports on shared stable keys; it does not validate picklist metadata.