When planning data exchange becomes bi-directional, complexity increases fast. You are not only loading actuals into planning; you are also pushing approved forecasts, workforce assumptions, and CAPEX outcomes back into downstream systems.
If this is not designed as a controlled integration product, period-end operations degrade under volume.
Integration profile
| Layer | Primary system | Direction | Frequency | Design objective |
|---|---|---|---|---|
| Actuals intake | ERP + subledgers | Source -> EPM | Hourly + close-day burst | Fresh planning baseline |
| Plan publish | EPM | EPM -> ERP/Data lake | Daily + on-approval | Trusted execution plan |
| Reference master data | MDM + ERP | Source -> EPM | Event-driven | Dimension alignment |
| Reconciliation stream | EPM + ERP | Bi-directional | Every run | Audit-grade traceability |
Bi-directional means both systems are authoritative for different objects. That boundary must be explicit in the contract.
Canonical data contract before any mapping
Define one canonical payload model for all planning exchanges. No direct source-to-target custom mapping per interface.
| Contract field | Type | Owner | Validation |
|---|---|---|---|
fiscal_period | string | Finance control | Must map to open calendar period |
scenario | string | FP&A | Must exist in scenario registry |
entity | string | Controllership | Must exist in legal entity master |
account | string | Accounting | Must map to account hierarchy leaf |
amount | decimal(18,4) | Source system | Must pass sign rule + tolerance |
currency | string | Treasury | Must be valid ISO and conversion-ready |
load_id | uuid | Integration runtime | Required for idempotent replay |
This makes each run deterministic and recoverable.
Delta extraction strategy for high-volume periods
Use watermark-based extraction with immutable run manifests. Avoid full refresh during close windows.
sql
1-- Incremental extraction from ERP staging view2select3 doc_id,4 line_id,5 fiscal_period,6 scenario,7 entity,8 account,9 amount,10 currency,11 source_last_updated_at12from erp_planning_export_v13where source_last_updated_at > :watermark_start14 and source_last_updated_at <= :watermark_end15 and fiscal_year = :target_year;Every extraction writes a manifest row:
json
1{2 "load_id": "97f6d79d-2e14-4f5f-97a8-58f3d2851f61",3 "window_start": "2026-03-02T00:00:00Z",4 "window_end": "2026-03-02T01:00:00Z",5 "record_count": 842391,6 "hash": "sha256:1f3c...8ab2",7 "status": "published"8}This gives replay confidence without data duplication.
End-to-end run control pattern
- Extract delta records from authoritative source.
- Standardize to canonical schema and attach
load_id. - Apply dimension and currency validations.
- Write accepted and rejected sets to separate lanes.
- Load accepted set into EPM import tables.
- Publish run-level reconciliation summary.
- If any critical validation fails, mark run
blockedand stop downstream
publish.
Use hard stops for control failures. Use soft warnings for non-material variance that is still traceable.
Publish-back pattern from EPM to ERP
For outbound planning numbers, publish only approved slices.
| Outbound object | Trigger | Gating rule | Target |
|---|---|---|---|
| Revenue forecast | Scenario approval | Approval status = locked | ERP planning schema |
| Workforce plan | Workforce sign-off | Headcount control check passed | HR/ERP feed |
| CAPEX plan | Portfolio committee decision | CAPEX threshold check passed | Asset planning model |
Do not push analyst working versions. Only publish governance-approved intersections.
Reconciliation design that auditors can actually use
Three reconciliation levels are required:
- Record-level: source doc and line traceability.
- Intersection-level: period x scenario x entity x account totals.
- Run-level: source extracted vs accepted vs rejected vs loaded.
Example run-level checkpoint:
text
1Run: 97f6d79d-2e14-4f5f-97a8-58f3d2851f612Extracted: 842,3913Accepted: 839,9554Rejected: 2,4365Loaded to EPM: 839,9556Variance to Source Amount: 0.00007Status: PASSFailure handling and restart strategy
Restart should be explicit, not manual improvisation.
- Use idempotent keys:
load_id + source_record_key. - Keep rejected records in a quarantine table.
- Reprocess only corrected rejects, not full historical windows.
- Archive manifests and reconciliation outputs for each run.
This is the difference between "integration works most of the time" and "integration is operationally reliable."
What to measure after go-live
| KPI | Target band | Why it matters |
|---|---|---|
| Delta run success rate | > 99% | Operational reliability |
| Replay duplication rate | 0% | Idempotency quality |
| Reconciliation exception rate | < 0.5% | Control maturity |
| End-to-end latency | < 20 min | Planning freshness |
A bi-directional planning integration should be treated like production infrastructure, not a one-time project artifact.