Transfer Photos through payloads

Zapier and Fulcrum webhooks do not permit images to be transferred through them. You can work around this limitation by creating a calculation field that pulls in the photo through the Fulcrum data shares.

This calculation field requires access to your form's data share, so you will need to make sure you have that data share enabled for the form. The code loops through all of the photos in the Photo field and creates an HTML img tag with the proper photo ids.

Make sure to change the link in there with your form's data share! Enjoy!

var html = "";
$photos.forEach(function(photo) {
html += "<p><img src='" + photo.photo_id + "/thumbnail.jpg'></p>"

For getting photos out of a repeatable section

var html = "";

var photos = REPEATABLEVALUES($repeatable_section, 'photos');
var flatPhotos = FLATTEN(photos);

flatPhotos.forEach(function(photo) {
html += "<p><img src='" + photo.photo_id + "/thumbnail.jpg'></p>"


Use this in a calculation field to pull up the altitude of a location. Resulting format looks like: 9.56445789337158. Measurement is in meters.


Distance between repeatable points

Since Fulcrum calculations are written in JavaScript, we can incorporate 3rd party scripts to provide helper functions. One excellent open source library for working with basic geospatial operations in JavaScript is Geolib. The example below demonstrates how to incorporate the Geolib library into a calculation field and use the getPathLength function to calculate the total distance between repeatable points.

/*! geolib 2.0.21 by Manuel Bieh
* Library to provide geo functions like distance calculation,
* conversion of decimal coordinates to sexagesimal and vice versa, etc.
* WGS 84 (World Geodetic System 1984)
* @author Manuel Bieh
* @url
* @version 2.0.21
* @license MIT
!function(t){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=t();else if("function"==typeof define&&define.amd)define([],t);else{var e;e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,e.geolib=t()}}(function(){var t;return function t(e,i,a){function n(s,r){if(!i[s]){if(!e[s]){var u="function"==typeof require&&require;if(!r&&u)return u(s,!0);if(o)return o(s,!0);var h=new Error("Cannot find module '"+s+"'");throw h.code="MODULE_NOT_FOUND",h}var l=i[s]={exports:{}};e[s][0].call(l.exports,function(t){var i=e[s][1][t];return n(i?i:t)},l,l.exports,t,e,i,a)}return i[s].exports}for(var o="function"==typeof require&&require,s=0;s<a.length;s++)n(a[s]);return n}({geolib:[function(e,i,a){!function(e,a){"use strict";function n(){}n.TO_RAD=Math.PI/180,n.TO_DEG=180/Math.PI,n.PI_X2=2*Math.PI,n.PI_DIV4=Math.PI/4;var o=Object.create(n.prototype,{version:{value:"2.0.21"},radius:{value:6378137},minLat:{value:-90},maxLat:{value:90},minLon:{value:-180},maxLon:{value:180},sexagesimalPattern:{value:/^([0-9]{1,3})°\s*([0-9]{1,3}(?:\.(?:[0-9]{1,2}))?)'\s*(([0-9]{1,3}(\.([0-9]{1,4}))?)"\s*)?([NEOSW]?)$/},measures:{value:Object.create(Object.prototype,{m:{value:1},km:{value:.001},cm:{value:100},mm:{value:1e3},mi:{value:1/1609.344},sm:{value:1/1852.216},ft:{value:100/30.48},in:{value:100/2.54},yd:{value:1/.9144}})},prototype:{value:n.prototype},extend:{value:function(t,e){for(var i in t)"undefined"!=typeof o.prototype[i]&&e!==!0||("function"==typeof t[i]&&"function"==typeof t[i].bind?o.prototype[i]=t[i].bind(o):o.prototype[i]=t[i])}}});"undefined"==typeof Number.prototype.toRad&&(Number.prototype.toRad=function(){return this*n.TO_RAD}),"undefined"==typeof Number.prototype.toDeg&&(Number.prototype.toDeg=function(){return this*n.TO_DEG}),o.extend({decimal:{},sexagesimal:{},distance:null,getKeys:function(t){if("[object Array]"{longitude:t.length>=1?0:a,latitude:t.length>=2?1:a,elevation:t.length>=3?2:a};var e=function(e){var i;return e.every(function(e){return"object"!=typeof t||(!t.hasOwnProperty(e)||function(){return i=e,!1}())}),i},i=e(["lng","lon","longitude"]),n=e(["lat","latitude"]),o=e(["alt","altitude","elevation","elev"]);return"undefined"==typeof n&&"undefined"==typeof i&&"undefined"==typeof o?a:{latitude:n,longitude:i,elevation:o}},getLat:function(t,e){return e===!0?t[this.getKeys(t).latitude]:this.useDecimal(t[this.getKeys(t).latitude])},latitude:function(t){return,t)},getLon:function(t,e){return e===!0?t[this.getKeys(t).longitude]:this.useDecimal(t[this.getKeys(t).longitude])},longitude:function(t){return,t)},getElev:function(t){return t[this.getKeys(t).elevation]},elevation:function(t){return,t)},coords:function(t,e){var i={latitude:e===!0?t[this.getKeys(t).latitude]:this.useDecimal(t[this.getKeys(t).latitude]),longitude:e===!0?t[this.getKeys(t).longitude]:this.useDecimal(t[this.getKeys(t).longitude])},a=t[this.getKeys(t).elevation];return"undefined"!=typeof a&&(i.elevation=a),i},ll:function(t,e){return,t,e)},validate:function(t){var e=this.getKeys(t);if("undefined"==typeof e||"undefined"==typeof e.latitude||"undefined"===e.longitude)return!1;var i=t[e.latitude],a=t[e.longitude];return!("undefined"==typeof i||!this.isDecimal(i)&&!this.isSexagesimal(i))&&(!("undefined"==typeof a||!this.isDecimal(a)&&!this.isSexagesimal(a))&&(i=this.useDecimal(i),a=this.useDecimal(a),!(i<this.minLat||i>this.maxLat||a<this.minLon||a>this.maxLon)))},getDistance:function(t,e,i,a){i=Math.floor(i)||1,a=Math.floor(a)||0;var n,s,r,u,h,l,d,c=this.coords(t),g=this.coords(e),f=6378137,m=6356752.314245,p=1/298.257223563,v=(g.longitude-c.longitude).toRad(),M=Math.atan((1-p)*Math.tan(parseFloat(c.latitude).toRad())),y=Math.atan((1-p)*Math.tan(parseFloat(g.latitude).toRad())),x=Math.sin(M),D=Math.cos(M),b=Math.sin(y),L=Math.cos(y),R=v,P=100;do{var N=Math.sin(R),E=Math.cos(R);if(l=Math.sqrt(L*N*(L*N)+(D*b-x*L*E)*(D*b-x*L*E)),0===l)return o.distance=0;n=x*b+D*L*E,s=Math.atan2(l,n),r=D*L*N/l,u=1-r*r,h=n-2*x*b/u,isNaN(h)&&(h=0);var I=p/16*u*(4+p*(4-3*u));d=R,R=v+(1-I)*p*r*(s+I*l*(h+I*n*(-1+2*h*h)))}while(Math.abs(R-d)>1e-12&&--P>0);if(0===P)return NaN;var S=u*(f*f-m*m)/(m*m),F=1+S/16384*(4096+S*(-768+S*(320-175*S))),w=S/1024*(256+S*(-128+S*(74-47*S))),k=w*l*(h+w/4*(n*(-1+2*h*h)-w/6*h*(-3+4*l*l)*(-3+4*h*h))),O=m*F*(s-k);if(O=O.toFixed(a),"undefined"!=typeof this.elevation(t)&&"undefined"!=typeof this.elevation(e)){var W=Math.abs(this.elevation(t)-this.elevation(e));O=Math.sqrt(O*O+W*W)}return this.distance=Math.round(O*Math.pow(10,a)/i)*i/Math.pow(10,a)},getDistanceSimple:function(t,e,i){i=Math.floor(i)||1;var a=Math.round(Math.acos(Math.sin(this.latitude(e).toRad())*Math.sin(this.latitude(t).toRad())+Math.cos(this.latitude(e).toRad())*Math.cos(this.latitude(t).toRad())*Math.cos(this.longitude(t).toRad()-this.longitude(e).toRad()))*this.radius);return o.distance=Math.floor(Math.round(a/i)*i)},getCenter:function(t){var e=t;if("object"==typeof t&&!(t instanceof Array)){e=[];for(var i in t)e.push(this.coords(t[i]))}if(!e.length)return!1;var a,o,s,r=0,u=0,h=0;e.forEach(function(t){a=this.latitude(t).toRad(),o=this.longitude(t).toRad(),r+=Math.cos(a)*Math.cos(o),u+=Math.cos(a)*Math.sin(o),h+=Math.sin(a)},this);var l=e.length;return r/=l,u/=l,h/=l,o=Math.atan2(u,r),s=Math.sqrt(r*r+u*u),a=Math.atan2(h,s),{latitude:(a*n.TO_DEG).toFixed(6),longitude:(o*n.TO_DEG).toFixed(6)}},getBounds:function(t){if(!t.length)return!1;var e=this.elevation(t[0]),i={maxLat:-(1/0),minLat:1/0,maxLng:-(1/0),minLng:1/0};"undefined"!=typeof e&&(i.maxElev=0,i.minElev=1/0);for(var a=0,n=t.length;a<n;++a)i.maxLat=Math.max(this.latitude(t[a]),i.maxLat),i.minLat=Math.min(this.latitude(t[a]),i.minLat),i.maxLng=Math.max(this.longitude(t[a]),i.maxLng),i.minLng=Math.min(this.longitude(t[a]),i.minLng),e&&(i.maxElev=Math.max(this.elevation(t[a]),i.maxElev),i.minElev=Math.min(this.elevation(t[a]),i.minElev));return i},getCenterOfBounds:function(t){var e=this.getBounds(t),i=e.minLat+(e.maxLat-e.minLat)/2,a=e.minLng+(e.maxLng-e.minLng)/2;return{latitude:parseFloat(i.toFixed(6)),longitude:parseFloat(a.toFixed(6))}},getBoundsOfDistance:function(t,e){var i,a,o=this.latitude(t),s=this.longitude(t),r=o.toRad(),u=s.toRad(),h=e/this.radius,l=r-h,d=r+h,c=this.maxLat.toRad(),g=this.minLat.toRad(),f=this.maxLon.toRad(),m=this.minLon.toRad();if(l>g&&d<c){var p=Math.asin(Math.sin(h)/Math.cos(r));i=u-p,i<m&&(i+=n.PI_X2),a=u+p,a>f&&(a-=n.PI_X2)}else l=Math.max(l,g),d=Math.min(d,c),i=m,a=f;return[{latitude:l.toDeg(),longitude:i.toDeg()},{latitude:d.toDeg(),longitude:a.toDeg()}]},isPointInside:function(t,e){for(var i=!1,a=-1,n=e.length,o=n-1;++a<n;o=a)(this.longitude(e[a])<=this.longitude(t)&&this.longitude(t)<this.longitude(e[o])||this.longitude(e[o])<=this.longitude(t)&&this.longitude(t)<this.longitude(e[a]))&&this.latitude(t)<(this.latitude(e[o])-this.latitude(e[a]))*(this.longitude(t)-this.longitude(e[a]))/(this.longitude(e[o])-this.longitude(e[a]))+this.latitude(e[a])&&(i=!i);return i},preparePolygonForIsPointInsideOptimized:function(t){for(var e=0,i=t.length-1;e<t.length;e++)this.longitude(t[i])===this.longitude(t[e])?(t[e].constant=this.latitude(t[e]),t[e].multiple=0):(t[e].constant=this.latitude(t[e])-this.longitude(t[e])*this.latitude(t[i])/(this.longitude(t[i])-this.longitude(t[e]))+this.longitude(t[e])*this.latitude(t[e])/(this.longitude(t[i])-this.longitude(t[e])),t[e].multiple=(this.latitude(t[i])-this.latitude(t[e]))/(this.longitude(t[i])-this.longitude(t[e]))),i=e},isPointInsideWithPreparedPolygon:function(t,e){for(var i=!1,a=this.longitude(t),n=this.latitude(t),o=0,s=e.length-1;o<e.length;o++)(this.longitude(e[o])<a&&this.longitude(e[s])>=a||this.longitude(e[s])<a&&this.longitude(e[o])>=a)&&(i^=a*e[o].multiple+e[o].constant<n),s=o;return i},isInside:function(){return this.isPointInside.apply(this,arguments)},isPointInCircle:function(t,e,i){return this.getDistance(t,e)<i},withinRadius:function(){return this.isPointInCircle.apply(this,arguments)},getRhumbLineBearing:function(t,e){var i=this.longitude(e).toRad()-this.longitude(t).toRad(),a=Math.log(Math.tan(this.latitude(e).toRad()/2+n.PI_DIV4)/Math.tan(this.latitude(t).toRad()/2+n.PI_DIV4));return Math.abs(i)>Math.PI&&(i=i>0?(n.PI_X2-i)*-1:n.PI_X2+i),(Math.atan2(i,a).toDeg()+360)%360},getBearing:function(t,e){e.latitude=this.latitude(e),e.longitude=this.longitude(e),t.latitude=this.latitude(t),t.longitude=this.longitude(t);var i=(Math.atan2(Math.sin(e.longitude.toRad()-t.longitude.toRad())*Math.cos(e.latitude.toRad()),Math.cos(t.latitude.toRad())*Math.sin(e.latitude.toRad())-Math.sin(t.latitude.toRad())*Math.cos(e.latitude.toRad())*Math.cos(e.longitude.toRad()-t.longitude.toRad())).toDeg()+360)%360;return i},getCompassDirection:function(t,e,i){var a,n;switch(n="circle"==i?this.getBearing(t,e):this.getRhumbLineBearing(t,e),Math.round(n/22.5)){case 1:a={exact:"NNE",rough:"N"};break;case 2:a={exact:"NE",rough:"N"};break;case 3:a={exact:"ENE",rough:"E"};break;case 4:a={exact:"E",rough:"E"};break;case 5:a={exact:"ESE",rough:"E"};break;case 6:a={exact:"SE",rough:"E"};break;case 7:a={exact:"SSE",rough:"S"};break;case 8:a={exact:"S",rough:"S"};break;case 9:a={exact:"SSW",rough:"S"};break;case 10:a={exact:"SW",rough:"S"};break;case 11:a={exact:"WSW",rough:"W"};break;case 12:a={exact:"W",rough:"W"};break;case 13:a={exact:"WNW",rough:"W"};break;case 14:a={exact:"NW",rough:"W"};break;case 15:a={exact:"NNW",rough:"N"};break;default:a={exact:"N",rough:"N"}}return a.bearing=n,a},getDirection:function(t,e,i){return this.getCompassDirection.apply(this,arguments)},orderByDistance:function(t,e){var i=[];for(var a in e){var n=this.getDistance(t,e[a]),o=Object.create(e[a]);o.distance=n,o.key=a,i.push(o)}return i.sort(function(t,e){return t.distance-e.distance})},isPointInLine:function(t,e,i){return(this.getDistance(e,t,1,3)+this.getDistance(t,i,1,3)).toFixed(3)==this.getDistance(e,i,1,3)},isPointNearLine:function(t,e,i,a){return this.getDistanceFromLine(t,e,i)<a},getDistanceFromLine:function(t,e,i){var a=this.getDistance(e,t,1,3),n=this.getDistance(t,i,1,3),o=this.getDistance(e,i,1,3),s=0,r=Math.acos((a*a+o*o-n*n)/(2*a*o)),u=Math.acos((n*n+o*o-a*a)/(2*n*o));return s=r>Math.PI/2?a:u>Math.PI/2?n:Math.sin(r)*a},findNearest:function(t,e,i,a){i=i||0,a=a||1;var n=this.orderByDistance(t,e);return 1===a?n[i]:n.splice(i,a)},getPathLength:function(t){for(var e,i=0,a=0,n=t.length;a<n;++a)e&&(i+=this.getDistance(this.coords(t[a]),e)),e=this.coords(t[a]);return i},getSpeed:function(t,e,i){var a=i&&i.unit||"km";"mph"==a?a="mi":"kmh"==a&&(a="km");var n=o.getDistance(t,e),s=1*e.time/1e3-1*t.time/1e3,r=n/s*3600,u=Math.round(r*this.measures[a]*1e4)/1e4;return u},computeDestinationPoint:function(t,e,i,a){var n=this.latitude(t),o=this.longitude(t);a="undefined"==typeof a?this.radius:Number(a);var s=Number(e)/a,r=Number(i).toRad(),u=Number(n).toRad(),h=Number(o).toRad(),l=Math.asin(Math.sin(u)*Math.cos(s)+Math.cos(u)*Math.sin(s)*Math.cos(r)),d=h+Math.atan2(Math.sin(r)*Math.sin(s)*Math.cos(u),Math.cos(s)-Math.sin(u)*Math.sin(l));return d=(d+3*Math.PI)%(2*Math.PI)-Math.PI,{latitude:l.toDeg(),longitude:d.toDeg()}},convertUnit:function(t,e,i){if(0===e)return 0;if("undefined"==typeof e){if(null===this.distance)throw new Error("No distance was given");if(0===this.distance)return 0;e=this.distance}if(t=t||"m",i=null==i?4:i,"undefined"!=typeof this.measures[t])return this.round(e*this.measures[t],i);throw new Error("Unknown unit for conversion.")},useDecimal:function(t){if("[object Array]"{var e=this;return{if(e.isDecimal(t))return e.useDecimal(t);if("object"==typeof t){if(e.validate(t))return e.coords(t);for(var i in t)t[i]=e.useDecimal(t[i]);return t}return e.isSexagesimal(t)?e.sexagesimal2decimal(t):t})}if("object"==typeof t&&this.validate(t))return this.coords(t);if("object"==typeof t){for(var i in t)t[i]=this.useDecimal(t[i]);return t}if(this.isDecimal(t))return parseFloat(t);if(this.isSexagesimal(t)===!0)return parseFloat(this.sexagesimal2decimal(t));throw new Error("Unknown format.")},decimal2sexagesimal:function(t){if(t in this.sexagesimal)return this.sexagesimal[t];var e=t.toString().split("."),i=Math.abs(e[0]),a=60*("0."+(e[1]||0)),n=a.toString().split(".");return a=Math.floor(a),n=(60*("0."+(n[1]||0))).toFixed(2),this.sexagesimal[t]=i+"° "+a+"' "+n+'"',this.sexagesimal[t]},sexagesimal2decimal:function(t){if(t in this.decimal)return this.decimal[t];var e=new RegExp(this.sexagesimalPattern),i=e.exec(t),a=0,n=0;i&&(a=parseFloat(i[2]/60),n=parseFloat(i[4]/3600)||0);var o=(parseFloat(i[1])+a+n).toFixed(8);return o="S"==i[7]||"W"==i[7]?parseFloat(-o):parseFloat(o),this.decimal[t]=o,o},isDecimal:function(t){return t=t.toString().replace(/\s*/,""),!isNaN(parseFloat(t))&&parseFloat(t)==t},isSexagesimal:function(t){return t=t.toString().replace(/\s*/,""),this.sexagesimalPattern.test(t)},round:function(t,e){var i=Math.pow(10,e);return Math.round(t*i)/i}}),"undefined"!=typeof i&&"undefined"!=typeof i.exports?(i.exports=o,"object"==typeof e&&(e.geolib=o)):"function"==typeof t&&t.amd?t("geolib",[],function(){return o}):e.geolib=o}(this)},{}]},{},[])("geolib")});
// end of Geolib code

