Flang, The Frontline Language

FLang is a scripting language that can be used in various parts of the recipe builder to further customize the behavior of recipes. The syntax is based on a subset of the Groovy programming language.

FLang allows you to access data from your workspace, for example, contact and message data. FLang provides variables and methods that enable this kind of access.

How to use FLang

FLang can be used on input fields that have a special puzzle icon, as well as recipient selectors and custom field selectors.

FLang can only be referenced as follows

    ${trigger.text}

If the ${ ... } is not used then Frontline will interpret the contents as is and will not try to evaluate the contents.


Available variables:

trigger

A trigger is the event that caused the current recipe to run, and any associated data. It is either:

  • An inbound Missed Call
  • A received SMS
  • A time trigger, for scheduled and repeating recipes
  • A inbound API request
  • A manually triggered recipe, called fire triggered recipe in the application.

The type of the trigger can be read as follows:

trigger.type // returns a String that is one of 'TEXT_MESSAGE', 'MISSED_CALL', 'TIME', 'FIRE'

If trigger is an SMS then we can access the following values

    trigger.text // contents of the SMS
    trigger.date // time that Frontline received the SMS
    trigger.sourceNumber // phone number that texted in

Additionally, SMS triggers have the following helper methods

    trigger.getWord(n) // returns the nth word of the message
    trigger.textWithoutFirstWord() // returns the text without the first word (convenient when the first word is a common keyword)

If trigger is a Missed Call, we can access these values

    trigger.date // time that FrontlineSMS received Missed call
    trigger.sourceNumber // phone number that issued this missed call

If trigger is simply Time, there are fewer values

    trigger.date // time that recipe was executed

Text Message and Missed Call triggers also have a convenience method that lets you access the contact who sent in the trigger

    trigger.getContact() // returns the Contact instance associated with trigger.sourceNumber
    trigger.contact.nameOrMobile() // returns the Contact who created the trigger's Mobile Number if present otherwise falls back to name
    trigger.contact.email() // returns the Contact who created the trigger's Email

Note that the Contact object will always be returned, whether or not the number has been saved in the Frontline contact database. You can confirm whether this is a known contact using the ‘isKnownContact’ property, described in the Contact section below.

If trigger is an inbound API Request, the following properties can be accessed

    trigger.apiKey // the API Key with which the API request was received on
    trigger.payload  // the JSON payload received bt Frontline
    trigger.sourceIp // the IP that issued the API request

When accessing data from the JSON payload like shown below it is mandatory to use bracket notation when accessing the keys from payload data. For example if you want to access message from the example below, you should access it in this way ${trigger.payload['message']} and not in this way ${trigger.payload.message} .This will ensure numerical keys and keys that start with number work as expected

 "apiKey": "your-API-Key",
"payload": {
"message":"Hi people!",
"recipients":[
{ "type":"smartgroupName", "value":"Kenyans" },
{ "type":"contactName", "value":"Sam Camper" },
{ "type":"contactName", "value":"Bobby" },
{ "type":"contactName", "value":"Darwin" },
{ "type":"groupName", "value":"Friends" },
{ "type":"mobile", "value":"+1234567890" },
{ "type":"mobile", "value":"+1234567891" }
]
}
}

Furthermore, API requests with payloads that conform to our Send SMS Request json format can use the following helper methods:

    trigger.sendSmsPayload.message // access the message content sent in the payload
    trigger.sendSmsPayload.allRecipients // extracts all the destination phone numbers from the recipients map in the payload. This can be used, for example, in the recipient selector of a SendMessageAction
    trigger.sendSmsPayload.groupNames // get the list of groups names from the payload. Cannot be used in RecipientSelector
    trigger.sendSmsPayload.contactNames // get the list of contact names from the payload. Cannot be used in RecipientSelector
    trigger.sendSmsPayload.groupNames // get a list of groups names from the payload. Cannot be used in RecipientSelector
    trigger.sendSmsPayload.mobiles // get the list of mobile numbers in the payload. Cannot be used in RecipientSelector
    trigger.sendSmsPayload.smartGroupNames // get a list of smartgroup names from the payload. Cannot be used in RecipientSelector
    trigger.sendSmsPayload.valid // checks whether there is a message and at least one recipient

