Code syntax
Code syntax is the set of rules and guidelines that dictate how your code should be structured in order for it to be both functional and understandable. Just as proper grammar and punctuation enhance the clarity of written language, adhering to code syntax ensures that your code is correctly interpreted by the platform and other developers. Consistent and well-defined syntax fosters readability, maintainability, and collaboration in software development projects.
On this page we will explore some common topics and share best practices related to Silverfin Templating Language syntax.
Arrays
An array is a list of variables of any type. To access all of the items in an array, you can loop through each item. Array indexing starts at zero. You can use the split filter to break a single string into an array of substrings.
Another method of creating arrays, especially useful when you are not working with a string, but any other object, is using the push method. You can first create an empty array using split and then add your object to the array using push.
To array or not to array
Use arrays to ✅
- Avoid repeating code, whether that's in the same template or across different ones
- Improve the readability of your code
Use "push method" to ✅
- Increase readability. When searching on "push" in your code, you easily find where arrays are built
- Easily use statements around items you want to push in and out of the array
{% assign all_directors_array = "" | split:"|" %}
{% if person.name != blank %}
{% push person.name to:directors_details_array %}
{% push chosen_type_mandat_value to:directors_details_array %}
{% push end_date to:directors_details_array %}
{% endif %}
{% comment %}finalize array{% endcomment %}
{% assign directors_details_array = directors_details_array | join:";" %}
{% push directors_details_array to:all_directors_array %}
{% result 'all_directors_array' all_directors_array %}
Don't use arrays when ❌
- Multiple conditions apply on one piece of code
- When there are a lot of exceptions, an array is too complicated to use, the code becomes cluttered with ifs and unless statements. Less lines != better code.
- In this case you could use a capture in case you need e.g. a sum before printing the actual items
Avoid nested arrays ⛔️
- A better approach is creating one array and defining any other parameters with dynamic variables
- Or you can create separate arrays alongside one another e.g. key_array, description_array, range_array
Methods
There are different ways to build arrays. While the "push" method is our preferred method, there can be use cases for other methods too. We'll provide a brief overview below.
Some general conventions that apply regardless of the method you use:
- Always call your array a logical name describing the type of array you are building and call it xxxxx_array ending on _array
- Always make sure that the defined array is empty ( "" )
- Use the symbols
| , & , ;, ""
to split between your variables. For nested arrays you can use two symbols eg.&&
- Use square bracket
[ ]
notation to access a specific item in an array. The array indexing starts at zero[forloop.index0]
Push
Preferred method to use in almost every case.
{% assign key_array = key_array | split:"|" %}
{% push "specification_liabilities" to:key_array %}
{% if use_nt15 or use_nt16 %}
{% push "specification_other_liabilities" to:key_array %}
{% push "specification_current_liabilities" to:key_array %}
{% endif %}
{% push "disc_current_liabilities" to:key_array %}
{% push "disc_subordinated_debt" to:key_array %}
Append
The append method is useful when creating arrays as options or option_values for dropdowns.
In this case you need to use pipes as the delimiter.
{% assign language_options = "English" %}
{% if company.country_code == "BE" %}
{% assign language_options = language_options | append:"|" | append:"Dutch" | append:"|" | append:"French" %}
{% endif %}
String
For short arrays, also when they are nested, you can create the array in one assign statement.
Use ;
to distinguish between your variables and |
to split between your items.
Splitting the string you just created will turn it into an array.
Example:
{% assign header_array = 'General information;vkt_1|Directors information;vkt_2_1|Accountant information;vkt_2_2' | split:'|' %}
{% for item in header_array %}
{% assign part = item | split:';' %}
{% assign title = part[0] %}
{% assign description = part[1] %}
{{ title }}: {{ description }}
{% endfor %}
Directors information: vkt_2_1
Accountant information: vkt_2_2
Parentheses
Parentheses in Arithmetic Expressions
While everybody is familiar with how brackets work in the context of arithmetic expressions, it may be worth supplying some text to emphasise that while parentheses can be used in the context of mathematical expressions, the same doesn’t apply to boolean expressions. For Mathematical expressions the standard order of operations applies i.e. PEMDAS
PEMDAS
Parentheses, Exponents, Multiplication and Division (from left to right), Addition and Subtraction (from left to right).
Parentheses - including nested parentheses can be fully utilised.
Please note the same does not apply to Boolean expressions. In tags with more than one and
or or
operator, operators are checked in order from right to left. You cannot change the order of operations using parentheses — parentheses are invalid characters in Silverfin templating language and will prevent your tags from working.
Workaround for Boolean Expressions
The most common workaround to parentheses in boolean expressions is the use of nested if-statements.
Have a look at te following examples:
From statement with parentheses:
test_filter_1 = (tall or has_beard) and green_eyes
To without:
{% assign test_filter_3 = false %}
{% if green_eyes %}
{% if tall or has_beard %}
{% assign test_filter_3 = true %}
{% endif %}
{% endif %}
From statement with parentheses:
test_filter_2 = tall or (has_beard and green_eyes)
To without:
{% assign test_filter_4 = false %}
{% if tall == false %}
{% if has_beard and green_eyes %}
{% assign test_filter_4 = true %}
{% endif %}
{% else %}
{% assign test_filter_4 = true %}
{% endif %}
Translations
There are some conventions you can apply to streamline the use of translations across your code.
It's good practice to store all translations together in one part. If you use translations, store them in part 1.
Standardize terminology
To ensure you use the same terminology & translations across different templates, you can store them in a shared part.
Translation definition
All translation definitions should use snake_case, which follows the guidelines for variable naming, but formatted as a string: "snake_case".
You always need to add a default translation to the definition. The default language of Silverfin is English, therefore the English translation should be the default. There's no need to specify the EN translation on top of that, since that's already covered with the default. The default translation should be the last or first translation within the tag.
To easily spot translations in your code, or search them, you can add prefix _t or suffix _t to the translation key.
Example of a correct translation definition:
{% t= "net_assets_increase_t" nl:"Toename netto activa" fr:"Augmentation de l'actif net" default:"Net assets increase" %}
You should stick to the same approach for any and all translations, regardless of the length of the text.
Translation tags
You can use dynamic elements in your translations instead of placing variables and translation tags in a sequence.
Don't do:
{% t "assets_value_equals" %} {{ curr_sign }} {{ assets_value_excl_cash }} {% t "without_cash" %}
Do:
{% t "assets_value_equals_excl_cash" curr_sign:curr_sign assets_value_excl_cash:assets_value_excl_cash %}
Note that any variable used in a translation tag with dynamic elements, should preferably have the same variable name that has already been defined as the dynamic element within the translation definition.
{% t "net_assets_change" increase_decrease:increase_decrease %}
Custom variables & collections
When it comes to custom collections (see: fori) there are some conventions to keep in mind to ensure your data is stored correctly in the database and can easily be maintained and accessed when needed.
Basic variable naming conventions apply, though there are things cases that need to be highlighted.
Unique identifier
While local variables can technically be reused if you reset them by turning them into empty strings, this is not the case for custom variables. You can’t assign a value to a custom variable with Liquid.
This turns the local variable into an empty string, removing any value it previously contained:
{% assign my_local_var = "" %}
This does not remove the value stored in the database for this custom variable:
{% assign custom.my_collection.unique_key = "" %}
Because it’s so important that custom variables have unique identifiers, as a rule you should use the key
attribute to link new items to other collections. Don’t use the forloop.index
, because the return from a forloop.index is reused while keys give a unique return.
Do NOT use forloop.index:
{% fori category in custom.categories %}
{% input category.name %}
{% capture items %}items_{{ forloop.index }}{% endcapture %}
{% for item in custom.[items] %}
{% input item.name %}
{% endfor %}
{% endfori %}
Do use category.key:
{% fori category in custom.categories %}
{% input category.name %}
{% capture items %}items_{{ category.key }}{% endcapture %}
{% for item in custom.[items] %}
{% input item.name %}
{% endfor %}
{% endfori %}
Namespace
Within the same template you can add all your inputs to the same custom collection by consistently using the same namespace. However, you should not re-use a namespace for fori loops. Fori’s should exist on their own and be given unique namespaces. If you reuse the same namespace there will already be persisted items in the fori collection, while it may not seem like it from a user perspective. Deleting an iteration of the fori can delete the content one of the other inputs that exist outside of the fori. It can also give conflicts with certain input types, such as attachments.
Do not reuse an existing namespace for a fori:
{% input custom.tax_questions.income_related as:text placeholder:"Explanation" %}
{% input custom.tax_questions.costs_related as:text placeholder:"Explanation" %}
{% input custom.tax_questions.other as:text placeholder:"Explanation" %}
{% fori question in custom.tax_questions %}
{% input question.topic as:text placeholder:"Explanation" %}
{% endfori %}
Do choose a unique namespace for each new collection:
{% input custom.tax_questions.income_related as:text placeholder:"Explanation" %}
{% input custom.tax_questions.costs_related as:text placeholder:"Explanation" %}
{% input custom.tax_questions.other as:text placeholder:"Explanation" %}
{% fori question in custom.additional_tax_questions %}
{% input question.topic as:text placeholder:"Explanation" %}
{% endfori %}
Connected collections
When a new custom collection needs to be created on top (and named after) another drop, the link between both needs to be chosen carefully.
For example, you can’t use text that has been inputted by users as the key or namespace for the new drop. Unsupported characters e.g. will cause liquid errors, and duplicate customs can occur when the same text is inputted twice.
As a rule, you should use the “key” attribute linked to the iteration of the existing collection to name the new one.
Don't do:
{% for varia in custom.varia_titles %}
**{{ varia.title }}**
{% capture title %}{{ varia.title }}{% endcapture %}
{% fori item in custom.[title] %}
{% input item.description %}
{% endfori %}
{% endfor %}
Do use the key:
{% for varia in custom.varia_titles %}
**{{ varia.title }}**
{% capture title %}title_{{ varia.key }}{% endcapture %}
{% fori item in custom.[title] %}
{% input item.description %}
{% endfori %}
{% endfor %}
Customs on accounts
When it comes to creating custom variables on the accounts drop, there are two possible approaches.
- Store the custom on the account
OR - Store the custom on the template
If you need to store the custom on a template you again need a unique identifier, when it comes to accounts you should use the account.id
instead of the key.
For inputs on a reconciliation text you should follow the second approach. That will ensure that the rollforward (copy data) functionality and the Silverfin Interact features work as expected. It’s essential that data is stored on the reconciliation text itself and not on the account.
On the account:
{% assign bank_accounts = #5 %}
{% for account in bank_accounts %}
{% input account.custom.bank.add_comment as:text placeholder:"Additional comment" %}
{% endfor %}
On the reconciliation text:
{% for account in bank_accounts %}
{% comment %}create db var linked to ID of an account{% endcomment %}
{% capture acc_id %}acc_{{ account.id }}{% endcapture %}
{% input custom.[acc_id].add_comment as:text placeholder:"Additional comment" %}
{% endfor %}
Customs on people
There are again two approaches to store custom data related to the people drop.
- Store the custom on the person
OR - Store the custom on the template
For the second approach you again need a unique identifier, when it comes to people you should use the persistent_id
instead of the key.
On the person:
{% for person in period.people %}
{% input person.custom.email_address %}
{% endfor %}
On the reconciliation text:
{% for person in period.people %}
{% comment %}create db var linked to ID of an person{% endcomment %}
{% capture person_id %}person_{{ person.persistent_id }}{% endcapture %}
{% input custom.[person_id].email_address placeholder:"Email address" %}
{% endfor %}
When it comes to data on the people drop there is no clear answer on what is the best approach. If you want to access this data across several templates, it would be easier to store it directly on the person with the first approach.
If not, then storing it on the template will lead to a better user experience when using the copy data and SF Interact features.
File inputs in collections
File inputs in collections behave in a particular way. Liquid only allows one file input (for each loop) in the collection. The technical reason is because these files are stored on collection level and not on the input TextProperty level. In theory, this shouldn't be a huge issue since file inputs allow multiple attachments.
{% comment %}Populate collection{% endcomment %}
{% fori code in custom.codes %}
{% input code.name %} {% input code.description %} {% input code.file as:file %}
{% endfori %}
{% comment %}Print collection{% endcomment %}
{% for code in custom.codes %}
{{ code.name }} {{ code.description }} {{ code.documents }} {% comment %}the file is stored on custom.collections, so in case of multiple file inputs they are all stored on the same TextProperty{% endcomment %}
{% endfor %}
Customs from other reconciliations
It is possible to use the exact same custom variable across a multitude of templates, while keeping it stored in only one of them. You should avoid doing this, as it is unclear to a user that the custom data is actually stored on a different reconciliation. This can also be confusing when using the copy data functionality. It is better to add a link to the other reconciliation and have the user fill out the data there.
❌ Imagine we are in template A. Don't do:
{% comment %}Fill something out in template B{% endcomment %}
{% input period.reconciliations.template_b.custom.namespace.key as:text %}
✅ Do:
{% comment %}User needs to fill out this information in template B{% endcomment %}
{% assign template_b = period.reconciliations.template_b %}
{% if template_b.exists? %}
{% linkto template_b %}Fill out the text in the reconciliation {{ template_b.name }}{% endlinkto %}
{% endif %}
Note that it's good practice to always verify another template exists before referring to it.
When accessing the custom on a different reconciliation directly, it's even mandatory to do so to avoid liquid errors.
Empty vs blank
If you want to check if a collection contains items or not, use empty.
In any other scenario, you would use blank. This includes checking if a specific custom variable contains a value.
{% fori item in custom.offices %}
{% input item.office_city %}
{% endfori %}
{% if custom.offices == empty %}
There are no offices in this collection
{% endif %}
Blank vs zero
Validating if a certain value is zero is different from validating whether it’s blank.
If you also want to check for blank, that condition needs to be added explicitly by adding '== blank'
The if-statement will always be false in the following situation where the variable is blank:
Below examples use a local variable, but it could also be a custom variable in which nothing has been inputted yet.
{% assign test_variable = %}
{% if test_variable == 0 %}
The value equals zero.
{% else %}
The value is different from zero, including blank.
{% endif %}
Instead of adding "== blank" to a condition that checks for zeroes, you can use 'INT()' on the variable you want to check. Doing so will convert blank variables into zeroes, which makes it no longer necessary to combine a check on zero and blank as that variable can never be blank anymore.
{% if INT(test_variable) == 0 %}
The value equals zero.
{% else %}
The value is different from zero, excluding blank.
{% endif %}
Blank account_collection
To check if accounts have been selected with an account_collection input, checking against blank won’t work. Instead, treat the account_collection as a range on the accounts drop, then check if there are any accounts within that range.
{% input custom.accounts.ranges as:account_collection range:"6,7" %}
Don´t do:
{% if custom.accounts.ranges == blank %}{% endif %}
Do:
{% assign accounts = period.accounts | range:custom.accounts.ranges %}
{% if accounts == blank %}{% endif %}
Registers vs variables
The main advantage of variables over registers, is that they offer more meaningful names, which makes code easier to maintain and review. They can also store any kind of value (not just monetary).
The main advantage of registers over variables, is that they offer more “short-hand” methods and code is less verbose than when using variables. For example, if we wanted to keep a running total of revenue and cost, displaying both values and the total at the bottom, observe how less code is required when using registers.
Variables
{% assign running_total = running_total + revenue %}
{% revenue %}
{% assign running_total = running_total + cost %}
{% revenue %}
{% running_total %}
Registers
{% $0+ revenue %}
{% $0+ cost %}
{{ $0 }}
While registers are still supported, we recommend to use variables instead. See this case on the Community for an elaborate example.
Results
Results are a powerful tool to build interconnected workflows. They are however also the most precarious type of code in STL which can easily lead to bad user experience if not handled properly. Always keep in mind that a result tag is actually a local variable in an unlimited number of other templates, reports, documents and insights segments you don’t know about. So changing or removing results is never allowed, and creating them can only be done cautiously. Moreover, results create dependencies. This makes handling them key in avoiding performance and cache issues.
Naming
Results should have names that are fully self-explanatory, as they can be used by clients in custom insights segments. Follow general variable naming conventions.
Make sure the name of the result tag is the same as the variable (especially in case dynamic variables are used).
{% assign category = "equity" %}
{% capture category_total %}{{ category }}_total{% endcapture %}
{% assign [category_total] = 1000000 %}
{% result category_total [category_total] %}
Renaming
Result tags can be called upon in custom client templates and permanent texts. Also, result tags can be used in custom insights segments and custom reports. Changing a result name or removing it can make any of such documents or overviews useless leading to a bad user experience.
Even within the context of Silverfin-created content, it is not easy to trace where a result tag created in a template may be called upon.
Therefore, do not 🚫
- change result names,
- remove the result,
- or make the result conditional.
Workflow design
Adding results should be done carefully and requires some planning of the data stream in your workflow. Good workflows usually have certain centerpiece templates that collect data into the workflow or distribute it into the separate templates. Having a proper understanding of the data flow across templates within a workflow and between workflows, certainly helps avoiding circular references, cache and performance issues.
More information on this topic will be added later on a dedicated "Architecture" page.
Circular references
Do not call upon results within the same template from a previous period.
Maintain an overview of the data flow in a workflow so that one template A will not call results from template B in case B may already have results called from A in its code. It doesn’t matter what variable of data is concerned.
Cache issues
For dependency chains, the maximum chain length is three, in which a call on a ledger account of a company already counts as one. Therefore one should think carefully of how data is sent around in a workflow and avoid creating chains that involve the risk of become longer than three, which may lead to cache issues. Please read more about this subject in cache issues. Make sure the data flow architecture of your workflow is known before you call upon a certain result from another template.
Avoid results in {% ic %} or {% nic %} tags in account templates.
Result value
A result tag should have an explainable meaning in an accounting context. A result tag usually encloses a certain piece of the template, an end result of a calculation, or a key parameter used for filtering data on a higher level.
You should not ❌ add tables in a result. From a liquid testing perspective it's very inconvenient.
Result tags also should not ❌ contain interactive elements such as linkto's or currency's with the accounts drop modal enabled.
Performance
Only call upon a certain result once in a template, by creating a local variable which can be referenced many times in a template without further performance impact.
{% assign chosen_language = period.reconciliations.settings.results.language_setting %}
{% locale chosen_language %}
....
{% endlocale %}
Avoid creating too many outgoing results in general, especially in account templates. This also facilitates good maintenance of your workflow data structure.
Be careful when adding results to a loop. Consider if you need to create that many outgoing results.
Booleans
A boolean is a type of input-field that creates a result that can only have one of two possible values: true or false. However, in Silverfin booleans can have a third value: blank.
Results
If you want to, for example, access the value of a boolean via Insights you'll need to add its value to a result.
To avoid sending blank results, you can do the following:
{% input custom.reviewed.check as:boolean %} {% t "Has this been reviewed?" %}
{% comment %}
create result for SF Insights
{% endcomment %}
{% assign reviewed = custom.reviewed.check | default:false %}
{% result 'reviewed' reviewed %}
The variable is assigned to the real value custom.reviewed.check or is assigned to false in the case the database variable is blank. The boolean can have three possible values in the database: true, false or blank. Though it is important to remember from the user perspective there are only two options and thus it is important to assign it to false when the outcome is blank so that Silverfin insights can pick up this value as it searches for a yes or no value.
Default true
A scenario you see quite often is booleans with default value true. That can be the case e.g. to show/hide paragraphs in text heavy reconciliations. For a user it looks like the boolean is ticked, if they untick it the text will be hidden or be shown greyed out, indicating it will not be included in the export.
In this case blank or true booleans yield the same result. Have a look at this example:
{% input custom.show.detailed_text as:boolean default:true %}
{% if custom.show.detailed_text != false %}
Detailed text
{% else %}
{% ic %}
<font color='CCCCCC'><i>Enable detailed text</i></font>
{% endic %}
{% endif %}
It's enough to check the database variable against false, since blank and true result in the same condition thanks to the default.
Updated 4 months ago