JSON Template with Liquid Syntax
The JSON your AssetFeed delivers is yours to define: outputs, conditions and loops in Liquid syntax, all available variables and the functions for text and lists.
Every list endpoint and every single asset endpoint in Metadata as JSON mode has its own JSON template (Liquid). You edit it in the Customize AssetFeed dialog in a code editor with syntax highlighting. The template is a JSON document into which you insert values, check conditions and iterate over lists using the Liquid syntax.
On Save, VAULO checks the syntax of the template. Whether every function you use exists only shows when the feed is fetched: a template with an unknown function answers with status code 500. So open the feed URL in the browser once after every change.
Basics
Two notations cover almost everything:
{{ … }}outputs a value, for example{{ feed.title }}.{% … %}controls the flow: conditions, loops, variables.
{
"title": "{{ feed.title }}",
"count": {{ page.totalItems }},
"assets": [
{% for asset in assets %}
{
"id": "{{ asset.id }}",
"isImage": {% if asset.fileType == "Image" %}true{% else %}false{% endif %}
}{% unless forloop.last %},{% endunless %}
{% endfor %}
]
}Conditions
{% if asset.iptc.title %}"{{ asset.iptc.title }}"{% else %}null{% endif %}
{% if page.hasNext and page.index == 1 %} … {% endif %}
{% unless forloop.last %},{% endunless %}
{% if asset.fileType == "Video" %} … {% elsif asset.fileType == "Image" %} … {% else %} … {% endif %}Comparisons use ==, !=, <, <=, > and >=, combined with and and or. A value counts as not met when it is missing or null. An empty text "" counts as met, though – check texts with string.size or replace them with object.default (see Functions).
Loops
{% for asset in assets %}
{{ forloop.index }} ← 0, 1, 2, … (starts at 0)
{{ forloop.first }} ← true on the first entry
{{ forloop.last }} ← true on the last entry
{% endfor %}
{% for keyword in asset.iptc.keywords limit: 5 %} … {% endfor %}Variables, comments and raw text
{% assign kind = asset.fileType %}
{% capture label %}{{ asset.filename }} ({{ kind }}){% endcapture %}
"label": "{{ label }}",
{% comment %} Not emitted {% endcomment %}
{% raw %}{{ stays literally as written }}{% endraw %}{{- and -%} respectively {%- and -%} strip whitespace and line breaks around a tag.
Functions
The classic Liquid filters such as | upcase, | date: "%Y" or | default: "…" are not available. VAULO uses the functions of the Scriban template engine: the name contains a dot, and arguments follow separated by spaces – no colon, no comma.
{{ asset.filename | string.upcase }} → LANDSCAPE10.JPG
{{ asset.filename | string.downcase }} → landscape10.jpg
{{ asset.filename | string.slice 0 9 }} → Landscape (from position 0, 9 characters)
{{ asset.iptc.description | string.truncate 20 }} → A very long capti...
{{ asset.filename | string.replace ".jpg" "" }} → Landscape10
{{ asset.filename | string.size }} → 15
{{ asset.filename | string.contains ".jpg" }} → true
{{ asset.filename | string.split "." | array.last }} → jpg
{{ asset.iptc.keywords | array.join ", " }} → Berlin, Summer, Party
{{ asset.iptc.keywords | array.size }} → 3
{{ asset.iptc.keywords | array.first }} → Berlin
{{ asset.iptc.title | object.default "Untitled" }} → fallback when the value is missing or empty
{{ asset.updatedAt | date.parse | date.to_string "%Y-%m-%d" }} → 2026-09-09Functions can be chained: {{ asset.iptc.keywords | array.join ", " | string.upcase }}. Everything about dates and times is under Formatting dates and times.
Variables
feed
Available in every template.
| Variable | Content |
|---|---|
feed.id | The ID of the feed |
feed.title | The title of the feed |
feed.url | The feed URL |
feed.generatedAt | Time of the response, ISO 8601 in UTC |
assets and asset
A list endpoint receives the list assets – one page of the published files. A single asset endpoint in Metadata as JSON mode receives the one file as asset. Every asset has these fields:
| Variable | Content |
|---|---|
asset.id | The ID of the file |
asset.assetId | The file's Asset ID from Data Studio, for example 000123. Empty while the file has no Asset ID yet – {% if asset.assetId %} checks for that |
asset.filename | File name with extension |
asset.contentType | MIME type, for example image/jpeg |
asset.fileType | Image, Video, Audio, Pdf, MicrosoftOffice, Text, Markdown, Photoshop, Illustrator or Unknown |
asset.updatedAt | Last change, ISO 8601 in UTC |
asset.url | Temporary direct URL to the file in the requested size |
asset.iptc | IPTC metadata of the active version (see below) |
asset.exif | EXIF data of the file (see below) |
asset.tenantProps | Custom metadata (see below) |
asset.iptc
All fields are texts; missing values are empty. asset.iptc.keywords is a list.
| Group | Fields |
|---|---|
| Description | title, headline, description, descriptionWriter, keywords, personInImage |
| Location | city, location, sublocation, state, country, countryCode |
| Creator and rights | creator, creatorsJobTitle, copyrightNotice, credit, source, webStatementOfRights, rightsUsageTerms |
| Administration | subjectCode, jobId, instructions, editStatus, category, supplementalCategories |
| Model and property | modelAge, additionalModelInformation, modelReleaseId, propertyReleaseId |
| Contact | creatorContactExtendedAddress, creatorContactCity, creatorContactRegion, creatorContactPostalCode, creatorContactCountry, creatorContactWorkEmail, creatorContactWorkTelephone, creatorContactWorkUrl |
| Image supplier | imageSupplierId, imageSupplierName, imageSupplierImageId |
| Image analysis | blurhash, dominantColor, isGrayscale (true or false) |
asset.exif
| Variable | Content |
|---|---|
exif.dateTime | Capture time as ISO 8601 or null |
exif.fNumber, exif.exposureTime, exif.isoSpeedRatings, exif.focalLength, exif.orientation | Capture values |
exif.model, exif.lensModel | Camera and lens |
exif.copyright, exif.colorProfile, exif.colorSpace | Further texts |
exif.gps.latitude, exif.gps.longitude, exif.gps.altitude | Coordinates as number or null |
exif.gps.timestamp | Timestamp as ISO 8601 or null |
exif.gps.imgDirection, exif.gps.imgDirectionRef, exif.gps.mapDatum, exif.gps.processingMethod, exif.gps.hPositioningError | Further GPS values |
asset.tenantProps
The values of your custom metadata from Data Studio, addressed by the property's slug or its slot ID, for example asset.tenantProps.articlenumber. Texts come as text, multi-selections as a list, numbers and yes/no values unchanged. Unset properties are empty.
page
Only in list endpoints.
| Variable | Content |
|---|---|
page.index | Current page, starting at 1 |
page.size | Assets per page |
page.totalItems | Total number of assets in the feed |
page.totalPages | Number of pages |
page.hasNext | true when there is a further page |
page.nextUrl | URL of the next page or null |
page.previousUrl | URL of the previous page or null |
Producing valid JSON
The external system expects valid JSON. These rules help:
- Always quote texts:
"{{ asset.filename }}". All text values are already escaped for JSON – quotes or line breaks in a caption will not break your JSON. - Commas in loops: No comma may follow the last entry. Write
{% unless forloop.last %},{% endunless %}after every element. - Missing values: Empty texts are emitted as
"".page.nextUrl,exif.dateTimeand the GPS numbers yieldnullwhen missing – you can emit them without quotes or guard them with{% if %}. - Emitting lists: Build arrays with a loop, for example
[{% for k in asset.iptc.keywords %}"{{ k }}"{% unless forloop.last %},{% endunless %}{% endfor %}]. - Check the result: Open the feed URL in the browser and run the result through a JSON validator before the external system relies on it.
Examples
Default template of a list endpoint
{
"version": 1,
"id": "{{ feed.id }}",
"title": "{{ feed.title }}",
"generatedAt": "{{ feed.generatedAt }}",
"nextPage": {% if page.nextUrl %}"{{ page.nextUrl }}"{% else %}null{% endif %},
"assets": [
{% for asset in assets %}
{
"id": "{{ asset.id }}",
"filename": "{{ asset.filename }}",
"contentType": "{{ asset.contentType }}",
"type": "{{ asset.fileType }}",
"updatedAt": "{{ asset.updatedAt }}",
"url": "{{ asset.url }}"
}{% unless forloop.last %},{% endunless %}
{% endfor %}
]
}Default template of a single asset endpoint
{
"id": "{{ asset.id }}",
"filename": "{{ asset.filename }}",
"contentType": "{{ asset.contentType }}",
"type": "{{ asset.fileType }}",
"updatedAt": "{{ asset.updatedAt }}",
"url": "{{ asset.url }}"
}Extended example with metadata
{
"feed": "{{ feed.title }}",
"page": {
"index": {{ page.index }},
"total": {{ page.totalPages }},
"next": {% if page.nextUrl %}"{{ page.nextUrl }}"{% else %}null{% endif %}
},
"items": [
{% for asset in assets %}
{
"id": "{{ asset.id }}",
"assetId": {% if asset.assetId %}"{{ asset.assetId }}"{% else %}null{% endif %},
"name": "{{ asset.filename | string.replace ".jpg" "" }}",
"title": "{{ asset.iptc.title | object.default asset.filename }}",
"caption": "{{ asset.iptc.description | string.truncate 200 }}",
"credit": "{{ asset.iptc.credit }}",
"keywords": [{% for keyword in asset.iptc.keywords %}"{{ keyword }}"{% unless forloop.last %},{% endunless %}{% endfor %}],
"articleNumber": "{{ asset.tenantProps.articlenumber }}",
"takenOn": {% if asset.exif.dateTime %}"{{ asset.exif.dateTime | date.parse | date.to_string "%Y-%m-%d" }}"{% else %}null{% endif %},
"location": {
"city": "{{ asset.iptc.city }}",
"latitude": {{ asset.exif.gps.latitude }},
"longitude": {{ asset.exif.gps.longitude }}
},
"image": "{{ asset.url }}",
"permalink": "{{ feed.url }}/assets/{{ asset.filename }}"
}{% unless forloop.last %},{% endunless %}
{% endfor %}
]
}APIs, Endpoints and URL Scheme
How to build the public interface of your AssetFeed: APIs with their own URL validity, list and single asset endpoints, placeholders in the URL scheme, response modes and the reserved query parameters.
Formatting Dates and Times
Timestamps such as updatedAt arrive as ISO 8601 values in UTC. This is how you output only the date, only the time or a localized format in the JSON template.