Migrate Microsoft Planner with Graph API & PowerShell: A Complete DIY Guide 

21 min read

Migrate Microsoft Planner with Graph API & PowerShell: A Complete DIY Guide 

Microsoft Planner is embedded into how many teams track work, especially when used together with Microsoft Teams. When it’s time to move plans between tenants or reorganize projects, though, there is still no native, end‑to‑end Planner migration tool from Microsoft. Admins end up stitching together PowerShell and Microsoft Graph calls to export and recreate plans by hand. 

If you want a high‑level view of all your options before diving into scripting, check out our Planner migration overview

We also explain in detail why Microsoft has never shipped a native cross‑tenant Planner migration feature in our article on why there is no native Planner migration

In this deep‑dive, we’ll walk through how to build your own Planner migration using the Microsoft Graph API and PowerShell. You’ll see the exact endpoints involved, how to authenticate with Microsoft Entra ID, how to export plans and tasks, and how to recreate them in a new tenant. We’ll also be honest about the hard parts: rate throttling, comments, and attachments that make manual scripting painful at scale and where a dedicated tool like Apps4.Pro Migration Manager is a better fit. 

1. What a DIY Planner migration really means 

When you say “Planner migration using Graph API,” what you’re really talking about is: 

  • Enumerating all relevant plans in a source tenant or team. 
  • Exporting their structure and content (buckets, tasks, details). 
  • Recreating equivalent plans in a destination tenant or team. 
  • Mapping users, permissions, and linked resources along the way. 

At a minimum, you need to reproduce: 

  • Plans: plan name, associated Microsoft 365 Group / Team, and basic settings. 
  • Buckets: all buckets in each plan, with their names and order. 
  • Tasks: titles, descriptions, assignments, labels, checklists, start/due dates, and completion status. 
  • Optional extras: comments, attachments (usually SharePoint / OneDrive links), and activity history. 

In a cross‑tenant scenario, the high‑level pattern is: 

  1. Connect to source tenant. 
  1. Export plans → buckets → tasks → task details. 
  1. Connect to destination tenant. 
  1. Recreate plans → buckets → tasks → task details, using new IDs and destination user accounts. 

If you’d like a broader context of where Planner fits in a full Microsoft 365 migration, the Planner migration overview walks through the big picture. 

2. Planner Graph API endpoints you’ll use 

If you’re new to Planner migrations, we recommend quickly reviewing our Planner migration overview and then coming back here for the low‑level Graph details. 

The core of a Planner migration is the Microsoft Graph Planner API. Here are the main endpoints your scripts will rely on: 

2.1 Core Planner endpoints 

  • List plans for a group   

Use this to find all Planner plans attached to a specific Microsoft 365 Group or Team. 

  • Get a plan’s tasks  

GET /planner/plans/{plan-id}/tasks 

This is your primary “export all tasks in a plan” endpoint. 

  • Get buckets for a plan  

GET /planner/plans/{plan-id}/buckets 

Use this to preserve the structure into which tasks are grouped. 

  • Get task details  

GET /planner/tasks/{task-id}/details 

This is where you pull description, checklist items, references (links), and other richer metadata that isn’t in the basic task object. 

  • Create a task  

POST /planner/tasks 

This is how you create tasks in the destination plan. 

  • Update a task or task details
    • PATCH /planner/tasks/{id} 
    • PATCH /planner/tasks/{id}/details 

You’ll need these to set fields that aren’t available at create time, and to push descriptions, checklists, and references after the task exists. Both require the correct ETag via If-Match header. 

2.2 Supporting Graph endpoints 

While Planner endpoints do most of the heavy lifting, you inevitably touch other Graph surfaces: 

  • Groups & Teams 
    • GET /groups
    • GET /groups/{id}

For discovering which groups/teams host the plans you want to migrate and for creating a destination plan attached to the right group. 

  • Users 
    • GET /users 

You’ll need this to map source users to destination users, especially in cross‑tenant migrations, so task assignments stay meaningful. 

  • Files / attachments Attachments often live in SharePoint or OneDrive. Moving them may require: 
    • SharePoint and OneDrive Graph endpoints to copy files.
    • Updating Planner task details with new reference URLs.

3. Authentication: Entra ID app registration and OAuth 2.0 

Before you can call the Graph API, you need an app registration in Microsoft Entra ID (formerly Azure AD) and a way to obtain tokens securely. 

