Rich text

πŸ“˜

New rich text formats

We are actively adding new rich texts formats to various objects in Asana. This may break existing apps. New apps should be built using parsers and display logic that is forward compatible with the forthcoming rich text formats. More details and ongoing updates can be found in this post in the developer forum.

The web product offers a number of rich formatting features when writing task notes, comments, project descriptions, and project status updates. These features include bold, italic, underlined, and monospaced text, as well as bulleted and numbered lists. Additionally, users can "@-mention" other users, tasks, projects, and many other objects within Asana to create links.

Example rich text:

<body>All these new tasks are <em>really</em> getting disorganized, so <a data-asana-gid="4168112"/> just made the new <a data-asana-gid="5732985"/> project to help keep them organized. <strong>Everyone</strong> should start using the <a data-asana-gid="6489418" data-asana-project="5732985"/> when adding new tasks there.</body>

Supported objects

The rich text field name for an object is equivalent to its plain text field name prefixed with html_. The following object types in Asana support rich text:

ObjectPlain text fieldRich text field
Tasksnoteshtml_notes
Projectsnoteshtml_notes
Storiestexthtml_text
Project status updatestexthtml_text
Project briefstexthtml_text
Teamsdescriptionhtml_description

Reading rich text

Rich text in the API is formatted as an HTML fragment, which is wrapped in a root <body> tag. Rich text is guaranteed to be valid XML; there will always be a root element, all tags will be closed, balanced, and case-sensitive, and all attribute values will be quoted. The following is a list of all the tags that are currently returned by the API:

TagMeaning in Asana
<body>None
<strong>Bold text
<em>Italic text
<u>Underlined text
<s>Strikethrough text
<code>Monospaced text
<ol>Ordered list
<ul>Unordered list
<li>List item
<a>Link

In addition, the following tags are supported in the rich text of only some objects:

TagMeaning in AsanaObjects Supported
<h1>, <h2>HeaderProject briefs, tasks
<hr>Horizontal ruleProject briefs, tasks
<img>Inline imageProject briefs, tasks
<table>, <tr>, <td>TableProject briefs
<object type="application/vnd.asana.external_media">External media embed (iframe)Project briefs
<object type="application/vnd.asana.Project_milestones">List of milestonesProject briefs
<object type="application/vnd.asana.Project_goals">List of goalsproject briefs

Note: The above lists will expand as new features are introduced to the Asana web product. Treat rich text as you would treat arbitrary HTML, and ensure that your code does not break when it encounters a tag not on this list.

Examples

Parsing in Python

!
from lxml import etree

html_text = "<body>...</body>"
root = etree.HTML(html_text)
user_ids = root.xpath('//a[@data-asana-type="user"]/@data-asana-gid')
for user_id in user_ids:
    print(user_id)

Parsing in Java

!
import com.jcabi.xml.XML;
import com.jcabi.xml.XMLDocument;
import java.util.List;

XML root = new XMLDocument("<body>...</body>");
List<String> userIds = root.xpath("//a[@data-asana-type=\"user\"]/@data-asana-gid");
for (String userId : userIds) {
    System.out.println(userId);
}

Parsing in JavaScript

!
var htmlText = '<body>...</body>'
var parser = new DOMParser()
var doc = parser.parseFromString(htmlText, "text/html")
var iterator = doc.evaluate(
    '//a[@data-asana-type="user"]/@data-asana-gid', doc, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE)
var node = iterator.iterateNext()
while (node) {
    console.log(node.nodeValue);
    node = iterator.iterateNext();
}

Links

While links are intuitive to understand when users view the rendered result in the Asana web product, an <a> tag and its href alone are insufficient to programmatically understand what the target of the link is. This is confused further by the fact that the formats of these links are frequently ambiguous. For example, an @-mention to a user generates a link to their "My Tasks", which looks identical to a link to a normal project.

Because of this, the API will return additional attributes in <a> tags to convey meaningful information about the target. The following is a complete list of attributes we may return inside an <a> tag, in addition to the usual href:

AttributeMeaning
data-asana-accessibleBoolean, representing whether or not the linked object is accessible to the current user. If the resource is inaccessible, no other data-asana-* attributes will be included in the tag.
data-asana-dynamicBoolean, represents if contents of the a tag is the canonical name of the object in Asana. If you want to set custom text that links to an Asana object, set data-asana-dynamic="false" when creating the tag.
data-asana-typeThe type of the referenced object. One of user, task, project, tag, conversation, project_status, team, or search.
data-asana-gidThe GID of the referenced object. If the referenced object is a user, this is the user's GID.
data-asana-projectIf the type of the referenced object is a task, and the link references that task in a particular project, this is the GID of that project.
data-asana-tagIf the type of the referenced object is a task, and the link references that task in a particular tag, this is the GID of that tag.

Here are some examples of how this behavior manifests:

  • Suppose a user with a name of "Tim" and a user GID of "53421" is @-mentioned. This will create a link to their "My Tasks" which is a project with a GID of "56789"
    • The raw link generated in Asana will be https://app.asana.com/0/56789/list.
    • The <a> tag returned in the API will be <a href="https://app.asana.com/0/56789/list" data-asana-accessible="true" data-asana-dynamic="true" data-asana-type="user" data-asana-gid="54321">@Tim</a>.
  • Suppose a link to a task with name "Buy milk" and GID "1234" being viewed in a project with GID "5678" is copied from the address bar and pasted into a comment.
    • The raw link generated in Asana will be https://app.asana.com/0/5678/1234.
    • The <a> tag returned in the API will be <a href="https://app.asana.com/0/5678/1234" data-asana-accessible="true" data-asana-dynamic="true" data-asana-type="task" data-asana-gid="1234" data-asana-project="5678">Buy milk</a>
  • Suppose another user @-mentions a project with GID "5678" that is private and not visible to you in the web product.
    • The raw link generated in Asana will be https://app.asana.com/0/5678/list.
    • The <a> tag returned in the API will be <a href="https://app.asana.com/0/5678/list" data-asana-accessible="false" data-asana-dynamic="true">Private Link</a>

Here is an example of what a complete rich comment might look like in the API:

<body>All these new tasks are <em>really</em> getting disorganized, so <a href="https://app.asana.com/0/4168466/list" data-asana-accessible="true" data-asana-dynamic="true" data-asana-type="user" data-asana-gid="4168112">@Tim Bizzaro</a> just made the new <a href="https://app.asana.com/0/5732985/list" data-asana-accessible="true" data-asana-dynamic="true" data-asana-type="project" data-asana-gid="5732985">Work Requests</a> project to help keep them organized. <strong>Everyone</strong> should start using the <a href="https://app.asana.com/0/5732985/6489418" data-asana-accessible="true" data-asana-dynamic="true" data-asana-type="task" data-asana-gid="6489418" data-asana-project="5732985">Request template</a> when adding new tasks there.</body>

πŸ“˜

Triggering an @-mention notification

When adding a story to a task, if the user is not already assigned or following the task, creating an @-mention link will not automatically generate a notification. To trigger a notification, you must first add the user as a follower to the task or assign the user to the task by updating the task's assignee property.

To trigger a notification by adding the user as a follower, first make an API call to
POST /tasks/{task_gid}/addFollowers and then wait a few seconds before making the call to
POST /tasks/{task_gid}/stories.

To trigger a notification by assigning the user to the task, first make the API call to
POST /tasks/{task_gid} to update the task's assignee property, and then make the call to
POST /tasks/{task_gid}/stories. There is no need to wait as with adding the user as a follower.

Inline images

Rich text can contain inline images.

πŸ“˜

Inline images are stored as attachments

Within the Asana app, inline images in the task description (i.e., the "body" of the task) do not appear in the index of image thumbnails nor as stories in the task. However, requests made to GET /attachments for a task will return all of the images in the task, including inline images.

For example, consider a task with one inline image in the task description, and other three images attached to the task (say, by calling POST /attachments). A request to GET /attachments will return all four images in the task. However, the task in the Asana app will only show three images in the index of image thumbnails (and three stories).

