Plan and execute ERP go-live cutovers that minimize business disruption. Covers cutover timelines, data migration sequencing, parallel run strategies, rollback criteria, team coordination, and the rehearsal practices that separate smooth transitions from disasters.
ERP cutover planning is the process of orchestrating the transition from an existing operational system (legacy, standalone, or fragmented) to a new enterprise resource planning (ERP) system, ensuring data integrity, business continuity, and minimal disruption during the switch. It matters most at the final phase of an ERP implementation: after configuration, data migration, and testing, when the organization is ready to declare “go-live” and live with the new system.
The cutover window is a defined period—typically 24 to 72 hours—during which all business operations are frozen in the old system and migrated to the new ERP. It is the operational heartbeat of the transition. The window begins with a final data synchronization and ends when the new system is live, validated, and the old system is decommissioned.
Define the window using a standard cutover.json file in the project root:
{
"windowStart": "2024-05-15T06:00:00Z",
"windowEnd": "2024-05-17T06:00:00Z",
"phase": "cutover",
"dependencies": [
"data-migration-complete",
"backup-snapshot-2024-05-14",
"test-run-verified"
],
"owners": [
"[email protected]",
"[email protected]"
],
"notifications": {
"slack": "#erp-go-live",
"email": "[email protected]"
}
}
This file is consumed by the deployment pipeline. A change to windowStart triggers a pre-cutover alert via curl to a Slack webhook:
curl -X POST \
-H "Content-Type: application/json" \
-d '{"text": "Cutover window starts in 6 hours: " + windowStart}' \
https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX
Failure to update cutover.json leads to a mismatch in the expected go-live date: users begin data entry in the new system before the data is ready, and the migration team is unprepared.
The cutover process consists of five phases, each with defined inputs, outputs, and validation checks:
./scripts/sync-data.sh --from=legacy --to=erp --batch=final --mode=full to copy all data from source systems to the ERP database.SELECT
table_name,
COUNT(*) AS row_count,
SUM(CASE WHEN is_active = false THEN 1 ELSE 0 END) AS inactive_count
FROM
erp_inventory
WHERE
last_updated >= '2024-05-15T00:00:00Z'
GROUP BY
table_name
HAVING
COUNT(*) > 0
ORDER BY
row_count DESC;
systemctl stop erp-staging and git push origin main:staging to lock code.windowStart, run ./scripts/lock-data.sh --schema=erp --duration=4h to prevent new entries in the old system and trigger a LOCK TABLE erp_sales IN EXCLUSIVE MODE in PostgreSQL../scripts/migrate-final.sh \
--source=legacy \
--target=erp \
--batch=final \
--validate \
--backup=erp-final-20240515 \
--fail-on=10
This script performs:
systemctl start erp-api erp-web erp-queue erp-warehouse
./scripts/validate-first-batch.sh --batch=1 to confirm that the first 100 sales orders are created and processed correctly.0 */6 * * * /opt/erp/bin/validate-postcutover.sh --since=2024-05-17T06:00:00Z
This script compares key metrics between old and new systems:
WITH new_system AS (
SELECT
DATE(created_at) AS day,
COUNT(*) AS new_count,
SUM(amount) AS new_total
FROM erp_sales
WHERE created_at >= '2024-05-17T06:00:00Z'
GROUP BY day
),
legacy_system AS (
SELECT
DATE(entry_date) AS day,
COUNT(*) AS legacy_count,
SUM(value) AS legacy_total
FROM legacy_sales
WHERE entry_date >= '2024-05-17T06:00:00Z'
GROUP BY day
)
SELECT
ns.day,
ns.new_count,
ls.legacy_count,
(ns.new_count - ls.legacy_count) AS delta
FROM new_system ns
JOIN legacy_system ls ON ns.day = ls.day
WHERE ns.new_count > 0 OR ls.legacy_count > 0
ORDER BY ns.day DESC;
Failure: if delta exceeds 10% of average daily volume, alert via curl to a PagerDuty integration.
./scripts/freeze-legacy.sh --reason=erp-live --duration=7d to archive all old data and disable user access.pg_dump -Fc -Z 9 -f /backups/legacy-final-2024-05-24.dump legacy_dbAt windowStart, a sequence of commands is executed in order:
./scripts/lock-data.sh --schema=erp --duration=4h
# 2. Final data migration
./scripts/migrate-final.sh \
--source=legacy \
--target=erp \
--batch=final \
--validate \
--backup=erp-final-20240515 \
--fail-on=10
# 3. Validate migration
./scripts/validate-migration.sh --batch=final --threshold=99.9
# 4. Start ERP services
systemctl start erp-api erp-web erp-queue erp-warehouse
# 5. Send go-live notification
curl -X POST \
-H "Content-Type: application/json" \
-d '{"text": "ERP cutover complete. Go-live at 2024-05-15T06:00:00Z"}' \
https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX
legacy and erp databases are in different timezones, windowStart is misaligned. The solution: set export TZ=UTC in all scripts and validate with SELECT NOW(); in both systems.migrate-final.sh fails silently if --validate is passed but no validation file exists. The script expects validation/config.json but does not check for its existence. A missing config file leads to incorrect validation results, often discovered only after post-cutover.systemctl start fails to start erp-queue due to a missing config.yml file in /etc/erp/. The service logs show Failed to load config: /etc/erp/config.yml not found but the startup script does not verify the file’s presence.LOCK TABLE is not released after 4 hours. The lock-data.sh script uses BEGIN; LOCK TABLE erp_sales IN EXCLUSIVE MODE; COMMIT; but does not wrap it in a transaction. If a long-running query is interrupted, the lock persists beyond the expected window.The most common failure mode: data inconsistency between old and new systems after cutover.
sales_order created in the legacy system during the cutover window is not migrated. Cause: the final migration script reads from legacy.sales_order but does not account for new records created after windowStart.ON CONFLICT DO NOTHING in the migration’s INSERT statement.created_at timestamp is in local time, but the ERP system expects UTC. The migration script uses NOW() in UTC, but the legacy system uses LOCALTIME. The result: orders appear to be created in the future.Define a validation/checks.json file with test cases:
[
{
"name": "Sales Orders Match",
"sql": "SELECT COUNT(*) FROM erp_sales WHERE status = 'confirmed'",
"expected": 1247,
"tolerance": 5,
"description": "All confirmed sales orders must be present in the ERP"
},
{
"name": "Inventory Balance Consistency",
"sql": "SELECT SUM(quantity) FROM erp_inventory WHERE warehouse = 'main'",
"expected": 18742,
"tolerance": 100,
"description": "Total inventory in main warehouse must be within 100 units of expected"
},
{
"name": "Customer Count Match",
"sql": "SELECT COUNT(DISTINCT customer_id) FROM erp_sales",
"expected": 384,
"tolerance": 10,
"description": "Number of unique customers in sales data must match"
}
]
Run validation with:
./scripts/validate-migration.sh --batch=final --threshold=99.9
This script:
checks.jsonsql queryexpected, using tolerance as a marginlogs/validation-20240515.logFailure output:
Validation failed for batch: final
- [FAIL] Sales Orders Match: expected 1247, got 1231 (diff: -16)
- [PASS] Inventory Balance Consistency: expected 18742, got 18823 (diff: +81)
- [FAIL] Customer Count Match: expected 384, got 373 (diff: -11)
Total: 2/3 checks passed (66.7%)
A cutover fails if the ERP system is unstable after go-live. The rollback plan is executed via ./scripts/rollback-cutover.sh:
#!/bin/bash
# rollback-cutover.sh
set -euo pipefail
export BATCH_ID="final-20240515"
export BACKUP_PATH="/backups/erp-final-20240515.dump"
echo "Starting cutover rollback for batch $BATCH_ID"
# 1. Stop ERP services
systemctl stop erp-api erp-web erp-queue erp-warehouse
# 2. Restore from backup
pg_restore -d erp_db -v $BACKUP_PATH
# 3. Re-enable old system
systemctl start legacy-api legacy-web
# 4. Update cutover.json to mark rollback
jq '.status = "rolled-back", .rollback_at = now | .reason = "System instability after go-live"' cutover.json > temp.json && mv temp.json cutover.json
# 5. Notify team
curl -X POST \
-H "Content-Type: application/json" \
-d '{"text": "ERP cutover rolled back due to instability"}' \
https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX
echo "Cutover rollback completed"
pg_restore fails to restore a large schema: the command runs, but pg_restore exits with code 0 even though a critical table is missing. Cause: the .dump file was created with pg_dump -Fc, but pg_restore is called without -1 (single transaction). The solution: always use -1 and check pg_restore output for WARNING: table "some_table" not found.cutover.json is not updated. The jq command fails silently if cutover.json does not exist. The rollback is executed, but no record is made. The team assumes the system is stable when it is not.systemctl start legacy-api is run, but the service does not respond to curl http://legacy-api:8080/health. Cause: the legacy-api binary is not in the PATH. The solution: use export PATH="/opt/legacy/bin:$PATH" in the script.The technical process is only half the story. The cutover team must communicate in real time.
echo "[cutover] $message" >> logs/slack.log../scripts/generate-cutover-report.sh:#!/bin/bash
# generate-cutover-report.sh
set -euo pipefail
echo "ERP Cutover Report: $(date)"
echo "===================="
echo "Status: $(jq -r '.status' cutover.json)"
echo "Window: $(jq -r '.windowStart' cutover.json) to $(jq -r '.windowEnd' cutover.json)"
echo "Duration: $(($(date -d "$(jq -r '.windowEnd' cutover.json)" +%s) - $(date -d "$(jq -r '.windowStart' cutover.json)" +%s))) seconds)"
echo "Validation Summary:"
./scripts/validate-migration.sh --batch=final --threshold=99.9 --format=short
echo "Recent Errors:"
grep "ERROR" logs/erp-api.log | tail -5
echo "Next Steps:"
echo " - Monitor system for 24 hours"
echo " - Update cutover.json with post-cutover metrics"
This report is posted daily to Slack and archived in /reports/cutover/.
cutover.json is updated and versionedThe cutover is not a task—it is a sequence of interdependent, time-sensitive, data-driven operations. Its success hinges on precise execution, validation, and communication. A failed cutover is not a system crash; it is a breakdown in the alignment of data, time, people, and process.
This page was rewritten on 17 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.
We use cookies for analytics (Google Analytics) and advertising (Google AdSense) to improve your experience and support free content. Privacy Policy