Adding an Attachment to a Planner Task

I have recently started working on a new add-in in Office and had reason to create an newly uploaded attachment to a Planner Task item.


I pulled out all my hair on this one as I was unable to figure out from the documentation how to do this. Hack, hack, hack — 3days later… well, I hope to save the world this frustration.

Here are the steps:

  1. First, get the file from an upload. Determine the type and convert all the binary in the file to a BASE64 string.
  2. Next, you need to GET the planners GROUP by the planner ID:${plannerId}
  3. Then you create the item in the group drive container, where name is the name of the file via a POST (with the based 64 in the body, content-type: text/plain):${plan.container.containerId}/drive/items/root:/${name}:/content
  4. This returns a driveItem object and from this you need the driveItem.webUrl, but this is the part that killed me. The documentation tells you to use an encoded URL. WRONG. This fails every single time. After a lot of looking over the documentation and seeing what they were submitting, you actually only replace the “.” and the “:” and white space / /. Here is a function I created in JS:
* Encodes the URL ins the special planner format that is for oif but not quite
* following the encodeURIComponent() specification…
* @param {String} url
* @returns {String}
#encodePlannerExternalReferenceUrl = (url) => {
// Encode specific characters: : . _
const encodedUrl = url.replace(/:/g, "%3A").replace(/\./g, "%2E").replace(/ /g, "%20");
return encodedUrl;
  1. Next, GET the task item by ID:${id}
  2. Then GET the task items details by ID:${id}/details
  3. For this next part, you use the “@odata.etag” prop the details and you will create a new entry with a PATCH:${taskId}/details.
  4. For this part you need to create a fetch body like this: { references: ${ref} }, where the ref is defined as:
const ref = {
/** @type {PlannerReference} */
[this.#encodePlannerExternalReferenceUrl(driveItem.webUrl)]: {
"@odata.type": "#microsoft.graph.plannerExternalReference",
alias: name,
type: "Other",
view raw ref.js hosted with ❤ by GitHub
  1. To make the above call you have to supply the @odata.etag. And this is the NEXT part that messed with my noodle. You do NOT use the whole value returned AND you have to place it in double quotes. Ugh! So here is more code I wrote to help with that:
const updatedTag = eTag.replace('W/"', '"').replace('\\"', "");
view raw etag.js hosted with ❤ by GitHub
  1. Then when you make the call you place the tag in the headers: If-Match. And do not forget to add the Content-type, and to see the returned result the Prefer as well:

The final submission looks like this:

And let me stress this. You must PATCH this, not “patch.” Another thing I found is that GET/get, PUT/put, DELETE/delete, and POST/post all work interchangeably. But if you “patch” you will get some bazaar error about CORS and that path is not supported. And if you hit the service with OPTIONS you will see all the supported methods are returned in CAPS. Oddly, all work with lowercase or upper case, except for PATCH.

So, there you have it. 3 days of my life.