VAULOVAULO Help Center

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-09

Functions 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.

VariableContent
feed.idThe ID of the feed
feed.titleThe title of the feed
feed.urlThe feed URL
feed.generatedAtTime 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:

VariableContent
asset.idThe ID of the file
asset.assetIdThe 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.filenameFile name with extension
asset.contentTypeMIME type, for example image/jpeg
asset.fileTypeImage, Video, Audio, Pdf, MicrosoftOffice, Text, Markdown, Photoshop, Illustrator or Unknown
asset.updatedAtLast change, ISO 8601 in UTC
asset.urlTemporary direct URL to the file in the requested size
asset.iptcIPTC metadata of the active version (see below)
asset.exifEXIF data of the file (see below)
asset.tenantPropsCustom metadata (see below)

asset.iptc

All fields are texts; missing values are empty. asset.iptc.keywords is a list.

GroupFields
Descriptiontitle, headline, description, descriptionWriter, keywords, personInImage
Locationcity, location, sublocation, state, country, countryCode
Creator and rightscreator, creatorsJobTitle, copyrightNotice, credit, source, webStatementOfRights, rightsUsageTerms
AdministrationsubjectCode, jobId, instructions, editStatus, category, supplementalCategories
Model and propertymodelAge, additionalModelInformation, modelReleaseId, propertyReleaseId
ContactcreatorContactExtendedAddress, creatorContactCity, creatorContactRegion, creatorContactPostalCode, creatorContactCountry, creatorContactWorkEmail, creatorContactWorkTelephone, creatorContactWorkUrl
Image supplierimageSupplierId, imageSupplierName, imageSupplierImageId
Image analysisblurhash, dominantColor, isGrayscale (true or false)

asset.exif

VariableContent
exif.dateTimeCapture time as ISO 8601 or null
exif.fNumber, exif.exposureTime, exif.isoSpeedRatings, exif.focalLength, exif.orientationCapture values
exif.model, exif.lensModelCamera and lens
exif.copyright, exif.colorProfile, exif.colorSpaceFurther texts
exif.gps.latitude, exif.gps.longitude, exif.gps.altitudeCoordinates as number or null
exif.gps.timestampTimestamp as ISO 8601 or null
exif.gps.imgDirection, exif.gps.imgDirectionRef, exif.gps.mapDatum, exif.gps.processingMethod, exif.gps.hPositioningErrorFurther 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.

VariableContent
page.indexCurrent page, starting at 1
page.sizeAssets per page
page.totalItemsTotal number of assets in the feed
page.totalPagesNumber of pages
page.hasNexttrue when there is a further page
page.nextUrlURL of the next page or null
page.previousUrlURL 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.dateTime and the GPS numbers yield null when 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 %}
  ]
}

On this page