Usage examples

  • To access the text of a trigger that could be an SMS: ${trigger.text}
  • To access the trigger’s date ${trigger.date}
  • To access a trigger’s source number ${trigger.sourceNumber}

customFields

The customFields property returns a list of all custom field definitions as groovy maps i.e.

[
    [name:'Some Name', type:'StringCustomFieldDefinition', defaultValue:'Some value'],
    [name:'Some Other Name', type:'CurrencyCustomFieldDefinition', defaultValue:400],
]

The data can be accessed using the Flang expression below

${customFields*.name}; Provides a groovy list of all custom fields names
${customFields*.type}; Provides a groovy list of all custom fields class names i.e. StringCustomFieldDefinition, CurrencyCustomFieldDefinition, DateCustomFieldDefinition
${customFields*.defaultValue}; Provides a groovy list of all custom fields default values

Collection

The collection property allows you get metadata about the collection that is being processed when evaluating a given Flang expression. This metadata can be accessed using the Flang expressions below;

${collection.type}; Provides the recipe collection's type.
${collection.shortDescription}; Provides the collection's short description.
${collection.introText}; Provides the collection's introText 
${collection.name}; Provides the collection's name

You can also get the URL link to an activity using the following Flang expression

${RecipeCollection?.findByType('collection type')?.getLink()}; //Provides the activitys url link i.e https://cloud.frontlinesms.com/w/1/activities/show/10

The difference between collection.some_method() and RecipeCollection.some_method() is that using collection reference the current collection in scope and using RecipeCollection references all collection available in the workspace. In this case we use the findByType('collection type') to search through all the collections available in the workspace. This method returns only one collection because the collection's type is unique and therefore therefore cannot return more than one result. Using the null safety operator (?) as shown in the snippet above will ensure that you do not get errors if a collection is not found.

userData

FrontlineSMS allows you to define a ‘message bank’, containing key-value pairs that you can reference in your recipes. To access them, use an expression of the form ${userData[key]} . For example, if you had a message bank variable with the key “next_party_location”, you can reference this in a recipe using

The next party will be at ${userData["next_party_location"]}

This will substitute the current value of the “next_party_location” message bank variable into the message, so the user would get a message saying, for example, “The next party will be at Joseph’s House”.

Note that if a userData lookup is not found, a placeholder will be sent in the message. Using the example above, if the “next_party_location” variable had been deleted, the outbound message would say “The next party will be at [Missing UserData key: next_party_location]”. More information about the message bank is available in the UserData documentation.  If you want to avoid this and simply have FLang return an empty string then use userData.properties instead:

${userData.properties['next_party_location']}

To get a list of all userData keys

userData.getAllKeys()

Types of userData

There are four types of userData: Text, SMS message multi-line text, Contact selector and Multi-selector drop-down.

Text

Allows a recipe builder to store textual user data variables whose values are short in length. The “next_party_location” example given above is a perfect candidate for this kind of user data type.

SMS message multi-line text

Allows a recipe builder to store textual user data variables whose values are long and would be best displayed on multiple lines.

Contact selector

Allows a recipe builder to reference the mobile number of a contact or the mobile numbers of members of a group or smart group as a value of a user data entry. The value of the user data is a comma separated string of mobile numbers if multiple contacts were selected.

Given a user data property whose key is developers, when one contact is selected, ${userData[“developers”]} would return the following.

+254123456

When multiple contacts are selected, ${userData[“developers”]} would return the following.

+254123456,+254654321

Multi-selector drop-down

Allows a recipe builder to select a value from a list of options and use it as a user data value. Assume the recipe builder would like to select a country amongst a list of options, the options and selected value would be stored as follows.

