# Write Liquid templates

Source: https://govform.com/resources/documentation/article/11-write-liquid-templates
Topic: Liquid Templates

Liquid lets a Govform 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 Govform `array` filter.
-   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

Govform 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 Govform `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 Govform `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.

## Create an answer spreadsheet download link

```
{{ "Download your answers" | excel_download_link }}
```

The filter generates a link to an authorised download of saved answers; it does not run a page action or save unsaved input. Configure XLSX or CSV, layout and repeated groups in **Service settings → Excel export**. See [Download saved answers as Excel or CSV](<https://govform.com/resources/documentation/article/21-10-service-excel-export>).

## Filter reference

-   [Arrays and collections filters](<https://govform.com/resources/documentation/article/34-01-liquid-built-in-filters-a-m>)
-   [Text and formatting filters](<https://govform.com/resources/documentation/article/34-02-liquid-built-in-filters-n-z>)
-   [Data, objects and encoding filters](<https://govform.com/resources/documentation/article/34-03-liquid-data-objects-and-encoding-filters>)
-   [Dates, times and number filters](<https://govform.com/resources/documentation/article/34-04-liquid-dates-times-and-number-filters>)
-   [Links and navigation filters](<https://govform.com/resources/documentation/article/34-05-liquid-links-and-navigation-filters>)
-   [Forms, fields and service filters](<https://govform.com/resources/documentation/article/34-06-liquid-forms-fields-and-service-filters>)

## Library regional defaults and payment results

Regional defaults are available through `library.profile`; ordinary number/currency filters use them without repeating arguments. Browser-zone timestamp display and Service-time-zone calendar calculations serve different purposes. Use [explicit timestamp formats for APIs](<https://govform.com/resources/documentation/article/34-07-liquid-regional-dates-currencies>).

`form.payment` is the read-only runtime payment outcome. It is separate from the service definition's `payment` settings. Amounts are in ISO minor units and outcomes can be simulated or recorded by staff. See [Payment completion and casework](<https://govform.com/resources/documentation/article/payment-journey-and-casework>).
