Microsoft Planner — a guided tour¶
Microsoft Planner is the task board behind Microsoft 365 groups: a plan is owned by a group and holds buckets (the columns of the board) and tasks (the work items). This page is a tour — read it top to bottom and you'll know how the model fits together, where the sharp edges are, and which script to open next.
This client targets the Graph endpoint (
graph.microsoft.com). Plans are reached through a Microsoft 365 group (client.groups.get_by_id(id).planner) or the signed-in user (client.me.planner).
The model in one picture¶
flowchart LR
G[M365 Group] --> P[Plan]
P --> D["details<br/>(category labels)"]
P --> B[Bucket]
B --> T[Task]
T --> TD["details<br/>(description · checklist)"]
T --> A["assignments<br/>(user ids)"]
A plan belongs to a Microsoft 365 group and contains buckets — the columns of the board. A task lives in a plan (optionally in a bucket) and carries its own details object: the description and the checklist. Tasks are assigned to users through an assignments map keyed by user id.
Start here (5 minutes)¶
quickstart.py is read-only: it lists the plans you can
access and the buckets and tasks inside each.
The resources¶
| Folder | What it covers |
|---|---|
plans/ |
Create, rename and delete a plan; label its categories; provision a whole plan from a template |
buckets/ |
Add, rename, look up and delete board buckets |
tasks/ |
Task lifecycle, assignments, task details (description + checklist), and querying |
reports/ |
Cross-plan reporting — overdue and due-soon tasks |
Creating and addressing¶
A plan is created for a group, so the group comes first:
group = client.groups.filter("groupTypes/any(a:a eq 'unified')").top(1).get().execute_query()[0]
plan = client.planner.plans.add("Sprint 7", group).execute_query()
Buckets and tasks are created through the plan, and the library routes each to the top-level set for you (Graph only allows creating them there):
bucket = plan.buckets.add("Backlog").execute_query()
task = plan.tasks.add("Draft proposal", bucket=bucket).execute_query()
task.assign_to(client.me.get().execute_query()).update().execute_query()
→ plans/lifecycle.py · tasks/lifecycle.py ·
tasks/assignments.py
Permissions¶
| Permission | Lets you |
|---|---|
Tasks.Read |
Read plans, buckets and tasks |
Tasks.ReadWrite |
Also create and update them |
Group.Read.All |
Read a group's plans |
Group.ReadWrite.All |
Create a plan in a group |
Tasks.ReadWrite.All |
App-only equivalent of Tasks.ReadWrite |
Sharp edges¶
- There is no "list all plans" endpoint. Enumerate the groups (or use
client.me.planner.plans) and read each plan.GET /planner/plansreturns “This entity set must be queried with a filter on owner property…”. - A plan's tasks and buckets ignore
$filter,$orderbyand$top. List them through the plan, then filter and sort in Python. Only$selectand the top-level$filter=planId eq '…'are honoured. Seetasks/query.py. - The top-level
/planner/bucketsset cannot be listed. Create buckets throughplan.buckets.add(...)(whichPOSTs to/planner/buckets) and list them back through the plan. - Updates and deletes carry the latest etag. Graph enforces optimistic
concurrency;
update()anddelete_object()sendIf-Match, and the library refreshes the etag after every update (Planner only returns it when asked withPrefer: return=representation). You never touch the etag yourself. - Task references (
references) are rejected by the v1.0 endpoint for every key form, so this tour does not use them — keep links in the description. - The checklist is an Open Type keyed by a client-generated id, and PATCH
merges it. Removing an item means sending
nullfor its id;set_checklist([...])does that when you want to replace the whole list. Seetasks/details.py.