3.1 Register an app in Microsoft Entra ID 

  1. In the Azure portal, go to Microsoft Entra ID → App registrations → New registration
  1. Give your app a friendly name (e.g., PlannerMigrationTool) and register it as a single tenant app, unless you have a multi‑tenant scenario. 
  1. Note down: 
    • Application (client) ID 
    • Directory (tenant) ID 
  1. Under Certificates & secrets, create a client secret, copy it immediately, and store it securely (Key Vault, secret manager, etc.). 
  1. Under API permissions, add Microsoft Graph permissions:
    •  For reading/writing Planner: Group.Read.All, Tasks.ReadWrite, or broader like Tasks.ReadWrite.All / Group.ReadWrite.All
    • For user resolution: User.Read.All. 
  1. Decide on permission type: 
    • Application permissions (apponly): Best for unattended migration jobs run by admins. 
    • Delegated permissions: Used when running as a signed‑in user; less common for large migrations. 

Make sure you grant admin consent for the app once permissions are added. 

3.2 Getting a token in PowerShell 

With the modern Microsoft Graph PowerShell SDK, authenticating with app‑only is straightforward: 

# Variables from your app registration 

$TenantId     = "<your-tenant-id>" 

$ClientId     = "<your-client-id>" 

$ClientSecret = "<your-client-secret>" 

Install-Module Microsoft.Graph -Scope CurrentUser 

Import-Module Microsoft.Graph 

Connect-MgGraph -TenantId $TenantId ` 

                -ClientId $ClientId ` 

                -ClientSecret $ClientSecret ` 

                -Scopes "https://graph.microsoft.com/.default" 

Select-MgProfile -Name "v1.0"

All subsequent calls via the Graph SDK (or Invoke-MgGraphRequest) will automatically carry the access token. 

If you prefer raw REST with Invoke-RestMethod, you can also obtain a token via a client‑credentials OAuth flow and supply it in the Authorization: Bearer <token> header

4. Using the Microsoft.Graph.Planner PowerShell module 

The Microsoft Graph PowerShell SDK breaks functionality into service‑specific modules, including one for Planner. It doesn’t cover every Planner feature, but it does save you from hand‑crafting many URLs. 

4.1 Installing and loading the module 

Install-Module Microsoft.Graph -Scope CurrentUser 

Import-Module Microsoft.Graph.Planner 

Common cmdlets you’ll likely use: 

  • Get-MgGroupPlannerPlan – list plans associated with a group. 
  • Get-MgPlannerPlanTask – list tasks in a plan. 
  • New-MgPlannerTask – create tasks. 
  • Get-MgPlannerTaskDetail / Update-MgPlannerTaskDetail – manage task details. 

Where Planner support is missing or limited, you can fall back to Invoke-MgGraphRequest or Invoke-RestMethod with raw URLs. 

4.2 Example: enumerate plans and tasks 

This example lists all groups, their plans, and each task’s basic info: 

# Assume Connect-MgGraph already run 

# Get all groups (filter in real scripts to narrow scope) 

$groups = Get-MgGroup -All -Property Id,DisplayName 

foreach ($group in $groups) { 

    $plans = Get-MgGroupPlannerPlan -GroupId $group.Id 

    foreach ($plan in $plans) { 

        Write-Host "Plan:" $plan.Title "in group" $group.DisplayName 

        $tasks = Get-MgPlannerPlanTask -PlannerPlanId $plan.Id 

        foreach ($task in $tasks) { 

            Write-Host "  Task:" $task.Title "Status:" $task.PercentComplete 

        } 

    } 

}

This pattern is the backbone of any Planner inventory or export script. 

For more advanced inventory generation (CSV/JSON export, filters, scheduling), you can also leverage the PowerShell scripts discussed in your existing knowledge‑base article on generating inventory. 

5. Step‑by‑step: export and recreate a Planner plan 

Let’s walk through a concrete migration flow. We’ll assume a cross‑tenant scenario, but the same steps apply within a single tenant. 

5.1 Step 1 – Build your inventory 

Before migrating anything, you should know what you have. A good inventory includes: 

  • Group name and ID. 
  • Plan name and ID. 
  • Bucket names and IDs. 
  • Task IDs, titles, statuses, due dates, and assigned users. 

A simple pattern: 

  1. Enumerate groups and plans (as in the previous section). 
  1. For each plan, pull buckets and tasks. 
  1. Save it all to a structured JSON file you can inspect and later feed into the migration flow. 

Example sketch: 

$inventory = @() 

foreach ($group in $groups) { 

    $plans = Get-MgGroupPlannerPlan -GroupId $group.Id 

    foreach ($plan in $plans) { 

        $buckets = Invoke-MgGraphRequest -Method GET ` 

                     -Uri "https://graph.microsoft.com/v1.0/planner/plans/$($plan.Id)/buckets" 

        $tasks = Get-MgPlannerPlanTask -PlannerPlanId $plan.Id 

        $inventory += [PSCustomObject]@{ 

            GroupId     = $group.Id 

            GroupName   = $group.DisplayName 

            PlanId      = $plan.Id 

            PlanTitle   = $plan.Title 

            Buckets     = $buckets.value 

            Tasks       = $tasks 

        } 

    } 

} 

