---
title: "Liquid script step - Workflow Suite"
description: "Read your store with the shopify filter and change it with action tags, all in one Liquid script step. Syntax, options, output and limits."
canonical: "https://docs.workflow-suite.app/liquid-step"
---

# Liquid script step

The **Liquid script** step runs a whole script written in Liquid, the template language you may know from Shopify themes. One script can read your store, decide, and change it. It suits people who think in Liquid rather than in JavaScript, and jobs that would otherwise take a query, a loop, a condition and an action.

For Liquid inside ordinary fields, see [Liquid in every field](https://docs.workflow-suite.app/liquid-in-fields.md).

## Add it

1. Press **+** and pick **Liquid script**.
2. Write the **Liquid script**. **Insert a variable at the end** adds a variable from the picker. A script can be 50 KB at most.
3. Optional: add **options**, values the script reads as `options.NAME`.

## Read the store: the shopify filter

Write a GraphQL **query**, pipe it through `shopify`, and you get the answer at once as `data` and `errors`:

```liquid
{% capture query %}
  query {
    product(id: {{ event.payload.admin_graphql_api_id | json }}) {
      id
      tags
      variants(first: 50) { nodes { id price compareAtPrice } }
    }
  }
{% endcapture %}

{% assign result = query | shopify %}
{% assign product = result.data.product %}
```

The filter only takes queries. A mutation is refused there.

## Change the store: the action tag

A **mutation** goes into an action tag:

```liquid
{% unless product.tags contains "sale" %}
  {% action "shopify" %}
    mutation {
      tagsAdd(id: {{ product.id | json }}, tags: ["sale"]) {
        userErrors { field message }
      }
    }
  {% endaction %}
{% endunless %}
```

Actions are **collected** while the script runs and performed afterwards, in order. So a script cannot read the answer of its own action. The first action that fails stops the rest, and `userErrors` fail the step.

Only `{% action "shopify" %}` with a GraphQL mutation is supported. For any other call use an **HTTP request** step, for email a **Send email** step. The editor lists other action types as a problem.

## Other tags and filters

| Syntax | What it does |
| --- | --- |
| `{% log %}` | Writes a line into the step's logs, up to 100 lines |
| `{% error "message" %}` | Fails the step with your message |
| `parse_json` | Turns JSON text into an object or a list |
| `json` | Turns a value into JSON text |
| `keys`, `values` | The keys or the values of an object |
| `graphql_arguments` | Writes an object as GraphQL arguments |
| `array`, `hash` | Empty list and empty object to start from: `{% assign found = array %}` |
| `event.preview` | `true` in a preview, so a script can behave differently in a test |

Standard Liquid works as usual: `assign`, `capture`, `if`, `unless`, `case`, `for` with `break`, and the standard filters. Including other templates (`render`, `include`) is not available, and an unknown filter is an error rather than silently ignored.

## Options

Options are named values next to the script, so you can change a tag or a threshold without touching the code. The script reads them as `options.NAME`. An option's value is a field like any other and takes variables.

A name that ends in `__number`, `__boolean` or `__array` (one entry per line) is converted to that type.

## What the step returns

| Variable | Holds |
| --- | --- |
| `steps.<id>.output.output` | What the script printed. When that is JSON, it is parsed, so later steps can read its fields or repeat over a list |
| `steps.<id>.output.logs` | The log lines |
| `steps.<id>.output.actions` | One entry per action: `type`, `ok` and `data`, the answer of the mutation |

A script that only prepares data simply prints JSON at the end:

```liquid
{{ found | json }}
```

> [!NOTE]
> **Preview**
> In a preview, queries run for real and actions are only listed: the step's result names the mutations it would have run. That makes a Liquid script safe to try against real data.

## Limits

- 20 seconds per step.
- 50 Shopify calls per step, queries and actions together.
- 256 KB of output.
- The script runs with the permissions you granted, and with the workflow's **Shopify Admin API version**. See [Every Shopify mutation and query](https://docs.workflow-suite.app/shopify-mutations-and-queries.md).

A script reads one page per query. Page through a long list with a `for` loop over a range and `break`, within the 50 calls, or let a scheduled workflow work in portions.

## Liquid script or code?

| | Liquid script | Run function |
| --- | --- | --- |
| Language | Liquid | JavaScript |
| Calls other systems | No | Yes, with `ctx.fetch` |
| Runs in a preview | Yes: reads run, changes are listed | No |
| Time limit | 20 seconds | 5 seconds |

See [Run code and functions](https://docs.workflow-suite.app/run-code.md).

The template **Tag products that are on sale** is a complete example.