If the attachment has been deleted, the HTML will contain data-asana-deleted="true", and some of the other attributes, such as the URLs, will not be present.

The image URLs expire after a few minutes.

Reading an inline image

<img
  data-asana-gid="1234"
  src="..."
  data-src-width="..."
  data-src-height="..."
  data-thumbnail-url="..."
  data-thumbnail-width="..."
  data-thumbnail-height="..."
  [data-asana-deleted="true"]
  data-asana-type="attachment"
  alt="title of the image"
  style="...">

External media embeds (iFrames)

You can embed Figma, Loom, YouTube, etc. within rich text. The effect is similar to an HTML iFrame.
There is a fixed, predefined list of external media sources that are supported:

  • Adobe XD
  • Canva
  • Figma
  • InVision
  • Loom
  • LucidChart
  • Miro
  • Vimeo
  • Whimsical
  • Wistia

Reading an external media embed

<object
  type="application/vnd.asana.external_media"
  data-asana-type="attachment"
  data-asana-gid="1234"
  data="{embeddable-url}"
>
  <a href="{linkable-url}">{linkable-url}</a>
</object>

Milestones and goals

Rich text can contain:

  • A list of all milestones in the specified project
  • A list of all goals that are supported by the specified project

When reading, the inner HTML of the <object> tag will contain a list of the first five milestones/goals, followed by "..." if there are more than five in total. When writing, the inner HTML
is ignored and can be empty.

Reading goals

<object
  type="application/vnd.asana.project_goals"
  data-asana-gid="<gid-of-project>"
  data-asana-type="project">[...]</object>

Reading defensively

We are actively adding new rich text formats to various objects in Asana. An existing app will break if not built defensively. Apps should use parsers and display logic that is forward compatible with unknown future rich text formats.

To do this, Asana provides two mechanisms to parse and display tags that the app doesn't explicitly support:

  • Defaults that render in a WebView
  • Guidelines for how to handle new tags

You can read more about rich text changes in this forum post.

Custom handling external media <object>

!
const richText = '<body><object style="display:block" type="application/vnd.asana.external_media" data="https://www.youtube.com/embed/VqnMA3K6-e0"><a href="https://www.youtube.com/embed/VqnMA3K6-e0">https://www.youtube.com/embed/VqnMA3K6-e0</a></object></body>'
const parser = new DOMParser();
const richTextDocument = parser.parseFromString(richText, "text/html");

const objects = richTextDocument.querySelectorAll("object");

for (let i = 0; i < objects.length; i++) {
    replacement = null;

    switch(objects[i].type) {
        case "application/vnd.asana.external_media":
            replacement = richTextDocument.createElement('iframe');
            replacement.width = 420;
            replacement.height = 315;
            replacement.src = objects[i].data;
            break;
        default:
            replacement = richTextDocument.createElement('div');
            replacement.innerHtml = objects[i].innerHTML;
    }

    if (replacement) {
        objects[i].parentElement.replaceChild(replacement, objects[i]);
    }
}

richTextDocument.body.childNodes.forEach (child => {
    document.body.append(child);
});

Render rich text in a WebView

You can expect the rich text HTML to render reasonably in a WebView if you apply the following CSS style to the wrapping DOM node: overflow-wrap: break-word; white-space: pre-wrap;. This won't look exactly like it does in Asana, but it will ensure users read it in the same way.

How to handle new tags (no WebView)

An <object> with an unhandled type

Render the <object> tag as a block and render the contained HTML with the same behavior as if it were not inside an <object>. We will never send an <object> tag nested inside another <object> tag.

An <img>

Fall back to either the alt text or the src link if the image can’t be displayed. Wrap the text with newlines like \n<alt text>\n since <img> tags are blocks.

Empty elements except <img> and <hr>

Empty tags are described here. It is okay to omit them. If the tag is a block, you may render as a new line .

Other semantic non-terminal tags

Ignore the tag and render whatever is inside. Follow the HTML convention for whether it is a block or not.