$inventory | ConvertTo-Json -Depth 5 | Set-Content ".\planner-inventory.json"

If you don’t want to build this from scratch, download and adapt our PowerShell scripts to generate inventory , which are designed for pre‑migration reporting across Microsoft 365 tenants. 

5.2 Step 2 – Export one plan’s structure and details 

When you’re ready to move a specific plan, zoom in: 

$planId = "<source-plan-id>" 

$headers = @{ Authorization = "Bearer $((Get-MgContext).AccessToken)" } 

# Buckets 

$bucketsUrl = "https://graph.microsoft.com/v1.0/planner/plans/$planId/buckets" 

$buckets    = (Invoke-RestMethod -Headers $headers -Uri $bucketsUrl -Method GET).value 

# Tasks 

$tasksUrl   = "https://graph.microsoft.com/v1.0/planner/plans/$planId/tasks" 

$tasks      = (Invoke-RestMethod -Headers $headers -Uri $tasksUrl -Method GET).value 

$export = @() 

foreach ($task in $tasks) { 

    $detailsUrl = "https://graph.microsoft.com/v1.0/planner/tasks/$($task.id)/details" 

    $details    = Invoke-RestMethod -Headers $headers -Uri $detailsUrl -Method GET 

    $export += [PSCustomObject]@ { 

        Task     = $task 

        Details = $details 

    } 

} 

$export | ConvertTo-Json -Depth 5 | Set-Content ".\plan-$planId-export.json" 

$buckets | ConvertTo-Json -Depth 5 | Set-Content ".\plan-$planId-buckets.json"

Now you have a full snapshot of a plan’s structure and task content that you can recreate in the destination. 

5.3 Step 3 – Create destination group, plan, and buckets 

In the destination tenant: 

  1. Authenticate using the destination app registration. 
  1. Decide if you’ll attach the new plan to an existing group or create a new one. 
  1. Create buckets with the same names and order. 

Example sketch using REST: 

# Assume you have $destHeaders with Authorization 

$destGroupId = "<destination-group-id>" 

# Create plan 

$planBody = @{ 

    owner = $destGroupId 

    title = "Migrated Plan - Sales FY26" 

} | ConvertTo-Json 

$planUrl = "https://graph.microsoft.com/v1.0/planner/plans" 

$newPlan = Invoke-RestMethod -Headers $destHeaders -Uri $planUrl -Method POST -Body $planBody 

# Recreate buckets 

$bucketMap = @{} # sourceBucketId -> destBucketId 

foreach ($sourceBucket in $sourceBuckets) { 

    $bucketBody = @{ 

        name   = $sourceBucket.name 

        planId = $newPlan.id 

        orderHint = $sourceBucket.orderHint 

    } | ConvertTo-Json 

    $bucketUrl  = "https://graph.microsoft.com/v1.0/planner/buckets" 

    $newBucket  = Invoke-RestMethod -Headers $destHeaders -Uri $bucketUrl -Method POST -Body $bucketBody 

    $bucketMap[$sourceBucket.id] = $newBucket.id 

}

The bucketMap table is critical so you can place tasks into the right bucket. 

5.4 Step 4 – Recreate tasks with POST /planner/tasks 

Tasks are usually migrated in two phases: 

  1. Create the bare task (title, bucket, plan, assignees, dates). 
  1. Update details (description, checklist, references). 

Example create call: 

