Dynamics 365 portals: Use liquid to return JSON or XML

For many business requirements to get a desired user experience you may push the limits that the entity list functionality provides. With liquid you can write your own service to return data in various formats using the web template Mime Type property. By returning your own data you can inject logic and specific formatting using liquid functionality, this will allow you to utilize new components or libraries to help you provide the specific experience your requirements demand. This post will look at how liquid can be used with web templates to return JSON or XML so that the data can be consumed and used to build a complex user experience.

With liquid there are many ways to query for data. There is the entities object which can be used to retrieve a single record by ID. To get lists of data you can use the entity list which behind is using one or many entity views and an entity view is a fetchxml query that defines the view. Entity list contains a feature, OData feed, that allows you to take an entity view and make it available as a service. The OData feed is a great way to get a RESTFul JSON return but it has many shortcomings. If your interested in trying out the OData feed functionality then check out the documentation still available on the Adxstudio Community site – Entity List OData Feeds.

If you want to directly write your own queries in web templates, perhaps dynamically constructing them, utilize entity permission relationship based data, then you can use the liquid fetchxml tag. Below is a little outline of the functions of this liquid object.

{% fetchxml my_query %}
  <fetch version="1.0" mapping="logical">
    <!-- Write FetchXML here, use Liquid in here if you want, to build XML dynamically. -->
{% endfetchxml %}
{{ my_query.xml | escape }}
{{ my_query.results.total_record_count }}
{{ my_query.results.more_records }}
{{ my_query.results.paging_cookie | escape }}
{% for result in my_query.results.entities %}
  {{ result.id | escape }}
{% endfor %}

Reference: Adxstudio Community Forums

With Dynamics 365 portals entity permissions is required by default and does not need to be referenced in the liquid tag. This differs from Adxstudio Portals v7.x, so if you are getting blank results using the fetchxml liquid object then ensure to first validate your entity permissions.

The fetchxml liquid tag and web templates Mime Type functionality provide the ability to build a web template that returns custom JSON or XML objects. With this you can build endpoints that intake custom parameters, perform logic while constructing the query, logic in returning the results, formatting the results and doing related record queries, all the while adhering to the entity permissions in place for that entity.

Below is an example of a web template that queries a custom course schedule entity with joins to related entities, course and instructor. The liquid code looks for 2 parameters location and category and if they exist it adds the fetch conditions for those parameters.