Gabon
> Kenya
Algeria
South Africa

Note that the selected value is stored with a > prefix. Using the example above, issuing a call to ${userData['country']} would return the value Kenya.

When no specific option is selected then querying for ${userData['country']} would return null.

More information on userData can be obtained from the userData section.

Contact

The Contact type represents a Contact in the Frontline workspace. In most places where FLang is available, there will be 2 contact instances available, who can be referenced using recipient and trigger.getContact(). All the default properties of Frontline workspace contact records are available:

    recipient.name // returns the name of the recipient Contact
    recipient.email
    recipient.notes
recipient.id // returns the database id of the existing contact recipient.mobile

All contact instances also have the following convenience methods:

    recipient.getCustomField('xyz') // returns the value of a particular custom field (in this case, 'xyz') from the contact's record
    recipient.getNameOrMobile() // returns the name of the recipient, if known, and uses the number if the name is not available, for example when the recipient of an outbound message is not a saved contact
    !trigger.contact.getCustomField('Balance')?.startsWith('RW') // allows a recipe to proceed if a custom field has NO set value, OR the set value does not include a certain string.
recipient.getLink() // returns a url to the detail view of this contact in the associated workspace. If contact is not saved, returns null
recipient.getOrCreateLink() // ensures contact is created in database then returns url for the contact

Because messages can be received from or sent to mobile numbers that are not saved in the Frontline workspace, the recipient and trigger.getContact() entities will often represent unknown contacts. FLang still lets you access these unknown senders/recipients as Contacts, but some properties such as notes or email will naturally be unavailable if the contact is not known. To check whether the current Contact is saved in the Frontline workspace, check the isKnownContact property of the Contact instance.

    recipient.isKnownContact // returns 'true' if the contact is saved in the Frontline workspace

Contact also allows FLang to query the Frontline database for Contacts. Available queries return a list or a single instance of the FLang Contact data type. To perform queries, use the query methods listed below. These are only available on the Contact data type (with a capital C), rather than on instances of a Contact, such as recipient.

Usage examples

  • To get a contact whose name is Bob
    ${Contact.findByName('Bob')}
  • To get a contact whose mobile is +12345678
    ${Contact.findByMobile('+12345678')}
    ${Contact.findByEmail('tim@example.com')}
  • To get a contact whose notes field contains sample notes
    ${Contact.findByNotes('sample notes')}

The queries above will return a FLang Contact instance, while the queries below will return a list of FLang Contact instances each representing a single contact.

  • To get a list of all contacts whose numbers starts with +254
    ${Contact.findAll{ contact -> contact.mobile.startsWith('+254') }}
  • To get a list of contacts who have a custom field on call which has the value today
    ${Contact.findAll{ contact -> contact.getCustomField('on call') == 'today' }}
  • To get a list of contacts whose status custom field starts with active
    ${Contact.findAll{ contact -> contact.getCustomField('status').startsWith('active') }}
  • * To search the Custom Fields of a subset of contacts within a pre-existing list of mobiles (contacts) e.g. a contact recipient list (Note add a _!_ just before the _contact?.getCustomField_ to negate the condition e.g. doesNotStartWith)
 ${trigger.fire['list-from-contact-selector'].split(',').findAll { mobile -> def contact = Contact.findByMobile(mobile); return contact?.getCustomField(userData['custom-field-name'])?.startsWith('Text that the value starts with');}.join(',')}"

The expression used in the inner ‘closure’ (inside the { contact -> ... }) determines which Contacts are returned. This closure is a test that is performed on each Contact in the Frontline workspace. Only those that match the specified condition will be returned.

All operations available on other contact instances, such as recipient, can be used on the contact instance in the findAll operation.

recipient

recipient refers to the recipient of a message sent through the SendMessageAction. This is an instance of the FLang Contact data type. Therefore you can reference specifics about the receiving contact irrespective of other recipients of the same message, which is especially useful when automating messages and/or sending a message to multiple recipients.