foreach ($sourceEntry in $export) { 

    $sourceTask    = $sourceEntry.Task 

    $sourceDetails = $sourceEntry.Details 

    $destBucketId = $bucketMap[$sourceTask.bucketId] 

    # Map user IDs between tenants (simple example) 

    $assignmentMap = @{} 

    foreach ($key in $sourceTask.assignments.PSObject.Properties.Name) { 

        $destUserId = $userMap[$key] # you maintain $userMap separately 

        if ($destUserId) { 

            $assignmentMap[$destUserId] = @{ "@odata.type" = "microsoft.graph.plannerAssignment" } 

        } 

    } 

    $taskBody = @{ 

        planId      = $newPlan.id 

        bucketId    = $destBucketId 

        title       = $sourceTask.title 

        assignments = $assignmentMap 

        startDateTime = $sourceTask.startDateTime 

        dueDateTime   = $sourceTask.dueDateTime 

    } | ConvertTo-Json 

    $taskUrl  = "https://graph.microsoft.com/v1.0/planner/tasks" 

    $newTask  = Invoke-RestMethod -Headers $destHeaders -Uri $taskUrl -Method POST -Body $taskBody 

    # Now push details 

    $detailsBody = @{ 

        description = $sourceDetails.description 

        checklist   = $sourceDetails.checklist 

        references  = $sourceDetails.references 

    } | ConvertTo-Json 

    # Get current details and ETag 

    $newDetailsUrl   =    "https://graph.microsoft.com/v1.0/planner/tasks/$($newTask.id)/details" 

    $newDetailsResp  = Invoke-WebRequest -Headers $destHeaders -Uri $newDetailsUrl -Method GET 

    $newDetails      = $newDetailsResp.Content | ConvertFrom-Json 

    $etag            = $newDetailsResp.Headers["ETag"] 

    $patchHeaders = $destHeaders.Clone() 

    $patchHeaders["If-Match"] = $etag 

    Invoke-RestMethod -Headers $patchHeaders -Uri $newDetailsUrl -Method PATCH -Body $detailsBody 

}

A few important notes: 

  • You must specify the current ETag in If-Match when updating task details. 
  • User mapping between tenants (for assignments) typically comes from a CSV or directory lookup that you maintain separately. 

6. Comments and attachments: the hard edges 

The pieces above already require careful scripting, but two areas make Planner migration especially tricky. 

6.1 Comments 

Planner comments: 

  • Are not simple fields on the task object. 
  • Traditionally lived in the associated group mailbox as conversations, not in the Planner task record itself. 
  • Have only partial, evolving Graph support for pulling them in a structured way. 

Implications: 

  • Many DIY scripts do not migrate comments at all. 
  • Others export comments via mailbox or audit APIs and paste them into the task description or attach them as a text file. 
  • Implementing a robust comment migration path significantly increases script complexity and test overhead. 

If comments are a “nice to have” for your business, you might explicitly call out in your script output which plans have comments and warn users that they won’t be moved 1:1. 

6.2 Attachments 

Planner attachments are rarely simple files attached directly to a task. They are usually: 

  • SharePoint or OneDrive file references. 
  • External URLs. 

To migrate them fully, you typically need to: 

  1. Identify the underlying storage location (SharePoint site, OneDrive, etc.). 
  1. Copy or move the file to a destination site or tenant using SharePoint/OneDrive Graph APIs. 
  1. Update the Planner task details in the destination to point to the new file location. 

This introduces: 

  • Additional authentication scopes and app permissions. 
  • Asynchronous operations (file copy jobs that take time to complete). 
  • More potential points of failure and throttling. 

For that reason, many homegrown scripts either: 

  • Only move the link (if both tenants can see the source content), or 
  • Ignore attachments altogether, or 
  • Handle them only for specific, high‑value plans. 

If you need full‑fidelity migration of attachments and comments with minimal scripting, Apps4.Pro Migration Manager for Planner is designed to cover those scenarios. 

7. Rate limiting and throttling (why 120 requests/min matters) 

Microsoft Graph applies throttling at several layers (service, tenant, app). Planner is no exception, and migrations tend to stress those limits because each plan requires a lot of calls. 

In practice: 

  • Each plan requires dozens to hundreds of requests: 
    • Read buckets, read tasks, read task details.
    • Create new plan, create buckets, create tasks, patch details, handle attachments.
  • You can easily hit thousands of calls for a modest migration. 

To avoid constant 429 Too Many Requests responses, many admins design their scripts to: 

  • Target a soft ceiling of around 120 requests per minute per app. 
  • Add a small delay between calls, such as 200–500 ms. 
  • Implement retry logic with exponential backoff. 

A simple retry function pattern: 