Writing rich text

When writing rich text to the API, you must provide similarly structured, valid XML. The text must be wrapped in a <body> tag, all tags must be closed, balanced, and match the case of supported tags, and attributes must be quoted. Invalid XML, as well as unsupported tags, will be rejected with a 400 Bad Request error. Only <a> tags support attributes, and any attributes on other tags will be similarly rejected.

Links

For <a> tags specifically, to make it easier to create @-mentions through the API, we only require that you provide the GID of the object you wish to reference. If you have access to that object, the API will automatically generate the appropriate href and other attributes for you. For example, to create a link to a task with GID "123", you can send the tag <a data-asana-gid="123"/> which will then be expanded to <a href="https://app.asana.com/0/0/123/f" data-asana-accessible="true" data-asana-dynamic="true" data-asana-type="task" data-asana-gid="123">Task Name</a>. You can also generate a link to a task in a specific project or tag by including a data-asana-project or data-asana-tag attribute in the <a> tag. All other attributes, as well as the contents of the tag, are ignored.

To keep the contents of your tag and make a custom vanity link, include the property data-asana-dynamic="false" when setting the contents of the tag. You would send <a data-asana-gid="123" data-asana-dynamic="false">This is some custom text!</a> and receive <a data-asana-accessible="true" data-asana-dynamic="false" data-asana-type="task" data-asana-gid="123">This is some custom text!</a>

If you do not have access to the referenced object when you try to create a link, the API will not generate an href for you, but will instead look for an href you provide. This allows you to write back <a> tags unmodified even if you do not have access to the resource. If you do not have access to the referenced object and no href is provided, your request will be rejected with a 400 Bad Request error. Similarly, if you provide neither a GID nor a valid href, the request will be rejected with the same error.

Inline images

To write an inline image, use data-asana-gid to identify the attachment you wish to write inline. To write HTML that includes an inline image:

  1. First, confirm the image you wish to write inline is already attached to the target object (e.g. task, project brief). If the target object does not have the image attached, you must first attach it before you can make the inline img write request. You can use the Asana web app to upload the image to your target object or attach it with an API request to: POST /attachments.
  2. Next, get the GID of the attachment you wish to write inline. You can either make a GET request to your target object to find the attachment's GID or make a GET request to the attachments endpoint to find the GID(s) for the attachment(s) on your target object.
  3. Once you've confirmed the image is attached to your target and you have the attachment's GID, you can make a request to write the rich text using the data-asana-gid field and the <img> tag.

Writing an inline image (after uploading the attachment)

<body><img data-asana-gid="1234"></body>

External media embeds

To write rich text that contains a new external media embed:

  1. Create URL attachment with a call to POST /attachments, with { ..., "resource_subtype": "external", "url": "<your_url>" }. Important: Use the URL that would appear in the browser address bar (e.g. https://youtube.com/watch?v=...), NOT the embeddable URL (e.g. https://youtube.com/embed/...).

  2. Make a second API call to write the rich text, using the returned GID as the data-asana-gid field of the <object> tag. You don't need the inner HTML and you only need a couple of the <object> tag attributes: All that is needed is <object type="application/vnd.asana.external_media" data-asana-gid="..."></object>

Writing an external media embed

<object
  type="application/vnd.asana.external_media"
  data-asana-gid="1234"
></object>

Writing defensively

When processing rich text and sending it back

It is okay to ignore tags or attributes on tags that are unknown for rendering/processing. It is important to send
everything back (attributes and inner content) to avoid data loss.

Note: <object> is an exception where it is acceptable to not send any inner content back (all inner content in <object> will be ignored).

If you plan to write an editor

If the tag and attributes are known, but it contains unknown attributes, it must be treated as unknown.

If a tag is unknown, first determine if the tag is block or inline, then render it as a block or inline atomic and
non-copiable (and non-cut-and-paste-able) editor node (all inner content is non-editable). This is because we don’t know if the unknown node has constraints on inner content or where it can appear. The node must also keep track of all attributes and inner content to be serialized back.