Microsoft Project Online retires September 30, 2026, migrate to a modern platform before it's too late.Start migration
Back to Blog
Migration

Project Online Resource Pool Migration: Capacity, Cost Rates, and Calendars Without Losing History

How to migrate the Project Online Enterprise Resource Pool, generic resources, cost rate tables, availability calendars, and assignment history, to a modern PM platform without breaking your reports.

Onplana TeamMay 1, 202612 min read

Project Online Resource Pool Migration: Capacity, Cost Rates, and Calendars Without Losing History

The Enterprise Resource Pool is the second-hardest thing to migrate out of Project Online, right after enterprise custom fields. It's also the part most teams underestimate, because the pool looks "just like a list of names" until you start tracing what's connected to it: cost-rate tables with date tiers, availability calendars, generic-resource role placeholders, capacity allocations, multi-project assignments, timesheet history, and the implicit cross-project levelling state.

Get the order wrong and you import projects against an empty destination pool, end up with duplicate auto-created resources, then spend two weeks reconciling assignments that should have been an hour-long lookup.

This post walks the full resource-pool migration path: what the pool actually contains, how to inventory it, the field-by-field mapping to a modern destination, and how to handle the four sub-systems that always cause issues, cost rates, calendars, generic resources, and history.

For the surrounding migration plan, start with How to Migrate from Microsoft Project Online. For the budget impact specifically of resource-pool work, see the data-migration line item in the cost of migrating from MS Project Online in 2026, pool migration typically runs 15–25% of the total data-migration labor.

📌 TL;DR: 6-step resource-pool migration plan

  1. Inventory the pool via OData, work, generic, material, cost resources (week 1)
  2. Deduplicate by email, retire abandoned resources, validate every active resource has an email (week 1)
  3. Export base calendars + per-resource exceptions in two passes (week 2)
  4. Snapshot cost-rate Table A with date tiers; flag B-E if used (week 2)
  5. Import the pool to destination FIRST, then import projects against it (week 3)
  6. Validate assignments + capacity on the pilot project before scaling (week 4)
What the Project Online resource pool actually contains Six connected sub-systems, each needs its own export pass Enterprise Resource Pool 1. Work resources People, name, email, role, max units, calendar 2. Generic resources Role placeholders "Senior Dev", "Designer" 3. Cost rate tables A through E, date tiers Currency-typed 4. Calendars Base + per-resource exceptions (vacation) 5. Assignments Resource × Task × Hours Drives leveling 6. Timesheet history Per-day actual hours Often years of data Each box has a separate OData feed and needs its own validation pass after import.

Step 1, Inventory the pool via OData

The PWA Resource Center UI shows you resources one row at a time. The OData feed gives you everything in a single query. The free Project Online Inventory Checklist frames this whole tenant-inventory step end to end — the resource pool is one of six categories most teams underestimate, and the checklist makes the omissions concrete before you start exporting.

Pull the resource list

GET https://your-tenant.sharepoint.com/sites/pwa/_api/ProjectData/Resources

Returns one row per resource with:

Column What it tells you
ResourceName Display name
ResourceEmailAddress Primary email, your matching key
ResourceType Work (people), Material, Cost
ResourceIsGeneric true for role placeholders
ResourceMaxUnits Default max % allocation (typically 100%)
ResourceStandardRate Default $/hr from rate Table A
ResourceCalendarUID The base calendar this resource follows
ResourceCostCenter Often a custom-field reference
ResourceDepartment Org-chart bucket

A typical mid-size PMO has 200–800 rows here. About 10–15% are usually retired-but-not-deleted (left the company years ago, kept for historical reporting).

Pull cost-rate detail

GET https://your-tenant.sharepoint.com/sites/pwa/_api/ProjectData/ResourceCostRates

Returns one row per (resource, rate-table-letter, effective-date-tier). For most tenants this is mostly Table A entries; B-E rows are sparse.

Pull base-calendar references