// set geolib library as a variable
var geolib = module.exports;

// empty array to hold coordinate objects
var pathCoordinates = [];

// if there is more than 1 item in the repeatable field named "sites", loop through the repeatable objects and push the coordinates into the pathCoordinates array
if ($sites && $sites.length > 1) {
  for (var i = 0; i < $sites.length; ++i) {
      latitude: $sites[i].geometry.coordinates[1],
      longitude: $sites[i].geometry.coordinates[0]

  // get the distance between points in meters
  var distance = geolib.getPathLength(pathCoordinates);

  // set the result of the calc field with a label
  SETRESULT(distance + " meters");
} else {

Copy and paste the entire code block above into the expression section of your calculation field, making sure to replace $sites with the data name of your repeatable field.

Convert decimal feet to x' y"

This expression will convert the feet measurement in decimal to feet' inches" format. In order to achieve this, you will need a numeric field feet in decimal and a calculation field for feet' inches". The following code can be copied and pasted into the calculation field's expression builder.

let feet = Math.floor($feet_in_decimal);
let inches = Math.round(($feet_in_decimal-feet)*12);

if($feet_in_decimal) {

Calculate Standard Deviation

This code will calculate the standard deviation from values entered into a numeric field in a repeatable section. Much of this code was taken from and adapted for use in Fulcrum's calculation fields.

var values = REPEATABLEVALUES($repeatable_section, 'repeatable_field');

function standardDeviation(values){
  var avg = average(values);

  var squareDiffs ={
    var diff = value - avg;
    var sqrDiff = diff * diff;
    return sqrDiff;

  var avgSquareDiff = average(squareDiffs);

  var stdDev = Math.sqrt(avgSquareDiff);
  return stdDev;

function average(data){
  var sum = data.reduce(function(sum, value){
    return sum + value;
  }, 0);

  var avg = sum / data.length;
  return avg;
if($repeatable_section) {

Copy and paste the entire code block above into the expression section of your calculation field, making sure to replace $repeatable_section with the data name of your repeatable section and replace repeatable_field with the data name of your repeatable field.

Sum of numbers in a repeatable

This example grabs numeric values from a choicefield (with options N/A, 0, 1, 2, 3) located in a repeatable. It converts the string values to numbers and then calculates the sum.

var array = REPEATABLEVALUES($name_of_repeatable, 'data_name_of_choicefield_score');

var totalScore = 0;

for (var i = 0; i < array.length; i++) {
  var value = CHOICEVALUE(array[i]);

  var score = 0;

  if (value === 'N/A'){
    score = 0;
  } else {
    score = Number(value);

  totalScore += score;


Scoring System using a Choice List

Suppose you have a choice list with numeric values as shown below:


You can use a set of choice fields using this pre-defined choice list and a calculation field to calculate the scores. The example below is adding up 5 choice field values and assumes that they are all required fields.

var scores = [];
scores.push(CHOICEVALUE($choice_field_1), CHOICEVALUE($choice_field_2), CHOICEVALUE($choice_field_3), CHOICEVALUE($choice_field_4), CHOICEVALUE($choice_field_5));

function getSum(a, b) {
  return parseInt(a) + parseInt(b);

var total = scores.reduce(getSum, 0);


Pull Selected Choice Option Label

This example can be used to pull in a selected choice option's label. This will only work on choice fields where the choice options are defined in the choice field. This will not work for predefined choice lists.

function findChoiceLabel(dataName) {
  var selectedChoiceValue = CHOICEVALUE(VALUE(dataName));

  var choice = FIELD(dataName).choices.find(function(choice) {
    return choice.value === selectedChoiceValue;

  return choice ? choice.label : null;


All you need to do is replace the 'choicefield' to the data name of the choice field in your app.

This next example demonstrates how to do the same as above, but for a Multiple Choice field.

function findChoiceLabels(dataName) {
  var selectedChoiceValues = CHOICEVALUES(VALUE(dataName));
  var selectedChoiceLabels = [];

  if ($choicefield) {
    for (let i = 0; i < selectedChoiceValues.length; i++) {
      var choice = FIELD(dataName).choices.find(function (choice) {
        return choice.value === selectedChoiceValues[i];

      selectedChoiceLabels.push(choice ? choice.label : null);

    return selectedChoiceLabels;


All you need to do is replace the 'choicefield'/$choicefield to the data name of the choice field in your app.

Combine Arrays Together

What this expression does is combine two arrays together into a single string that is displayed in the calculation field.

Field types like choice fields and classification sets create arrays from the values selected. In situations where you wish to combine two arrays together use the expression below.

var combined = ARRAY(CHOICEVALUES($first_choice_field),

SETRESULT(combined.join(', '));

Combine Field Values

Most fields in Fulcrum will create a string from the data entered into the field. In those cases the CONCATENATE() or the CONCAT() expression can be used to combine field values.

This can be useful when you want to customize or use more than five fields for the title.

var concatenated = CONCAT($date_field, ", ", $text_field, "-", $numeric_field, ": ", 
                          "Version ", CHOICEVALUE($choice_field), "check");


// Example Outcome: 2021/01/01 Damage-1: Version I-495 check

Days/Years Between Two Date Fields

This will return the difference between two date fields in either years or days depending on which SETRESULT() function is used.

Resulting format looks like: 4.

var date1 = new Date($date_field1);
var date2 = new Date($date_field2);
var timeDiff = Math.abs(date2.getTime() - date1.getTime());
var diffDays = Math.ceil(timeDiff / (1000 * 3600 * 24));

SETRESULT(diffDays / 365); //for years
// SETRESULT(diffDays); //for days

Years & Months Between Two Date Fields

This will return the difference between two date fields in years and months.

function monthDiff(d1, d2) {
  var months;
  months = (d2.getFullYear() - d1.getFullYear()) * 12;
  months -= d1.getMonth();
  months += d2.getMonth();

  return months <= 0 ? 0 : months;

var months = monthDiff($date_1, $date_2) // Change $date_1 and $date_2 to your date fields
var years = months/12

function yearsToYearsMonthsDays(value) {
  var totalDays = value * 365;
  var years = Math.floor(totalDays/365);
  var months = Math.floor((totalDays-(years *365))/30);
  var days = Math.floor(totalDays - (years*365) - (months * 30));
  var result = years + " years, " + months + " months, "


Hours Between Two Time Fields

This will return the difference between two time fields in hours. In this example, $time_1 field is the starting time and $time_2 field is the ending time.

var today = new Date();
var year = today.getFullYear();
var month = today.getMonth();
var date = today.getDate();
var hour1 = parseInt($time_1.slice(0, 2));
var min1 = parseInt($time_1.slice(3));
var hour2 = parseInt($time_2.slice(0, 2));
var min2 = parseInt($time_2.slice(3));

var date1 = new Date(year, month, date, hour1, min1);
var date2 = new Date(year, month, date, hour2, min2);

var timeDiff = Math.abs(date2.getTime() - date1.getTime()) / (1000*3600);

Compare Values

Use this in a calculation field to compare multiple values and return a specific value based on the values entered.

// Define the variables that you wish to use
var firstField = $field_one;
var secondField = $field_two;

// Create a function that looks at two or more variables and compares their results.
// If choice 1 is equal to 'this value' and choice 2 is equal to 'this value' then return 'this value'
function compare (choice1, choice2) {
  if (choice1 === 'A value' && choice2 === 'A value') {
    return 'A value';
  } else if (choice1 === 'A value' && choice2 === 'B value') {
    return 'B value';
  } else if (choice1 === 'B value' && choice2 === 'B value') {
    return 'C value';
  } else if (choice1 === 'B value' && choice2 === 'C value') {
    return 'D value';

//Runs the compare function using the variables defined above and creates a 'result' variable with the value returned from the compare function.
var result = compare(firstField, secondField);


Count Blank Fields in a Section

This example sums up the number of unanswered (blank) fields in a defined section.

var elements = FIELD('section_one').elements;

var blankCount = 0;

for (var i = 0; i < elements.length; i++) {
  if (ISBLANK(VALUE(elements[i].data_name))) {
    blankCount += 1;


Count Number of Child Records

// Repeatables are represented as Array variables. So returning the number of repeatable
// child records is the same as the length of the array.


Count Multiple Choice Field Selections

The following expression returns the number of selections in a multiple choice field.

// the `|| []` at the end accounts for CHOICEVALUES() returning null when the field is blank
var selections = CHOICEVALUES($multiple_choice_field) || [];

// selections is an Array of the values
// selections.length is the number of selections

if (selections.length >= 3) {
  SETRESULT('There are at least 3 selections');
} else {
  SETRESULT('There are less than 3 selections');

Get Day of the Week From a Date

// define an array of the days of the week to use as a lookup structure
var daysOfWeek = [

// This converts a date field in the record to a JavaScript date object
var d = DATEVALUE($the_date_field);

// If you wanted to use today's date or any other specific date ...
// var d = new Date();
// var d = new Date('4/15/1984');


Degrees Decimal Minutes

Use these in calculation fields to return latitude/longitude in degrees decimal minutes format.

Return degrees decimal minutes from LATITUDE() & LONGITUDE().
Please consider FLOOR versus CEILING for your respective hemisphere/side-of-dateline.
Below are 2 expression blocks, intended for 2 separate Calculation fields (Display Format: Text) within a Fulcrum app.

if (LATITUDE()) {
  var lat = FLOOR(LATITUDE(), 1);
  var latdm = ROUND(ABS((LATITUDE() - lat) * 60), 6);
  SETRESULT(lat + " " + latdm);
} else {

if (LONGITUDE()) {
  var lon = CEILING(LONGITUDE(), 1);
  var londm = ROUND(ABS((LONGITUDE() - lon) * 60), 6);
  SETRESULT(lon + " " + londm);
} else {

Hours Between Pair of Date and Time Fields

This code will calculate the hours between a pair a start date and start time field and a end date and end time field.

Copy and paste the entire code block above into the expression section of your calculation field, making sure to replace $start_date_field, $start_time_field, $end_date_field, and $end_time_field with the data names of your date and time fields.

function diff_hours(dt2, dt1) {
  var diff =(dt2.getTime() - dt1.getTime()) / 1000;
  diff /= (60 * 60);
  return Math.abs(Math.round(diff));

var startDate = $start_date_field;
var startTime = $start_time_field;
var endDate = $end_date_field;
var endTime = $end_time_field;

var startTimeStamp = new Date(startDate.getFullYear() + '-' + startDate.getMonth() + '-' + startDate.getDate() + ' ' + startTime + ':00');
var endTimeStamp = new Date(endDate.getFullYear() + '-' + endDate.getMonth() + '-' + endDate.getDate() + ' ' + endTime + ':00');

SETRESULT(diff_hours(endTimeStamp, startTimeStamp));

This is a simpler code example that leverages DATEVALUE() to produce the full timestamp from the date and time fields.

var startDate = $start_date_field;
var startTime = $start_time_field;
var endDate = $end_date_field;
var endTime = $end_time_field;

var current = DATEVALUE(endDate, endTime);
var previous = DATEVALUE(startDate, startTime);
var duration = ((((current - previous)/1000)/60)/60);
//for minutes use:
//var duration = (((current - previous)/1000)/60);

Get Address from Address Field (in Australia)

Addresses in Australia, and other parts of the world, match up differently than in the USA. Here is an example of how to pull out city and state in Australia.

Grabbing the city:

var city = '';
if ($address.locality && $address.locality !== '') {
  city += $address.locality;

Grabbing the state:

var state;
if ($address.admin_area && $address.admin_area !== '') {
  state = $address.admin_area;

Grab Photo ID

Assuming the max number of photos is 1, this expression grabs the one photo id in the form and adds it to the feature id field. $feature_id and $photos are both strings, so you can add them together with a '+'.

Note: Photo elements have two properties. They look like this: json {"photo_id: "", "caption": "Test caption"}.

var photo;

if ($photos.length > 0) {
  photo = $photos[0].photo_id;
  SETRESULT($feature_id + photo);
} else {

Get Last Item in a Classification Set

Use this in a calculation field to pull the last item capture in a classification set field.


Return the Repeatable Latest Date

This will return the latest date captured in a date field that is in a repeatable section.

var dates = REPEATABLEVALUES($inspections, 'rep_date') || [];


Or if you need to customize the logic for how it gets calculated, you can use this equivalent example:

var dates = REPEATABLEVALUES($repeatable_section, 'date_field');
var maxDate;

if (dates && dates.length > 0) {
  maxDate = dates[0];

  dates.forEach(function(currentDate) {
    if (currentDate > maxDate) {
      maxDate = currentDate;


Obtain Values from Fields Within a Section

This example will return a string with all the values that are stored in fields that are within a section.

var dataNames = PLUCK(FIELD('section').elements, 'data_name');

var stringValues = [];

for (var i = 0; i < dataNames.length; ++i) {
  var type = FIELD(dataNames[i]).type;
  var value = VALUE(dataNames[i]);
  var stringValue = value;

  if (!ISBLANK(value)) {
    if (type === 'ChoiceField') {
      stringValue = CHOICEVALUES(value).join(', ');


SETRESULT(stringValues.join(', '));

Calculate Radius for Plot Sampling

One way to conduct unbiased systematic sampling is through fixed radius plots. A fixed radius plot is great for sampling trees that are less than the specified breakpoint diameter.



This calculation field reveals the radius that you should be working with. It pulls in the acre size you would like to survey and the slope value at the site to calculate the length of the radius.



You can read more about the methods on the US Forest Service site.

var threeHundredth = [
  {"slope": [0,17],"radius":6.8},
  {"slope": [18,30],"radius":6.9},
  {"slope": [31,39],"radius":7.0},
  {"slope": [40,47],"radius":7.1},
  {"slope": [48,53],"radius":7.2},
  {"slope": [54,59],"radius":7.3},
  {"slope": [60,67],"radius":7.4},
  {"slope": [68,72],"radius":7.5},
  {"slope": [73,77],"radius":7.6},
  {"slope": [78,82],"radius":7.7},
  {"slope": [83,88],"radius":7.8},
  {"slope": [89,92],"radius":7.9},
  {"slope": [93,98],"radius":8.0},
  {"slope": [99,102],"radius":8.1},
  {"slope": [103,108],"radius":8.2},
  {"slope": [109,112],"radius":8.3},
  {"slope": [113,117],"radius":8.4},
  {"slope": [118,122],"radius":8.5},
  {"slope": [123,125],"radius":8.6},
  {"slope": [130,130],"radius":8.7},
  {"slope": [135,135],"radius":8.8},
  {"slope": [140,140],"radius":8.9},
  {"slope": [145,145],"radius":9.0},
  {"slope": [150,150],"radius":9.1}
oneHundredth = [
  {"slope": [0,17],"radius": 11.8},
  {"slope": [18,22],"radius": 11.9},
  {"slope": [23,30],"radius": 12.0},
  {"slope": [31,36],"radius": 12.1},
  {"slope": [37,42],"radius": 12.2},
  {"slope": [43,47],"radius": 12.3},
  {"slope": [48,49],"radius": 12.4},
  {"slope": [50,53],"radius": 12.5},
  {"slope": [54,57],"radius": 12.6},
  {"slope": [58,61],"radius": 12.7},
  {"slope": [62,65],"radius": 12.8},
  {"slope": [66,67],"radius": 12.9},
  {"slope": [68,70],"radius": 13.0},
  {"slope": [71,74],"radius": 13.1},
  {"slope": [75,77],"radius": 13.2},
  {"slope": [78,80],"radius": 13.3},
  {"slope": [81,83],"radius": 13.4},
  {"slope": [84,86],"radius": 13.5},
  {"slope": [87,89],"radius": 13.6},
  {"slope": [90,92],"radius": 13.7},
  {"slope": [93,95],"radius": 13.8},
  {"slope": [96,98],"radius": 13.9},
  {"slope": [99,101],"radius": 14.0},
  {"slope": [102,104],"radius": 14.1},
  {"slope": [105,107],"radius": 14.2},
  {"slope": [108,109],"radius": 14.3},
  {"slope": [110,112],"radius": 14.4},
  {"slope": [113,115],"radius": 14.5},
  {"slope": [116,117],"radius": 14.6},
  {"slope": [118,120],"radius": 14.7},
  {"slope": [121,124],"radius": 14.8},
  {"slope": [125,125],"radius": 14.9},
  {"slope": [130,130],"radius": 15.1},
  {"slope": [135,135],"radius": 15.3},
  {"slope": [140,140],"radius": 15.4},
  {"slope": [145,145],"radius": 15.6},
  {"slope": [150,150],"radius": 15.8}

if (CHOICEVALUE($acre_size) == '1/300'){
  for (var i = 0; i < threeHundredth.length; i++) {
    if ($slope >= threeHundredth[i].slope[0] && $slope <= threeHundredth[i].slope[1]){
} else if (CHOICEVALUE($acre_size) == '1/100') {
  for (var i = 0; i < 37; i++) {
    if ($slope >= oneHundredth[i].slope[0] && $slope <= oneHundredth[i].slope[1]){

Vector Geometries from Repeatables

Assume you have a repeatable field called vector_vertices with location enabled, location required, and a minimum count set to 2. Every repeatable record will act as a vector node or vertex and we can use calculation fields to combine all of these vertices into a well-known text (WKT) geometry object field at the parent record level.

Line Geometry:

var coords = [];
if ($vector_vertices) {
  for (var i = 0; i < $vector_vertices.length; ++i) {
    coords.push($vector_vertices[i].geometry.coordinates[0] + ' ' + $vector_vertices[i].geometry.coordinates[1]);
  SETRESULT('LINESTRING (' + coords + ')');
} else {

Polygon Geometry:

var coords = [];
if ($vector_vertices && $vector_vertices.length > 2) {
  for (var i = 0; i < $vector_vertices.length; ++i) {
    coords.push($vector_vertices[i].geometry.coordinates[0] + ' ' + $vector_vertices[i].geometry.coordinates[1]);
  coords.push($vector_vertices[0].geometry.coordinates[0] + ' ' + $vector_vertices[0].geometry.coordinates[1]);
  SETRESULT('POLYGON ((' + coords + '))');
} else {

To view the polygon vector geometries in CARTO, you could use the following SQL query, where polygon_wkt is the name of the calculation field and repeatable_vector_geometries is the name of the table:

SELECT cartodb_id, ST_Transform (ST_GeomFromText(polygon_wkt, 4326), 3857) AS the_geom_webmercator FROM repeatable_vector_geometries

Return Sum of Specific Type

Designed to be used in conjunction with a repeatable section. This example can be used to return the sum of values for a specific variable that is captured in a choice field inside a repeatable section.

In the example code below the cost_field captures the cost while the item_type_field is used to capture the type of item that the cost is associated with in a child record. The code then loops through comparing the two arrays and returns a sum of all values in the cost_field where the item_type_field is equal to Item A.

  var cost = REPEATABLEVALUES($repeatable_section, 'cost_field');
  var items = REPEATABLEVALUES($repeatable_section, 'item_type_field').map(CHOICEVALUE);

  var total = 0;

  for (var i = 0; i < cost.length; ++i) {
    if (items[i] === 'Item A') {
      total += NUM(cost[i]);


Return the Most Frequent Repeatable Value

This example uses the REPEATABLEVALUES() expression to return the most frequent value captured in a field in a repeatable section.

function mostFrequentValue(array) {
  if (array === null) {
    return null;
  var frequencies = {};
  var mostFrequentCount = 0;
  var mostFrequentValue = null;
  array.forEach(function(value) {
    if (frequencies[value] === null) {
      frequencies[value] = 1;
    } else {
      frequencies[value] += 1;
    if (frequencies[value] > mostFrequentCount) {
      mostFrequentCount = frequencies[value];
      mostFrequentValue = value;
  return mostFrequentValue;
var values = REPEATABLEVALUES($repeatable_section, 'field_in_repeatable');
var value = mostFrequentValue(values);

Split Classifications into Separate Fields

Assume you have a classification field called site_location with the following hierarchy:

Province > District > Commune

If you wanted to split the classifications into separate fields, you could use the following calculations:


if ($site_location && $site_location.choice_values[0]) {


if ($site_location && $site_location.choice_values[1]) {


if ($site_location && $site_location.choice_values[2]) {

Split Value Captured in a Field

This can be used to split a value captured in a field and pull out only the part of the value that you wish to use or display. The different components of the values need to be separated by a common character. In the example below the common character is a dot ..

The most common use case is to split a choice value captured in a choice field that includes data that you want to use or display somewhere else the record. For example, if you have the cost associated with the choice and wish to use this value in a mathematical expression. So if you had a value of wrench.15, the expression below would return 15.

if (CHOICEVALUE($choice_field)!= null) {
  var selection = CHOICEVALUE($choice_field)
  // split the selection string into an array around the '.'
  var splitSelection = selection.split(".");

  //set the result to the second value in the splitSelection array, use [0] for the first value

Star Ratings from a Numeric Range

Assuming $rating is a numeric field with a minimum of 1 and a maximum of 5, generate a star rating out of maximum of 5 stars.

This will take a numeric field value in $rating and convert it into actual star characters.

SETRESULT(Array(FLOOR($rating + 1)).join('★') + Array(FLOOR(6 - $rating)).join('☆'));

Entering "3" gives the output: "★★★☆☆".

Sum Nullable Fields

Sometimes you need to sum up fields that don't always have values. If you want the blank value to be treated as zero, this can be done using the following pattern:

SETRESULT(($number_1 || 0) + ($number_2 || 0) + ($number_3 || 0));

// OR
// SETRESULT(SUM($number_1 || 0, $number_2 || 0, $number_3 || 0));

Generate a Full Timestamp

Resulting format looks like: 2015-11-23 16:36:14.


Note that since calculation fields are constantly evaluated, this timestamp will always be overwritten with the current time. If you want to add the initial timestamp and prevent it from changing, you can wrap this in a ONCE function like so:


Unix Timestamp for Unique Record Value

While Fulcrum generates a record id for every record in Fulcrum, this id is quite long and sometimes too long to use. What you can do is use the Unix Timestamp to generate a unique numeric record value that can be more manageable to use.

The ONCE() expression is used to ensure that the expression will only be run one time.

The expression below will generate unique value with 13 characters.


The example below divides the unix timestamp by 1000 and drops the decimals places using the FLOOR() expression. This generates a unique value with 10 characters.


The example below reduces the unique value to 9 characters.

ONCE(FLOOR(( - 1451606400000) / 1000));

Reference Record Created At Time

The record's this object contains a handful of information including record created/updated time, location, and duration. The expression below will generate a time stamp of record creation date and time.

var unix = new Date(this.featureCreatedAt * 1000);
var year = unix.getFullYear();
var month = unix.getMonth()+1;
var date = unix.getDate();
var hour = unix.getHours();
var min = unix.getMinutes();
var sec = unix.getSeconds();
var created_time = year + '-' + month + '-' + date + ' ' + hour + ':' + min + ':' + sec;


Reference Record Updated At Time

The record's this object contains a handful of information including record created/updated time, location, and duration. The expression below will generate a time stamp of record update date and time.

var unix = new Date(this.featureUpdatedAt * 1000);
var year = unix.getFullYear();
var month = unix.getMonth()+1;
var date = unix.getDate();
var hour = unix.getHours();
var min = unix.getMinutes();
var sec = unix.getSeconds();
var updated_time = year + '-' + month + '-' + date + ' ' + hour + ':' + min + ':' + sec;


Track Who Has Updated a Record

Maintain a field for tracking record updates by field technicians only

if (ISMOBILE()) {

Values from Choice Field in Repeatable

Due to the nature of choice fields, the REPEATABLEVALUES() expression cannot be used on its own when pulling values from a choice field inside a repeatable section. Instead use the example below.

 if ($repeatable_section == null){
 } else {
   REPEATABLEVALUES($repeatable_section, 'choice_field_data_name').map(CHOICEVALUE);

Generate the Week Number

Use this in a calculation field to return the week number from the current date or from a date field.

function weekNum( d ) {
    // Create a copy of this date object
    d = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()));
    // ISO week date weeks start on monday
    // so correct the day number
    d.setUTCDate(d.getUTCDate() + 4 - (d.getUTCDay()||7));
    // Get first day of year
    var yearStart = new Date(Date.UTC(d.getUTCFullYear(),0,1));
    // Calculate full weeks to nearest Thursday
    var weekNo = Math.ceil(( ( (d - yearStart) / 86400000) + 1)/7);
    // Return week number
    return weekNo;

// Use to pull current date
// var now = new Date();
// weekNum(now);

var date = DATEVALUE($date));