Quick Tips

Build Improved Navigation for Your Einstein Analytics App

An Often Overlooked Item

When I started working in Salesforce Wave (Einstein Analytics – now Tableau CRM), I started building individual lenses and dashboards to be digested by the greater corporate audience. This works ok but it relies on the user to know where they want to go (and what exists!).

A Different Approach

Using a primary dashboard that acts as a ‘menu’ or ‘directory’ – you can provide critical information to your end user not only some key KPI data for your organization, but also a directory that guides and direct your users to drilldown to additional dashboards! This will let them know about dashboards that provide critical insights that they otherwise may not have even known existed! I’ve found that having a guide like this will allow you to also publish instructions and key new feature information with each release. This helps cut down on the amount of questions you need to field.

Key Ideas & Items For Consideration

  • Use pages to place information on multiple ‘screens’ with links between each of them
  • Use links directly to your other dashboards
  • Use a ‘home’ type of link system on each of your individual dashboards that will take the user back to the directory page
  • Decide if you want to share some ‘key’ high-level KPI type of data on this home base dashboard
  • Consider adding instructions and new feature information on this page as well

Please let me know in the comments if you have any other ideas or if you have implemented something similar in your work. Happy SAQL’ing!

The Model View Controller (MVC) Method of Building EA Dashboard Interactivity With Bindings – Part 2

In Part 1 we looked at the MVC method of building Einstein Analytics dashboards conceptually.  Now we are going to review specific examples and code to put it into practice.  First, let me say that I’m not sure everything that is accomplished is fully supported by the platform.  So know that it could potentially break at any point in time.  However, it has worked for a few years and while some examples for how you could use this can be accomplished via other methods, especially now with the ‘advanced editor’ via the UI – there are use cases where I’ve found this to be the only method that works.

A Few Use Cases

I have used this MVC design pattern in other programming languages so when I ran into roadblocks in trying to design a dashboard with certain functionality I tried it as a workaround and I was very happy when it ended up working!  I have used this in cases where we have a single widget (or multiple that leverage the same step/query) but need a variety of control from the end user.  Sure, if they just want to switch the grouping and/or the measure we would be fine.  But what if at the same time they needed to toggle the specific function that is being calculated on the fly (for example a windowing function).  These functions need to already have the stream organized and ‘grouped’ properly prior to the calculation.  Moreover, you need to specify that grouping and measure within the function.  On top of all of this, I’ve had to build dashboards that toggle between not only various groupings but completely different timeframes (hour, year-week, year-month-day, month) and use different summary calculations (avg, running total, etc.) and these all have to be specified within the function and the stream already ‘prepped’ with the proper information.

A Relatively Simple Example

What follows is a relatively simple example where we are only changing the measure, toggling between ActivityDate and CreatedDate and also toggling whether we are viewing the ‘actual’ count of records or a Moving Average period of 10, 15, 30, 45 or 60 days.  Just know that you could easily expand on this and add additional toggles for some of the use cases I mentioned above.  Once you set up the dashboard in MVC you have laid the groundwork to add additional functionality relatively easily.


In order to build this we need our visual components which are the widgets and the static steps for the Toggles.  These are fairly simple and can be input via the UI:


Then, you need to create a Controller Step that will use toggles to ‘feed’ code into the main step that feeds the chart.

Below is the full code for the Controller Step.  Essentially it takes a single ‘row’ (limit 1) of the dataset that we are using so we ‘know’ we will have at least one result and then we use a series of case statements in order to evaluate the user selections and output code based on those:

"ControllerStep": {

"broadcastFacet": true,

"groups": [],

"label": "ControllerStep",

"numbers": [],

"query": "q = load \"activity\";\nq = foreach q generate case when \"{{coalesce(cell(static_1.selection, 0, \"Display\"), \"Actual\").asString()}}\" == \"Actual\" then \"sum(count)\" when \"{{coalesce(cell(static_1.selection, 0, \"Display\"), \"Actual\").asString()}}\" == \"10 Day MA\" && \"{{coalesce(cell(static_2.selection, 0, \"Value\"), \"ActivityDate\").asString()}}\" == \"ActivityDate\" then \"avg(sum(count)) over ([-10..0] partition by all order by ('ActivityDate_Year~~~ActivityDate_Month~~~ActivityDate_Day'))\" when \"{{coalesce(cell(static_1.selection, 0, \"Display\"), \"Actual\").asString()}}\" == \"15 Day MA\" && \"{{coalesce(cell(static_2.selection, 0, \"Value\"), \"ActivityDate\").asString()}}\" == \"ActivityDate\" then \"avg(sum(count)) over ([-15..0] partition by all order by ('ActivityDate_Year~~~ActivityDate_Month~~~ActivityDate_Day'))\" when \"{{coalesce(cell(static_1.selection, 0, \"Display\"), \"Actual\").asString()}}\" == \"30 Day MA\" && \"{{coalesce(cell(static_2.selection, 0, \"Value\"), \"ActivityDate\").asString()}}\" == \"ActivityDate\" then \"avg(sum(count)) over ([-30..0] partition by all order by ('ActivityDate_Year~~~ActivityDate_Month~~~ActivityDate_Day'))\" when \"{{coalesce(cell(static_1.selection, 0, \"Display\"), \"Actual\").asString()}}\" == \"45 Day MA\" && \"{{coalesce(cell(static_2.selection, 0, \"Value\"), \"ActivityDate\").asString()}}\" == \"ActivityDate\" then \"avg(sum(count)) over ([-45..0] partition by all order by ('ActivityDate_Year~~~ActivityDate_Month~~~ActivityDate_Day'))\" when \"{{coalesce(cell(static_1.selection, 0, \"Display\"), \"Actual\").asString()}}\" == \"60 Day MA\" && \"{{coalesce(cell(static_2.selection, 0, \"Value\"), \"ActivityDate\").asString()}}\" == \"ActivityDate\" then \"avg(sum(count)) over ([-60..0] partition by all order by ('ActivityDate_Year~~~ActivityDate_Month~~~ActivityDate_Day'))\" when \"{{coalesce(cell(static_1.selection, 0, \"Display\"), \"Actual\").asString()}}\" == \"Actual\" then \"sum(count)\" when \"{{coalesce(cell(static_1.selection, 0, \"Display\"), \"Actual\").asString()}}\" == \"10 Day MA\" && \"{{coalesce(cell(static_2.selection, 0, \"Value\"), \"CreatedDate\").asString()}}\" == \"CreatedDate\" then \"avg(sum(count)) over ([-10..0] partition by all order by ('CreatedDate_Year~~~CreatedDate_Month~~~CreatedDate_Day'))\" when \"{{coalesce(cell(static_1.selection, 0, \"Display\"), \"Actual\").asString()}}\" == \"15 Day MA\" && \"{{coalesce(cell(static_2.selection, 0, \"Value\"), \"CreatedDate\").asString()}}\" == \"CreatedDate\" then \"avg(sum(count)) over ([-15..0] partition by all order by ('CreatedDate_Year~~~CreatedDate_Month~~~CreatedDate_Day'))\" when \"{{coalesce(cell(static_1.selection, 0, \"Display\"), \"Actual\").asString()}}\" == \"30 Day MA\" && \"{{coalesce(cell(static_2.selection, 0, \"Value\"), \"CreatedDate\").asString()}}\" == \"CreatedDate\" then \"avg(sum(count)) over ([-30..0] partition by all order by ('CreatedDate_Year~~~CreatedDate_Month~~~CreatedDate_Day'))\" when \"{{coalesce(cell(static_1.selection, 0, \"Display\"), \"Actual\").asString()}}\" == \"45 Day MA\" && \"{{coalesce(cell(static_2.selection, 0, \"Value\"), \"CreatedDate\").asString()}}\" == \"CreatedDate\" then \"avg(sum(count)) over ([-45..0] partition by all order by ('CreatedDate_Year~~~CreatedDate_Month~~~CreatedDate_Day'))\" when \"{{coalesce(cell(static_1.selection, 0, \"Display\"), \"Actual\").asString()}}\" == \"60 Day MA\" && \"{{coalesce(cell(static_2.selection, 0, \"Value\"), \"CreatedDate\").asString()}}\" == \"CreatedDate\" then \"avg(sum(count)) over ([-60..0] partition by all order by ('CreatedDate_Year~~~CreatedDate_Month~~~CreatedDate_Day'))\" else \"avg(sum(count)) over ([-60..0] partition by all order by ('ActivityDate_Year~~~ActivityDate_Month~~~ActivityDate_Day'))\" end as 'WindowFunction', 'ActivityDate_Year' + \"~~~\" + 'ActivityDate_Month' + \"~~~\" + 'ActivityDate_Day' as 'ActivityDate_Year~~~ActivityDate_Month~~~ActivityDate_Day', count() as 'count';\nq = limit q 1;",

"receiveFacetSource": {

"mode": "all",

"steps": []


"selectMode": "single",

"strings": [],

"type": "saql",

"useGlobal": true,

"visualizationParameters": {

"parameters": {

"borderColor": "#e0e5ee",

"borderWidth": 1,

"cell": {

"backgroundColor": "#ffffff",

"fontColor": "#16325c",

"fontSize": 12


"columnProperties": {},

"columns": [],

"customBulkActions": [],

"header": {

"backgroundColor": "#f4f6f9",

"fontColor": "#16325c",

"fontSize": 12


"innerMajorBorderColor": "#a8b7c7",

"innerMinorBorderColor": "#e0e5ee",

"maxColumnWidth": 300,

"minColumnWidth": 40,

"mode": "variable",

"numberOfLines": 1,

"showActionMenu": true,

"showRowIndexColumn": true,

"totals": true,

"verticalPadding": 8


"type": "table"



Then, we use those outputs from the ‘ControllerStep’ in our primary step that feeds the visual chart on the dashboard:

"ActivityDate_Year_Ac_1": {

"broadcastFacet": false,

"groups": [],

"label": "ActivityDate_Year_Ac_1",

"numbers": [],

"query": "q = load \"activity\";\nq = filter q by date('ActivityDate_Year', 'ActivityDate_Month', 'ActivityDate_Day') in [dateRange([2018,4,28], [2018,8,12])];\nq = group q by ({{coalesce(cell(static_2.selection, 0, \"FirstGroup\"), \"'ActivityDate_Year', 'ActivityDate_Month', 'ActivityDate_Day'\").asString()}});\nq = foreach q generate {{coalesce(cell(static_2.selection, 0, \"Year\"), \"'ActivityDate_Year'\").asString()}} + \"~~~\" + {{coalesce(cell(static_2.selection, 0, \"Month\"), \"'ActivityDate_Month'\").asString()}} + \"~~~\" + {{coalesce(cell(static_2.selection, 0, \"Day\"), \"'ActivityDate_Day'\").asString()}} as {{coalesce(cell(static_2.selection, 0, \"2ndGroupOrder\"), \"'ActivityDate_Year~~~ActivityDate_Month~~~ActivityDate_Day'\").asString()}}, count(q) as 'count';\nresult = group q by {{coalesce(cell(static_2.selection, 0, \"2ndGroupOrder\"), \"'ActivityDate_Year~~~ActivityDate_Month~~~ActivityDate_Day'\").asString()}};\nresult = foreach result generate {{coalesce(cell(static_2.selection, 0, \"2ndGroupOrder\"), \"'ActivityDate_Year~~~ActivityDate_Month~~~ActivityDate_Day'\").asString()}}, {{coalesce(cell(ControllerStep.result, 0, \"WindowFunction\"), \"sum(count)\").asString()}} as 'MovingAvg';\nresult = order result by ({{coalesce(cell(static_2.selection, 0, \"2ndGroupOrder\"), \"'ActivityDate_Year~~~ActivityDate_Month~~~ActivityDate_Day'\").asString()}} asc);\nresult = limit result 2000;",

"receiveFacetSource": {

"mode": "none",

"steps": []


"selectMode": "single",

"start": [],

"strings": [],

"type": "saql",

"useGlobal": false,

"visualizationParameters": {

"parameters": {

"autoFitMode": "keepLabels",

"showPoints": false,

"legend": {

"descOrder": false,

"showHeader": true,

"show": true,

"customSize": "auto",

"position": "right-top",

"inside": false


"axisMode": "multi",

"tooltip": {

"showBinLabel": true,

"measures": "",

"showNullValues": true,

"showPercentage": true,

"showDimensions": true,

"showMeasures": true,

"customizeTooltip": false,

"dimensions": ""


"visualizationType": "time",

"missingValue": "connect",

"dashLine": {

"measures": "",

"showDashLine": false


"timeAxis": {

"showTitle": true,

"showAxis": true,

"title": ""


"title": {

"fontSize": 14,

"subtitleFontSize": 11,

"label": "",

"align": "center",

"subtitleLabel": ""


"trellis": {

"flipLabels": false,

"showGridLines": true,

"size": [




"enable": false,

"type": "x",

"chartsPerLine": 4


"fillArea": true,

"showActionMenu": true,

"showZero": true,

"measureAxis2": {

"sqrtScale": false,

"showTitle": true,

"showAxis": true,

"title": "",

"customDomain": {

"showDomain": false



"measureAxis1": {

"sqrtScale": false,

"showTitle": true,

"showAxis": true,

"title": "",

"customDomain": {

"showDomain": false



"valueType": "none",

"theme": "wave",

"applyConditionalFormatting": true,

"drawArea": {

"measure": "",

"showDrawArea": false,

"bounding1": "",

"bounding2": ""



"type": "chart"



I didn’t do it here but for a more polished look you can even update all display names on the chart axis with the proper string based on the user selection.

Please let me know if you have had success with this.  Please let me know if you have a better solution!  Honestly this is a bit of a hack that I’ve come up with but it has saved me countless times!  Every time I think I can do it via normal binding functionality I run into gaps and limitations due to issues with having to already have the stream prepped for the function or specifying the measure and grouping in the function which you cannot do with normal binding syntax.  This has been a lifesaver and I hope it is helpful to you.  Until next time, keep riding the Wave (or Einstein Analytics)!  Once Wave Always Wave!

Convert Decimal Time Into Hours and Minutes – HH:MM – Wave Einstein Analytics

Salesforce Quick Tips

Thought I would write about this Analytics Quick Tip as I have found zero information related to this online and I would imagine it would be a pretty common use case.

The Problem With Decimal Hours

Often you have ‘duration’ type measures stored as ‘number of seconds.’  Think of the duration of a service ticket from one stage to the next, or the duration of a sales call.  Many times you want to aggregate all of these together for a particular user or particular day.  Once you’ve aggregated them together, you can easily convert these into decimal hours by dividing by 60 to arrive at # of minutes and 60 again to arrive at # of hours (divide by 3600 once to shortcut this step).  The trouble is then you have a confusing metric of decimal hours.  3.45 hours does not equal 3 hours and 45 minutes.  It is closer to 3 hours and 30 minutes!  4.87 does not equal 4 hours and 87 minutes!  No matter how many times you try and educate your users on decimal hours it is confusing for them to discuss with their teams and properly rank and compare them to each other.

A SAQL Solution For Converting Decimal Hours to HH:MM

I think it’s helpful to break each step down into its component part before we get into the code of how we are going to accomplish this.

We want to take 4.87 and convert to 4:52.

  1. First we want to take out the # preceding the decimal point as it is already a valid # of hours. (4)
  2. Then we want to add a colon to separate hours from minutes. (4:)
  3. Then we want to take the remaining number post decimal point and multiply by 60 (this number will be formatted to only 2 digits and placed after the colon). (4:52 – we arrive at this by taking the entire 4.87 and subtracting the truncated number of 4 which leaves .87 and then we multiply that by 60)

Simple corresponding example below without formatting and where ‘decimalhour’ is already in decimal hours.  You would just replace ‘decimalhours’ with your duration measure and divide by 3600 if converting from seconds.

1. trunc(sum('decimalhours'))
2. + ":" +
3. sum('decimalhours') - trunc(sum('decimalhours')) * 60

Here is an example of the full saql w/ formatting.

 number_to_string(trunc(sum('decimalhours')),"#") + ":" + number_to_string(((sum('decimalhours')) - (trunc(sum('decimalhours')))) * 60,"0#") as 'HH:MM'

Now we have a very easy to read table in hours and minutes!

Owner Total Time
Bruce Kennedy 4:36
Catherine Brown 4:02
Chris Riley 3:55
Dennis Howard 3:29

The only issue I’ve found is when someone is right on the hour, say 4:00 and it only displays a single zero. I’ve solved for this with a case statement as in all of my use cases we are 5 hours and below so I only need 5 case statements.  This is very easy to do.  I had first tried replace but it would then replace all scenarios where there is a zero after the colon.  I’m sure there is a more elegant solution but I have not had the chance to spend very much time on this piece and the case statement solved my particular situation.

Did this solution help you?  Do you have another method to solve the same problem?  Let me know in the comments.  I would love to hear from you!

Can’t Clone Date or Range Widgets – Multiple Pages Workaround

Date or Range Widgets – The Problem

I wanted to create an interactive, multiple page dashboard like the one below:

The issue is that I wanted the same date widget to be accessible on every page as your user may want to modify the dates at any time.  Typically, you can create all of the widgets (containers, numbers, charts, lists/dropdowns) on a single page and then select them to add them to other pages, as below:

The problem is you cannot do this with Date widgets.  As of Winter ’19 (in a pre-release org) you get an error message that states: “Warning! Can’t clone Date or Range widgets.”  hmmmm….OK

Warning! Can't clone Date or Range widgets.

Warning! Can’t clone Date or Range widgets.

As the video demonstrates above, the “Add to Page” feature works fine for dropdown lists for use in faceting or bindings along with many other widget types but not for dates…

Workaround #1 – Clone Entire Page

If you are lucky enough to place the date widget onto the main page and set it up prior to creating your additional pages you can just clone the entire page and your date widget will be cloned (despite the error message) onto every page using not only the same step but the same widget (a linked widget just as you would expect).

Date Widget Is Linked!

Of course, this is only an option if you have not yet created your additional pages. In my opinion, it is the preferred workaround because it create a truly ‘linked’ widget and changes to the widget will not have to be manually replicated onto additional pages.  Additionally, it is a no-code workaround which is another plus for many.

Workaround #2 – Update Steps

This workaround consists of creating a second, third, … (as many as necessary) date widgets for each additional page required in your dashboard.  Then, you will update the JSON to point these new date widgets to the original step that the original date widget is using from page 1.

After creating the date widget onto the canvas, actually set up the underlying step by clicking on the “Date” link and selecting a dataset and date field.  You can choose any date field because we will be updating the underlying step manually in a minute.

This step of actually setting up a step is necessary because you cannot drag the date step from the list of steps to the date widget.

Can’t drag date step to date widget – I guess that’s why it’s italicized!

Now that we have the original date widget and step from page 1 AND all of the additional date widgets on subsequent pages we CMD+E or Ctrl+E into the JSON!

Locate the original date widget and step from page 1 (you can also click on the widget prior to accessing the JSON and view the details there).

In this case I can see that my widget from page 1 is using the step “Last_Activity_1”.  You will want to save this value because we want to update the date widgets from the other pages to also use this step.  Once you have located the other date widgets and the steps they currently leverage you can update them.

I like to use the Find & Replace tool that is hidden until you CMD+F or Ctrl+F twice (that’s right, use the same keyboard shortcut twice in a row!  Magic!).  Now it’s simply a matter of finding the incorrect values and replacing them with the correct value.

Once I have replaced all of them (only the ‘Step’ values), the widgets are linked to the same step and the user can now update the date widget on any page and their selection will persist while navigating to any other page.

Have you encountered this before?  I suppose a third workaround may be using code to actually add the same widget (linked) onto each page through JSON however this would be a lot more work for virtually the same result.

How long will we have to live without the ability to drag date steps to the widget or use the Add to Page functionality for date widgets? (Even though we can currently clone the entire page with the date widget included).  Let me know your thoughts and your best Einstein Analytics workarounds in the comments!

Copy Salesforce Matrix Report & Other Tables Into Excel Without Check Boxes

Salesforce Quick Tips

Copy Salesforce Matrix Report Into Excel

As much as we’d like to keep and share data in the cloud it’s sometimes necessary to take your salesforce data and place it in a spreadsheet to share with others.  You can always export the detail view (tabular view with one row per record) into a .csv or .xls format.  However, sometimes it’s beneficial to be able to take your summarized matrix view and paste directly into Excel.  Especially in cases where you may have written a lot of complex report formulas (these don’t export unless you use “Printable View”) or added other aggregations: sum, min, max, avg.  In order to replicate in Excel you would need to create a pivot table on the detailed export.  Why rebuild if you don’t have to?

Check Boxes: The Issue With Copying and Pasting From Salesforce Matrix Reports

The primary issue with copying and pasting from Salesforce matrix reports is that the copy/paste includes the checkboxes on the left side of the table.  I’ll show you how to get rid of those pesky boxes and copy/paste like a pro!  Note – you can also use the “Printable View” button, however this trick works in a pinch and works for more than just reports as noted below!

Salesforce Matrix Report

Step 1: Copy the entire table as displayed below.

Copy Salesforce Matrix Report Cells

Very important to copy every cell, so start above the table if necessary to ensure you’ve copied the first and last cells.

Make Sure to Copy From Above Report Results

Step 2: Ctrl + c to Copy!

Step 3: Paste into Excel.

Paste Salesforce Matrix Report in Excel

Salesforce Matrix Report In Excel…not pretty!

See the checkboxes in column A?  You can delete the column or try selecting them with your mouse, and still you won’t be able to remove them using those methods!  We’ll show you how you can select the checkboxes, so you can remove them!

Step 4: Press F5 and click “Special…”

F5 In Excel Click Special

Step 5: Select “Objects” and click OK.

Select Objects and OK

Step 6: Now the checkboxes are selected, so hit the “Delete” key on your keyboard to get rid of them!

The Check Boxes are Selected and Delete

Step 7: Now the formatting needs some work.

Fix Excel Formatting

Step 8: Remove Column A.  Fix colors, borders, font and size.  Now you have one version of a Salesforce Matrix Report in Excel without the check boxes!

Final Excel Table Copied From Matrix Report in Salesforce

Other Salesforce Tables

This trick also works for other Salesforce tables that you want to copy and paste from, like a list of standard or custom fields on an object:

You can just highlight, copy & paste and then use the trick above to remove the “Indexed” fields and/or check box images.


This saves you time from rebuilding and ensures accuracy (no more copying and pasting individual #’s).  You can keep the formulas and summaries that you worked so hard to build in Salesforce and get them into Excel for your users who need them presented this way.  Please remember to also try the “Printable View” button which also may meet your needs, however it won’t work for lists of fields and other tabular data out of your Salesforce org!  Do you have any other tips and suggestions for working with Salesforce reports in Excel?  I would love to hear them!  Comment below or tweet them @SFDC_r!

Avoid Long Load Time To Edit Dashboard Component Report

Salesforce Quick Tips Edit Dashboard Component Report

Edit Dashboard Component Report

Have you ever wanted to edit the underlying report for a dashboard component and found it cumbersome?  A dashboard executes with a ‘running user’ and this user feeds into all of the underlying reports.  This is great, because it means that you can create a single set of reports and not have to edit the reports for the dashboard to provide useful data to an entire set of your users.

The Problem – Edit Dashboard Component Report

Because your user is likely placed very high up in the role hierarchy (so that you have access to all or most of the Org’s records) when you click on a dashboard component to view the underlying report, it can take many minutes (or even timeout).  Your users don’t have this issue, they click the dashboard component and the user running the report would only see records at or below them in the role hierarchy – likely running quickly.  You as the admin, however, can only finally click “Customize” to edit the report after waiting a long time (and that’s if it doesn’t timeout).

The other option to avoid the above dilemma (and wait) is to navigate to “Reports” and locate the report you want to edit.  Instead of running the report, click the arrow and “Customize.”  The issue here is that you have to know which report you want to edit, which probably requires “Editing” the Dashboard to view the exact name of the report for the specific component.  This can be time consuming and cumbersome as well.

A Better Solution – Salesforce Quick Tip!

I present to you a better solution.  Simply right click the Dashboard Component you would like to edit the underlying report for and copy the URL.  Paste the URL into your browser (probably a new tab) it should look something like this: https://[YOURSERVERURL].salesforce.com/00O61000003p9Nj if it happens to have anything after the Report ID – just remove it so it looks like the above.  Now add “/e?” to the end of the URL.

Final URL: https://[YOURSERVERURL].salesforce.com/00O61000003p9Nj/e?

Problem Solved

Now just hit Enter and you will be taken directly to editing the underlying report – and it will load quick.  Never get stuck waiting for a report to load just to edit it!

What quick tips do you have related to Dashboards or Reports?  Feel free to post in the comments or on Twitter!

Future-Proof Your Org w/ a Relative Salesforce Server URL

Salesforce Quick Tips Logo

Use A Dynamic/Relative Salesforce Server URL

Today, I’m going to show you how to future-proof your Salesforce server url.  Instead of specifying a specific Salesforce server, we will use a relative Salesforce server URL.  What does that mean exactly?  Well, have you ever created hyperlinks used in many email templates that point to a page within Salesforce?  Are they “hard links” that point to a specific Salesforce server, “https://na30.salesforce.com/” for example?

If Salesforce moves your org to a different server, you will have to go into each and every email template and make the change!

The Solution/Workaround – Relative URL’s In Email Templates

Create a formula field on the User object using the following formula:

LEFT($Api.Enterprise_Server_URL_270, FIND( "/services", 
$Api.Enterprise_Server_URL_270) -1)

Then, use this field whenever you need to reference the Salesforce Server URL.  If Salesforce moves your org to a different server, no changes necessary!

Do you have any other tips for future-proofing your org?  Post in the comments or message on Twitter!

Also check out the Go With The Salesforce Flow series!