GET https://your-tenant.sharepoint.com/sites/pwa/_api/ProjectData/ResourceAvailability

Tells you which calendar each resource is anchored to and any availability changes (typically one row per [resource, week] showing the available units).

Pull historical assignment density

GET https://your-tenant.sharepoint.com/sites/pwa/_api/ProjectData/Assignments?$filter=AssignmentStartDate ge 2024-01-01T00:00:00

Filter by start date to scope to "recently active", used in step 2 to retire dormant resources.

Step 2, Deduplicate and retire on the source side

Mature pools accumulate cruft. Two cleanups before export drastically reduce migration labor.

Typical resource-pool composition, mid-size PMO (450 resources) Cleanup on the source side cuts migration labor by ~30% Actively assigned in last 12mo (270 · 60%) Dormant, no recent assignments (90 · 20%) Duplicates (45 · 10%) Already-retired (45 · 10%) Migrate the green band; archive the rest. Half the source-side rows do not need to land in the destination at all.

Deduplicate by email

Sort the OData export by ResourceEmailAddress. Any email appearing more than once is a duplicate, usually two records for the same person created by different admins over the years. Pick one as canonical (typically the one with more recent assignments) and merge the others' assignments onto it on the source side via Project Server PowerShell:

# Pseudo-PowerShell
Get-SPProjectResource -Email "alice@acme.com" |
  Group-Object Email |
  Where-Object { $_.Count -gt 1 } |
  ForEach-Object {
    $canonical = $_.Group | Sort-Object LastModifiedDate -Descending | Select-Object -First 1
    $duplicates = $_.Group | Where-Object { $_.Id -ne $canonical.Id }
    $duplicates | ForEach-Object { Merge-SPProjectResource -From $_.Id -To $canonical.Id }
  }

Retire dormant resources

Anything with zero assignments in the last 12 months either left the company or moved off PWA. Move them to a Retired group on the source side rather than migrating them. They stay queryable in the OData export for historical reporting; they don't pollute the new tool's resource picker.

Validate every active resource has an email

The destination platform almost certainly matches by email. Run:

SELECT ResourceName FROM Resources
WHERE ResourceType = 'Work'
  AND ResourceIsGeneric = false
  AND (ResourceEmailAddress IS NULL OR ResourceEmailAddress = '')

Fix every row that comes back. If you skip this, the destination import either auto-creates a synthetic user or skips the resource entirely; both produce a mess.

Step 3, Export calendars in two passes

Project Online's calendar model is two-layered: a base calendar (e.g. "Standard 5x8" or "France 35-hour week") that defines the working pattern, plus per-resource exceptions that override the base for a specific resource (e.g. "Maria is on vacation July 1-14").

Both layers need to migrate. The base calendars first, then the exceptions referenced by the migrated resources.

Pass 1: base calendars

GET https://your-tenant.sharepoint.com/sites/pwa/_api/ProjectData/Calendars

Returns one row per calendar with the working-day pattern. Most tenants have 1-5 base calendars total. Map each to a WorkingCalendar row in the destination.

Pass 2: per-resource exceptions

GET https://your-tenant.sharepoint.com/sites/pwa/_api/ProjectData/CalendarExceptions

Returns one row per [resource, date-range, override-type] exception. Common types: vacation, sick leave, training, conference, sabbatical.

These re-create as CalendarException rows on the destination side, linked to the imported user by email-resolved user-id.

Watch the date-range semantics. Project Online stores exceptions as start/end inclusive. Some destination platforms use start-inclusive / end-exclusive. Off-by-one shifts here cause "Maria is shown working on her last vacation day" UI bugs that are subtle and slow to notice. Validate one exception per type on the pilot.

Step 4, Migrate cost rates with date tiers preserved

Cost rates are where Project Online's data model is genuinely complex. Each resource can have up to five rate tables (A/B/C/D/E), and each table can have multiple date-tiered entries (e.g. Table A: $80/hr from 2024-01-01, $85/hr from 2025-01-01, $90/hr from 2026-01-01).