All functions and data available on Contact are available on recipients.

Groups and Smart Groups

You can access the workspace Groups and Smart Groups in FLang also so that these can be incorporated where needed:


SmartGroup.findByName('Smart Group Name').size
${ if (SmartGroup.findByName('Smart Group Name').size > 0) { return true; } else
{ return false; }

//Get the names of all the smart groups in a workspace
SmartGroup.getAll()*.name

//Get the number of smartGroups in a workspace
SmartGroup.getAll().size()

//Get the names of all the groups in a workspace
Group.getAll()*.name

//Get the number of groups in a workspace
Group.getAll().size()

trigger contact

trigger.getContact() refers to the sender of the inbound message or missed call that triggered the current recipe. This is an instance of the FLang Contact data type.

All functions and data available on Contact are available on trigger.getContact().

now()

now() gives a java date instance set to the workspace's timezone. This means that it is possible to access all the methods available on the java.util.Date class provided by java.

To learn how to use the now() date instance refer to Date Operations

getStringSimilarity

getStringSimilarity(a, b) returns the similarity of string a to string b. 1.00 represents identical strings. 0.00 represents completely different strings.

base64()

base64() allows you to encode a string to its base64 representation, which is useful when talking to some web APIs.

    ${base64('hi')} // returns aGk=

Date Operations

With FLang we can get date object from other sources. For example, a contact could have a date custom field. A date custom field will also return a java date instance. This means that the same date operations are available to this date custom field.

With any java date instance, we can

  • Display the date
  • Format the date
  • Do date operations with other date object e.g check if date is more recent than another date
  • Modify the date e.g add days, weeks, months, years

Example Usage

Assume that userDate represents our example date object.

Display/Format date

  • To simply display the date with no formatting
    ${userDate} // returns Tue Aug 26 11:43:20 UTC 2014
  • To display the date with special format, which we define
    ${userDate.format('yyyy-MM-dd')} // returns 2014-01-01
    ${userDate.format('yyyy-<<-dd HH:mm')} //returns 2014-01-01 12:00

Date operations can be done as follows.

  • To check if dates are greater than or equals. This kind of comparison checks to the millisecond precision.
    ${userDate > now()}
    ${userDate < now()}
    ${userDate == now()}
  • To check if date is today
    ${userDate.clearTime() == now().clearTime()}
  • Modify the date

    ${use(TimeCategory) { userDate + 3.minutes}} //Add 3 minutes
    ${use(TimeCategory) { userDate + 1.hours}} //Add 1 hour
    ${use(TimeCategory) { userDate + 5.hours}} //Add 5 hours
    ${userDate  + 1} // Add 1 day
    ${use(TimeCategory) { userDate + 1.days }} // Another way of adding one day
    ${use(TimeCategory) { userDate + 1.months}} // Add 1 month
    ${use(TimeCategory) { userDate + 2.years}} // Add 2 years

    // Let's use now()
    ${use(TimeCategory) { now() + 1.months}} // Add 1 month to the current date
    
    // Let's format the date above
    ${use(TimeCategory) { (now() + 1.months).format('dd-MM-yyyy') }} //returns 12-12-2014 if run on 12th November 2014

RecipeManager

This section applies to recipe collection builders.

Actions that make a request to third party service; using the stateMap

  • Making Conditions which look at the response coming back

The builder has a configuration where the recipe builder can specify a logical name for the response object coming back from the third party service: the stateMap.

This logical name can then be used by Conditions in other chained recipes to refer to the response object stateMap. For example, we can give the response the following logical name:

response_blah

We can then retrieve the response and response status code from it in flang.

This example flang checks that the response contains the word "success:

${stateMap['response_blah'].text.contains('success')}

This example flang checks that the response status code was 200:

${stateMap['response_blah'].statusCode == 200}

If the response object is JSON then the object needs to be parsed before being accessed and utilised e.g below this would return the value contained in variable-name:

${parseJSON(stateMap['response_blah']).data-group.variable-name}

Advanced things

Inspecting a list

The expression below is used to check if a list contains an element. It returns value of true if the list contains the values otherwise it return a boolean value false


    ${userData['list'].contains('animal')}

In the example above you are checking if a userData entry called list (which you expect to be a list) has an entry called animal.

Comparing two lists

The expression below is an example of a comparison to check whether or not a word in a comma-separated list (in this case a UserData item called list (no spaces!)) is present in a string (in this case the trigger.text). <br/> To have this as a ‘not’ i.e. none of the words are in the list add a ! to the very start after the ${:


    ${userData['list'].toLowerCase().split(',').any{ listword -> trigger.text.toLowerCase().split('[\\p{Punct}\\s]+').any{ textword -> textword == listword } } }

The above returns a true/false if there is a match in the 2 lists. You can also return the first match (often all you need if the list has a unique set of words and you just need to know which word in the list is in the SMS) with the expression below. e.g for extracting the word as a Contact for a recipient field or as text for a tag:

 ${userData['list'].toLowerCase().split(',').find { listword -> trigger.text.toLowerCase().split('[\\p{Punct}\\s]+').any { textword -> textword == listword } } }

The expression below is used to check if a list contains all the elements of another list.


    ${userData['list1'].containsAll(list2)}

In this case list2 is the other list.

You can also do the following.


    ${userData['list1'].containsAll(['cats', 'dogs'])}

Getting unique entries from a List

To remove duplicate entries from a List, you can coerce it into a Set as follows.


   def numbersList = [1, 2, 3, 3]
   def numbersSet = numbersList as Set
   assert numbersSet.size() == 3

Manipulating Strings

Tokenize and split a String

You can use the tokenize() method on a String to split up a String value using a String, char or whitespace as a delimiter. This method returns an instance of java.util.ArrayList.

The split method is very similar to the tokenize() method. The main difference between using tokenize() and using split is that tokenize() ignores white space whereas split() doesn’t.

For example suppose you have the following String with the value animals are good, (notice the double spacing). Using tokenize() and split() will result into two different results. i.e


    def reply = 'animals  are good'
    assert reply.tokenize(' ') == ['animals', 'are', 'good']
    assert reply.split(' ') == ['animals', ' ', 'are', 'good']

More examples on this below


    def s = 'one two three four'
    def resultList = s.tokenize() // when called without arguments, uses the whitespace as the
    // delimiter
    assert resultList.class.name == 'java.util.ArrayList'
    assert ['one', 'two', 'three', 'four'] == resultList

    def resultArray = s.split() // when called without arguments, uses the whitespace as the
    // delimiter
    assert resultArray instanceof String[]
    assert ['one', 'two', 'three', 'four'] == resultArray

    def s1 = 'Java:Groovy'
    assert ['Java', 'Groovy'] == s1.tokenize(":")
    assert ['Java', 'Groovy'] == s1.tokenize(':' as char)

Useful FLang Expression Conditions

Check if the entire SMS is an email

${trigger.text.trim().matches(/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/)}

Extract international phone number from a formatted number

Often mobile numbers are presented with prettified format, e.g. “+1 (234) 56-789”. The following expression extracts the number in the international format that Frontline expects:

    "+1 (234)-567".findAll(/\d+|\+/).join() // returns "+1234567"

Format a number that may require a thousand seperator

The following snippet adds a thousand seperator to a number. See https://docs.oracle.com/javase/tutorial/java/data/numberformat.html for more details. Scroll to the The DecimalFormat Class section

def count = 12345677; // Some long number needing a thousands seperator
def pattern = "###,###,###"; // # Represents a digit
def formatValue = new java.text.DecimalFormat(pattern);
def formatedValue = formatValue.format(count);
Have more questions? Submit a request

0 Comments

Please sign in to leave a comment.