Write Liquid templates
Liquid lets a Govforms service read journey data and turn it into the value required by a setting. That value is not always page content: the same language can decide a condition, build an API payload, calculate a field value, create Markdown or HTML, or supply a short label.
Start with the output contract: identify whether the setting expects a Boolean, JSON, XML, Markdown, HTML, plain text or another value. Then write and test the smallest template that produces exactly that shape.
Where Liquid is used
| Use | Expected result | Typical example |
|---|---|---|
| Condition template | true or false | Decide whether a route or action should run. |
| API action URL, headers or body | URL text, JSON, XML or another API-specific payload | Send selected answers and action results to another service. |
| Markdown or HTML content | Readable content | Personalise information, confirmation or review content. |
| Assign action or pre-populated value | A string, number, object, array or field-specific value | Store a calculation or prepare repeated rows. |
| Labels, button text and other short settings | Plain text | Include a name or status in a concise label. |
Liquid syntax essentials
Use output tags to print a value, and tag blocks to control logic:
{{ first_name }}
{% if applicant_age >= 18 %}
Adult
{% else %}
Under 18
{% endif %}
- Do not put parentheses around Liquid conditions. Write
{% if age >= 18 %}, not{% if (age >= 18) %}. - Liquid does not support JavaScript-style array literals. Use
split,parse_json, or the Govformsarrayfilter. - Add non-output notes with
{% comment %}...{% endcomment %}. - Use stable IDs for fields, actions and conditions. Visible labels can change without changing those IDs.
Understand the Liquid context
Govforms makes the current journey context available to Liquid. Field values sit at the root, while service metadata and runtime results are grouped under named objects.
| Context | What it contains | Example |
|---|---|---|
| Field IDs at the root | The current value of each field. | {{ first_name }} |
form | Journey and service metadata, the signed-in user where available, URL query parameters, page position and repeating-row context. | {{ form.submissionId }} |
conditions | The current true or false result of each condition, keyed by Condition ID. | {% if conditions.is_eligible %} |
actions | Results from actions that have already run, keyed by Action ID. | {{ actions.find_account.response }} |
carts | Keyed collections that persist across pages in the journey. | {{ carts.appointments[selected_id] }} |
now | The current date and time. | {{ now | format_date: "dd MMMM yyyy" }} |
Homepage templates can also receive journey collections such as open, completed, workflow, closed and cancelled journeys. Each entry uses the same context shape, so read only the fields and metadata required by the homepage component.
Read field values
Most fields are available directly by Field ID:
{{ first_name }}
{{ contact_method }}
{{ selected_topics | join: ", " }}
A radio or select field contains the selected item ID. A checkbox field contains an array of selected item IDs. Use lookup_item_label when a person-facing label is needed.
Structured fields expose properties:
{{ appointment_date.day }}/{{ appointment_date.month }}/{{ appointment_date.year }}
{{ appointment_time.hour }}:{{ appointment_time.minute }}
{{ home_address.line1 }}, {{ home_address.postcode }}
{{ chosen_location.lat }}, {{ chosen_location.lng }}
Date fields use day, month and year; time fields use hour and minute. Address fields can include line1 to line5, postcode and country. Map-select fields can include lat, lng, bng, nearPlace and nearFullDescription.
Use repeating-group values
Groups do not have a user-entered Liquid ID. The outer group’s identifier is generated and is not referenced by conditions, actions or templates. Give each field inside the group a stable Field ID and use those IDs directly.
Every inner field becomes an array with one value per repetition. Use one field as the loop and the same index to read its neighbours:
{% for name in attendee_name %}
- {{ attendee_name[forloop.index0] }} — {{ attendee_email[forloop.index0] }}
{% endfor %}
When a template is evaluated inside a row, currentRepeat0 and this identify the zero-based row. For pre-population, configure each inner field—not the group—and return a JSON array with one value per row:
{% assign names = actions.get_staff.response.data | map: "staffName" %}
{{ names | json }}
Read form metadata and query parameters
Common metadata includes form.submissionId, form.libraryId, form.serviceId, form.title, form.environment, status values and current-user information where authentication provides it.
Read a URL query parameter through form.query:
{{ form.query.caseId }}
Query parameters are rebuilt from the current URL on each page. They do not automatically survive a redirect. Pass required parameters explicitly in the Redirect action or with a navigation filter such as page_link.
Read condition results
Conditions are keyed by Condition ID and resolve to true or false. They can be reused without reproducing the condition logic:
{% if conditions.can_submit %}
Ready to submit
{% else %}
Check the missing information
{% endif %}
A condition template itself should produce a clear Boolean result:
{% if applicant_age >= 18 and country_code == "GB" %}true{% else %}false{% endif %}
Read action results
Only actions that have already run are available. Every action result can include responseCode, executionTime and error. Action-specific values depend on the action type.
| Action result | Typical access |
|---|---|
| API response | {{ actions.get_record.response.data.items }} |
| Feed lookup rows | {{ actions.find_rows.response }} or {{ actions.find_rows.results }} |
| Assigned value | {{ actions.calculate_total.value }} |
| Generated PDF | {{ actions.create_pdf.base64 }} |
| Authorization result | {{ actions.check_access.hasAccess }} |
| Uploaded file result | {{ actions.upload_document.fileUrl }} |
Test the failure path as well as the successful response. Check responseCode or error before assuming nested response properties exist.
Work with carts
A cart stores items by Cart ID and Item ID and persists across pages. Values can be strings, numbers, objects or arrays.
{{ carts.appointments }}
{{ carts.appointments.monday_morning }}
{{ carts.appointments.monday_morning.location }}
{{ carts.appointments[form.query.itemId].location }}
Populate carts with “Add or update items in a cart” actions and remove entries with the corresponding remove action. Use bracket notation when the item ID comes from a field or query parameter.
Produce the right output type
Boolean
Return true or false for condition and authorization checks. Keep the rule readable and test missing values.
JSON
Use json to serialize an object or array. Use to_json_string when a string value needs its own surrounding JSON quotes, or jsonstr when it is already inside quotes:
{
"submissionId": {{ form.submissionId | to_json_string }},
"name": {{ full_name | to_json_string }},
"topics": {{ selected_topics | json }},
"note": "{{ case_note | jsonstr }}"
}
Use parse_json to turn JSON text into an object or array. There is no Govforms to_json filter; use json, jsonstr or to_json_string according to the required shape.
XML
Escape dynamic text for XML rather than HTML:
<request><name>{{ full_name | xml_escape }}</name></request>
Markdown or HTML
Markdown is usually the clearest choice for headings, lists and links in content settings. When a setting accepts HTML, escape untrusted text before inserting it into markup:
## Application for {{ full_name }}
- Reference: {{ form.submissionId }}
<strong>{{ full_name | escape }}</strong>
Plain text and typed values
Labels normally expect short plain text. Assign actions and pre-populated values may expect a number, object or array instead. Confirm the target field’s value shape before returning structured data.
Create arrays and objects
Create an array from text with split, an empty array with parse_json, or an array from variables with the Govforms array filter:
{% assign routes = "email,phone,post" | split: "," %}
{% assign empty_items = "[]" | parse_json %}
{% assign selected = first_item | array: second_item, third_item %}
Use set to add or replace an object property. It is different from the built-in push filter, which appends an item to an array.
Test before publishing
- Confirm that the setting supports Liquid and identify its exact output type.
- Test normal, blank, missing, repeated, very long and unexpected values.
- Test every branch of condition logic and both success and failure action results.
- Validate JSON or XML payloads with representative quotes, line breaks and non-ASCII characters.
- Check that IDs are stable and refer to inner fields rather than a repeating group.
- Test revisiting a page, resuming a draft and running an action more than once.
- Test in User view and the appropriate QA environment before deployment.