The 80/20 rule

In practice 80% of organisations only use Table A. The B-E tables were a 2003-era feature for handling "standard / overtime / weekend" rate variations that modern teams handle differently, typically via separate timesheet entries with billable flags rather than separate rate tables.

Default migration: export Table A only, with all date tiers preserved. Treat B-E as an exception, flag them during inventory and decide explicitly how to handle them.

Mapping date tiers to a destination

Project Online Onplana equivalent
Table A, single rate, no date tier RateCard with userId, hourlyRate, no effectiveTo
Table A, multiple date tiers Multiple RateCards with effectiveFrom + effectiveTo
Table B-E (if used) Separate RateCards keyed by label ("Table B", "Table C", etc.)
Resource has a department-default rate RateCard at scope=role, keyed by roleKey
Generic resource standard rate RateCard at scope=role, keyed by the role placeholder

Handling open-ended rates

Project Online's date-tier rate "$85/hr from 2025-01-01" with no explicit end date means "until further notice." On the destination side this lands as effectiveTo: null which Onplana renders as "ongoing." When a new tier is added (e.g. "$90/hr from 2026-01-01") the old tier needs its effectiveTo set to 2025-12-31 so the timelines abut without overlap.

Onplana's overlap detection (returns 409 RATE_CARD_OVERLAP with a suggestedEndDate) makes this exact case a single click, see the rate-card overlap inline-fix released in 2026-04-30, but if you're migrating in bulk, pre-process the rate-tier list so tiers abut cleanly before importing.

Step 5, Order matters: pool first, then projects

The single most common Project Online migration mistake: importing projects before importing the resource pool. The resulting failure mode is one of these three, each unpleasant:

  1. Imports fail because assignments reference resource IDs the destination doesn't know about.
  2. Imports auto-create resources per project, the same person ends up in the destination 8 times, each with one project's assignments.
  3. Imports succeed but assignments land on the wrong people because the destination matched on display name and your source pool had two "John Smith" entries.

The fix is sequencing.

Import order, each step depends on the previous Skipping ahead breaks resolution; doing it in order is boring and works 1. Pool Resources 2. Calendars Base + excpt 3. Rates Cost cards 4. Projects Headers + tasks 5. Assign Resolve by email 6. Timesheets Optional / archive Historical timesheets land last (or get archived to a CSV). They don't gate any other import; you can defer this step.

What "import the pool first" looks like

In Onplana specifically, the import wizard splits these into separate phases:

  1. Connect tenant, provide PWA URL, authenticate with Microsoft 365.
  2. Pool import, wizard reads /Resources + /CalendarExceptions + /ResourceCostRates, creates org members (matched by email against existing accounts), associates each with their base calendar, lays down rate-card rows.
  3. Project import, wizard reads /Projects + /Tasks + /Assignments. Assignments resolve against the populated org pool by email; unmatched land as warnings (not errors) so the user can fix on the source side or assign manually post-import.

Steps 1-2 take about 5 minutes for a 450-resource pool. Step 3 takes longer (depends on project count) but doesn't fail on resource resolution because the pool is already there.

Step 6, Validate on the pilot project

Same checklist shape as the headline migration guide, but specific to resource-pool concerns.

Resource validation

  • Every active resource from source has a matching destination member (email match)
  • Generic resources land as roles, not as people
  • Material + cost resources land as their destination equivalent (or are explicitly archived)
  • Each resource's base calendar matches the source
  • Per-resource exceptions reproduce on the right dates (timezone-safe)

Rate validation

  • Default rate from source Table A matches destination rate-card
  • Date-tiered rates abut without overlap (no 409s on import)
  • Currency matches the source organisation's default
  • Generic-resource role-rates apply correctly when a role is assigned

Capacity validation

  • ResourceMaxUnits translates to the destination's capacity model (Onplana: weekly hours via ResourceCapacity)
  • Capacity Planner shows the right available hours per week
  • Over-allocation detection fires for the same weeks the source flagged