function Invoke-GraphWithRetry { 

    param( 

        [string]$Method, 

        [string]$Uri, 

        [hashtable]$Headers, 

        [object]$Body = $null 

    ) 

    $maxRetries = 5 

    $delay      = 2 

    for ($attempt = 1; $attempt -le $maxRetries; $attempt++) { 

        try { 

            return Invoke-RestMethod -Method $Method -Uri $Uri -Headers $Headers -Body $Body -ErrorAction Stop 

        } 

        catch { 

            $response = $_.Exception.Response 

            if ($response -and ($response.StatusCode.value__ -eq 429 -or $response.StatusCode.value__ -ge 500)) { 

                $retryAfter = $response.Headers["Retry-After"] 

                if ($retryAfter) { 

                    Start-Sleep -Seconds [int]$retryAfter 

                } else { 

                    Start-Sleep -Seconds $delay 

                    $delay *= 2 

                } 

            } 

            else { 

                throw 

            } 

        } 

    } 

    throw "Request to $Uri failed after $maxRetries attempts." 

}

Using this for every Graph call helps your script survive transient throttling, but it also means large migrations can take hours or days, especially if you are cautious with the rate. 

8. Why manual Graph + PowerShell doesn’t scale forever 

For a single plan or a small set of team boards, a custom script is often fine. As the scope grows, the hidden costs become more obvious. 

Many of these limitations exist because Planner was never designed with cross‑tenant migration as a first‑class scenario-something we cover in more detail in Why there is no native Planner migration

Key limitations of DIY scripts: 

  • No batch endpoint for Planner 
    There is no native bulk export/import. You orchestrate thousands of individual HTTP calls and keep track of success/failures yourself. 
  • Complex mapping logic 
    You must maintain and persist mapping tables for: 
    • Source vs destination group IDs. 
    • Plan IDs.
    • Bucket IDs.
    • Task IDs.
    • User IDs between tenants.
  • Partial API coverage : Comments, attachments, and activity history either have limited Graph coverage or live in other services (mailbox, SharePoint). You end up writing multiple mini‑migrators. 
  • PowerShell SDK gaps :
    Microsoft.Graph.Planner simplifies some calls but doesn’t give complete coverage or a turnkey migration flow. You inevitably mix cmdlets and raw REST calls, juggling different patterns. 
  • Operational risk 

Bugs in the script can: 

  • Create duplicate plans.
  • Mis-assign tasks to the wrong users.
  • Spam users with notifications.
  • Leave you with partial migrations that are hard to roll back.

For many organizations, the tipping point comes when they need to migrate dozens or hundreds of plans or support a tenant‑to‑tenant consolidation on a tight timeline. 

At that scale, building and maintaining your own migration pipeline tends to cost more time and risk than adopting a purpose‑built product like Apps4.Pro Migration Manager for Planner

9. When to use a dedicated Planner migration tool 

A dedicated migration tool such as Apps4.Pro Migration Manager for Planner uses the same underlying Graph APIs but wraps them in: 

  • A GUI for selecting, mapping, and scheduling plan migrations. 
  • Built‑in logic for ID mapping, retries, and throttling. 
  • Support for special cases such as: 
    • Teams‑linked Planner boards.
    • Large batches of plans across multiple groups. 
    • More complete handling of comments and attachments where technically possible. 

Apps4.Pro can migrate Planner plans from one tenant to another, or between different Microsoft 365 Groups/Teams in the same tenant, while preserving tasks, buckets, assignments, attachments, and comments. 

A simple rule of thumb: 

  • If you just need to move 1–3 plans and you’re comfortable with PowerShell, the patterns in this article are usually enough. 
  • If you’re planning a broader tenant‑to‑tenant migration or re‑homing Planner at scale, using Apps4.Pro Migration Manager for Planner is almost always faster, safer, and cheaper than maintaining custom scripts long‑term. 

Migrate Everything to Microsoft 365

Exchange Online SharePoint Online OneDrive For Business Microsoft Teams Microsoft Planner Viva Engage (Yammer) Microsoft Bookings Microsoft Forms Power Automate Microsoft Power BI Exchange Online SharePoint Online OneDrive For Business Microsoft Teams Microsoft Planner Viva Engage (Yammer) Microsoft Bookings Microsoft Forms Power Automate Microsoft Power BI
  • No Data Loss
  • Zero Downtime
  • ISO-Certified Protection

Start your free 15-days trial today !


4.5 out of 5

Bot Logo

Apps4.Pro Bot

Hey!👋 Ready to make your Microsoft 365 migration journey easier? Tell me what you’re looking.

What gets migrated?
I have a sales question
I'm here for tech support
Learn about Apps4.Pro