{% fetchxml feed %}
<fetch version="1.0" output-format="xml-platform" mapping="logical" distinct="false" count="10" returntotalrecordcount="true" {% if request.params['page'] %} page="{{request.params['page']}}" {% else %} page="1" {% endif %}>
  <entity name="dpx_courseschedule">
    <attribute name="dpx_number" />
    <attribute name="createdon" />
    <attribute name="dpx_starttime" />
    <attribute name="dpx_endtime" />
    <attribute name="dpx_courselocationid" />
    <attribute name="dpx_courseid" />
    <attribute name="dpx_accountid" />
    <attribute name="dpx_cost" />
    <attribute name="dpx_coursescheduleid" />
    <order attribute="createdon" descending="true" />
    <filter type="and">
      <condition attribute="statecode" operator="eq" value="0" />
      {% if request.params['location'] %}
        <condition attribute="dpx_courselocationid" operator="eq" value="{{ request.params['location'] | xml_escape }}" />
      {% endif %}
    <link-entity name="dpx_instructor" from="dpx_instructorid" to="dpx_instructorid" visible="false" link-type="outer" alias="instructorlink">
      <attribute name="dpx_contactid" />
      <attribute name="dpx_number" />
    <link-entity name="dpx_course" from="dpx_courseid" to="dpx_courseid" alias="courselink">
      <attribute name="dpx_level" />
      <attribute name="dpx_lengthunit" />
      <attribute name="dpx_length" />
      <attribute name="dpx_coursecategoryid" />
      {% if request.params['category'] %}
        <filter type="and">
          <condition attribute="dpx_coursecategoryid" operator="eq" value="{{ request.params['category'] | xml_escape }}" />
      {% endif %}
{% endfetchxml %}{
  "totalcount": {{ feed.results.total_record_count }},
  "morerecords": {{ feed.results.more_records }},
  "page": {{ request.params['page'] | default: 0 }},
  "results": [
    {% for item in feed.results.entities %}
        "starttime": "{{ item.dpx_starttime | date_to_iso8601 }}",
        "endtime": "{{ item.dpx_endtime | date_to_iso8601 }}",
        "instructorname": "{{ item['instructorlink.dpx_contactid'].name }}",
        "courselevel": "{{ item['courselink.dpx_level'].label }}",
        "location": {
          "id" : "{{ item.dpx_courselocationid.id }}",
          "name": "{{ item.dpx_courselocationid.name }}"
      }{% unless forloop.last %},{% endunless %}
    {% endfor -%}

The fetchxml result is then formatted into a JSON object using the forloop liquid object to iterate through each entity record. Linked entity attributes are easily accessed via the linked entity alias {{ entityRecord['alias.attribute'] }}. With the return being JSON you will want to set the Mime Type property to application/json.

Another example using Case (incident) where we also use a N:N relationship of all child cases in the custom data object returned with the referenced attribute to load the entity relationship.

{% fetchxml feed %}
<fetch version="1.0" output-format="xml-platform" mapping="logical" distinct="false" count="10" returntotalrecordcount="true" {% if request.params['page'] %} page="{{request.params['page']}}" {% else %} page="1" {% endif %}>
  <entity name="incident">
    <attribute name="ticketnumber" />
    <attribute name="prioritycode" />
    <attribute name="title" />
    <attribute name="createdon" />
    <attribute name="customerid" />
    <attribute name="ownerid" />
    <attribute name="statecode" />
    <attribute name="incidentid" />
    <attribute name="caseorigincode" />
    <order attribute="title" descending="false" />
{% endfetchxml %}{
  "totalcount": {{ feed.results.total_record_count }},
  "morerecords": {{ feed.results.more_records }},
  "page": {{ request.params['page'] | default: 0 }},
  "results": [
    {% for item in feed.results.entities %}
        "ticketnumber": "{{ item.ticketnumber }}",
        "title": "{{ item.title }}",
        "customer":  {
          "id" : "{{ item.customerid.id }}",
          "name": "{{ item.customerid.name }}"
        "incident_parent_incident": [
            {% for parent in item.incident_parent_incident.referenced %}
                "parentticketnumber": "{{ parent.ticketnumber }}"
              }{% unless forloop.last %},{% endunless %}
            {% endfor %}
      }{% unless forloop.last %},{% endunless %}
    {% endfor -%}

If you instead wanted to return XML then it is just a matter of updating the Mime Type of the web template to application/xml and the code to output XML instead of the JSON format. Below is a sample of the first example but returning XML.

<!--FETCHXML query -->
{% endfetchxml %}<?xml version="1.0" encoding="UTF-8" ?>
  <totalcount>{{ feed.results.total_record_count }}</totalcount>
  <morerecords>{{ feed.results.more_records }}</morerecords>
  <page>{{ request.params['page'] | default: 0 }}</page>
    {% for item in feed.results.entities %}
        <starttime>{{ item.dpx_starttime | date_to_iso8601 }}</starttime>
        <endtime>{{ item.dpx_endtime | date_to_iso8601 }}</endtime>
        <instructorname>{{ item['instructorlink.dpx_contactid'].name }}</instructorname>
        <courselevel>{{ item['courselink.dpx_level'].label }}</courselevel>
          <id>{{ item.dpx_courselocationid.id }}</id>
          <name>{{ item.dpx_courselocationid.name }}</name>
    {% endfor %}

Once you have setup your web template with your liquid logic and Mime Type you need to get a URL for it. Create a page template of the Type, Web Template, and the previous Web Template referenced. As well ensure that Use Website Header and Footer is unchecked so that all that is returned is the data formed by the web template.

Now using the portals front-end editor, create a new web page using the new page template that references the web template. This will give your web template a URL and you can now refer to this endpoint within other JavaScript on the site. Here is a small jQuery sample calling a JSON endpoint URL and logging the result to the browser console. You can also do testing of your endpoints with Postman.

      method: "GET",
      url: "/cases-json/"
    .done(function( msg ) {

Note for entity permissions to function beyond anonymous a cookie for authentication must be attached to the request. JavaScript on the site already making requests will include the necessary cookie by default.

If your interested in learning more about liquid techniques I will be giving a webinar with xRMVirtual on April 26th at 12pm EST – Advanced Liquid Templates for Dynamics 365 portals.

Dynamics 365 portals: Events and iCalendar Download with Liquid

A popular component of Adxstudio Portals was its event management system, which unfortunately has not made the transition to Dynamics 365 portals. There will be a new event management system coming in the future (as seen at eXtreme365 in Lisbon), which will come with a portal component that will list and allow registration to events. Until then you may still want to create some simple event functionality using the out of box configuration based components that allows you to list events and allow users to add an event to their own calendar. In this post we will use Liquid Templates to create your own display of events from a custom entity with an entity list and an iCalendar download (Add to Calendar button) so that users can add it to their own calendar.

First we are starting with an entity (this could be any entity that already exists or creating a new one) that includes the event information. All you need for an event is really a subject and a date. If you want to get a little more complete then we want a start date, end date, subject, description, location. It’s really up to you and the functionality or detail you want to contain in your event. I am starting with an entity called Group Meetings which includes the following fields:

To display the events on the portal you can use an entity list with a custom web template to create a custom output of the information instead of just a grid/table display. Here is the entity list web template used on xrmvirtual.com (I have removed the paging to keep the code length displayed here brief).

{% assign meetingDetails = sitemarkers["Meeting Details"] %}

{% entitylist id:page.adx_entitylist.id %}
  <div class="meeting-list-body">
    {% entityview id:params.view, search:params.search, order:params.order, page:params.page, pagesize:params.pagesize, metafilter:params.mf %}
      {% if entityview.records == empty %}
        <div class="alert alert-info">
            {% if entitylist.empty_list_text %}
              <p>{{ entitylist.empty_list_text | escape }}</p>
            {% else %}
              <p>No items matching your selected criteria were found.</p>
            {% endif %}
      {% else %}
        {% for meeting in entityview.records %}
          <div class="media">
            <div class="media-left jumbotron-icon">
              <span class="fa fa-calendar fa-2"></span>
            <div class="media-body">
              <h4 class="media-heading"><a href="{{ meetingDetails.url }}?id={{meeting.id}}">{{meeting.xv_name}}</a></h4>
              <p class="meeting-date">Speaker: <span class="meeting-speaker">{{meeting.xv_primaryspeakerid.name}}</span> | <time datetime="{{meeting.xv_starttime | date_to_iso8601}}"></time></p>
              <div class="meeting-abstract">
              <div class="meeting-actions">
                {% if meeting.xv_recordingposted | false and meeting.xv_recordingurl %}
                  <a href="{{meeting.xv_recordingurl}}" target="_blank" class="btn btn-success btn-sm">
                    <span class="fa fa-video-camera"></span>&nbsp;
                    Download Recording
                {% endif %}
                {% if now < meeting.xv_starttime and meeting.xv_meetingurl %}
                  <a href="{{meeting.xv_meetingurl}}" target="_blank" class="btn btn-primary btn-sm">
                    <span class="fa fa-calendar"></span>&nbsp;
                    Join Meeting
                {% endif %}                
                {% if now < meeting.xv_starttime %}
                  {% assign iCal = sitemarkers["XRM iCal"] %}
                  <a href="{{iCal.Url}}?id={{meeting.id}}" target="_blank" class="btn btn-default btn-sm">
                    <span class="fa fa-calendar-plus-o"></span>&nbsp;
                    Add to Calendar
                {% endif %}
        {% endfor %}
      {% endif %}
    {% endentityview %}
{% endentitylist %}

If you review the code it is basically taking the entity list that the page references {% entitylist id:page.adx_entitylist.id %}, does a check to make sure the entity view is not empty {% if entityview.records == empty %}, if it is not then it iterates through the items with a for loop {% for meeting in entityview.records %}. Within the for loop we have the individual record with the {{meeting}} object, which we then format the attributes of it with some HTML.

As this entity list template is used for upcoming and past meetings there are also some checks so that we can conditional show certain elements on each meeting, one of those being if there is a meeting URL and we are before the start time then show the join meeting button {% if now < meeting.xv_starttime and meeting.xv_meetingurl %}.

When building an event display the highest requested feature is to provide a download or add to calendar button so that users can easily add the event to their own calendars. With liquid templates we can easily satisfy this requirement. Not well documented and really only 1 public example is the web templates MIME type. With the MIME type field on web template we can actually set a custom type that the browser will use to interpret the content it is trying to process. If you don't set a MIME type then this will default the MIME type to text/html. For browsers to detect the content as calendar data then we can set the MIME type to text/calendar and then within the web template define the standard iCalendar format.

Let's start off with creating a new Web Template called iCal Download Handler. At the bottom of the form fill in the MIME type with text/calendar so that when a browser accesses this content it tries to interpret it using the calendar format. Now for the contents of the web template we want to output the standard iCalendar format. You can read through the entire RFC (if you really want) for iCalendar here to understand all the formatting options. Alternatively you can review the Wikipedia iCalendar document, as well below is a simple single event example.

{% assign meeting = entities.xv_groupmeeting[request.params.id] %}
{% if meeting %}
PRODID: -//xrmvirtual.com//NONSGML ical.net 2.1//EN
DTEND:{{meeting.xv_endtime | date_to_iso8601 | remove: '-'}}
DTSTAMP:{{meeting.xv_starttime | date_to_iso8601 | remove: '-'}}
DTSTART:{{meeting.xv_starttime | date_to_iso8601 | remove: '-'}}
X-ALT-DESC;FMTTYPE=text/html:{{meeting.xv_abstract}} {% if meeting.xv_meetingurl %}<p><a href="{{meeting.xv_meetingurl}}">Join meeting...</a></p>{% endif %}
UID: {{meeting.id}}
{% endif %}

The first check in the template is that we assume the ID of the event we want to provide as a download is provided as a query string parameter called 'id'. We use this parameter and the liquid entities object to retrieve the group meeting record. If the meeting exists then we output the iCalendar format with attributes from the group meeting as values for the iCalendar attributes.

The date/time iCalendar attributes do expect date/times formatted in the ISO8601 format and does not included any dashes. Luckily there are liquid filters that can help us achieve this exact format. Taking the meeting start or end time which is stored as a CRM date/time object we can apply the date_to_iso8601 filter and then apply the string filter to pull out the dashes that are included in that format remove: '-'.

In the SUMMARY attribute just simply use the liquid to expose a CRM attribute from the meeting object, {{meeting.xv_name}}. For the description (abstract in XRM Virtual group meeting) because we are using the CK editor to provide rich text for the description contents this is stored as HTML we need to provide the X-ALT-DESC iCalendar attribute and tell it the format FMTTYPE=text/html so that the encoding is properly interpreted. To this we also add a simple conditional check to see if there is a URL and then include the HTML to generate that link as part of the description. Finally the UID attribute of iCalendar format we just make the CRM GUID of the record.

To make this web template accessible we need to give it a URL. First create a new Page Template with the type of Web Template. Ensure the set the Use Website Header and Footer is not selected and set the Web Template to your iCal Download Handler. It is important that the Use Website Header and Footer is turned off as this will ensure that when this template is rendered it only includes the content in the web template and none of the scaffolding of the portal (like the header and footer HTML). With your page template create a new Web Page that references the new Page Template.

Page Template:

For ease of access in web template I also created a Site Marker that refers to the web page, then used the following code in my event list template to get the URL and pass the ID of the record:

{% assign iCal = sitemarkers["XRM iCal"] %}
<a href="{{iCal.Url}}?id={{meeting.id}}" target="_blank" class="btn btn-default btn-sm">
  <span class="fa fa-calendar-plus-o"></span>&nbsp;
  Add to Calendar

From this we have taken a custom entity that has event type data and displayed it in a nicely presented format on the portal and included an add to calendar or download calendar item functionality so that users can include it in their own calendar. Now because of the various versions of iCalendar that vendors have implemented you may notice this iCalendar format does not work on every device, you can though create specific web/liquid templates for the various formats and provide links to each of them or you can look at JavaScript libraries that help provide this format. AddEvent is a common plugin that can be used freely for personal use and licensed for commercial sites that will provide an easy implementation in a web template (below is a sample) that will cover all iCalendar formats.

AddEvent Liquid Template Sample:

<span class="addtocalendar  atc-style-blue">
  <var class="atc_event">
    <var class="atc_date_start">{{ event.adoxio_startdate | date: 'yyyy-MM-dd hh:mm:ss' }}</var>
    <var class="atc_date_end">{{ event.adoxio_enddate | date: 'yyyy-MM-dd hh:mm:ss' }}</var>
    <var class="atc_timezone">America/Vancouver</var>
    <var class="atc_title">{{ event.adoxio_name }}</var>
    <var class="atc_description">{{ event.adoxio_name }}</var>
    <var class="atc_location">{{ event.adoxio_name }}</var>
    <var class="atc_organizer">City of Victoria</var>
    <var class="atc_organizer_email">info@adoxio.com</var>

Determine your Dynamics 365 portal data center

When you deploy a portal for Dynamics 365 you always want it located as close to your Dynamics 365 instance physically so that the latency for all communications between the portal and Dynamics 365 is as low as possible. With Microsoft managing the portal provisioning they take care of this for you. You still may want to determine the location for hosting of other services that will communicate with your portal, like when your looking at deploying a companion web app for your portal it would be ideal if it is the same data center as your portal.

You can quickly determine your major data center region just by looking at the address to your Dynamics 365 instance. The following link has a listing of all current major regions – Discover the URL for your organization using the Organization Service.  However within these major regions are regional data centers. For instance, https://*.crm.dynamics.com (North America which should actually be called USA/Mexico now since there is a region for Canada) has 3 different regional data centers and they are even creating secondary data centers regionally.  Within North America (or USA/Mexico) you could be in West US, Central US, East US, or even East US2.  Within CRM3 (Canada) you could be in Canada Central or Canada East as your primary data center. You can see all the Azure datacenter locations in the following link – Azure Datacenter locations.

Previous ways to determine your data center have involved spinning up an Azure VM and moving it through regions while doing latency tests.  You can also use a IP locator service to help you determine provided the IP address you testing has the proper location data registered with it (this sometimes can give false information as IP addresses can be re-routed to different regions).  Another way which the guys over at Peak Engagement blogged about is using the debug information page which reveals information about your Dynamics instance including the server name, database server, database name and much more.  If you look at patterns you may notice that the names of the servers are based on those regional data centers.

The Dynamics 365 portal has something similar but you can even use this technique on any Dynamics 365 portal without even being the owner of that site.  On every request response to the portal if you inspect the headers you will notice 2 additional response header keys added specifically for the Dynamics 365 portal, x-ms-portal-app and x-ms-request-id. The x-ms-request-id is just a unique GUID for each request, likely exposed to help assist with debugging. The x-ms-portal-app is a value for the site itself, it remains constant through all requests. Taking a look at the value found, there is the site GUID which is the GUID of the Azure Web App and then appended to that is a data center code.

You can easily get to this yourself without even being logged into the portal. The request headers are available on all requests. To look at this value open your browsers developer tools (F12), then select the network tab, open the details of any request and view the response headers. Below is a screenshot of Chrome’s developer tools highlighted with the navigation tips.

Below is a list of the data center region codes found on Dynamics 365 portals from the portals I have checked thus far.

  • EUw – West Europe
  • EUn – North Europe
  • USw – West US
  • GCv – US Government Cloud Virginia
  • GCi – US Government Cloud Iowa
  • USe2 – East US2
  • CAc – Canada Central
  • AUse – Australia Southeast

This is not an exhaustive list, if you come across others then feel free to drop a note in the comments.

You can now use this information to help you when picking a hosting location for a companion app as this region should be in same as your Dynamics 365 instance so you can attempt to get the best performance between applications and the backend Dynamics 365 instance.

Note there are instances where the Dynamics 365 instance and the portal will not be located in the same data center. This is not often the case but instances of this have seen that East US can host Dynamics 365 and East US2 can host the portal, this type of setup might also occur in other locations.

Dynamics 365 portals – Display Activities with Timeline

A new feature launched at the inception of CRM portals but never really documented is the ability to display entity record related activities on the portal through a new timeline feature. This feature was created to enhance the portal case management functionality by allowing the display of any activities related to the entity record (using the regarding field). This functionality is actually configuration based through a new entity form metadata type of “Timeline”, which can also be used with your own entity forms with any entity that is activity enabled.

To add the timeline to your own entity form there are a couple configuration steps you will need to make to successfully setup. To use the timeline metadata type the entity your working with will need to be activity enabled otherwise the metadata option will not display. You can activity enable your entity by opening the System Customizer and navigating to the entity information, on the general tab under Communication & Collaboration ensure Activities is selected.


You can now create an entity form in either Edit or Read-Only modes where you can then add the timeline metadata type. Below is the minimum configuration for an entity form for edit mode:


Once you have your entity form created, we need to add the associated entity form metadata for the timeline. Scroll to the bottom of the entity form configuration or from the related entities navigation select Entity Form Metadata and create a new item. In the new metadata form, select the type as “Timeline”.


Once on the type “Timeline” the options panel below will update to reveal the new configuration options for activities. At this time it is mostly focused around the portal users ability to contribute content as an activity, and what file upload functionality they have. You can also select the “Advanced Options” to reveal a number of additional labels that can be modified.


The portal timeline feature utilizes the activitypointer entity in Dynamics to query for the related activities and supports the following activity entities. It does not support custom activities it appears at this time, however you can expose additional activity types (like task) or custom activities through sub grid functionality.

  • Portal Comment
  • Appointment
  • Phone Call
  • Email

For this configuration to properly function Entity Permissions need to be configured. We are going to assume you have already created an entity permission of the entity, in the case of mine, the entity Economic Development Sites (adoxio_economicdevelopmentsite), with the scope of contact (there is a related contact field that is a lookup to the contact entity), the privileges (read, write, create, append, append to), and with the desired web roles set.

There needs to be 2 additional entity permissions added, one for the activitypointer entity, and another for the Portal Comment activity. Both entity permissions should have the scope of parent and set the parent entity permission to the permission of the entity being viewed. This ensures that only the activities which are related to the record are accessible.

Activity (activitypointer) – Privileges: Read


Portal Comment(adx_portalcomment) – Privileges: Read, Create, Append


Portal Comment is a new activity entity that replaces the use of notes/annotations for user submitted content. This was done so that a entity relationship to contact was established and it would support native entity permissions. Annotations are still used to hold the contents of attachments on the portal comment activity record. Back office staff can create new Portal Comment activities within Dynamics regarding the entity record and once marked complete will be displayed on the portal.

Once all the configurations are complete, you can now view your edit or read-only entity form with an included timeline view of activities.


Dynamics 365 portals – Extend Search Results with Handlebars

With the Adxstudio Portals v7 product I can’t count the number of times that Adoxio has had to extend the search functionality with enhancements for customer requirements. With the latest release of Dynamics 365 portals, Microsoft has really made extending and customizing the out of box search extremely easily and powerful. The search is now built on top an asynchronous call to ensure page load performance is fast and the results all rendered with Handlebars templates. They have really gone a step further by taking those Handlebars templates and making them accessible through the Web Templates functionality; results display, paging, sorting, and the new facets are all available as Web Templates that you can customize and configure.

You can read more about the new base search functionality from a blog post on the Dynamics Team MSDN Blog, Search enhancements in Portal capabilities for Microsoft Dynamics 365.

If you’re not familiar with Handlebars you might be wondering what it is. Handlebars is a JavaScript framework, in which you can build semantic templates, with no frustration, or so they say 😉 – read more about why semantic templates help developers from Martin Brennan, Semantic templates with Mustache.js and Handlebars.js. Think of a template as being a view of how you want each item in the results rendered; put the title in an H3, description text in a P, add this class, etc. Why this is important is that the framework has a lot of power that beyond just formatting that allows you to easily modify the templates so you can create your own views, but also extend the functionality by adding helper methods with custom logic.

With the functionality of Handlebars helpers you can build functions to process the data submitted to a template. So if you wanted to do some sort of conditional processing or format on each item rendered, building your own helper can help you meet a certain requirement. Recently on the Dynamics Community Forum a member was looking to extend each result item with additional data from the full entity itself. So if the result was a Knowledge Article they might want to also show the subject or other categorization metadata in the search result. The default data context returned by the search to keep performance high has a limited set of attributes that really only include what you see rendered in a result. With a Handlebars helper and the power of Liquid we actually have the capability to go get the detail data for that particular entity record result.

For this example the Handlebars helper is going to make a synchronous AJAX request passing the entity ID of the result item, which will be then processed by a Web Template using Liquid to go get additional data that will then be included in the result item. First we need to setup our Web Template that will take the entity ID and go get the additional data and format it for us.

{% assign eid = request.params['entityid'] %}
{% assign wpage = entities.adx_webpage[eid] %}

<p><strong>Name:</strong> {{wpage.adx_name}}</p> 

We are going to assume for this that the result is always a web page but you could easily add an additional parameter for the entity logical name and then have conditional branching to handle different entities. Basically the template just takes the query string parameter entityid, uses the liquid entities object to query the web page entity for the desired ID then displays the page name back. You could access any of the attributes in the web page or whatever entity you had queried.

Once the Web Template is created we now need to make it accessible to the AJAX call we are going to later configure. To do so we need to add a Page Template that uses this Web Template. Create a new Page Template from Portals > Page Templates. Ensure to set the Type to “Web Template”, select the Web Template as the template you just created, and deselect the “Use Website Header and Footer”. All we want this page to return is the HTML that is to be included in the result.


Now to make your page template accessible via a URL you will need to create a Web Page that surfaces the Web Template your Page Template refers to.


You can test your template is accessible by just directly calling its URL in your browser with an ID included, https://testingportal0001.microsoftcrmportals.com/search-details/?entityid=94216ad7-7864-e611-80d7-00155db4fa48. All you should get back is the simple HTML in the template since we removed the Website Header and Footer.

Now we need to register our Handlebars helper that is going to request this template. To do so we need to add some custom JavaScript to the search results page. This can be easily done with the front-side editor or you can access the web page from the CRM (Portals > Web Pages > Search). If using the front-side editor, login with your Administrative account, run a search query and then select the Edit from the CMS control, select the Options tab, and then paste in the following to the custom JavaScript section.

Handlebars.registerHelper("search_details", function (obj) {
    return $.ajax({
      url: "/search-details/?entityid=" + obj.fn(this),
      type: "GET",
      async: false

Basically it makes a synchronous AJAX request (it needs to be synchronous as the it is already an asynchronous request for the results) to the URL we created with our Web Page passing the entityID. It will return the HTML as the responseText which is then returned as the result of the Handlebars helper. You would be able to put whatever logic you want within this function, whatever JavaScript allows you to, which is A LOT.

Now that we have our helper function, and our template, its time to modify the search results Web Template that is provided out of the box to include a call to our new helper. Navigate in CRM to Portals > Web Templates, open the “Faceted Search – Results Template” and you want to add the following within the {{#each items}} where you want your extra details to show up: {{#search_details}}{{entityID}}{{/search_details}}. This will call our helper method passing the entity ID of the result item.

{{#each items}}
  <h3><a title="{{title}}" href="{{url}}">{{title}}</a></h3>
  <p class="fragment">{{{fragment}}}</p>
   {{#each tags}}
    <span class="{{cssClass}}">{{./label}}</span>

With Dynamics 365 portals you also need to be very aware of entity permissions. Our liquid query will return nothing if we do not provide permission to that entity. You will need to decide exactly how to configure the entity permission, for this example I am just adding an entity permission for Web Page with a Global scope and the Web Roles Anonymous and Authenticated User. It is important to be careful and aware of what you configure with entity permissions as they are what gate and protect the data in your CRM.

Now that it is all configured, head back to the portal and refresh your search results or perform a search query. Your results should now include your extra template on each and every result.


My template for this example was simple, but with the power of liquid querying you could really include anything here. This example also focused on adding additional details but Handlebar helpers can be used for a lot more.