A useful pre-flight here: drop a representative .mpp from your portfolio into the free Resource Heatmap before the migration begins. It surfaces the weekly overallocation pattern your destination capacity model has to reproduce, which is the single most common source of "the migration looked clean but the planner says we're red everywhere now" tickets.

Assignment validation

  • Every assignment that existed in the source exists in the destination
  • Assignment hours match (watch for unit conversion: source may store work in minutes, destination in hours)
  • Cross-project resource leveling produces equivalent results

The four traps that account for ~80% of resource-migration tickets

1. Display-name matching instead of email matching

Already covered in the FAQ but worth repeating: source pools have inconsistent display names accumulated over years. Email is the only stable key. Reject any importer that asks you to confirm matches by display name.

2. Material and cost resources blindly migrated as people

Project Online stores three resource types: Work (people), Material (equipment, consumables), and Cost (expense line-items like "travel"). Most modern PM platforms only model people. If you import Material and Cost resources as people, your destination user list ends up with rows like "Conference Room A" and "Travel Budget", which then show up in every assignee picker as humans you can assign tasks to.

Fix: filter the source export to ResourceType = 'Work' for the people import. Material and Cost resources usually map to expense categories or budget line-items in the destination, handle them separately as project-level metadata, not as resources.

3. Calendar exception timezone shifts

Calendar exceptions in Project Online are stored as date-range entries that the UI renders in the resource's local timezone. Some importers naïvely store these as UTC timestamps, which silently shifts every exception by the source organisation's UTC offset.

Fix: treat calendar exceptions as date-only (YYYY-MM-DD), never as timestamps with timezone. See the date-only rendering pattern Onplana documents in CLAUDE.md note 71, same trap, different surface.

4. Historical timesheet over-import

Bringing across 7 years of timesheet history "just in case" creates a 200K-row import that takes hours to run, complicates validation, and surfaces every historical data-quality issue (resources that don't exist anymore, projects that were archived, time codes that were retired). 90% of the value of historical timesheets is in the most recent 6-12 months, the rest is rarely queried.

Fix: pick a cutoff date (typically "start of current fiscal year" or "12 months ago"), migrate from there, archive the rest as a CSV/Parquet file you can query in Power BI for the 1-2 times per year someone needs a historical lookup.

Migrating to Onplana specifically

If Onplana is the destination, the resource-pool path is paved:

  • Built-in OData reader, Settings → Import → Project Online reads /Resources, /CalendarExceptions, /ResourceCostRates, /Calendars in one connect. Pool lands first; projects-and-assignments import is a separate wizard phase that resolves against the populated pool.
  • Email-matched import, never matches by display name. Unmatched resources surface as warnings, not auto-created duplicates.
  • Generic-resource → role mapping, auto-detects ResourceIsGeneric=true and creates role placeholders rather than synthetic users.
  • Calendar import with per-resource exceptions, base calendars from /Calendars, exceptions from /CalendarExceptions, both lands cleanly as WorkingCalendar + CalendarException.
  • Rate-card overlap detection, pre-existing rate cards that would overlap a new tier surface inline with a "↺ End previous on YYYY-MM-DD" suggestion, not a bare 409 error.
  • Free tier, you can run the entire pool + calendar import on the FREE plan, validate the result, and upgrade only when you start importing project assignments at scale.

Start your free migration →

Related reading


Inventory the rest of your tenant before you export the pool The resource pool is one of six tenant inventory categories most migrations miss. The free Project Online Inventory Checklist walks every category — resources, custom fields, integrations — with notes, owners, and PDF export. → Open the inventory checklist

Need help with a specific resource-pool shape that this post didn't cover, generic-resource resolution, B-E rate tables, or per-region availability calendars? Get in touch, these are the questions we field weekly.

Microsoft Project OnlineResource PoolMigrationPWAODataResource ManagementCost RatesCapacity PlanningPMOProject Server

Ready to make the switch?

Start your free Onplana account and import your existing projects in minutes.