This guide is for migration operators — people who configure, run, monitor, and troubleshoot migrations using the Azure DevOps Migration Platform. It covers everything from first config to production troubleshooting.
For architecture internals, see architecture.md. For module developer details, see module-development-guide.md.
Binary: devopsmigration
1. Create config file (migration.json)
2. devopsmigration queue --config migration.json --follow # Export + monitor
3. Review Identities/prepare-report.json, edit mapping.json
4. devopsmigration queue --config migration.json --follow # Import
5. devopsmigration manage status --job <id> # Check final state
All configuration lives in a single JSON file, nested under the MigrationPlatform root key. Keys are PascalCase.
{
"MigrationPlatform": {
"ConfigVersion": "1.0",
"Mode": "Export | Prepare | Import | Migrate",
"Package": {
"WorkingDirectory": "D:\\exports\\run-001",
"CreatePackage": false
},
"Source": { ... },
"Target": { ... },
"Tools": { ... },
"Modules": { ... },
"Policies": { ... },
"Environment": { ... }
}
}| Mode | What It Does |
|---|---|
Export |
Extract data from source into the package |
Prepare |
Validate target readiness, generate mapping reports |
Import |
Load a pre-exported package into the target |
Migrate |
Full Export → Prepare → Import in one job |
Credential fields (AccessToken) resolve in this order:
$ENV:VARNAME→ reads environment variable (fails if unset or empty)- Non-empty literal → used as-is
- Null or empty → Windows-integrated auth
Always use $ENV: for secrets. Never put PATs in config files.
The CLI auto-appends org and project folders:
WorkingDirectory: "D:\\exports" → D:\exports\<org>\<project>\
"Source": {
"Type": "AzureDevOpsServices",
"Url": "https://dev.azure.com/myorg",
"Project": "MyProject",
"ApiVersion": "7.1",
"Authentication": {
"Type": "AccessToken",
"AccessToken": "$ENV:MIGRATION_PAT"
}
}"Source": {
"Type": "TeamFoundationServer",
"Url": "http://tfs.internal:8080/tfs/DefaultCollection",
"Project": "MyProject",
"Authentication": {
"Type": "Windows"
}
}- Runs in a dedicated net481 agent (Windows-only)
- Windows-integrated auth only
- Cannot use blob store packages — file paths only
- Package output is identical to ADO exports
"Source": {
"Type": "Simulated",
"Seed": 42,
"Generator": {
"Projects": [
{
"Name": "SimulatedProject1",
"WorkItemTypes": [
{ "Type": "Bug", "Count": 5, "RevisionsPerItem": 3 },
{ "Type": "Task", "Count": 5, "RevisionsPerItem": 2 }
]
}
]
}
}Same seed = identical data every run. Use simulated source/target to validate your config before connecting to live systems.
Modules execute in a recommended order: Identities → Nodes → Teams → WorkItems. Each module handles both export and import for its domain.
What it does: Exports user/group descriptors from the source; provides identity mapping to all other modules during import.
Configuration:
"Modules": {
"Identities": {
"Enabled": true,
"DefaultIdentity": "migration-service@contoso.com"
}
}Package layout:
Identities/
descriptors.jsonl ← One identity per line (export output)
mapping.json ← Operator-editable overrides (never auto-modified)
prepare-report.json ← Auto-matched & unresolved (Prepare output)
unresolved.json ← Identities that couldn't be resolved (import output)
Operator workflow:
- Export →
descriptors.jsonlwritten - Prepare → Target queried for matches.
prepare-report.jsongenerated showing auto-matched and unresolved identities - Review → Open
prepare-report.json. For each unresolved identity, add an explicit mapping tomapping.json - Import → Resolution order:
mapping.json→ auto-match by UPN/display name → fallback toDefaultIdentity
Important:
mapping.jsonis never overwritten by the tool. It is your file to edit. Back it up.
Resolution rules during import:
| Priority | Source | Behavior |
|---|---|---|
| 1 | mapping.json entry |
Explicit operator override — always wins |
| 2 | UPN/display name match | Auto-resolved against target |
| 3 | No match found | Logged to unresolved.json, DefaultIdentity used |
What it does: Exports and imports area/iteration classification trees.
Configuration:
"Modules": {
"Nodes": {
"Enabled": true,
"ReplicateSourceTree": true,
"AutoCreateNodes": true
}
}| Option | Default | What It Does |
|---|---|---|
ReplicateSourceTree |
true |
Copy the full source area/iteration tree to the target |
AutoCreateNodes |
true |
Auto-create any referenced path that doesn't exist on the target |
Package layout:
Nodes/
source-tree.json ← Full source tree snapshot
referenced-paths.json ← Paths referenced by exported work items
prepare-report.json ← Path existence validation against target
Gotcha: If both
ReplicateSourceTreeandAutoCreateNodesarefalse, import assumes the target tree already exists. Any missing path causes a failure.
What it does: Exports and imports team membership, settings, sprint iterations, member capacity.
Configuration:
"Modules": {
"Teams": {
"Enabled": true,
"AlwaysExport": false,
"Extensions": {
"TeamSettings": true,
"NodeTranslation": true,
"TeamIterations": true,
"TeamMembers": true,
"IdentityLookup": true,
"TeamCapacity": true
}
}
}| Extension | What It Exports/Imports |
|---|---|
TeamSettings |
Board config, backlog level, bugs behaviour, working days |
NodeTranslation |
Records team area/iteration paths for node reference tracking |
TeamIterations |
Sprint/iteration assignments (including default and backlog iterations) |
TeamMembers |
Team membership with admin flags |
IdentityLookup |
Resolves team member identities via the identity translation tool. Members whose identity resolves to the configured default are skipped (logged as unresolvable) rather than imported under the wrong identity. |
TeamCapacity |
Per-member, per-sprint capacity data |
Default team is not assigned automatically. The Azure DevOps API does not support explicitly setting a project's default team. When the import encounters the source default team it logs a structured warning ("target API does not support explicit default team assignment") and continues. After import, set the default team manually in the target project via Project Settings → Teams.
Untranslatable area/iteration paths are skipped. If a team's area or iteration path cannot be mapped to the target classification tree, that path is excluded (with a warning) rather than written through unchanged — ensure the source classification tree is replicated (
Nodes.ReplicateSourceTree = true) so paths resolve.
| Option | Default | What It Does |
|---|---|---|
AlwaysExport |
false |
When false, skips teams whose team.json already exists (resumable). When true, re-fetches every team. |
Package layout:
Teams/
{team-slug}/
team.json ← Team definition (settings, iterations, members, capacity)
prepare-report.json ← Team/group validation against target
What it does: High-fidelity work item revision export and import — the core of the migration.
Configuration:
"Modules": {
"WorkItems": {
"Enabled": true,
"Scope": {
"Query": "SELECT [System.Id] FROM WorkItems WHERE [System.TeamProject] = @project ORDER BY [System.ChangedDate] ASC",
"Filters": [
{ "Mode": "Include", "Field": "System.AreaPath", "Pattern": "^TeamAlpha" },
{ "Mode": "Exclude", "Field": "System.State", "Pattern": "Removed" }
]
},
"Extensions": {
"Revisions": { "Enabled": true },
"Links": { "Enabled": true },
"Attachments": { "Enabled": true },
"Comments": { "Enabled": true },
"EmbeddedImages": { "Enabled": true }
}
}
}The Query selects which work items to process. @project is substituted with the configured project name.
Filters apply post-fetch, using AND logic:
| Filter Mode | Behavior |
|---|---|
Include |
Retain only items where field matches the regex pattern |
Exclude |
Discard items where field matches the regex pattern |
- Multiple filters are AND-combined
- Missing field on an item → passes
Exclude, failsInclude - Patterns are case-insensitive with a 2-second regex timeout
- Tip: Use indexed fields (
System.AreaPath,System.WorkItemType) to minimize API pre-fetch time
Example — Export only Bugs and Tasks from a specific area:
"Filters": [
{ "Mode": "Include", "Field": "System.WorkItemType", "Pattern": "^(Bug|Task)$" },
{ "Mode": "Include", "Field": "System.AreaPath", "Pattern": "^MyProject\\\\TeamAlpha" }
]| Extension | What It Does | When to Disable |
|---|---|---|
Revisions |
Export full revision history. false = latest state only |
Speed over fidelity (e.g. one-time snapshot) |
Links |
Related links, external links, hyperlinks | If link relationships are not needed on target |
Attachments |
Download and store binary attachment files | Large packages with many attachments — consider a second pass |
Comments |
Fetch comment versions from the ADO Comments API | If comments are not needed |
EmbeddedImages |
Download inline images from HTML/Markdown fields, rewrite URLs | If HTML fields don't contain embedded images |
Streaming import processes each revision in four ordered stages. The cursor advances after each stage, enabling fine-grained resume:
CreatedOrUpdated → AppliedFields → AppliedLinks → UploadedAttachments → Completed
If import is interrupted mid-revision, it resumes from the last completed stage — not from the beginning.
When importing into a target that already has some work items, configure an ID resolution strategy to seed the ID map:
"Extensions": {
"WorkItemResolutionStrategy": {
"Enabled": true,
"Parameters": {
"Strategy": "TargetField",
"FieldName": "Custom.SourceWorkItemId"
}
}
}| Strategy | How It Finds Existing Target IDs |
|---|---|
TargetField |
Reads a custom field (e.g. Custom.SourceWorkItemId) on each target item |
TargetHyperlink |
Scans hyperlinks on target items for URLs matching a source pattern |
Package layout:
WorkItems/
yyyy-MM-dd/ ← Chronological date folder
<ticks>-<workItemId>-<revisionIndex>/
revision.json ← Revision state
comment.json ← Comment versions (if enabled)
attachment-<filename> ← Binary attachments (if enabled)
Tools are shared services configured under MigrationPlatform.Tools. They are pure transformations — no I/O, no mutable state. Tools are consumed by modules and extensions during import.
Applies declarative field transformations to work item revisions. Transform groups execute in array order.
Configuration:
"Tools": {
"FieldTransform": {
"Enabled": true,
"TransformGroups": [
{
"Name": "MyTransformGroup",
"Enabled": true,
"ApplyTo": ["Bug", "UserStory"],
"Transforms": [ ... ]
}
]
}
}ApplyTois optional — omit to apply to all work item types- Groups execute in array order; transforms within a group execute in array order
- Each transform must specify at minimum
TypeandField
| Type | Description | Key Parameters |
|---|---|---|
MapValue |
Translate field values via a dictionary | Field, ValueMap (key→value pairs) |
CopyField |
Copy value from one field to another | Field (source), TargetField |
CopyFieldBatch |
Copy multiple fields in one declaration | Array of source→target pairs |
SetField |
Set a field to a literal constant | Field, Value |
ClearField |
Null out a field's value | Field |
ExcludeField |
Remove a field from the revision entirely | Field |
RegexField |
Regex find-and-replace on a field value | Field, Pattern, Replacement |
MergeFields |
Concatenate multiple fields into one | SourceFields, TargetField, FormatTemplate |
CalculateField |
Compute a value from an expression | Field, Expression |
ConditionalField |
Apply a transform only when a condition is met | Field, Condition, inner transform |
ConditionalTag |
Add/remove a tag based on a condition | Condition, Tag |
FieldToTag |
Promote a field's value to a tag | Field |
MergeToTag |
Merge multiple field values into a tag | SourceFields, Separator |
TreeToTag |
Flatten a hierarchical path to tag values | Field (area/iteration path) |
Recipe 1 — Remap State values (e.g. "Active" → "In Progress"):
{
"Name": "StateRemapping",
"ApplyTo": ["Bug", "UserStory"],
"Transforms": [
{
"Type": "MapValue",
"Field": "System.State",
"ValueMap": {
"Active": "In Progress",
"Resolved": "Done",
"New": "To Do"
}
}
]
}Recipe 2 — Copy a custom field to a standard field:
{
"Name": "CopyPriority",
"Transforms": [
{
"Type": "CopyField",
"Field": "Custom.OldPriority",
"TargetField": "Microsoft.VSTS.Common.Priority"
}
]
}Recipe 3 — Strip a prefix from area paths using regex:
{
"Name": "CleanAreaPaths",
"Transforms": [
{
"Type": "RegexField",
"Field": "System.AreaPath",
"Pattern": "^OldProject\\\\Legacy\\\\",
"Replacement": "NewProject\\"
}
]
}Recipe 4 — Tag work items by their source area path:
{
"Name": "TagByArea",
"Transforms": [
{
"Type": "TreeToTag",
"Field": "System.AreaPath"
}
]
}Recipe 5 — Set a tracking field on all imported items:
{
"Name": "MarkAsMigrated",
"Transforms": [
{
"Type": "SetField",
"Field": "Custom.MigratedFrom",
"Value": "ADO-OldOrg"
}
]
}Recipe 6 — Exclude a deprecated field:
{
"Name": "RemoveLegacyField",
"Transforms": [
{
"Type": "ExcludeField",
"Field": "Custom.DeprecatedField"
}
]
}Recipe 7 — Conditionally tag items based on field value:
{
"Name": "TagHighPriority",
"Transforms": [
{
"Type": "ConditionalTag",
"Field": "Microsoft.VSTS.Common.Priority",
"Condition": "== 1",
"Tag": "P1-Migrated"
}
]
}Controls area/iteration path remapping, node auto-creation, and source tree replication during import.
"Tools": {
"NodeTranslation": {
"Enabled": true,
"ReplicateSourceTree": true,
"AutoCreateNodes": true,
"SkipOnUnresolvableArea": false,
"SkipOnUnresolvableIteration": false,
"AreaLanguageOverride": null,
"IterationLanguageOverride": null,
"AreaPathMappings": [
{ "Match": "^OldProject\\\\", "Replacement": "NewProject\\" }
],
"IterationPathMappings": []
}
}| Option | Default | Purpose |
|---|---|---|
ReplicateSourceTree |
false |
Copy full source area/iteration tree to target before import |
AutoCreateNodes |
true |
Auto-create any referenced path that doesn't exist on target |
SkipOnUnresolvableArea |
false |
Skip a work item (instead of failing) when its area path can't be resolved |
SkipOnUnresolvableIteration |
false |
Skip instead of fail on unresolvable iteration paths |
AreaLanguageOverride |
null |
Override localised root node name (e.g. "Area" in English, "Bereich" in German) |
IterationLanguageOverride |
null |
Same for iteration paths |
AreaPathMappings |
[] |
Regex-based area path transformations. Each entry: Match (regex) + Replacement. Applied in order. |
IterationPathMappings |
[] |
Same as AreaPathMappings but for iteration paths |
Common pattern — Rename project prefix:
"AreaPathMappings": [
{ "Match": "^OldProject\\\\", "Replacement": "NewProject\\" }
]Localisation — German TFS instance:
"AreaLanguageOverride": "Bereich",
"IterationLanguageOverride": "Iteration"Provides a default fallback identity for unmapped accounts.
"Tools": {
"IdentityLookup": {
"Enabled": true,
"DefaultIdentity": "migration-service@contoso.com"
}
}During work item import, tools are applied in this order:
- IdentityLookup — resolves identity fields to target identities
- NodeTranslation — remaps area/iteration paths, creates missing nodes
- FieldTransform — applies all declared transform groups in array order
Each tool sees the output of the previous tool. Plan your transforms accordingly — a FieldTransform that references System.AreaPath will see the path after NodeTranslation has remapped it.
Optional tuning knobs under MigrationPlatform.Policies:
"Policies": {
"Retries": { "Max": 8 },
"Throttle": { "MaxConcurrency": 4 },
"Checkpoints": { "Interval": 300 },
"Validation": {
"ContinueOnError": false,
"WorkItemCountTolerance": 0,
"FailOnUnresolvedIdentities": false,
"SampleRate": 0.05
}
}| Policy | Default | What It Controls |
|---|---|---|
Retries.Max |
3 |
Maximum retry attempts for transient API failures |
Throttle.MaxConcurrency |
2 |
Maximum parallel API requests |
Checkpoints.Interval |
300 |
Seconds between checkpoint flushes |
Validation.ContinueOnError |
false |
If true, pre-flight failures are logged but don't halt import |
Validation.WorkItemCountTolerance |
0 |
Items that may be missing in post-flight without failing (0 = exact match) |
Validation.FailOnUnresolvedIdentities |
false |
If true, any unresolved identity causes import failure |
Validation.SampleRate |
0.05 |
Fraction of items sampled in post-flight validation (0–1.0) |
| Command | Purpose |
|---|---|
queue --config <path> [--follow] [--level WARNING] [--force-fresh] [--port 5200] |
Submit a migration job |
prepare --config <path> |
Submit a Prepare job (validate target, generate reports) |
queue options:
| Option | Default | Purpose |
|---|---|---|
--follow |
implicit in standalone | Stream diagnostic logs inline |
--level |
Information |
Agent diagnostic minimum level (WARNING, ERROR, Information) |
--force-fresh |
false |
Delete module cursors so enumeration restarts (identity map preserved) |
--port |
5100 |
Override control plane port in standalone mode |
| Command | Purpose |
|---|---|
manage list |
List all jobs, status & progress |
manage status --job <uuid> |
Job state and per-module progress |
manage progress --job <uuid> |
Fetch ProgressEvent records as NDJSON |
manage diagnostics --job <uuid> [--level WARNING] |
Download agent logs (optionally filtered) |
manage pause --job <uuid> |
Signal agent to checkpoint and pause |
manage resume --job <uuid> |
Resume a paused job |
manage cancel --job <uuid> |
Cancel a queued or running job |
manage login --url <endpoint> |
Authenticate with control plane |
manage logout --url <endpoint> |
Revoke session token |
| Command | Purpose |
|---|---|
queue --config <inventory-config.json> |
Submit an inventory job (Mode: Inventory) |
queue --config <dependencies-config.json> |
Submit a dependency analysis job (Mode: Dependencies) |
| Command | Purpose |
|---|---|
config new [--output my-migration.json] [--force] |
Interactive wizard to create config file |
config set <key> <value> |
Set user-level preference |
config get <key> |
Read user-level preference |
| Command | Purpose |
|---|---|
controlplane start [--port 5100] |
Start bundled Control Plane host in foreground |
tui |
Open interactive Terminal UI for live job monitoring |
--config <path>supplied → use directly$Env:MigrationPlatform_Scenario_Folder→ scan & promptpreferences.json→scenario-folder→ scan & prompt./scenariossubfolder of cwd → use if found*.jsonin cwd → scan & prompt- Nothing found → warning with guidance
| Option | Short | Default | Description |
|---|---|---|---|
--config |
-c |
(auto-resolution) | Path to migration config file |
--verbose |
-v |
false |
Verbose console output |
--disable-telemetry |
— | false |
Suppress all telemetry export |
--port |
— | 5100 |
Control plane port (standalone mode) |
The platform uses four tiers of validation:
Runs before any job submission:
- Config parses as valid JSON
ConfigVersionis supported- Required fields present (
Mode,Package.WorkingDirectory,Modules) - Module names are registered
- Policy values are in valid ranges
Failure: Non-zero exit, no network call made.
Runs before job submission:
- Source/target reachable
- Projects exist
- Credentials valid with required permissions
- Package path accessible
Failure: Non-zero exit with human-readable error.
Runs inside the agent before ImportAsync:
manifest.jsonexists and is valid- All
revision.jsonfiles are valid - Attachment files exist and hashes match
- Identity mapping integrity verified
Failure: Fail-fast by default. Set Validation.ContinueOnError: true to log and continue (not recommended for production).
Runs after ImportAsync completes:
- Work item count parity (within
WorkItemCountTolerance) - Link and attachment integrity (sample-based)
- Unresolved identities recorded
- All module cursors at
Completed
Output: validation-report.json written regardless of pass/fail.
<WorkingDirectory>/
.migration/
migration-config.json ← Config snapshot written by agent at startup
plan.json ← Execution plan persisted after every task transition
inventory.complete.json ← Inventory completion marker
prepare.complete.json ← Prepare completion marker
<org>/<project>/
manifest.json ← Package metadata, source info, included types
Identities/ ← Identity descriptors and mappings
Nodes/ ← Classification tree snapshots
Teams/ ← Team definitions by slug
WorkItems/ ← Revision folders by date
.migration/ ← Project-scoped cursor files
/.migration/
inventory.complete.json ← Marker: Inventory completed
prepare.complete.json ← Marker: Prepare completed
/{org}/{project}/.migration/
inventory.workitems.cursor.json ← Inventory resume position for WorkItems in this project
export.workitems.cursor.json ← Export resume position for WorkItems in this project
import.workitems.cursor.json ← Import resume position for WorkItems in this project
idmap.dbis locked exclusively during job execution. A second agent against the same package will fail fast.
Set Package.CreatePackage: true to zip the package after export and unzip before import. Guarantees order preservation and streaming compatibility.
devopsmigration queue --config migration.json --follow
# (Interrupted)
# Re-run the same command:
devopsmigration queue --config migration.json --followCursors resume from last checkpoint. Identity map preserved. Logs append.
devopsmigration queue --config migration.json --force-freshDeletes module cursor files so enumeration restarts. Does NOT delete idmap.db (prevents duplicates) or mapping.json (operator-edited).
# Use a simulated scenario to validate config
devopsmigration queue --config scenarios/roundtrip-simulated.json --followSet Enabled: false on orgs you don't want to iterate. They still participate in GUID→project name resolution:
"Organisations": [
{ "Url": "https://dev.azure.com/org1", "Projects": ["MyProject"], "Enabled": true },
{ "Url": "https://dev.azure.com/org2", "Enabled": false },
{ "Url": "https://dev.azure.com/org3", "Enabled": false }
]Discovery runs only against org1/MyProject. Cross-org link GUIDs pointing at org2/org3 are still resolved to readable names.
Validation.FailOnUnresolvedIdentities defaults to false. Always review Identities/prepare-report.json after Prepare and populate mapping.json.
Multiple filters are AND-combined. This catches operators who expect OR:
"Filters": [
{ "Mode": "Include", "Field": "System.AreaPath", "Pattern": "^TeamA" },
{ "Mode": "Exclude", "Field": "System.State", "Pattern": "Removed" }
]Result: Items in TeamA area AND NOT in Removed state.
It deletes *.cursor.json files — not idmap.db (prevents duplicates) and not mapping.json (operator-edited).
TFS agent is net481 and only supports file:// paths.
queue --follow combines SSE (stage transitions, cursors) and polling (aggregate counters every ~5s). TFS jobs populate ProgressEvent.Metrics; .NET 10 jobs use the separate telemetry endpoint.
Default false = fail-fast on pre-flight validation failure. Setting true allows partial/corrupted imports. Not recommended for production.
If ReplicateSourceTree: false AND AutoCreateNodes: false, any missing area/iteration path on the target causes failure.
| Error | Cause | Fix |
|---|---|---|
| "Config parses as invalid JSON" | Syntax error | Validate with VS Code (schema auto-applied) or jq |
| "Unknown key at path ..." | Typo in config | Hover in VS Code for valid keys |
| "Module 'Foo' not registered" | Misspelled module name | Check Modules section keys |
"Source and Organisations cannot both be set" |
Mode 1/Mode 2 conflict | Use one, not both |
| Error | Cause | Fix |
|---|---|---|
| "Source project 'X' not found" | Wrong project name or URL | Verify Source.Project matches exactly |
| "Target credentials insufficient" | Missing write permission | Ensure PAT has required scopes |
| "Package URI not accessible" | Path doesn't exist or blob creds invalid | Check path; verify SAS token |
| Error | Cause | Fix |
|---|---|---|
| "agent.lock exists; package in use" | Another agent running | Wait for first job to complete or delete agent.lock |
| "Cursor file corrupted" | Interrupted write | Use --force-fresh to restart |
| Error | Cause | Fix |
|---|---|---|
| "Attachment hash mismatch" | Corruption during download | Re-export from source |
| "Work item count delta exceeds tolerance" | Missing items in target | Check import logs; increase WorkItemCountTolerance or investigate |
The scenarios/ directory contains ready-to-use config templates:
| Category | Scenarios |
|---|---|
| Discovery | discovery-dependency-ado-single-project.json, inventory-ado-single-project.json, inventory-ado-multi-project.json, inventory-multi-org.json, inventory-simulated.json, inventory-tfs-windows-auth.json |
| Export | queue-export-ado-workitems-single-project.json, queue-export-ado-workitems-inline-comments.json, queue-export-workitems-simulated-source.json |
| Import | queue-import-workitems-simulated-fixture.json, queue-import-workitems-simulated-target.json |
| Full Migration | roundtrip-simulated.json |
| Regression | regression-no-filter-scopes.json |
Copy a scenario, customize for your environment, and go.
- Start with defaults — Only override policies if you need to.
- Always run Prepare first — Catches 80% of issues before Import.
- Version-control
mapping.json— It's your operator-edited file. Back it up. - Use
manage diagnostics— Download logs after job completion to diagnose issues. - Copy from
scenarios/— Use existing configs as templates. $ENV:for all secrets — Never commit PATs to config files.- Test with Simulated first — Validate your config against simulated data before live systems.
- Review identity mappings — Always check
prepare-report.jsonand populatemapping.jsonfor unresolved identities.