# Welcome to DataJet

DataJet - Shopify automation platform with unlimited capabilities!

### DataJet - Shopify Automation Platform

DataJet is a powerful automation platform for Shopify that lets you build custom workflows, integrations, and automations using a Liquid-based scripting engine.

#### What Can You Automate?

**Store Operations**

* Manage orders, customers, products, and metaobjects (create, update, delete, tag)
* Automate inventory tracking and adjustments
* Process bulk operations across your catalog

**External Integrations**

* Connect to ERPs, CRMs, and third-party APIs via HTTP/REST
* Sync data with FTP/SFTP servers (upload, download, manage files)
* Send transactional emails with custom HTML templates
* Trigger and extend Shopify Flows

**Data Processing**

* Import CSV, JSON, and XML files from any source
* Transform and map data between systems
* Export reports and data feeds automatically

**Frontend Features**

* Power real-time inventory lookups on your storefront
* Build custom forms with backend processing
* Create gift card balance checkers
* Serve dynamic content to your theme

**Scheduled Tasks**

* Run scripts on schedules from every 10 minutes to monthly
* React to 70+ Shopify webhook events in real-time
* Build custom shipping rate calculators

#### Getting Started

✨ **Option 1: Build with AI (Recommended)** Use our **AI Assistant** to create automations through natural conversation. Simply describe what you want to automate, and the AI will generate, modify, and configure scripts for you — no coding required.

* Open **Build with AI** from the app menu
* Describe your automation in plain English
* Review and activate the generated script
* Ask follow-up questions to refine the logic

*Example: "Create a script that tags customers as VIP when they spend over $500"*

**Option 2: Use Pre-built Scripts** Browse the **Scripts Library** for ready-to-use automations. Configure variables and activate — no coding required.

**Option 3: Write Custom Scripts** Use the **Scripts Console** to write scripts in Liquid with our extended tags for HTTP, GraphQL, FTP, email, and more.

**Option 4: Let Us Build It** Contact us at **<support@datajet.app>** and we'll set up any automation you need at no additional cost.


# Introduction

### Creating a new script

Any store automation would start with setting up a script.

After opening the Scripts Console, you can select the type of script you want to create. Following that, you should see a blank template where you can insert all your logic with the help of the Liquid language.

After a script is created, it is in an inactive state. You can activate it through context menu triggered with a cog wheel next to the script title. Now your script is live. To see it running, you can either wait for your store events to trigger the script (if your script responds to webhooks), wait for the scheduler to run it, trigger it manually (for scheduled scripts with a manual trigger), query the autogenerated endpoint (for HTTP scripts), or wait for incoming emails received in your DataJet email inbox (for Email scripts).

<figure><img src="/files/FmzmHMTQj4A1kI7qHv9n" alt=""><figcaption></figcaption></figure>

### Running a script

Scripts can be triggered in a number of different ways:

* Automatically: following a schedule of your choice (scheduled script with a timer trigger).
* Manually: by clicking the 'Run' button next to the script title.
* Via HTTP call: hitting the autogenerated endpoint may trigger the script (and return a response).
* Event in your store: if your script responds to any event in your store (e.g., a new order).
* Incoming email: if your scripts responds to email received to your DataJet email address (<your-store@task.datajet-app.com>)

Each currently running script will appear under the 'Runs' section. Here, you can also copy the run ID or stop the script.

### Script output

Each script will log specific actions. All logs appear under the 'Logs' section. Each log belongs to one of the following categories:

* INFO: provides simple informational logs to inform you about the current status.
* WARNING: indicates something unexpected happened; however, script execution can continue.
* CRITICAL: denotes a critical error, resulting in script execution being stopped.
* ACTION: signifies that an action has been performed. Actions utilize app credits.

{% hint style="info" %}
You might opt in for email notifications anytime critical error happens. Just go to the app settings and provide your email address.
{% endhint %}

### App credits

Whenever a task performs an action (which can range from uploading a file to FTP, adding a tag, creating a fulfillment, etc.), a certain amount of credits will be deducted from your credits quota. Your credits quota depends on the plan you are subscribed to and renews at the beginning of each new billing period. The *DataJet Plan* comes with unlimited credits.

{% hint style="info" %}
Automatic email notifications are sent when usage reaches 80% of available credits.
{% endhint %}


# Scripts

Scripts (also referred to as tasks) are the starting points of any custom integrations. Each task is built using Liquid code. This is the same Liquid syntax that you know from Shopify, however, it has a few extensions. All available extensions are detailed in this documentation under the Liquid section.

There are six different types of scripts:

* [*Input*](/scripts/blank/input)*:* This script accepts a file uploaded by the user as an input. The file is uploaded through the user-facing dashboard. It is useful when you need to import data in bulk or modify Shopify objects based on a source file..
* [*HTTP*](/scripts/blank/http)*:* This script will be triggered anytime the autogenerated endpoint is called.
* [*Scheduled*](/scripts/blank/scheduled)*:* This script runs on a schedule. It can also be triggered manually by the user.
* [*Event*](/scripts/blank/event)*:* This script triggers anytime one of the Shopify events is detected, such as a new order or an updated customer.
* [*Shipping Rate*](/scripts/blank/shipping-rate)*:* This task returns custom shipping rates to the checkout.

### Script timeout

All scripts except shipping and http script have a timeout of 24 hours. After that time script automatically stops.

Shipping and http scripts have a timeout of 60 seconds. If you are using http script to perform long running tasks - consider using run filter to delegate the task to another script. Run triggers another script asynchronously meaning that it does not contribute to 60 second timeout.


# Input

Input task is triggered manually by the user. Before it runs, it will ask you to upload a file. The uploaded file is then available in the code editor via the preloaded `file` object.

### The `file` object

The code editor has a `file` object preloaded. Unlike other preloaded objects, `file` is **not** a parsed array of rows. Instead, it is an object with the following properties:

| Property       | Type   | Description                                                                     |
| -------------- | ------ | ------------------------------------------------------------------------------- |
| `file.name`    | string | The original filename including extension (e.g. `"products.csv"`, `"feed.xml"`) |
| `file.content` | string | The raw text content of the uploaded file                                       |
| `file.size`    | number | The file size in bytes                                                          |

Because `file.content` is a raw string, you need to parse it yourself using the appropriate filter for your file format.

### Parsing file content

Use the filename to detect the format and parse accordingly:

```liquid
{% assign content_parsed = "" %}

{% if file.name contains ".csv" %}
  {% assign content_parsed = file.content | parse_csv %}
{% elsif file.name contains ".xml" %}
  {% assign content_parsed = file.content | parse_xml %}
{% else %}
  {% log file.content %}
{% endif %}

{% if content_parsed %}
  {% for row in content_parsed %}
    {% log row %}
  {% endfor %}
{% endif %}
```

#### CSV files

The `parse_csv` filter parses a CSV string into an array of objects. The first row is used as headers — each subsequent row becomes an object with header names as keys.

```liquid
{% assign rows = file.content | parse_csv %}
{% for row in rows %}
  {{ row["Your Column Name"] | log }}
{% endfor %}
```

#### XML files

The `parse_xml` filter parses an XML string into a Liquid object. Elements become object keys, text content becomes values, and repeated elements become arrays.

```liquid
{% assign data = file.content | parse_xml %}
{% for product in data.products.product %}
  {% log product.title %}
{% endfor %}
```

#### JSON files

For JSON files, use the `parse_json` filter:

```liquid
{% assign data = file.content | parse_json %}
{% log data %}
```

#### Plain text and other formats

If the file is not CSV, XML, or JSON, you can work with `file.content` directly as a string. Use Liquid string filters like `split`, `strip`, etc.

```liquid
{% assign lines = file.content | split: "\n" %}
{% for line in lines %}
  {% log line %}
{% endfor %}
```

### Example: GraphQL mutation with CSV import

This example reads a CSV file with an `email` column and creates a Shopify customer for each row:

```liquid
{% capture mutation %}
  mutation customerCreate($input: CustomerInput!) {
    customerCreate(input: $input) {
      customer {
        id
      }
      userErrors {
        field
        message
      }
    }
  }
{% endcapture %}

{% json variables %}
  {
    "input": {
      "email": ""
    }
  }
{% endjson %}

{% assign rows = file.content | parse_csv %}

{% for row in rows %}
  {% assign variables.input.email = row['email'] %}
  {% assign result = mutation | graphql: variables %}

  {% if result.customerCreate.userErrors != empty %}
    {{ result.customerCreate.userErrors[0] | log }}
  {% else %}
    {{ "Created new customer with id: " | append: result.customerCreate.customer.id | log }}
  {% endif %}
{% endfor %}
```

Lines 1-13 capture a GraphQL mutation for creating a customer. Lines 15-21 define the variables required for the mutation. The CSV file is then parsed using `parse_csv`, and the `for` loop iterates through each row, assigning the row's email to `variables.input.email`. With mutation input prepared, the GraphQL query executes and creates the customer.

### Example: XML product feed import

```liquid
{% assign catalog = file.content | parse_xml %}
{% assign products = catalog.catalog.products.product %}

{% for product in products %}
  {% log product.sku | append: ": " | append: product.name | append: " ($" | append: product.price | append: ")" %}
{% endfor %}
```


# HTTP

Anytime you create HTTP task, an endpoint will be generated. By calling this endpoint, you can trigger the task. Additionally, you can return a response to any client that called this endpoint. It might be useful when you need to fetch store's data that is not available through Shopify Storefront API (inventory levels lookup, gift card balance lookup etc.).

### Calling autogenerated task endpoint

After your task is created, you can find autogenerated endpoint in app dashboard (Scripts Console, bottom left section after selecting task)

It will look something like that:

`https://your-store-name.myshopify.com/apps/datajet/task/task_id`

When calling the endpoint from your storefront you can use this short version:

`/apps/datajet/task/task_id`

{% hint style="info" %}
Task responds only to POST and GET request.
{% endhint %}

Additionally, you need to include header parameter named `token`. You will find value for this header just under autogenerated endpoint. Token validation can be disabled in task options.

{% hint style="danger" %}
Endpoint won't work if your storefront is password protected. Disable storefront password or contact us to get development endpoint for your task.
{% endhint %}

### Returning response to client

Let's have a look at simple example in which our task is going to return `"status": "ok"` response to client.

```javascript
{% json response %}
    {
        "body": {
            "status": "ok"
        },
        "status": 200
    }
{% endjson %}
```

In above example, `response` is a global object. This variable is going to be always returned to the client when HTTP task is called. This is what would be returned to client:

```javascript
{
    status: "ok"
}
```

### Request rate limits

All request use Fixed Window algorithm for incoming request control.

{% hint style="info" %}
Request are limited to 60 requests / minute
{% endhint %}

When limit is reached a `429 Too Many Requests` code is returned. Additionally you can find current limits in response headers:

* ratelimit-limit
* ratelimit-policy
* ratelimit-remaining
* ratelimit-reset<br>


# Scheduled

This task runs on a schedule defined by the user. Schedule expression follows linux cron expression syntax. It means that you can define any schedule you want!

{% hint style="info" %}
You can also disable the schedule and run the task manually anytime you want.
{% endhint %}

Task might be useful when you want to perform specific, repetitive actions on the products, customers or orders or any other objects. For example, you will be able to make all products unavailable at 10PM and make them available again at 8AM.


# Event

This task is triggered anytime an event in Shopify happens. When task is created, Shopify webhook is created as well. The payload of the webhook will be available for your use inside task's code.

Here is an example of how you can add a tag to order anytime new order is created.

{% code lineNumbers="true" %}

```javascript
{% assign order = payload %}

{% capture mutation %}
  mutation tagsAdd($id: ID!, $tags: [String!]!) {
    tagsAdd(id: $id, tags: $tags) {
      node {
        id
      }
      userErrors {
        field
        message
      }
    }
  }
{% endcapture %}

{% json variables %}
  {
    "id": "{{ order.admin_graphql_api_id }}",
    "tags": "datajet-tag"
  }
{% endjson %}

{% assign result = mutation | graphql: variables %}
```

{% endcode %}

Object payload is preloaded anytime task is triggered that is why it is available for us at very first line of the code. For readability we assign payload to `order` variable.

If you are not sure what is inside webhook payload you can reference this site provided by Shopify:

{% embed url="<https://shopify.dev/docs/admin-api/rest/reference/events/webhook>" %}

Complete list of events available in the app:

`checkouts/delete`\
`checkouts/create`\
`checkouts/update`\
`collections/delete`\
`collections/create`\
`collections/update`\
`customers/create`\
`customers/delete`\
`customers/disable`\
`customers/enable`\
`customers/update`\
`customer_groups/create`\
`customer_groups/delete`\
`customer_groups/update`\
`draft_orders/create`\
`draft_orders/delete`\
`draft_orders/update`\
`fulfillments/create`\
`fulfillments/update`\
`inventory_items/create`\
`inventory_items/delete`\
`inventory_items/update`\
`inventory_levels/connect`\
`inventory_levels/disconnect`\
`inventory_levels/update`\
`locations/create`\
`locations/delete`\
`locations/update`\
`orders/cancelled`\
`orders/create`\
`orders/delete`\
`orders/edited`\
`orders/fulfilled`\
`orders/paid`\
`orders/partially_fulfilled`\
`orders/updated`\
`order_transactions/create`\
`products/create`\
`products/delete`\
`products/update`\
`refunds/create`\
`shop/update`\
`tender_transactions/create`\
`themes/create`\
`themes/delete`\
`themes/publish`\
`themes/update`


# Shipping Rate

{% hint style="info" %}
Shipping Rate task is available only on Advanced Shopify plan or higher.
{% endhint %}

With this task you can create your custom shipping rates returned on checkout shipping step. Task allows creating complex logic to calculate shipping rate based on:

* cart items
* shipping address
* customer (Shopify Script Editor required)

Incoming request has following format:

```
  {
    "rate": {
      "origin": {
        "country": "CA",
        "postal_code": "K2P1L4",
        "province": "ON",
        "city": "Ottawa",
        "name": null,
        "address1": "150 Elgin St.",
        "address2": "",
        "address3": null,
        "phone": "16135551212",
        "fax": null,
        "email": null,
        "address_type": null,
        "company_name": "Jamie D's Emporium"
      },
      "destination": {
        "country": "CA",
        "postal_code": "K1M1M4",
        "province": "ON",
        "city": "Ottawa",
        "name": "Bob Norman",
        "address1": "24 Sussex Dr.",
        "address2": "",
        "address3": null,
        "phone": null,
        "fax": null,
        "email": null,
        "address_type": null,
        "company_name": null
      },
      "items": [
        {
          "name": "Short Sleeve T-Shirt",
          "sku": "",
          "quantity": 1,
          "grams": 1000,
          "price": 1999,
          "vendor": "Jamie D's Emporium",
          "requires_shipping": true,
          "taxable": true,
          "fulfillment_service": "manual",
          "properties": null,
          "product_id": 48447225880,
          "variant_id": 258644705304
        }
      ],
      "currency": "USD",
      "locale": "en"
    }
  }
```

You can access this payload through `request` object:

```
{{request.body.rate | log }}
```

Above line outputs payload to logs.

You can now parse available information to calculate additional shipping rates returned on checkout shipping step.

Example response would look like this:

```
{% json response %}
  {
    "body": {
      "rates": [
        {
          "service_name": "canadapost-overnight",
          "service_code": "ON",
          "total_price": "1295",
          "description": "This is the fastest option by far",
          "currency": "CAD",
          "min_delivery_date": "2013-04-12 14:48:45 -0400",
          "max_delivery_date": "2013-04-12 14:48:45 -0400"
        }
      ]
    }
  }
{% endjson %}
```

On checkout you should see:<br>

<figure><img src="/files/lVgk6IEVFSTmDOl6u8w5" alt=""><figcaption></figcaption></figure>


# Shipping Rate Context

## Shipping Rate Context

The **Shipping Rate Context** feature enables you to access logged-in customer information within your Carrier Calculated Shipping Rate scripts. This allows you to create personalized shipping options based on customer data, such as offering free shipping to VIP customers or applying custom rates based on customer tags.

### Requirements

* **Shopify Plus** plan (Checkout UI Extensions are only available on Shopify Plus)
* Shipping Rate script configured in your store

### How It Works

The Shipping Rate Context feature consists of two parts:

1. **Checkout UI Extension** - A hidden block that runs during checkout and attaches customer metadata to cart line items
2. **Shipping Rate Script** - Your custom Liquid script that reads the customer metadata and applies shipping logic

#### Data Flow

```
Customer enters checkout
        ↓
Shipping Rate Context extension runs
        ↓
Customer data (email, ID) is attached to line items as `_datajet` property
        ↓
Carrier Calculated Shipping request is made
        ↓
Your shipping rate script reads `_datajet` property
        ↓
Script queries customer data (e.g., tags) via GraphQL
        ↓
Custom shipping rates are returned based on customer data
```

### Setup Instructions

#### Step 1: Add the Shipping Rate Context Block to Checkout

1. Navigate to **Settings > Checkout** in your Shopify admin
2. Click **Customize** to open the checkout editor
3. In the checkout editor, add a new block
4. Search for and select **Shipping Rate Context** from the DataJet app
5. The block is invisible to customers and can be placed anywhere in the checkout
6. Save your changes

#### Step 2: Create Your Shipping Rate Script

In your Carrier Calculated Shipping rate script, you can now access customer data through the `_datajet` property on line items.

### Customer Data Available

The `_datajet` property contains a JSON object with the following structure:

```json
{
  "customer": {
    "email": "customer@example.com",
    "id": "1234567890"
  }
}
```

| Field            | Description                                                    |
| ---------------- | -------------------------------------------------------------- |
| `customer.email` | The logged-in customer's email address                         |
| `customer.id`    | The Shopify customer ID (numeric, without the `gid://` prefix) |

> **Note:** If the customer is not logged in, the `customer` field will be `null`.

### Example: Free Shipping for Tagged Customers

The following example demonstrates how to offer free shipping to customers with a specific tag (e.g., "FREE-SHIPPING"):

```liquid
{% capture customer_query %}
  query($id: ID!) {
    customer(id: $id) {
      id
      displayName
      email
      tags
    }
  }
{% endcapture %}

{% assign free_shipping = false %}
{% assign required_customer_tag = "FREE-SHIPPING" %}

{% comment %}
  _datajet property only exists on line item if Shipping Rate Context
  Checkout UI block has been added to store's Checkout
{% endcomment %}
{% assign metadata = request.body.rate.items | map: "properties._datajet" | compact | first %}

{% if metadata %}
  {% assign parsed_metadata = metadata | parse %}
  {% assign logged_in_customer = parsed_metadata.customer %}
{% endif %}

{% if logged_in_customer != blank %}

  {% json customer_variables %}
    {
      "id": "gid://shopify/Customer/{{logged_in_customer.id}}"
    }
  {% endjson %}

  {% graphql query: customer_query, variables: customer_variables as result %}
  {% assign customer_tags = result.customer.tags %}

  {% if customer_tags contains required_customer_tag %}
    {% assign free_shipping = true %}
  {% endif %}
{% endif %}

{% comment %} Returning response {% endcomment %}
{% json response %}
  {
    "body": {
      "rates": [
        {% if free_shipping %}
          {
            "service_name": "Free Shipping",
            "service_code": "FS",
            "total_price": "0",
            "currency": "USD"
          }
        {% endif %}
      ]
    }
  }
{% endjson %}

{% return response %}
```

### Use Cases

Here are some common scenarios where Shipping Rate Context is useful:

#### VIP Customer Shipping

Offer free or discounted shipping to loyalty program members or VIP customers identified by tags.

#### B2B Shipping Rates

Apply different shipping rates for wholesale/B2B customers vs. retail customers.

#### Regional Shipping Rules

Combine customer data with address information to create sophisticated regional shipping rules.

#### Subscription Customer Benefits

Provide special shipping options for customers subscribed to your membership program.

### Troubleshooting

#### Customer data is not available

1. **Verify the extension is installed**: Go to **Settings > Checkout > Customize** and confirm the "Shipping Rate Context" block is added
2. **Check customer login status**: The `_datajet` property only contains customer data when the customer is logged in during checkout
3. **Confirm Shopify Plus**: This feature requires a Shopify Plus plan

#### The `_datajet` property is missing

* Ensure the Shipping Rate Context checkout block is properly configured and saved
* The block must be active in the live checkout (not just in draft/preview mode)

#### GraphQL query returns null

* Verify the customer ID format includes the full GID prefix: `gid://shopify/Customer/{id}`
* Check that your app has the required permissions to query customer data


# Shipping Calculator Block

The **Shipping Calculator** is a theme app extension block that lets shoppers preview shipping rates from any storefront page (product detail page or cart page) before they reach checkout. The block reuses your active **Shipping Rate** script — no new script type or backend code is required.

{% hint style="info" %}
This block calls the same `/task/{id}` endpoint that Shopify's Carrier Service uses at checkout, but goes through the app proxy. Your existing shipping\_rate script handles both flows.
{% endhint %}

## How It Works

1. Merchant adds one of two blocks to a theme section in the theme editor:
   * **Shipping Calculator (Product)** — for product detail pages
   * **Shipping Calculator (Cart)** — for the cart page
2. Shopper enters address fields (postal code is required, other fields are toggleable per block).
3. The block sends a `POST` request to `/apps/{proxy-subpath}/task/{script_id}` containing the destination address, line items, currency, locale, and logged-in customer info.
4. Your shipping\_rate script receives the payload as `request.body.rate` (identical shape to the checkout flow) and returns rates.
5. The block parses the response and renders each rate inline.

### Data flow

```
Shopper fills form → Block builds payload → POST /apps/{proxy}/task/{id}
        ↓
DataJet runs your shipping_rate script with request.body.rate
        ↓
Script returns { "rates": [...] }
        ↓
Block renders rates list to shopper
```

## Setup

### Step 1: Activate a Shipping Rate Script

You must have an active **Shipping Rate** script in DataJet before the calculator can return rates. See [Shipping Rate](/scripts/blank/shipping-rate) for setup. Note its **script ID** (or **handle**) — you'll paste it into the block settings.

{% hint style="warning" %}
Shopify allows only one active shipping\_rate script per merchant. If you don't have one, you'll get an empty response.
{% endhint %}

### Step 2: Add the Block to Your Theme

1. In Shopify admin, open **Online Store > Themes** and click **Customize** on the active theme.
2. Navigate to a product page (for the Product block) or the cart page (for the Cart block).
3. In a section that accepts blocks, click **Add block** and pick:
   * **Shipping Calculator (Product)** on a product section, or
   * **Shipping Calculator (Cart)** on the cart section.
4. Paste the script ID from Step 1 into the **Shipping rate script ID** field.
5. Configure which address fields to show (see below) and save.

## Block Settings

Both blocks expose the same settings:

| Setting                    | Type     | Default              | Description                                                                         |
| -------------------------- | -------- | -------------------- | ----------------------------------------------------------------------------------- |
| `script_id`                | text     | *required*           | Script ID (Mongo ObjectId) or handle of your active shipping\_rate script.          |
| `title`                    | text     | `Calculate shipping` | Heading rendered above the form. Leave blank to hide.                               |
| `button_label`             | text     | `Calculate`          | Submit button text.                                                                 |
| `show_country`             | checkbox | `true`               | Render a country input.                                                             |
| `show_province`            | checkbox | `false`              | Render a province / state input.                                                    |
| `show_city`                | checkbox | `true`               | Render a city input.                                                                |
| `show_street`              | checkbox | `false`              | Render a street (address1) input.                                                   |
| `show_address2`            | checkbox | `false`              | Render an address line 2 input.                                                     |
| `prefill_customer_address` | checkbox | `true`               | When the shopper is logged in, pre-fill the inputs from `customer.default_address`. |

The postal/ZIP code input is always rendered.

## Request Payload

The block POSTs JSON in the same shape your shipping\_rate script already understands:

```json
{
  "rate": {
    "destination": {
      "country": "US",
      "postal_code": "10001",
      "city": "New York",
      "name": "Bob Norman"
    },
    "items": [
      {
        "name": "Short Sleeve T-Shirt",
        "sku": "TS-001",
        "quantity": 1,
        "grams": 1000,
        "price": 1999,
        "vendor": "Acme",
        "product_id": 48447225880,
        "variant_id": 258644705304,
        "properties": null,
        "requires_shipping": true,
        "taxable": true
      }
    ],
    "currency": "USD",
    "locale": "en",
    "customer": {
      "id": 12345,
      "email": "bob@example.com",
      "first_name": "Bob",
      "last_name": "Norman",
      "tags": ["VIP"]
    }
  },
  "_datajet_source": "storefront_calculator",
  "_datajet_mode": "product"
}
```

Notes:

* **`destination`** — only includes keys whose form inputs are present and non-empty. Postal code is always present; the others are gated by block settings.
* **`items`** — for the **Product** block, contains a single item built from `product.selected_or_first_available_variant` at render time. For the **Cart** block, all line items are fetched from `/cart.js` at submit time.
* **`origin`** — **not included**. The storefront does not know the merchant's warehouse address. If your script needs `request.body.rate.origin`, hardcode it or fetch it from a DataJet [variable](/misc/variables).
* **`customer`** — `null` when the shopper is logged out. Otherwise contains `id`, `email`, `first_name`, `last_name`, `tags`.
* **`_datajet_source`** — always `"storefront_calculator"`. Use this to distinguish calls from the block from real Shopify Carrier Service calls.
* **`_datajet_mode`** — `"product"` or `"cart"`.

### Distinguishing block calls in your script

If you want different logic when the block calls vs. when Shopify's Carrier Service calls at checkout:

```liquid
{% if request.body._datajet_source == "storefront_calculator" %}
  {% comment %} request from the storefront block — origin is missing {% endcomment %}
{% else %}
  {% comment %} request from Shopify Carrier Service at checkout {% endcomment %}
{% endif %}
```

## Response Format

The block expects the same response your shipping\_rate script already returns:

```json
{
  "rates": [
    {
      "service_name": "Standard",
      "service_code": "STD",
      "total_price": "1295",
      "currency": "USD",
      "description": "5–7 business days"
    }
  ]
}
```

`total_price` is in cents (matching Shopify's convention). The block formats it via `Intl.NumberFormat` using the rate's `currency`. If `description` is present, it's rendered under the rate name.

States the block renders:

| Condition                       | UI                                                                             |
| ------------------------------- | ------------------------------------------------------------------------------ |
| `rates` array has entries       | List of rates, one per row, with name + formatted price + optional description |
| `rates` is empty                | Empty-state copy ("No shipping options available for this address.")           |
| Non-2xx response or fetch error | Error copy ("Could not calculate shipping. Please try again.")                 |

## Example: Different Rates for Block vs. Checkout

A typical pattern is to return broad estimates from the block but precise per-carrier rates at checkout:

```liquid
{% if request.body._datajet_source == "storefront_calculator" %}
  {% json response %}
    {
      "body": {
        "rates": [
          { "service_name": "Standard (estimate)", "service_code": "EST", "total_price": "999", "currency": "USD" }
        ]
      }
    }
  {% endjson %}
{% else %}
  {% comment %} full carrier lookup for real checkout request {% endcomment %}
  {% comment %} ... your existing logic ... {% endcomment %}
{% endif %}

{% return response %}
```

## Logged-In Customer Data

When the shopper is logged in, the block renders customer info server-side via Liquid's `customer` global and includes it in `request.body.rate.customer`. This means:

* `customer.tags` is available **without** needing the [Shipping Rate Context](/scripts/blank/shipping-rate/shipping-rate-context) checkout extension. The block delivers tags directly in the payload.
* If `prefill_customer_address` is enabled and `customer.default_address` exists, the form inputs are pre-populated.

## Limitations

* **Variant changes on PDP** — the Product block snapshots the current variant at server-render time. If the shopper switches variants on the PDP, the payload still references the originally rendered variant until the page reloads.
* **One block per page** — multiple instances on the same page work, but each makes its own request. There is no shared state.
* **No origin** — the block does not provide `request.body.rate.origin`. Scripts that depend on it must source it elsewhere.
* **App proxy only** — the request goes through the Shopify app proxy. Make sure your app proxy is configured in `shopify.app.toml` and the subpath in the block's JS asset matches.

## Troubleshooting

#### Form submits but no rates appear

* Confirm a shipping\_rate script is **active** in DataJet.
* Check the **script ID** in block settings matches the active script.
* Inspect the script's run logs in DataJet — look for the request body and any errors.

#### Error message appears immediately

* Open the browser Network tab and inspect the `POST /apps/{proxy}/task/{id}` response.
* `404` usually means the app proxy subpath in the block's JS does not match `shopify.app.toml`'s `[app_proxy].subpath`.
* `5xx` means the script threw — check DataJet's script logs.

#### Customer fields don't pre-fill

* Pre-fill only works when the shopper is logged in **and** has a default address saved on their Shopify customer profile.
* Confirm `prefill_customer_address` is checked in block settings.

#### Cart block sends wrong line items

* The cart is fetched from `/cart.js` at submit time. If items look stale, force a hard reload to clear any cached cart state.


# Creating a custom script

Built-in code editors give you a chance to build any integration you require. Possibilities are endless - your limit is your imagination.\
\
There are a couple of things to keep in mind when creating a task.\
\
**1. Permissions**

To maintain the highest level of security, the app requests only very basic permissions when installed. However, when creating a task, some additional permissions might be required. For example, you might need the `write_orders` permission to add a tag to an order. The app automatically detects the required permissions when the task code is compiled. However, you need to assist the compiler a bit.\
\
When defining REST/GraphQL requests do it at very top of the script:

```javascript
{%- comment -%}Start REST and GraphQL definitions{%- endcomment -%}
{% capture mutation %}
  mutation tagsAdd($id: ID!, $tags: [String!]!) {
    tagsAdd(id: $id, tags: $tags) {
      node {
        id
      }
      userErrors {
        field
        message
      }
    }
  }
{% endcapture %}

```

\
Next, you can use dummy input to tell compiler what variables it should expect.

```javascript
{%- comment -%} Feed compiler with dummy values to evaluate permissions required.{%- endcomment -%}
{% if mode.compiler %}
  {% json dummy_mutation_variables %}
    {
      "id": "gid://shopify/Order/123456",
      "tags": "test"
    }
  {% endjson %}
  {% assign result = mutation | graphql: dummy_mutation_variables %}
{% endif %}
{%- comment -%}END REST and GraphQL definitions{%- endcomment -%}
```

Above code snippet won't be executed when your task actually runs. It is because it is wrapped in `if` condition. `mode.compiler` is set to `true` only when creating/editing task.

After adding the above code, the compiler knows what to expect from your task and can easily evaluate the permissions it needs to run. You can now save it and return to the app dashboard. You will be prompted to update app permissions.

<figure><img src="/files/SexkcdcKh3tZchSqNdvb" alt=""><figcaption></figcaption></figure>

{% hint style="warning" %}
After clicking the *Update* button, the app should prompt you for additional permissions. After granting permissions you should be redirected back to Scripts Console.
{% endhint %}

Here is how you would tell compiler about any REST request:

```javascript
{%- comment -%}Start REST and GraphQL definitions{%- endcomment -%}
{% json fulfillment_request_options %}
    {
      "path": "",
      "method": "POST",
      "body": {
        "fulfillment": {
          "location_id": "123456",
          "notify_customer": false,
          "status": "success"
        }
      }
    }
{% endjson %}

{%- comment -%} Feed compiler with dummy values to evaluate permissions required.{%- endcomment -%}
{% if mode.compiler %}
  {% assign order_fulfillment_endpoint = "/orders/123456/fulfillments.json" %}
  {% assign fulfillment_request_options["path"] = order_fulfillment_endpoint %}
  {% assign result = fulfillment_request_options | rest %}
{% endif %}
{%- comment -%}END REST and GraphQL definitions{%- endcomment -%}
```


# Functions

Run your own JavaScript inside Shopify checkout — validate carts, customize delivery options and payment methods, apply automatic discounts, reshape cart lines.

Functions let you run your own JavaScript **inside Shopify checkout**. Unlike scripts, which run on DataJet's infrastructure in response to events and schedules, functions are deployed to Shopify and executed by Shopify itself — synchronously, while the buyer is checking out. That makes them the right tool for rules that must be enforced before an order can be placed.

You manage functions in the **Functions Console** section of the app.

{% hint style="info" %}
Not to be confused with the Liquid [function](/liquid/tags/function) tag, which calls a reusable Liquid script from another script. This section is about Shopify checkout functions.
{% endhint %}

There are five categories of functions:

* [*Cart and Checkout Validation*](/functions/functions/cart-and-checkout-validation)*:* block checkout with custom error messages — order limits, address rules, customer restrictions and any other rule you can express in code.
* [*Delivery Customization*](/functions/functions/delivery-customization)*:* hide, rename or reorder the delivery options shown at checkout.
* [*Payment Customization*](/functions/functions/payment-customization)*:* hide, rename or reorder payment methods.
* [*Discounts*](/functions/functions/discounts)*:* apply automatic product and order discounts based on your own rules.
* [*Cart Transform*](/functions/functions/cart-transform)*:* merge cart lines into bundles, expand bundle SKUs, update line presentation (one per store).

### Functions vs. scripts

|               | Scripts                           | Functions                         |
| ------------- | --------------------------------- | --------------------------------- |
| Runs on       | DataJet's infrastructure          | Shopify's checkout infrastructure |
| Trigger       | Events, webhooks, schedules, HTTP | Every checkout, automatically     |
| Language      | Liquid                            | JavaScript                        |
| Can call APIs | Yes (graphql, http, rest…)        | No — sandboxed, input data only   |
| Purpose       | Automation and integrations       | Checkout rules and customization  |
| Credits       | Consume credits                   | Free — no credits used            |

### Creating a function

In the Functions Console, open a category folder and click **Add new**. You choose:

* **Title** — up to 100 characters; letters, numbers, spaces and `_-|[]` are allowed.
* **Family** — decides which checkout data your code receives as input. See [Families and Variables](/functions/functions/families-and-variables) for the full list. The family can't be changed after creation, so pick the one that covers the data your rule needs.
* **Run on** (validations only) — the checkout steps where your code executes: *Cart*, *Checkout interaction* and/or *Checkout completion*. The default is checkout interaction + completion.

New functions are created **turned off**, with starter code that documents the expected return shape for the category. Turn a function on from its menu in the tree, or with the Active toggle in the editor.

{% hint style="info" %}
The number of functions you can have active at the same time depends on your plan:

Development and sandbox stores get the full limit of 25 (Shopify's maximum). During the free trial you can activate 1 function. You can create and edit any number of functions — the limit only applies to turning them on.

Active discounts additionally count against Shopify's store-wide cap of 25 automatic discounts, shared with the discounts created outside DataJet.
{% endhint %}

| Plan          | Active functions |
| ------------- | ---------------- |
| Basic Plan    | —                |
| Advanced Plan | 1                |
| Pro Plan      | 5                |
| DataJet Plan  | 25               |

### Permissions

Managing functions requires additional access scopes (checkout validations, delivery customizations, payment customizations, discounts and cart transforms). If they haven't been granted yet, the Functions Console shows an **Additional permissions required** banner — click **Grant permissions** and approve the request.

### Writing code

Your code is a JavaScript **function body**. It receives two variables: `input`, containing the checkout data selected by the function's family, and `vars`, holding your [code variables](/functions/functions/families-and-variables) (merged global + function scope). It returns a result whose shape depends on the category — validation errors, or delivery/payment/discount/transform operations. The editor's autocomplete knows the exact shape of both — type `input.` or `vars.` to explore them.

```javascript
var errors = [];

if (parseFloat(input.cart.cost.totalAmount.amount) > 5000) {
  errors.push({ message: 'Orders above 5000 are not allowed', target: '$.cart' });
}

return errors;
```

The sandbox is minimal by design:

* ES2020 JavaScript — no browser APIs, no `fetch`, no network access.
* The only data available is the `input` object and your `vars`.
* Execution is synchronous and must be fast — it runs on every checkout.

{% hint style="info" %}
Functions **fail open**: if your code throws an error, checkout continues as if the function returned nothing. Broken code never blocks your buyers — but it also means a faulty rule silently stops being enforced, so test your changes on a real checkout after saving.
{% endhint %}

### The editor

* **Save** (or `Cmd/Ctrl+S`) deploys your code — if the function is active, the new version is live at checkout immediately.
* Unsaved changes are kept as local drafts per function, surviving tab switches.
* **Function / Global / Query** open the three variable scopes — code variables (per function and shared) and the family's query variables. See [Families and Variables](/functions/functions/families-and-variables).
* **Last saved** opens the version history (`Cmd/Ctrl+Shift+H`) — every save is committed to git, and you can view and restore previous versions.

Every function can also be opened directly by link: `/functions/<id>` (the ID shown under the tree).

### Managing functions from the dashboard

The main dashboard's **Functions** tab lists all functions. Clicking one expands its **Function configuration** — the merchant-facing form driven by variable `metadata`, for adjusting values without touching code. The tab also supports bulk turn on/off, delete, and **export/import**: selected functions download as a `.datajet` file (code, settings and variables included) that can be imported on another store.

### The Shopify admin panel

Functions appear in the Shopify admin too — validations under **Settings → Checkout → Checkout rules**, customizations under their settings pages, discounts on the **Discounts** page. Cart transforms are the exception: they have no admin surface and are managed only from the Functions Console. Opening a DataJet rule there shows a panel with an **Open in DataJet** button that jumps straight to the function in the Functions Console.

### Runtime errors and logs

There is no in-app test run yet — functions execute live at checkout. If your code throws, the error is recorded in the function's execution logs on Shopify's side and checkout proceeds unaffected. A Logs panel in the Functions Console is coming soon.


# Cart and Checkout Validation

Block checkout with custom error messages — order limits, address rules, customer restrictions and any rule you can express in JavaScript.

A validation function decides whether the buyer may continue through checkout. Your code inspects the cart and returns a list of errors: an empty list allows checkout, any error blocks it and is shown to the buyer.

```javascript
var errors = [];

if (parseFloat(input.cart.cost.totalAmount.amount) > 5000) {
  errors.push({ message: 'Orders above 5000 are not allowed', target: '$.cart' });
}

return errors;
```

Validations run on Shopify's servers and are enforced throughout checkout, so they can't be bypassed by the client.

### Return value

Return an **array of errors**. Each error is either:

* a string — shown at the top of checkout, or
* an object `{ message, target }` — shown at a specific checkout field.

Returning an empty array allows the checkout to proceed.

Instead of the errors array you can also return the full Shopify operations shape:

```javascript
return { operations: [{ validationAdd: { errors: errors } }] };
```

### Error targets

The `target` controls where the message appears at checkout:

| Target                                              | Where it's shown          |
| --------------------------------------------------- | ------------------------- |
| `$.cart`                                            | Top of checkout (default) |
| `$.cart.buyerIdentity.email`                        | Email field               |
| `$.cart.buyerIdentity.phone`                        | Phone field               |
| `$.cart.deliveryGroups[0].deliveryAddress.address1` | Shipping address field    |
| `$.cart.billingAddress.address1`                    | Billing address field     |
| `$.cart.poNumber`                                   | PO number field           |

For the address targets, the same set of fields is available on both shipping and billing: `address1`, `address2`, `city`, `zip`, `provinceCode`, `countryCode`, `firstName`, `lastName`, `company`, `phone`.

{% hint style="warning" %}
Address and billing fields are only present in the `input` if the function's family selects them — for example the *Address*, *Billing address* or *Billing + Shipping address* families. Pick the family to match the data your rule checks. See [Families and Variables](/functions/functions/families-and-variables).
{% endhint %}

### Run on — checkout steps

Validations can run at up to three points of the buyer journey, chosen with the **Run on** setting:

| Step                 | When your code runs                      |
| -------------------- | ---------------------------------------- |
| Cart                 | On the cart page, before checkout starts |
| Checkout interaction | While the buyer fills in checkout        |
| Checkout completion  | At the final submit, when the buyer pays |

The default is *Checkout interaction* + *Checkout completion*. Outside the selected steps the function returns no errors and the buyer is not blocked. At least one step must be selected.

### Examples

**Order value limit:**

```javascript
var errors = [];

if (parseFloat(input.cart.cost.totalAmount.amount) > 5000) {
  errors.push({ message: 'Orders above 5000 are not allowed', target: '$.cart' });
}

return errors;
```

**Require a phone number:**

```javascript
var errors = [];

if (!input.cart.buyerIdentity || !input.cart.buyerIdentity.phone) {
  errors.push({ message: 'Please provide a phone number', target: '$.cart.buyerIdentity.phone' });
}

return errors;
```

**Block PO boxes in the shipping address** (use an address family):

```javascript
var errors = [];

(input.cart.deliveryGroups || []).forEach(function (group) {
  var address = group.deliveryAddress;
  if (address && /p\.?\s*o\.?\s*box/i.test(address.address1 || '')) {
    errors.push({
      message: 'We cannot ship to PO boxes',
      target: '$.cart.deliveryGroups[0].deliveryAddress.address1'
    });
  }
});

return errors;
```

**Limit quantity for tagged products** (use the *Product* family with the `hasProductTags` variable set to `["limited"]`):

```javascript
var errors = [];

(input.cart.lines || []).forEach(function (line) {
  var product = line.merchandise && line.merchandise.product;
  var isLimited = product && product.hasTags && product.hasTags.some(function (t) { return t.hasTag; });
  if (isLimited && line.quantity > 2) {
    errors.push({ message: 'Maximum 2 units of limited products per order', target: '$.cart' });
  }
});

return errors;
```

### Notes

* An error message blocks checkout entirely — the buyer cannot proceed until the condition is resolved.
* If your code throws, the validation is skipped and checkout continues (fail open).
* Shopify allows at most 25 active validations per store; your plan may allow fewer — see [Functions](/functions/functions).
* See also: [Delivery Customization](/functions/functions/delivery-customization), [Payment Customization](/functions/functions/payment-customization).


# Delivery Customization

Hide, rename or reorder the delivery options shown at checkout with JavaScript.

A delivery customization changes how delivery options are presented at checkout. Your code inspects the cart and returns a list of operations — hide an option, rename it, or move it to a different position.

```javascript
var ops = [];

input.cart.deliveryGroups.forEach(function (group) {
  group.deliveryOptions.forEach(function (option) {
    if (option.title === 'Express') {
      ops.push({ deliveryOptionHide: { deliveryOptionHandle: option.handle } });
    }
  });
});

return { operations: ops };
```

The available delivery options are in `input.cart.deliveryGroups[].deliveryOptions`, each with a `handle` and `title`.

### Operations

Return an array of operations, or `{ operations: [...] }`. Each operation is an object with exactly one of these keys:

| Operation | Shape                                                                     | Effect                                     |
| --------- | ------------------------------------------------------------------------- | ------------------------------------------ |
| Hide      | `{ deliveryOptionHide: { deliveryOptionHandle: '...' } }`                 | Removes the option from checkout           |
| Rename    | `{ deliveryOptionRename: { deliveryOptionHandle: '...', title: '...' } }` | Changes the displayed title                |
| Move      | `{ deliveryOptionMove: { deliveryOptionHandle: '...', index: 0 } }`       | Moves the option to a position (0 = first) |

Returning an empty array leaves the delivery options unchanged. Operations of any other shape are ignored.

{% hint style="warning" %}
When reordering shipping options, the cheapest option must stay selected by default — this is Shopify policy.
{% endhint %}

### Examples

**Hide express shipping for heavy carts** (use the *General* or *Cart* family):

```javascript
var ops = [];
var total = parseFloat(input.cart.cost.totalAmount.amount);

if (total < 50) {
  input.cart.deliveryGroups.forEach(function (group) {
    group.deliveryOptions.forEach(function (option) {
      if (option.title === 'Express') {
        ops.push({ deliveryOptionHide: { deliveryOptionHandle: option.handle } });
      }
    });
  });
}

return { operations: ops };
```

**Rename an option for VIP customers** (use the *Address + Customer* or *Shipping* family with `hasCustomerTags` set to `["vip"]`):

```javascript
var ops = [];
var customer = input.cart.buyerIdentity && input.cart.buyerIdentity.customer;
var isVip = customer && customer.hasTags && customer.hasTags.some(function (t) { return t.hasTag; });

if (isVip) {
  input.cart.deliveryGroups.forEach(function (group) {
    group.deliveryOptions.forEach(function (option) {
      if (option.title === 'Standard') {
        ops.push({ deliveryOptionRename: { deliveryOptionHandle: option.handle, title: 'Standard (free for VIP)' } });
      }
    });
  });
}

return { operations: ops };
```

### Notes

* Delivery customizations don't use the **Run on** steps — they apply whenever delivery options are shown.
* If your code throws, no operations are applied and checkout shows the options unchanged (fail open).
* See also: [Cart and Checkout Validation](/functions/functions/cart-and-checkout-validation), [Payment Customization](/functions/functions/payment-customization).


# Payment Customization

Hide, rename or reorder payment methods at checkout with JavaScript.

A payment customization changes which payment methods are offered at checkout and how they are presented. Your code inspects the cart and returns a list of operations.

```javascript
var ops = [];

input.paymentMethods.forEach(function (method) {
  if (method.name === 'Cash on Delivery') {
    ops.push({ paymentMethodHide: { paymentMethodId: method.id } });
  }
});

return { operations: ops };
```

The available payment methods are in `input.paymentMethods`, each with an `id` and `name`.

### Operations

Return an array of operations, or `{ operations: [...] }`. Each operation is an object with exactly one of these keys:

| Operation     | Shape                                                              | Effect                                                                         |
| ------------- | ------------------------------------------------------------------ | ------------------------------------------------------------------------------ |
| Hide          | `{ paymentMethodHide: { paymentMethodId: '...' } }`                | Removes the payment method                                                     |
| Rename        | `{ paymentMethodRename: { paymentMethodId: '...', name: '...' } }` | Changes the displayed name                                                     |
| Move          | `{ paymentMethodMove: { paymentMethodId: '...', index: 0 } }`      | Moves the method to a position (0 = first)                                     |
| Order review  | `{ orderReviewAdd: { ... } }`                                      | B2B on Shopify Plus: submits the checkout as a draft order for merchant review |
| Payment terms | `{ paymentTermsSet: { ... } }`                                     | Shopify Plus only: net terms, due dates, deposits                              |

Returning an empty array leaves the payment methods unchanged. Operations of any other shape are ignored.

{% hint style="warning" %}
Wallets with logos (Shop Pay, Apple Pay, Google Pay) can be **hidden** but not renamed or reordered.
{% endhint %}

### Examples

**Hide Cash on Delivery above an order value** (use the *General* or *Cart* family):

```javascript
var ops = [];
var total = parseFloat(input.cart.cost.totalAmount.amount);

if (total > 500) {
  input.paymentMethods.forEach(function (method) {
    if (method.name === 'Cash on Delivery') {
      ops.push({ paymentMethodHide: { paymentMethodId: method.id } });
    }
  });
}

return { operations: ops };
```

**Put invoice payment first for B2B customers** (use a family that selects the customer, e.g. *Customer*):

```javascript
var ops = [];
var customer = input.cart.buyerIdentity && input.cart.buyerIdentity.customer;
var isB2B = customer && customer.hasTags && customer.hasTags.some(function (t) { return t.hasTag; });

if (isB2B) {
  input.paymentMethods.forEach(function (method) {
    if (method.name === 'Invoice') {
      ops.push({ paymentMethodMove: { paymentMethodId: method.id, index: 0 } });
    }
  });
}

return { operations: ops };
```

### Notes

* Payment customizations don't use the **Run on** steps — they apply whenever payment methods are shown.
* If your code throws, no operations are applied and checkout shows the methods unchanged (fail open).
* See also: [Cart and Checkout Validation](/functions/functions/cart-and-checkout-validation), [Delivery Customization](/functions/functions/delivery-customization).


# Discounts

Apply automatic product and order discounts at checkout with JavaScript.

A discount function applies automatic discounts to cart lines or the whole order. Your code inspects the cart and returns a list of operations with discount candidates.

```javascript
var candidates = input.cart.lines.map(function (line) {
  return {
    targets: [{ cartLine: { id: line.id } }],
    value: { percentage: { value: 10 } },
  };
});

return {
  operations: [
    { productDiscountsAdd: { selectionStrategy: 'FIRST', candidates: candidates } },
  ],
};
```

The cart lines are in `input.cart.lines`, each with an `id` your candidates target. The discount's own settings are in `input.discount` (`discountClasses`).

{% hint style="info" %}
The function's **title** is shown to buyers at checkout as the discount name — pick something merchant-shippable like `10% off accessories`.
{% endhint %}

### Operations

Return an array of operations, or `{ operations: [...] }`. Each operation is an object with exactly one of these keys:

| Operation         | Shape                                                                        | Effect                          |
| ----------------- | ---------------------------------------------------------------------------- | ------------------------------- |
| Product discounts | `{ productDiscountsAdd: { selectionStrategy: 'FIRST', candidates: [...] } }` | Discounts individual cart lines |
| Order discounts   | `{ orderDiscountsAdd: { selectionStrategy: 'FIRST', candidates: [...] } }`   | Discounts the order subtotal    |

Each candidate is:

```javascript
{
  message: 'Loyalty discount',                 // optional, shown next to the discount
  targets: [{ cartLine: { id: line.id } }],    // product discounts target cart lines
  // targets: [{ orderSubtotal: { excludedCartLineIds: [] } }],  // order discounts
  value: { percentage: { value: 10 } },        // or { fixedAmount: { amount: '10.0' } }
}
```

`selectionStrategy` decides what happens when several candidates match the same target: `FIRST` applies the first one, `MAXIMUM` the largest. Returning an empty array applies no discount. Operations of any other shape are ignored.

### How DataJet manages the discount

* Discount functions appear in the Shopify admin under **Discounts** as automatic discounts.
* The function covers the **product** and **order** discount classes; discount codes and shipping discounts are not part of discount functions.
* Enabling in the Functions Console activates the discount immediately (no start/end scheduling); disabling deactivates it.
* DataJet discounts combine with other product, order and shipping discounts.
* Shopify allows at most **25 active automatic discounts per store** — this cap is shared with the discounts the merchant creates outside DataJet.

### Examples

**10% off products with a tag** (use the *Product* family with the `hasProductTags` variable):

```javascript
var candidates = [];

input.cart.lines.forEach(function (line) {
  var product = line.merchandise && line.merchandise.product;
  var tagged = product && product.hasTags && product.hasTags.some(function (t) { return t.hasTag; });
  if (tagged) {
    candidates.push({
      message: 'Sale',
      targets: [{ cartLine: { id: line.id } }],
      value: { percentage: { value: 10 } },
    });
  }
});

if (!candidates.length) return { operations: [] };
return { operations: [{ productDiscountsAdd: { selectionStrategy: 'FIRST', candidates: candidates } }] };
```

**Order discount above a threshold from vars** (use the *General* or *Cart* family; set `MIN_ORDER_TOTAL` and `ORDER_DISCOUNT_PERCENT` as code variables):

```javascript
var total = parseFloat(input.cart.cost.totalAmount.amount);
var min = parseFloat(vars.MIN_ORDER_TOTAL || '200');
var percent = parseFloat(vars.ORDER_DISCOUNT_PERCENT || '5');

if (total < min) return { operations: [] };

return {
  operations: [
    {
      orderDiscountsAdd: {
        selectionStrategy: 'FIRST',
        candidates: [
          {
            message: percent + '% off orders over ' + min,
            targets: [{ orderSubtotal: { excludedCartLineIds: [] } }],
            value: { percentage: { value: percent } },
          },
        ],
      },
    },
  ],
};
```

### Notes

* Discount functions don't use the **Run on** steps — they run whenever the cart changes.
* All amounts are in the presentment currency (what the buyer pays).
* If your code throws, no discount is applied (fail open).
* See also: [Cart and Checkout Validation](/functions/functions/cart-and-checkout-validation), [Delivery Customization](/functions/functions/delivery-customization), [Payment Customization](/functions/functions/payment-customization).


# Cart Transform

Merge cart lines into bundles, expand bundle SKUs into components, and update line presentation with JavaScript.

A cart transform reshapes the lines of the cart itself — merge several lines into a single bundle line, expand one line into its components, or update how a line is presented. Your code inspects the cart and returns a list of operations.

```javascript
var ops = [];

var bundleLines = input.cart.lines.filter(function (line) {
  var product = line.merchandise && line.merchandise.product;
  return product && product.hasTags && product.hasTags.some(function (t) { return t.hasTag; });
});

if (bundleLines.length >= 2) {
  ops.push({
    linesMerge: {
      cartLines: bundleLines.map(function (l) { return { cartLineId: l.id, quantity: l.quantity }; }),
      parentVariantId: 'gid://shopify/ProductVariant/…',
      title: 'Bundle',
      price: { percentageDecrease: { value: 10 } },
    },
  });
}

return { operations: ops };
```

{% hint style="info" %}
A store can have **only one cart transform function** — the Functions Console lets you create a single one, in the *General* or *Tags* family. Its title is internal: buyers never see it.
{% endhint %}

### Operations

Return an array of operations, or `{ operations: [...] }`. Each operation is an object with exactly one of these keys:

| Operation | Shape                                                                                                      | Effect                                                               |
| --------- | ---------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
| Merge     | `{ linesMerge: { cartLines: [{ cartLineId, quantity }], parentVariantId, title?, image?, price? } }`       | Combines lines into one bundle line, presented as the parent variant |
| Expand    | `{ lineExpand: { cartLineId, title?, image?, expandedCartItems: [{ merchandiseId, quantity, price? }] } }` | Splits one line into its component items                             |
| Update    | `{ lineUpdate: { cartLineId, title?, image?, price? } }`                                                   | Overrides a line's title, image or price                             |

Price shapes: merge uses `price: { percentageDecrease: { value: 10 } }`; expanded items use `price: { adjustment: { fixedPricePerUnit: { amount: '10.0' } } }`.

Returning an empty array leaves the cart unchanged. Operations of any other shape are ignored.

{% hint style="warning" %}
`lineUpdate` (price/title/image overrides) works on **Shopify Plus and development stores only** — prefer merge and expand for rules that must work everywhere.
{% endhint %}

### The input

* Cart lines are `input.cart.lines`, each with an `id`, `quantity`, `cost` and merchandise (variant `id`, `sku`, product data — the *Tags* family adds product and customer `hasTags`).
* There is **no cart-level cost** — money lives per line in `line.cost`, and `input.presentmentCurrencyRate` at the root converts shop-currency amounts to what the buyer pays.
* The variant IDs you reference (`parentVariantId`, `merchandiseId`) must be existing, published variants — supply them via [variables](/functions/functions/families-and-variables) so merchants can set them without code changes.

### How DataJet manages the transform

* The function runs on **every cart change**, so keep the code fast and return `{ operations: [] }` early when there's nothing to do.
* Turning the function off doesn't delete anything — the off switch is stored in the function's configuration and the code simply stops producing operations.
* Deleting the function removes it from Shopify entirely, including its configuration — the code remains available in the version history.
* Cart transforms have no page in the Shopify admin; they're managed only from the Functions Console.

### Example: expand a bundle SKU

*Family: General* — set `BUNDLE_SKU` and the component variant IDs as [code variables](/functions/functions/families-and-variables).

```javascript
var ops = [];
var componentIds = (vars.BUNDLE_COMPONENT_IDS || '').split(',');

input.cart.lines.forEach(function (line) {
  var variant = line.merchandise;
  if (variant && variant.sku === vars.BUNDLE_SKU && componentIds.length > 1) {
    ops.push({
      lineExpand: {
        cartLineId: line.id,
        expandedCartItems: componentIds.map(function (id) {
          return { merchandiseId: 'gid://shopify/ProductVariant/' + id.trim(), quantity: 1 };
        }),
      },
    });
  }
});

return { operations: ops };
```

### Notes

* Cart transforms don't use the **Run on** steps — they apply whenever the cart changes.
* If your code throws, the cart is left unchanged (fail open).
* See also: [Cart and Checkout Validation](/functions/functions/cart-and-checkout-validation), [Delivery Customization](/functions/functions/delivery-customization), [Payment Customization](/functions/functions/payment-customization), [Discounts](/functions/functions/discounts).


# Families and Variables

Families decide which checkout data a function receives as input; variables parameterize that data — tags to check, collections to match, attribute keys to read.

Functions run in a sandbox with no API access — the only data your code sees is the `input` object, and its contents are fixed when the function is created by choosing a **family**. The family can't be changed later, so pick the one that covers everything your rule needs.

When creating a function, the modal shows the exact **Available input** for the selected family and category. In the editor, autocomplete knows the same shape — type `input.` to explore what's available.

Every family is available in the validation, delivery, payment and discount categories — the same family selects the same data in each (discount inputs additionally carry cart line `id`s for targeting). [Cart Transform](/functions/functions/cart-transform) is the exception: it supports the *General* and *Tags* families only.

## Families

| Family                     | Input data                                                                                                                              | Variables                                                                                                   |
| -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| General                    | Broad cart snapshot: totals, buyer identity and customer (name, amount spent, number of orders), lines and products, purchasing company | —                                                                                                           |
| Product                    | Cart lines with product tags, collection membership, a product metafield, line attributes                                               | `hasProductTags`, `inAnyCollection`, `productMetafieldNamespace`, `productMetafieldKey`, `lineAttributeKey` |
| Customer                   | Customer identity, tags, a customer metafield, amount spent, order count                                                                | `hasCustomerTags`, `customerMetafieldNamespace`, `customerMetafieldKey`                                     |
| Address                    | Shipping address fields, product tags, a cart attribute                                                                                 | `hasProductTags`, `cartAttributeKey`                                                                        |
| Address + Customer         | Shipping address, customer identity and tags, the selected delivery option                                                              | `hasCustomerTags`                                                                                           |
| Address + Product          | Shipping address, product tags and SKUs                                                                                                 | `hasProductTags`                                                                                            |
| Billing address            | Billing address fields, product tags                                                                                                    | `hasProductTags`                                                                                            |
| Billing + Shipping address | Both billing and shipping address fields                                                                                                | —                                                                                                           |
| Cart                       | Full cost breakdown (subtotal, tax, duty, compare-at), cart and line attributes                                                         | `cartAttributeKey`, `lineAttributeKey`                                                                      |
| Selling plan               | Selling plan allocations on cart lines (subscriptions), product tags                                                                    | `hasProductTags`                                                                                            |
| Shipping                   | The selected delivery option (title, method type), product and customer tags, a cart attribute                                          | `hasProductTags`, `hasCustomerTags`, `cartAttributeKey`                                                     |
| Tags                       | Product and customer tags together, line attributes                                                                                     | `hasProductTags`, `hasCustomerTags`, `lineAttributeKey`                                                     |
| Time                       | Shop local time comparisons, product and customer tags                                                                                  | `hasProductTags`, `hasCustomerTags`, `dateTimeAfter`, `dateTimeBefore`, `timeAfter`, `timeBefore`           |

## Two kinds of variables

Functions have two variable systems, opened from the segmented buttons in the editor toolbar:

* **Function** and **Global** — *code variables*: values your code reads through the `vars` object, like `vars.MAX_ORDER_TOTAL`. Use them instead of hardcoding thresholds and settings.
* **Query** — *query variables*: values that parameterize the function's **data query** on Shopify's side, like which tags each product is checked against.

## Code variables

Code variables use the **same format as** [**script variables**](/misc/variables): keys are `type#NAME` (a bare name implies `short_text`), plus an optional `metadata` object that turns variables into merchant-friendly input fields.

```json
{
  "short_text#MAX_ORDER_TOTAL": "5000",
  "checkbox#BLOCK_PO_BOXES": true,
  "metadata": {
    "MAX_ORDER_TOTAL": { "subtype": "number", "helpText": "Orders above this total are blocked" }
  }
}
```

Your code reads the stripped names through `vars`:

```javascript
if (parseFloat(input.cart.cost.totalAmount.amount) > parseFloat(vars.MAX_ORDER_TOTAL)) {
  return ['Orders above ' + vars.MAX_ORDER_TOTAL + ' are not allowed'];
}
return [];
```

* **Function** variables belong to one function. **Global** variables are shared with scripts and with every function on the store; a function variable overrides a global with the same name.
* The `metadata` property never reaches your code — it only drives the input fields. All metadata options from [script variables](/misc/variables) are supported.
* Values keep their JSON types: a `checkbox` arrives as a boolean, `json` as an object. There is no `secrets` access inside functions — don't store credentials in variables (the sandbox has no network access to use them anyway).

### Editing configuration from the dashboard

On the main dashboard's **Functions** tab, clicking a function expands its **Function configuration** — the same metadata-driven form merchants use for script tasks. Anyone can adjust values (a threshold, a toggle) without opening the code; saving applies to the live function on the next checkout.

## Query variables

Checkout data is fetched by a query that runs **before** your code — so anything dynamic in that query has to be set up front. That's what query variables are for: they parameterize the function's data query on Shopify's side. For example, `hasProductTags` controls which tags each product is checked against, and the result arrives in your `input` already resolved.

Open the **Query** button in the editor and provide a JSON object with the family's keys:

```json
{
  "hasProductTags": ["limited", "preorder"],
  "inAnyCollection": ["gid://shopify/Collection/123456789"]
}
```

| Variable                                              | Type                           | What it does                                                             |
| ----------------------------------------------------- | ------------------------------ | ------------------------------------------------------------------------ |
| `hasProductTags`                                      | array of strings               | Each product in `input` gets `hasTags: [{ tag, hasTag }]` for these tags |
| `hasCustomerTags`                                     | array of strings               | The customer gets `hasTags: [{ tag, hasTag }]` for these tags            |
| `inAnyCollection`                                     | array of collection IDs        | Each product gets `inCollections: [{ collectionId, isMember }]`          |
| `productMetafieldNamespace` / `productMetafieldKey`   | string                         | Each product gets `metafield: { value }` for this namespace/key          |
| `customerMetafieldNamespace` / `customerMetafieldKey` | string                         | The customer gets `metafield: { value }`                                 |
| `cartAttributeKey`                                    | string                         | The cart gets `attribute: { value }` for this key                        |
| `lineAttributeKey`                                    | string                         | Each cart line gets `attribute: { value }` for this key                  |
| `dateTimeAfter` / `dateTimeBefore`                    | string `"YYYY-MM-DDTHH:MM:SS"` | `input.shop.localTime` gets a boolean comparison against shop local time |
| `timeAfter` / `timeBefore`                            | string `"HH:MM:SS"`            | Same, for time of day                                                    |

**Reading the resolved values in code:**

```javascript
// hasProductTags: ["limited"]
var product = input.cart.lines[0].merchandise.product;
var isLimited = product && (product.hasTags || []).some(function (t) { return t.hasTag; });

// cartAttributeKey: "gift_wrap"
var giftWrap = input.cart.attribute && input.cart.attribute.value;

// Time family, timeAfter: "22:00:00"
var isLate = input.shop.localTime.timeAfter;
```

### Notes

* Only the family's own query variables are accepted — unknown keys are rejected on save.
* List variables can have at most **100 items** (Shopify's input query limit).
* Collection IDs can be provided as plain numbers (`123456789`) — they are converted to full Shopify IDs automatically.
* Empty values are dropped, so you can leave template keys you don't use untouched.
* Families without variables (*General*, *Billing + Shipping address*) need no setup — their input is fixed.
* Changing variables affects the live function on the next checkout, just like saving code.


# Examples

Ready-to-use function examples — order limits, address rules, tag-based restrictions, delivery and payment customizations.

Each example lists the **category** (which folder to create the function in), the **family** to pick, and any **variables** to set. Copy the code into the Functions Console editor, adjust, save and turn the function on.

{% hint style="info" %}
All money values arrive in the **presentment currency** — what the buyer sees and pays at checkout. `currencyCode` is selected once per input; every amount in the same run shares that currency.
{% endhint %}

{% hint style="warning" %}
Guard nullable objects: `buyerIdentity`, `customer`, `purchasingCompany`, `deliveryAddress`, `metafield`, `attribute` and `sellingPlanAllocation` are `null` when absent (e.g. `buyerIdentity` for anonymous buyers). Reading a property of `null` throws — and a thrown function **fails open**, silently skipping your rule for exactly the buyers it targets.
{% endhint %}

## Validation

### Order value limit with a configurable threshold

Blocks checkout when the cart total exceeds a limit the merchant can edit on the dashboard — no code changes needed.

*Category: Cart and checkout validation · Family: General*

**Function variables** (the **Function** button in the editor toolbar):

```json
{
  "short_text#MAX_ORDER_TOTAL": "500",
  "metadata": {
    "MAX_ORDER_TOTAL": {
      "subtype": "number",
      "helpText": "Orders above this total are blocked at checkout"
    }
  }
}
```

**Code:**

```javascript
var errors = [];
var limit = parseFloat(vars.MAX_ORDER_TOTAL);

if (limit && parseFloat(input.cart.cost.totalAmount.amount) > limit) {
  errors.push({
    message: 'Orders above ' + vars.MAX_ORDER_TOTAL + ' cannot be completed. Please reduce your order total.',
    target: '$.cart'
  });
}

return errors;
```

### Currency-aware order limit

Applies the limit only when the buyer checks out in a specific currency.

*Category: Cart and checkout validation · Family: General*

```javascript
var errors = [];
var total = input.cart.cost.totalAmount;

if (total.currencyCode === 'USD' && parseFloat(total.amount) > 500) {
  errors.push({
    message: 'Orders above $500 USD cannot be completed.',
    target: '$.cart'
  });
}

return errors;
```

{% hint style="info" %}
To enforce a limit **across all currencies**, define per-currency thresholds in a `json` function variable (e.g. `{ "USD": 500, "EUR": 450 }`) and look up `vars.LIMITS[total.currencyCode]` in your code.
{% endhint %}

### Block PO boxes in the shipping address

*Category: Cart and checkout validation · Family: Address*

```javascript
var errors = [];

(input.cart.deliveryGroups || []).forEach(function (group) {
  var address = group.deliveryAddress;
  if (!address) return;
  var text = ((address.address1 || '') + ' ' + (address.address2 || ''))
    .toLowerCase().replace(/[^a-z]/g, '');
  if (text.indexOf('pobox') > -1 || text.indexOf('postbox') > -1) {
    errors.push({
      message: 'We cannot ship to PO boxes. Please use a street address.',
      target: '$.cart.deliveryGroups[0].deliveryAddress.address1'
    });
  }
});

return errors;
```

The error appears directly under the address field thanks to the `target` — see [error targets](/functions/functions/cart-and-checkout-validation#error-targets).

### Require login to buy gift cards

*Category: Cart and checkout validation · Family: General*

```javascript
var errors = [];

var buyer = input.cart.buyerIdentity;

input.cart.lines.forEach(function (line) {
  var product = line.merchandise.product;
  if (product && product.isGiftCard && (!buyer || !buyer.isAuthenticated)) {
    errors.push('Please log in to purchase gift cards.');
  }
});

return errors;
```

### Quantity limit for tagged products

Limits products carrying a specific tag to 2 units per order.

*Category: Cart and checkout validation · Family: Product*

**Query variables** (the **Query** button in the editor toolbar):

```json
{
  "hasProductTags": ["limited"]
}
```

**Code:**

```javascript
var errors = [];

input.cart.lines.forEach(function (line) {
  var product = line.merchandise && line.merchandise.product;
  var isLimited = product && (product.hasTags || []).some(function (t) { return t.hasTag; });
  if (isLimited && line.quantity > 2) {
    errors.push('Maximum 2 units of limited products per order.');
  }
});

return errors;
```

### Wholesale-only bulk orders

Requires a customer tag for quantities above 100.

*Category: Cart and checkout validation · Family: Customer*

**Query variables:** `{ "hasCustomerTags": ["wholesale"] }`

```javascript
var errors = [];
var customer = input.cart.buyerIdentity && input.cart.buyerIdentity.customer;
var isWholesale = customer && (customer.hasTags || []).some(function (t) { return t.hasTag; });

input.cart.lines.forEach(function (line) {
  if (line.quantity > 100 && !isWholesale) {
    errors.push('Quantities above 100 require a wholesale account. Contact us to apply.');
  }
});

return errors;
```

## Delivery customization

### Hide express shipping for heavy carts

*Category: Delivery customization · Family: General*

```javascript
var ops = [];
var totalWeight = input.cart.lines.reduce(function (sum, line) {
  return sum + (line.merchandise.weight || 0) * line.quantity;
}, 0);

if (totalWeight > 20) {
  input.cart.deliveryGroups.forEach(function (group) {
    group.deliveryOptions.forEach(function (option) {
      if (option.title === 'Express') {
        ops.push({ deliveryOptionHide: { deliveryOptionHandle: option.handle } });
      }
    });
  });
}

return { operations: ops };
```

### Rename a delivery option for loyal customers

*Category: Delivery customization · Family: Customer*

```javascript
var ops = [];
var customer = input.cart.buyerIdentity && input.cart.buyerIdentity.customer;

if (customer && customer.numberOfOrders > 10) {
  input.cart.deliveryGroups.forEach(function (group) {
    group.deliveryOptions.forEach(function (option) {
      if (option.title === 'Standard') {
        ops.push({
          deliveryOptionRename: {
            deliveryOptionHandle: option.handle,
            title: 'Standard (free for loyal customers)'
          }
        });
      }
    });
  });
}

return { operations: ops };
```

## Payment customization

### Hide Cash on Delivery for B2B buyers

*Category: Payment customization · Family: Customer*

```javascript
var ops = [];

if (input.cart.buyerIdentity && input.cart.buyerIdentity.purchasingCompany) {
  input.paymentMethods.forEach(function (method) {
    if (method.name.indexOf('Cash on Delivery') > -1) {
      ops.push({ paymentMethodHide: { paymentMethodId: method.id } });
    }
  });
}

return { operations: ops };
```

### Move a preferred payment method to the top

*Category: Payment customization · Family: General*

```javascript
var ops = [];

input.paymentMethods.forEach(function (method) {
  if (method.name === 'Shopify Payments') {
    ops.push({ paymentMethodMove: { paymentMethodId: method.id, index: 0 } });
  }
});

return { operations: ops };
```

{% hint style="warning" %}
Wallets with logos (Shop Pay, Apple Pay, Google Pay) can be hidden but not renamed or reordered.
{% endhint %}

## Discounts

### 10% off products with a tag

*Category: Discounts · Family: Product*

**Query variables:**

```json
{
  "hasProductTags": ["sale"]
}
```

**Code:**

```javascript
var candidates = [];

input.cart.lines.forEach(function (line) {
  var product = line.merchandise && line.merchandise.product;
  var tagged = product && product.hasTags && product.hasTags.some(function (t) { return t.hasTag; });
  if (tagged) {
    candidates.push({
      message: 'Sale',
      targets: [{ cartLine: { id: line.id } }],
      value: { percentage: { value: 10 } },
    });
  }
});

if (!candidates.length) return { operations: [] };
return { operations: [{ productDiscountsAdd: { selectionStrategy: 'FIRST', candidates: candidates } }] };
```

### Order discount above a threshold

*Category: Discounts · Family: General* — set `MIN_ORDER_TOTAL` and `ORDER_DISCOUNT_PERCENT` as code variables.

```javascript
var total = parseFloat(input.cart.cost.totalAmount.amount);
var min = parseFloat(vars.MIN_ORDER_TOTAL || '200');
var percent = parseFloat(vars.ORDER_DISCOUNT_PERCENT || '5');

if (total < min) return { operations: [] };

return {
  operations: [{
    orderDiscountsAdd: {
      selectionStrategy: 'FIRST',
      candidates: [{
        message: percent + '% off orders over ' + min,
        targets: [{ orderSubtotal: { excludedCartLineIds: [] } }],
        value: { percentage: { value: percent } },
      }],
    },
  }],
};
```

{% hint style="info" %}
The discount function's **title** is shown to buyers at checkout as the discount name.
{% endhint %}

## Cart transform

### Merge tagged products into a bundle

*Category: Cart transform · Family: Tags*

**Query variables:**

```json
{
  "hasProductTags": ["bundle"]
}
```

**Code** — set `BUNDLE_PARENT_VARIANT_ID` as a code variable (an existing, published variant that represents the bundle):

```javascript
var lines = input.cart.lines.filter(function (line) {
  var product = line.merchandise && line.merchandise.product;
  return product && product.hasTags && product.hasTags.some(function (t) { return t.hasTag; });
});

if (lines.length < 2 || !vars.BUNDLE_PARENT_VARIANT_ID) return { operations: [] };

return {
  operations: [{
    linesMerge: {
      cartLines: lines.map(function (l) { return { cartLineId: l.id, quantity: l.quantity }; }),
      parentVariantId: 'gid://shopify/ProductVariant/' + vars.BUNDLE_PARENT_VARIANT_ID,
      title: 'Bundle',
      price: { percentageDecrease: { value: 10 } },
    },
  }],
};
```

{% hint style="warning" %}
A store can have only **one** cart transform function, and `lineUpdate` operations (price/title/image overrides) work on Shopify Plus and development stores only.
{% endhint %}

## Time-based rules

### Block checkout outside business hours

*Category: Cart and checkout validation · Family: Time*

**Query variables:**

```json
{
  "timeAfter": "09:00:00",
  "timeBefore": "17:00:00"
}
```

**Code:**

```javascript
var errors = [];
var withinHours = input.shop.localTime.timeAfter && input.shop.localTime.timeBefore;

if (!withinHours) {
  errors.push('Orders can only be placed between 9:00 and 17:00. Please come back later.');
}

return errors;
```

`timeAfter` / `timeBefore` arrive as pre-computed booleans against the shop's local time — your code never parses dates. See [Families and Variables](/functions/functions/families-and-variables) for the date-time variants.


# Introduction

Liquid template language is used to code custom [hooks](broken://pages/-MQfF6ZiuCzKRscAc3TJ). DataJet supports all standard filters and tags known from Shopify. Complete documentation can be found here:

{% embed url="<https://shopify.dev/docs/themes/liquid/reference>" %}


# Tags


# clear

Explicitly removes variables from memory to help garbage collection. Use this tag to free up memory after processing large arrays or objects that are no longer needed.

```liquid
{% json products %}
  [
    { "id": 1, "title": "Product 1" },
    { "id": 2, "title": "Product 2" },
    { "id": 3, "title": "Product 3" }
  ]
{% endjson %}

{% assign product_titles = products | map: "title" %}

{% comment %} Process the data... {% endcomment %}
{% log product_titles %}

{% comment %} Clear variables when no longer needed {% endcomment %}
{% clear products, product_titles %}

{% comment %} Variables are now removed from memory {% endcomment %}
{% log products %}
```

Results in following output:

```
["Product 1", "Product 2", "Product 3"]
null
```

#### Syntax

```liquid
{% clear variable_name %}
{% clear var1, var2, var3 %}
```

| Parameter         | Description                             |
| ----------------- | --------------------------------------- |
| `variable_name`   | Name of the variable to clear           |
| `var1, var2, ...` | Multiple comma-separated variable names |

#### Use Cases

**After processing large CSV files:**

```liquid
{% assign all_rows = file_content | split: "\n" %}
{% assign parsed_data = all_rows | parse_csv %}

{% comment %} Process parsed_data... {% endcomment %}

{% clear file_content, all_rows %}
```

**After GraphQL pagination:**

```liquid
{% assign all_products = "[]" | parse %}

{% for page in (1..10) %}
  {% graphql query:products_query as page_result %}
  {% assign all_products = all_products | concat: page_result.products.edges %}
  {% clear page_result %}
{% endfor %}
```

**End of script cleanup:**

```liquid
{% comment %} Clear all large variables before script ends {% endcomment %}
{% clear ALL_PARSED_PRODUCT, ALL_EXIST_SKU, IMPORT_FILES, LOCAL_FILES %}
```

#### Notes

* Removes variables from all scopes (local and global)
* Sets variable to `null` before deletion to assist garbage collection
* No error is thrown if variable doesn't exist
* Useful for long-running scripts that process large datasets


# doc

The `doc` tag lets you add documentation to your scripts. Documentation is displayed as a hover tooltip in the editor when you hover over a `function` or `run` handle that references the script.

The `doc` block is ignored at runtime — it produces no output and has no effect on script execution.

### Syntax

```liquid
{% doc %}
  Description of what this script does.

  @param {type} param_name - Description of the parameter
  @example
  {% function "handle", param_name:"value" as result %}
{% enddoc %}
```

Place the `doc` block at the top of your script, before any logic.

### Tags

#### @description

Describes what the script does. You can also write the description as plain text at the top of the block — if no `@description` tag is present, the first lines of text are used as the description.

**Explicit:**

```liquid
{% doc %}
  @description Sends a tracking event to Klaviyo.
{% enddoc %}
```

**Implicit (no @description tag needed):**

```liquid
{% doc %}
  Sends a tracking event to Klaviyo.
{% enddoc %}
```

Both produce the same result.

#### @param

Documents a parameter that the script expects. Supports type annotations, optional markers, and descriptions.

**Full format:**

```liquid
@param {string} email - The customer's email address
```

* `{string}` — parameter type (e.g. `string`, `number`, `object`, `array`, `boolean`)
* `email` — parameter name
* `- The customer's email address` — description

**Optional parameters** — wrap the name in square brackets:

```liquid
@param {number} [retry_count] - Number of retries (default: 3)
```

**Minimal format** — only the name is required:

```liquid
@param email
```

#### @example

Shows a usage example. Everything after `@example` until the next tag or end of the block is treated as example code.

```liquid
@example
{% function "send_metric", email:customer.email, metric:"signup" as result %}
```

You can include multiple `@example` tags for different use cases.

### Full example

**Function script** (handle: `send_metric_to_klaviyo`):

```liquid
{% doc %}
  Sends a tracking event to the Klaviyo API.

  @param {string} email - Customer email address
  @param {string} metric - The metric/event name to track
  @param {string} [property] - Optional property value for the event

  @example
  {% function "send_metric_to_klaviyo", email:"user@example.com", metric:"Purchase" as result %}

  @example
  {% function "send_metric_to_klaviyo", email:customer.email, metric:"Signup", property:"Premium" as result %}
{% enddoc %}

{% json body %}
{
  "data": {
    "type": "event",
    "attributes": {
      "properties": {
        "membership": {{ property | default: "" | json }}
      },
      "metric": {
        "data": {
          "type": "metric",
          "attributes": {
            "name": {{ metric | default: "" | json }}
          }
        }
      },
      "profile": {
        "data": {
          "type": "profile",
          "attributes": {
            "email": {{ email | default: "" | json }}
          }
        }
      }
    }
  }
}
{% endjson %}

{% json request_options %}
{
  "url": "https://a.klaviyo.com/client/events/?company_id={{KLAVIYO_TOKEN}}",
  "method": "POST",
  "headers": {
    "Content-Type": "application/json",
    "Accept": "application/json",
    "Revision": "2024-05-15"
  },
  "body": {{ body | json }}
}
{% endjson %}
{% http url:request_options.url, method:request_options.method, body:request_options.body, headers:request_options.headers as response %}
{% return response %}
```

When you hover over `"send_metric_to_klaviyo"` in another script, the editor displays:

* The script name
* The description
* A list of parameters with types and descriptions
* Usage examples

### Editor integration

The `doc` block is syntax highlighted in the editor:

* `@param`, `@description`, `@example` tags are displayed in **bold green**
* All other content is displayed in grey

This makes documentation blocks visually distinct from executable code.


# email

Sends an email with HTML content. The content between the opening and closing tags becomes the email body.

```liquid
{% email to: "customer@example.com", subject: "Order Confirmation" %}
  <h1>Thank you for your order!</h1>
  <p>Your order #{{ order.name }} has been confirmed.</p>
  <p>Total: {{ order.total_price | money }}</p>
{% endemail %}
```

#### Syntax

```liquid
{% email to: "recipient@example.com", subject: "Subject line" %}
  HTML content here...
{% endemail %}
```

#### Parameters

| Parameter     | Required | Description             |
| ------------- | -------- | ----------------------- |
| `to`          | Yes      | Recipient email address |
| `subject`     | No       | Email subject line      |
| `cc`          | No       | CC recipient(s)         |
| `bcc`         | No       | BCC recipient(s)        |
| `replyTo`     | No       | Reply-to email address  |
| `attachments` | No       | File attachments        |

#### Examples

**Basic email:**

```liquid
{% email to: "admin@store.com", subject: "Daily Report" %}
  <h2>Sales Report for {{ "now" | date: "%B %d, %Y" }}</h2>
  <p>Total orders: {{ orders.size }}</p>
{% endemail %}
```

**With CC and Reply-To:**

```liquid
{% email
  to: "customer@example.com",
  cc: "sales@store.com",
  replyTo: "support@store.com",
  subject: "Your Invoice"
%}
  <p>Please find your invoice attached.</p>
{% endemail %}
```

**Dynamic recipient:**

```liquid
{% for customer in customers %}
  {% email to:customer.email, subject:"Special Offer Just for You" %}
    <p>Hi {{ customer.first_name }},</p>
    <p>We have a special offer waiting for you!</p>
  {% endemail %}
{% endfor %}
```

**With attachments:**

```liquid
{% email
  to: "customer@example.com",
  subject: "Your Report",
  attachments: report_files
%}
  <p>Please find your requested reports attached.</p>
{% endemail %}
```

#### Notes

* Emails are sent from DataJet's verified sender address
* Daily email limit applies per store
* Store must be trusted and have email sending enabled
* HTML content is supported in the email body
* Each email sent consumes 1 credit


# exit

Immediately terminates script execution. Use this tag to stop processing when a condition is met or an error occurs.

```liquid
{% if order.cancelled %}
  {% log "Order is cancelled, skipping processing" %}
  {% exit %}
{% endif %}

{% comment %} This code will not run if order is cancelled {% endcomment %}
{% log "Processing order..." %}
```

#### Syntax

```liquid
{% exit %}
```

#### Use Cases

**Early termination on validation failure:**

```liquid
{% if customer.email == blank %}
  {% log "Customer email is required" %}
  {% exit %}
{% endif %}

{% email to: customer.email, subject: "Welcome!" %}
  <p>Thanks for signing up!</p>
{% endemail %}
```

**Skip processing based on conditions:**

```liquid
{% if product.tags contains "do-not-sync" %}
  {% exit %}
{% endif %}

{% comment %} Sync product to external system {% endcomment %}
{% http url: api_endpoint, method: "POST", body: product_data as http_result %}
```

**Stop after error response:**

```liquid
{% graphql query: my_query as result %}

{% if result.errors %}
  {% log result.errors %}
  {% exit %}
{% endif %}

{% log "Query successful" %}
```

**Conditional processing with multiple checks:**

```liquid
{% unless shop.plan_name == "enterprise" %}
  {% log "Feature only available for enterprise plans" %}
  {% exit %}
{% endunless %}

{% unless order.total_price > 1000 %}
  {% log "Order value too low for VIP processing" %}
  {% exit %}
{% endunless %}

{% log "Processing VIP order..." %}
```

#### Notes

* Execution stops immediately when `{% exit %}` is reached
* No code after the `exit` tag will be executed
* Useful for guard clauses and early returns
* Does not produce any output


# flow

Triggers a Shopify Flow workflow with a custom payload. Use this tag to integrate DataJet scripts with Shopify Flow automations. The flow needs to be created in Shopify Flow and the trigger name needs to be: Event Payload Trigger V2.

Read more about DataJet and Shopify Flow [here](/integrations/shopify-flow/v2).

```liquid
{% json my_payload %}
  {
    "customer_id": "{{ customer.id }}",
    "order_total": {{ order.total_price }},
    "action": "send_reward"
  }
{% endjson %}

{% flow payload: my_payload as result %}

{% if result.ok %}
  {% log "Flow triggered successfully" %}
{% else %}
  {% log "Failed to trigger flow" %}
{% endif %}
```

#### Syntax

```liquid
{% flow payload: payload_variable as result_variable %}
```

#### Parameters

| Parameter | Required | Description                                |
| --------- | -------- | ------------------------------------------ |
| `payload` | Yes      | Object containing data to send to the Flow |

#### Result Object

The result variable contains:

| Property | Type    | Description                               |
| -------- | ------- | ----------------------------------------- |
| `ok`     | boolean | `true` if Flow was triggered successfully |
| `body`   | object  | Response body from Flow trigger (if any)  |

#### Flow Receives

When triggered, the Shopify Flow receives:

| Field           | Description                        |
| --------------- | ---------------------------------- |
| `Script ID`     | The ID of the DataJet script       |
| `Script Handle` | The handle of the DataJet script   |
| `Run ID`        | The current execution run ID       |
| `Payload`       | Your custom payload as JSON string |

#### Use Cases

**Trigger post-order processing:**

```liquid
{% json order_payload %}
  {
    "order_id": "{{ order.id }}",
    "customer_email": "{{ order.email }}",
    "line_items": {{ order.line_items | map: "sku" | json }}
  }
{% endjson %}

{% flow payload: order_payload as result %}
```

**Send customer data to Flow for segmentation:**

```liquid
{% json customer_payload %}
  {
    "customer_id": "{{ customer.id }}",
    "total_spent": {{ customer.total_spent }},
    "orders_count": {{ customer.orders_count }},
    "tags": {{ customer.tags | json }}
  }
{% endjson %}

{% flow payload: customer_payload as result %}

{% unless result.ok %}
  {% log "Warning: Could not trigger customer segmentation flow" %}
{% endunless %}
```

**Chain DataJet script with Flow actions:**

```liquid
{% comment %} Process inventory data {% endcomment %}
{% assign low_stock_products = products | where_exp: "p", "p.inventory_quantity < 10" %}

{% if low_stock_products.size > 0 %}
  {% json alert_payload %}
    {
      "alert_type": "low_stock",
      "product_count": {{ low_stock_products.size }},
      "products": {{ low_stock_products | map: "title" | json }}
    }
  {% endjson %}

  {% flow payload: alert_payload as result %}
  {% log "Low stock alert sent to Flow" %}
{% endif %}
```

#### Notes

* Requires Shopify Flow to be installed and configured
* Each flow trigger consumes 1 credit
* Payload is automatically serialized to JSON
* Use Shopify Flow to handle notifications, tagging, or other actions based on the payload
* The Flow trigger uses DataJet's custom trigger "Event Payload Trigger V2"


# ftp\_delete

Deletes a file or directory on the FTP/SFTP server. Must be used inside an `ftp_session` block.

```liquid
{% ftp_session host: "ftp.example.com", user: "myuser", password: "mypass" %}
  {% ftp_delete target: "/temp/old_file.csv" as result %}

  {% if result.ok %}
    {% log "File deleted" %}
  {% endif %}
{% endftp_session %}
```

#### Syntax

```liquid
{% ftp_delete target: "/path/to/file_or_directory" as result_variable %}
```

#### Parameters

| Parameter   | Required | Description                                       |
| ----------- | -------- | ------------------------------------------------- |
| `target`    | Yes      | Path to the file or directory to delete           |
| `recursive` | No       | Set to `true` to delete directories with contents |

#### Result Object

| Property | Type        | Description                                         |
| -------- | ----------- | --------------------------------------------------- |
| `ok`     | boolean     | `true` if deletion succeeded                        |
| `error`  | string/null | Error message if `ok` is `false`, `null` on success |

Example result:

```json
{
  "ok": true,
  "error": null
}
```

#### Examples

**Delete a single file:**

```liquid
{% ftp_session host: FTP_HOST, user: FTP_USER, password: FTP_PASSWORD %}
  {% ftp_delete target: "/temp/report.csv" as result %}

  {% if result.ok %}
    {% log "File deleted successfully" %}
  {% else %}
    {% log result.error %}
  {% endif %}
{% endftp_session %}
```

**Delete an empty directory:**

```liquid
{% ftp_session host: FTP_HOST, user: FTP_USER, password: FTP_PASSWORD %}
  {% ftp_delete target: "/old_folder" as result %}
{% endftp_session %}
```

**Delete directory with all contents (recursive):**

```liquid
{% ftp_session host: FTP_HOST, user: FTP_USER, password: FTP_PASSWORD %}
  {% ftp_delete target: "/temp/old_exports", recursive: true as result %}

  {% if result.ok %}
    {% log "Directory and all contents deleted" %}
  {% endif %}
{% endftp_session %}
```

**Clean up old files:**

```liquid
{% ftp_session host: FTP_HOST, user: FTP_USER, password: FTP_PASSWORD %}
  {% ftp_list path: "/logs" as list_result %}

  {% assign cutoff = "now" | date: "%s" | minus: 604800 %}
  {% comment %} 604800 seconds = 7 days {% endcomment %}

  {% for file in list_result.files %}
    {% if file.modifiedAt < cutoff %}
      {% ftp_delete target: file.path as delete_result %}
      {% if delete_result.ok %}
        {% log "Deleted old file: " %}
        {% log file.name %}
      {% endif %}
    {% endif %}
  {% endfor %}
{% endftp_session %}
```

**Process, archive, then delete original:**

```liquid
{% ftp_session host: FTP_HOST, user: FTP_USER, password: FTP_PASSWORD %}
  {% ftp_list path: "/inbox", pattern: "*.csv" as files %}

  {% for file in files.files %}
    {% ftp_download from: file.path as download %}

    {% if download.ok %}
      {% comment %} Process the file... {% endcomment %}
      {% assign content = download.file.name | content %}

      {% comment %} Delete original after successful processing {% endcomment %}
      {% ftp_delete target: file.path as delete_result %}

      {% if delete_result.ok %}
        {% log "Processed and removed: " %}
        {% log file.name %}
      {% endif %}
    {% endif %}
  {% endfor %}
{% endftp_session %}
```

#### Notes

* Must be used inside an `ftp_session` block
* Consumes 2 credits per operation
* Use `recursive: true` with caution - it permanently deletes all contents
* Cannot delete non-empty directories without `recursive: true`
* Operation cannot be undone


# ftp\_download

Downloads a file from the FTP/SFTP server and stores it in DataJet storage. Must be used inside an `ftp_session` block.

```liquid
{% ftp_session host: "ftp.example.com", user: "myuser", password: "mypass" %}
  {% ftp_download from: "/exports/data.csv" as result %}

  {% if result.ok %}
    {% log "Downloaded: " %}
    {% log result.file.name %}
  {% endif %}
{% endftp_session %}
```

#### Syntax

```liquid
{% ftp_download from: "/path/to/file" as result_variable %}
```

#### Parameters

| Parameter | Required | Description                                                |
| --------- | -------- | ---------------------------------------------------------- |
| `from`    | Yes      | Path to the file on the FTP server                         |
| `public`  | No       | Set to `true` to make the file publicly accessible via URL |

#### Result Object

| Property      | Type        | Description                                         |
| ------------- | ----------- | --------------------------------------------------- |
| `ok`          | boolean     | `true` if download succeeded                        |
| `error`       | string/null | Error message if `ok` is `false`, `null` on success |
| `file.name`   | string      | Name of the downloaded file                         |
| `file.public` | boolean     | Whether the file is publicly accessible             |
| `file.url`    | string      | Public URL (only present if `public: true`)         |

Example result:

```json
{
  "ok": true,
  "error": null,
  "file": {
    "name": "data.csv",
    "public": false
  }
}
```

#### Examples

**Basic download:**

```liquid
{% ftp_session host: FTP_HOST, user: FTP_USER, password: FTP_PASSWORD %}
  {% ftp_download from: "/exports/products.csv" as result %}

  {% if result.ok %}
    {% assign file_content = result.file.name | content %}
    {% log file_content %}
  {% else %}
    {% log result.error %}
  {% endif %}
{% endftp_session %}
```

**Download with public URL:**

```liquid
{% ftp_session host: FTP_HOST, user: FTP_USER, password: FTP_PASSWORD %}
  {% ftp_download from: "/reports/monthly.pdf", public: true as result %}

  {% if result.ok %}
    {% log "File available at:" %}
    {% log result.file.url %}
  {% endif %}
{% endftp_session %}
```

**Download and process CSV:**

```liquid
{% ftp_session host: FTP_HOST, user: FTP_USER, password: FTP_PASSWORD, sftp: true %}
  {% ftp_download from: "/data/inventory.csv" as download %}

  {% if download.ok %}
    {% assign csv_content = download.file.name | content %}
    {% assign rows = csv_content | parse_csv %}

    {% for row in rows %}
      {% log row %}
    {% endfor %}
  {% endif %}
{% endftp_session %}
```

**Download multiple files:**

```liquid
{% ftp_session host: FTP_HOST, user: FTP_USER, password: FTP_PASSWORD %}
  {% ftp_list path: "/exports", pattern: "*.csv" as list_result %}

  {% for file in list_result.files %}
    {% ftp_download from: file.path as download %}
    {% if download.ok %}
      {% log "Downloaded: " %}
      {% log file.name %}
    {% endif %}
  {% endfor %}
{% endftp_session %}
```

#### Notes

* Must be used inside an `ftp_session` block
* Consumes 2 credits per download
* Downloaded files are stored in DataJet storage
* Use `| content` filter to read the file contents after download
* File size limits apply (10MB default, 50MB for enhanced stores)


# ftp\_move

Moves or renames a file on the FTP/SFTP server. Must be used inside an `ftp_session` block.

```liquid
{% ftp_session host: "ftp.example.com", user: "myuser", password: "mypass" %}
  {% ftp_move from: "/inbox/data.csv", to: "/processed/data.csv" as result %}

  {% if result.ok %}
    {% log "File moved successfully" %}
  {% endif %}
{% endftp_session %}
```

#### Syntax

```liquid
{% ftp_move from: "/source/path", to: "/destination/path" as result_variable %}
```

#### Parameters

| Parameter | Required | Description              |
| --------- | -------- | ------------------------ |
| `from`    | Yes      | Current path of the file |
| `to`      | Yes      | New path for the file    |

#### Result Object

| Property | Type        | Description                                         |
| -------- | ----------- | --------------------------------------------------- |
| `ok`     | boolean     | `true` if move succeeded                            |
| `error`  | string/null | Error message if `ok` is `false`, `null` on success |
| `from`   | string      | Original file path                                  |
| `to`     | string      | New file path                                       |

Example result:

```json
{
  "ok": true,
  "error": null,
  "from": "/inbox/data.csv",
  "to": "/processed/data.csv"
}
```

#### Examples

**Move file to archive folder:**

```liquid
{% ftp_session host: FTP_HOST, user: FTP_USER, password: FTP_PASSWORD %}
  {% ftp_move from: "/imports/orders.csv", to: "/archives/orders.csv" as result %}

  {% if result.ok %}
    {% log "File archived" %}
  {% else %}
    {% log result.error %}
  {% endif %}
{% endftp_session %}
```

**Rename a file:**

```liquid
{% ftp_session host: FTP_HOST, user: FTP_USER, password: FTP_PASSWORD %}
  {% assign timestamp = "now" | date: "%Y%m%d_%H%M%S" %}
  {% assign new_name = "/data/report_" | append: timestamp | append: ".csv" %}

  {% ftp_move from: "/data/report.csv", to: new_name as result %}
{% endftp_session %}
```

**Process and archive files:**

```liquid
{% ftp_session host: FTP_HOST, user: FTP_USER, password: FTP_PASSWORD %}
  {% ftp_list path: "/inbox", pattern: "*.csv" as list_result %}

  {% for file in list_result.files %}
    {% comment %} Download and process {% endcomment %}
    {% ftp_download from: file.path as download %}

    {% if download.ok %}
      {% assign content = download.file.name | content %}
      {% comment %} ... process content ... {% endcomment %}

      {% comment %} Move to processed folder {% endcomment %}
      {% assign archive_path = "/processed/" | append: file.name %}
      {% ftp_move from: file.path, to: archive_path as move_result %}

      {% if move_result.ok %}
        {% log "Processed and archived: " %}
        {% log file.name %}
      {% endif %}
    {% endif %}
  {% endfor %}
{% endftp_session %}
```

**Move to dated archive folder:**

```liquid
{% ftp_session host: FTP_HOST, user: FTP_USER, password: FTP_PASSWORD %}
  {% assign today = "now" | date: "%Y-%m-%d" %}
  {% assign archive_folder = "/archives/" | append: today | append: "/" %}

  {% ftp_list path: "/completed" as files %}

  {% for file in files.files %}
    {% if file.type == "file" %}
      {% assign destination = archive_folder | append: file.name %}
      {% ftp_move from: file.path, to: destination as result %}
    {% endif %}
  {% endfor %}
{% endftp_session %}
```

#### Notes

* Must be used inside an `ftp_session` block
* Consumes 2 credits per operation
* Destination directories are created automatically if they don't exist
* Can be used to rename files by moving to same directory with different name
* Works for both files and directories


# ftp\_session

Establishes a connection to an FTP or SFTP server. All FTP operations (`ftp_list`, `ftp_download`, `ftp_upload`, `ftp_move`, `ftp_delete`) must be placed inside this block.

```liquid
{% ftp_session host: "ftp.example.com", user: "username", password: "password" %}
  {% ftp_list path: "/data" as files %}
  {% log files %}
{% endftp_session %}
```

#### Syntax

```liquid
{% ftp_session host: "hostname", user: "username", password: "password" %}
  ... FTP operations ...
{% endftp_session %}
```

#### Parameters

| Parameter  | Required | Default              | Description                       |
| ---------- | -------- | -------------------- | --------------------------------- |
| `host`     | Yes      | -                    | FTP server hostname               |
| `user`     | Yes      | -                    | Username for authentication       |
| `password` | Yes      | -                    | Password for authentication       |
| `port`     | No       | 21 (FTP) / 22 (SFTP) | Server port                       |
| `sftp`     | No       | `false`              | Set to `true` for SFTP connection |

#### Examples

**Standard FTP connection:**

```liquid
{% ftp_session host: "ftp.example.com", user: "myuser", password: "mypass" %}
  {% ftp_list path: "/" as result %}
  {% log result.files %}
{% endftp_session %}
```

**SFTP connection:**

```liquid
{% ftp_session host: "sftp.example.com", user: "myuser", password: "mypass", sftp: true %}
  {% ftp_download from: "/exports/data.csv" as result %}
  {% log result %}
{% endftp_session %}
```

**Custom port:**

```liquid
{% ftp_session host: "ftp.example.com", user: "myuser", password: "mypass", port: 2121 %}
  {% ftp_list path: "/files" as result %}
{% endftp_session %}
```

**Using variables for credentials:**

```liquid
{% ftp_session host: FTP_HOST, user: FTP_USER, password: FTP_PASSWORD, sftp: FTP_SFTP %}
  {% ftp_list path: FTP_PATH as result %}
  {% for file in result.files %}
    {% log file.name %}
  {% endfor %}
{% endftp_session %}
```

#### Notes

* Connection is automatically closed when the block ends
* All FTP operation tags must be nested inside `ftp_session`
* Supports both FTP (port 21) and SFTP (port 22)
* Connection timeout is 5 minutes by default
* Each FTP operation inside the session consumes 2 credits


# ftp\_upload

Uploads content or a file to the FTP/SFTP server. Must be used inside an `ftp_session` block.

```liquid
{% ftp_session host: "ftp.example.com", user: "myuser", password: "mypass" %}
  {% ftp_upload to: "/uploads/data.csv", content: csv_content as result %}

  {% if result.ok %}
    {% log "Upload successful" %}
  {% endif %}
{% endftp_session %}
```

#### Syntax

```liquid
{% ftp_upload to: "/path/to/destination", content: content_variable as result_variable %}
{% ftp_upload to: "/path/to/destination", file: "filename_in_storage" as result_variable %}
```

#### Parameters

| Parameter | Required | Description                               |
| --------- | -------- | ----------------------------------------- |
| `to`      | Yes      | Destination path on the FTP server        |
| `content` | Yes\*    | Raw content to upload                     |
| `file`    | Yes\*    | Name of file in DataJet storage to upload |

\*Either `content` or `file` is required, but not both.

#### Result Object

| Property    | Type        | Description                                         |
| ----------- | ----------- | --------------------------------------------------- |
| `ok`        | boolean     | `true` if upload succeeded                          |
| `error`     | string/null | Error message if `ok` is `false`, `null` on success |
| `file.name` | string      | Name of the uploaded file                           |

Example result:

```json
{
  "ok": true,
  "error": null,
  "file": {
    "name": "inventory.csv"
  }
}
```

#### Examples

**Upload text content:**

```liquid
{% capture csv_content %}name,sku,quantity
Product A,SKU001,100
Product B,SKU002,50{% endcapture %}

{% ftp_session host: FTP_HOST, user: FTP_USER, password: FTP_PASSWORD %}
  {% ftp_upload to: "/imports/inventory.csv", content: csv_content as result %}

  {% if result.ok %}
    {% log "CSV uploaded successfully" %}
  {% else %}
    {% log result.error %}
  {% endif %}
{% endftp_session %}
```

**Upload JSON data:**

```liquid
{% json export_data %}
  {
    "products": {{ products | json }},
    "exported_at": "{{ 'now' | date: '%Y-%m-%d %H:%M:%S' }}"
  }
{% endjson %}

{% assign json_content = export_data | json %}

{% ftp_session host: FTP_HOST, user: FTP_USER, password: FTP_PASSWORD, sftp: true %}
  {% ftp_upload to: "/data/products.json", content: json_content as result %}
{% endftp_session %}
```

**Upload file from DataJet storage:**

```liquid
{% ftp_session host: FTP_HOST, user: FTP_USER, password: FTP_PASSWORD %}
  {% ftp_upload to: "/backups/report.pdf", file: "generated_report.pdf" as result %}

  {% if result.ok %}
    {% log "File uploaded from storage" %}
  {% endif %}
{% endftp_session %}
```

**Upload to nested directory (auto-created):**

```liquid
{% ftp_session host: FTP_HOST, user: FTP_USER, password: FTP_PASSWORD %}
  {% assign today = "now" | date: "%Y/%m/%d" %}
  {% assign destination = "/archives/" | append: today | append: "/export.csv" %}

  {% ftp_upload to: destination, content: csv_data as result %}
  {% comment %} Directories /archives/2024/01/15/ will be created automatically {% endcomment %}
{% endftp_session %}
```

#### Notes

* Must be used inside an `ftp_session` block
* Consumes 2 credits per upload
* Directories in the destination path are created automatically if they don't exist
* Use `content` for raw text/data, use `file` for files already in DataJet storage
* Maximum content size depends on your plan limits


# ftp\_list

Lists files and directories at a specified path on the FTP/SFTP server. Must be used inside an `ftp_session` block.

```liquid
{% ftp_session host: "ftp.example.com", user: "myuser", password: "mypass" %}
  {% ftp_list path: "/data" as result %}

  {% if result.ok %}
    {% for file in result.files %}
      {% log file.name %}
    {% endfor %}
  {% endif %}
{% endftp_session %}
```

#### Syntax

```liquid
{% ftp_list path: "/directory/path" as result_variable %}
```

#### Parameters

| Parameter | Required | Description                                    |
| --------- | -------- | ---------------------------------------------- |
| `path`    | Yes      | Directory path to list                         |
| `pattern` | No       | Glob pattern to filter files (e.g., `"*.csv"`) |

#### Result Object

| Property | Type        | Description                                         |
| -------- | ----------- | --------------------------------------------------- |
| `ok`     | boolean     | `true` if listing succeeded                         |
| `error`  | string/null | Error message if `ok` is `false`, `null` on success |
| `files`  | array       | Array of file objects                               |

Example result:

```json
{
  "ok": true,
  "error": null,
  "files": [
    {
      "name": "data.csv",
      "type": "file",
      "size": 1024,
      "path": "/exports/data.csv",
      "modifiedAt": 1704067200
    }
  ]
}
```

#### File Object Properties

| Property     | Type   | Description                         |
| ------------ | ------ | ----------------------------------- |
| `name`       | string | File or directory name              |
| `path`       | string | Full path to the file               |
| `type`       | string | `"file"` or `"dir"`                 |
| `size`       | number | File size in bytes                  |
| `modifiedAt` | number | Unix timestamp of last modification |

#### Examples

**List all files:**

```liquid
{% ftp_session host: FTP_HOST, user: FTP_USER, password: FTP_PASSWORD %}
  {% ftp_list path: "/exports" as result %}

  {% if result.ok %}
    {% log "Found files:" %}
    {% for file in result.files %}
      {% log file.name %}
    {% endfor %}
  {% else %}
    {% log result.error %}
  {% endif %}
{% endftp_session %}
```

**Filter by pattern:**

```liquid
{% ftp_session host: FTP_HOST, user: FTP_USER, password: FTP_PASSWORD %}
  {% ftp_list path: "/data", pattern: "*.csv" as csv_files %}

  {% for file in csv_files.files %}
    {% log file.name %}
  {% endfor %}
{% endftp_session %}
```

**Filter files only (exclude directories):**

```liquid
{% ftp_session host: FTP_HOST, user: FTP_USER, password: FTP_PASSWORD %}
  {% ftp_list path: "/uploads" as result %}

  {% for item in result.files %}
    {% if item.type == "file" %}
      {% log item.name %}
    {% endif %}
  {% endfor %}
{% endftp_session %}
```

**Sort by modification date:**

```liquid
{% ftp_session host: FTP_HOST, user: FTP_USER, password: FTP_PASSWORD %}
  {% ftp_list path: "/logs" as result %}

  {% assign sorted_files = result.files | sort: "modifiedAt" | reverse %}
  {% assign latest_file = sorted_files | first %}
  {% log latest_file.name %}
{% endftp_session %}
```

#### Notes

* Must be used inside an `ftp_session` block
* Consumes 2 credits per operation
* Pattern uses glob syntax (e.g., `*.csv`, `report_*.txt`, `**/*.json`)
* Returns empty `files` array if directory is empty


# function

Functions allow you to create short, reusable scripts that can be called from any of your other scripts. They are useful for encapsulating logic you use in multiple places - API calls, data transformations, calculations, and more.

To create a function, select the `Add new function script` button.

### Syntax

```liquid
{% function "function_handle", param1:value1, param2:value2 as result %}
```

* `"function_handle"` — the handle (or ID) of the function script to call.
* `param1:value1, param2:value2` — named parameters. Each becomes a variable inside the function.
* `as result` — captures the function's return value into a variable.

### Parameters

Named parameters let you pass values directly into the function scope. Each `key:value` pair becomes a variable accessible inside the function code.

Values can be variables, string literals, or any Liquid expression:

```liquid
{% assign user_email = "support@code57.pl" %}

{% function "send_email", email:user_email, subject:"Welcome!" as email_result %}
```

Inside the function, `email` and `subject` are available as regular variables.

You can pass as many parameters as needed:

```liquid
{% comment %}Inside webhook orders/paid script{% endcomment %}
{% assign order = payload %}
{% function "create_order_note", order_id:order.id, message:"Note text", priority:"high", notify:true as note_result %}
{% log note_result %}
```

### Return values

Functions return a value using the `return` tag. The returned value is captured by the `as` clause.

**Function code** (handle: `add_numbers`):

```liquid
{% comment %}Inside function script{% endcomment %}
{% assign sum = a | plus: b %}
{% return sum %}
```

**Calling the function:**

```liquid
{% function "add_numbers", a:5, b:3 as sum_result %}
{% log "This is result: " | append: sum_result %}
```

Output: `The sum is: 8`

The `as sum_result` clause saves the return value so you can use it later in your script — log it, pass it to another function, or use it in conditions:

```liquid
{% function "add_numbers", a:5, b:3 as sum_result %}
{% if sum_result > 5 %}
  {% log "Sum is greater than 5" %}
{% endif %}
```

#### Without `as`

The `as variable` clause is optional. If omitted, the function's return value is written directly to the output:

```liquid
{% function "add_numbers", a:5, b:3 %}
```

This also outputs `8`, but you cannot reference the result later in your script.

### Example: Send metrics to Klaviyo

**Function code** (handle: `send_metric_to_klaviyo`):

```liquid
{% json body %}
{
  "data": {
    "type": "event",
    "attributes": {
      "properties": {
        "membership": {{ property | default: "" | json }}
      },
      "metric": {
        "data": {
          "type": "metric",
          "attributes": {
            "name": {{ metric | default: "" | json }}
          }
        }
      },
      "profile": {
        "data": {
          "type": "profile",
          "attributes": {
            "email": {{ email | default: "" | json }}
          }
        }
      }
    }
  }
}
{% endjson %}

{% json request_options %}
{
  "url": "https://a.klaviyo.com/client/events/?company_id={{KLAVIYO_TOKEN}}",
  "method": "POST",
  "headers": {
    "Content-Type": "application/json",
    "Accept": "application/json",
    "Revision": "2024-05-15"
  },
  "body": {{ body | json }}
}
{% endjson %}
{% http url:request_options.url, method:request_options.method, body:request_options.body, headers:request_options.headers as response %}
{% return response %}
```

Notice that `email`, `property`, and `metric` are used directly as variables — they are injected into the function scope when the function is called.

**Calling the function from another script:**

```liquid
{% assign user_email = "support@code57.pl" %}
{% assign membership_level = "GOLD" %}

{% function "send_metric_to_klaviyo", email:user_email, property:membership_level, metric:"MEMBERSHIP UPDATE" as fn_result %}

{% log fn_result %}
```

After execution, `fn_result` contains the result of the HTTP call made inside `send_metric_to_klaviyo`.

### Example: Reusable GraphQL helper

**Function code** (handle: `get_product_title`):

```liquid
{% capture query %}
  query { product(id: "{{ product_id }}") { title } }
{% endcapture %}
{% graphql query:query as gql_result %}
{% return gql_result.data.product.title %}
```

**Calling the function:**

```liquid
{% function "get_product_title", product_id:"gid://shopify/Product/123456" as title %}
{% log title %}
```

### Example: Calling a function inside a loop

Functions are especially useful inside loops where you need to perform the same operation for each item:

```liquid
{% for item in order.line_items %}
  {% function "check_inventory", variant_id:item.variant_id, quantity:item.quantity as stock_status %}
  {% if stock_status.available == false %}
    {% log "Out of stock: " | append: item.title %}
  {% endif %}
{% endfor %}
```

### Legacy format: JSON payload

The old format using a single JSON payload variable is still supported. All keys from the payload object are spread into the function scope.

```liquid
{% json fn_input %}
{
  "email": "support@code57.pl",
  "property": "GOLD",
  "metric": "MEMBERSHIP UPDATE"
}
{% endjson %}

{% function "send_metric_to_klaviyo", fn_input as fn_result %}

{% log fn_result %}
```

> **Note:** Named parameters and the JSON payload format cannot be mixed in a single call. Use one or the other.

### Limitations

* **No nested function calls** — you cannot call a function from inside another function.
* **Task variables are not available** inside functions. Pass any values you need as parameters.
* **Global variables are accessible** inside functions.


# graphql

Executes queries and mutations against Shopify's GraphQL Admin API. The response is stored in a variable for further processing.

```liquid
{% graphql query: product_query, variables: my_variables as result %}

{% if result.product %}
  {% log result.product.title %}
{% endif %}
```

#### Syntax

```liquid
{% graphql query: query_string, variables: variables_object as result_variable %}
```

#### Parameters

| Parameter   | Required | Description                                           |
| ----------- | -------- | ----------------------------------------------------- |
| `query`     | Yes      | GraphQL query or mutation string                      |
| `variables` | No       | Object containing variables for the GraphQL operation |

#### Result Object

The result contains the returned data directly accessible at the top level. Query fields are available as properties on the result variable.

Example query:

```liquid
{% graphql query: product_query, variables: vars as result %}
{% comment %} Access result.product directly, not result.data.product {% endcomment %}
```

If errors occur, they are available in the `errors` property:

| Property | Type       | Description                                    |
| -------- | ---------- | ---------------------------------------------- |
| `errors` | array/null | Array of error objects if the operation failed |

Example result for a product query:

```json
{
  "product": {
    "id": "gid://shopify/Product/123",
    "title": "Example Product"
  }
}
```

#### Examples

**Basic product query:**

```liquid
{% capture query %}
  query getProduct($id: ID!) {
    product(id: $id) {
      id
      title
      handle
      status
    }
  }
{% endcapture %}

{% json variables %}
  {
    "id": "gid://shopify/Product/{{ product_id }}"
  }
{% endjson %}

{% graphql query: query, variables: variables as result %}

{% if result.product %}
  {% log "Product title:" %}
  {% log result.product.title %}
{% endif %}
```

**Update product with mutation:**

```liquid
{% capture mutation %}
  mutation updateProduct($input: ProductInput!) {
    productUpdate(input: $input) {
      product {
        id
        title
      }
      userErrors {
        field
        message
      }
    }
  }
{% endcapture %}

{% json variables %}
  {
    "input": {
      "id": "gid://shopify/Product/{{ product_id }}",
      "title": "{{ new_title }}"
    }
  }
{% endjson %}

{% graphql query: mutation, variables: variables as result %}

{% if result.productUpdate.userErrors.size > 0 %}
  {% for error in result.productUpdate.userErrors %}
    {% log error.message %}
  {% endfor %}
{% else %}
  {% log "Product updated successfully" %}
{% endif %}
```

**Paginated query with cursor:**

```liquid
{% capture query %}
  query getCustomers($cursor: String, $query: String) {
    customers(first: 50, after: $cursor, query: $query) {
      edges {
        node {
          id
          email
          firstName
          lastName
        }
      }
      pageInfo {
        hasNextPage
        endCursor
      }
    }
  }
{% endcapture %}

{% assign cursor = null %}
{% assign all_customers = "" | split: "" %}

{% for i in (1..100) %}
  {% json variables %}
    {
      "cursor": {{ cursor | json }},
      "query": "tag:vip"
    }
  {% endjson %}

  {% graphql query: query, variables: variables as result %}

  {% for edge in result.customers.edges %}
    {% assign all_customers = all_customers | push: edge.node %}
  {% endfor %}

  {% if result.customers.pageInfo.hasNextPage %}
    {% assign cursor = result.customers.pageInfo.endCursor %}
  {% else %}
    {% break %}
  {% endif %}
{% endfor %}

{% log "Total customers found: " | append: all_customers.size %}
```

**Query with dynamic variables:**

```liquid
{% capture query %}
  query getOrder($id: ID!) {
    order(id: $id) {
      id
      name
      totalPriceSet {
        shopMoney {
          amount
          currencyCode
        }
      }
      lineItems(first: 50) {
        edges {
          node {
            title
            quantity
          }
        }
      }
    }
  }
{% endcapture %}

{% assign order_gid = "gid://shopify/Order/" | append: order.id %}

{% json variables %}
  {
    "id": "{{ order_gid }}"
  }
{% endjson %}

{% graphql query: query, variables: variables as order_data %}

{% assign total = order_data.order.totalPriceSet.shopMoney.amount %}
{% log "Order total:" | append: total %}
```

**Handle errors gracefully:**

```liquid
{% capture query %}
  query getInventory($id: ID!) {
    inventoryItem(id: $id) {
      id
      tracked
      inventoryLevels(first: 10) {
        edges {
          node {
            available
            location {
              name
            }
          }
        }
      }
    }
  }
{% endcapture %}

{% json variables %}
  {
    "id": "gid://shopify/InventoryItem/{{ inventory_item_id }}"
  }
{% endjson %}

{% graphql query: query, variables: variables as result %}

{% if result.errors %}
  {% log "GraphQL error occurred:" %}
  {% for error in result.errors %}
    {% log error.message %}
  {% endfor %}
{% else %}
  {% for level in result.inventoryItem.inventoryLevels.edges %}
    {% log level.node.location.name %}
    {% log level.node.available %}
  {% endfor %}
{% endif %}
```

**Bulk operation query:**

```liquid
{% capture mutation %}
  mutation bulkOperationRunQuery($query: String!) {
    bulkOperationRunQuery(query: $query) {
      bulkOperation {
        id
        status
      }
      userErrors {
        field
        message
      }
    }
  }
{% endcapture %}

{% capture bulk_query %}
{
  products {
    edges {
      node {
        id
        title
        variants {
          edges {
            node {
              id
              sku
              inventoryQuantity
            }
          }
        }
      }
    }
  }
}
{% endcapture %}

{% json variables %}
  {
    "query": {{ bulk_query | json }}
  }
{% endjson %}

{% graphql query: mutation, variables: variables as result %}

{% if result.bulkOperationRunQuery.bulkOperation %}
  {% log "Bulk operation started:" %}
  {% log result.bulkOperationRunQuery.bulkOperation.id %}
{% endif %}
```

#### Notes

* Consumes 1 credit per operation
* Uses the store's API version configured in DataJet
* GraphQL errors are automatically logged to the script logs
* Variables must be a valid object (use `json` tag to construct complex variables)
* For large datasets, use pagination with cursors to avoid timeouts
* Result properties are accessed directly (e.g., `result.products`, not `result.data.products`)
* Refer to [Shopify's GraphQL Admin API documentation](https://shopify.dev/docs/api/admin-graphql) for available queries and mutations


# http

Makes HTTP requests to external APIs and endpoints. The response is stored in a variable for further processing.

```liquid
{% http url: "https://api.example.com/data", method: "GET" as response %}

{% if response.ok %}
  {% log "Request successful: " | append: response.status %}
{% endif %}
```

#### Syntax

```liquid
{% http url: endpoint, method: http_method, headers: headers_object, body: body_object as result_variable %}
```

#### Parameters

| Parameter | Required | Description                                                                              |
| --------- | -------- | ---------------------------------------------------------------------------------------- |
| `url`     | Yes      | The endpoint URL to call                                                                 |
| `method`  | Yes      | HTTP method: `"GET"`, `"POST"`, `"PUT"`, `"PATCH"`, `"DELETE"`                           |
| `headers` | No       | Object containing request headers                                                        |
| `body`    | No       | Request payload (automatically JSON-stringified unless Content-Type specifies otherwise) |
| `raw`     | No       | Set to `true` to return raw text instead of parsed JSON                                  |

#### Result Object

| Property     | Type          | Description                                       |
| ------------ | ------------- | ------------------------------------------------- |
| `url`        | string        | The final URL (after redirects)                   |
| `status`     | number        | HTTP status code (200, 404, 500, etc.)            |
| `statusText` | string        | HTTP status message ("OK", "Not Found", etc.)     |
| `headers`    | object        | Response headers as key-value pairs               |
| `ok`         | boolean       | `true` if status is 200-299                       |
| `body`       | object/string | Parsed JSON response (or raw text if `raw: true`) |

Example result:

```json
{
  "url": "https://api.example.com/data",
  "status": 200,
  "statusText": "OK",
  "headers": {
    "content-type": "application/json"
  },
  "ok": true,
  "body": {
    "success": true,
    "data": []
  }
}
```

#### Examples

**Simple GET request:**

```liquid
{% http url: "https://api.example.com/products", method: "GET" as response %}

{% if response.ok %}
  {% for product in response.body.products %}
    {% log "Product: " | append: product.name %}
  {% endfor %}
{% else %}
  {% log "Request failed: " | append: response.status %}
{% endif %}
```

**POST request with JSON body:**

```liquid
{% json request_body %}
  {
    "name": "{{ product.title }}",
    "sku": "{{ product.sku }}",
    "price": {{ product.price }}
  }
{% endjson %}

{% json headers %}
  {
    "Content-Type": "application/json",
    "Authorization": "Bearer {{ api_token }}"
  }
{% endjson %}

{% http url: "https://api.example.com/products", method: "POST", headers: headers, body: request_body as response %}

{% if response.status == 201 %}
  {% log "Product created with ID: " | append: response.body.id %}
{% else %}
  {% log "Error: " | append: response.body.message %}
{% endif %}
```

**PUT request to update resource:**

```liquid
{% assign endpoint = "https://api.example.com/orders/" | append: order.id %}

{% json headers %}
  {
    "Content-Type": "application/json",
    "X-API-Key": "{{ API_KEY }}"
  }
{% endjson %}

{% json body %}
  {
    "status": "shipped",
    "tracking_number": "{{ tracking_number }}"
  }
{% endjson %}

{% http url: endpoint, method: "PUT", headers: headers, body: body as response %}

{% if response.ok %}
  {% log "Order updated successfully" %}
{% endif %}
```

**Form-urlencoded POST request:**

```liquid
{% json headers %}
  {
    "Content-Type": "application/x-www-form-urlencoded"
  }
{% endjson %}

{% assign form_body = "grant_type=client_credentials&client_id=" | append: CLIENT_ID | append: "&client_secret=" | append: CLIENT_SECRET %}

{% http url: "https://auth.example.com/oauth/token", method: "POST", headers: headers, body: form_body as response %}

{% if response.ok %}
  {% assign access_token = response.body.access_token %}
  {% log "Token obtained successfully" %}
{% endif %}
```

**Send data to webhook:**

```liquid
{% json payload %}
  {
    "event": "order_created",
    "order_id": {{ order.id }},
    "customer_email": "{{ order.email }}",
    "total": {{ order.total_price }},
    "timestamp": "{{ 'now' | date: '%Y-%m-%dT%H:%M:%SZ' }}"
  }
{% endjson %}

{% json headers %}
  {
    "Content-Type": "application/json"
  }
{% endjson %}

{% http url: WEBHOOK_URL, method: "POST", headers: headers, body: payload as response %}

{% log "Webhook response: " | append: response.status %}
```

**GET raw text response (e.g., CSV):**

```liquid
{% http url: "https://api.example.com/export.csv", method: "GET", raw: true as response %}

{% if response.ok %}
  {% assign csv_content = response.body %}
  {% assign rows = csv_content | parse_csv %}

  {% for row in rows %}
    {% log row %}
  {% endfor %}
{% endif %}
```

**Send XML data:**

```liquid
{% capture xml_body %}<?xml version="1.0" encoding="UTF-8"?>
<order>
  <id>{{ order.id }}</id>
  <total>{{ order.total_price }}</total>
</order>
{% endcapture %}

{% json headers %}
  {
    "Content-Type": "text/xml"
  }
{% endjson %}

{% http url: "https://api.example.com/orders", method: "POST", headers: headers, body: xml_body as response %}

{% log "XML request status: " | append: response.status %}
```

**Integrate with external CRM:**

```liquid
{% json headers %}
  {
    "Content-Type": "application/json",
    "Authorization": "Bearer {{ CRM_API_KEY }}"
  }
{% endjson %}

{% json contact_data %}
  {
    "email": "{{ customer.email }}",
    "firstName": "{{ customer.first_name }}",
    "lastName": "{{ customer.last_name }}",
    "properties": {
      "total_orders": {{ customer.orders_count }},
      "total_spent": {{ customer.total_spent }}
    }
  }
{% endjson %}

{% http url: "https://api.crm.com/contacts", method: "POST", headers: headers, body: contact_data as response %}

{% if response.ok %}
  {% log "Contact synced to CRM" %}
{% else %}
  {% log "CRM sync failed: " | append: response.body.error %}
{% endif %}
```

#### Notes

* Consumes 1 credit per request
* Request body is automatically JSON-stringified unless Content-Type is:
  * `application/x-www-form-urlencoded`
  * `multipart/form-data`
  * `text/plain`
  * `text/xml`
* Rate limited responses (429) are automatically retried with backoff
* Server errors are retried up to 3 times before failing
* Request timeout is 5 minutes
* Use `raw: true` when expecting non-JSON responses (CSV, XML, plain text)


# json

Captures content and parses it as JSON, allowing immediate access to properties. Works like `capture` but automatically converts the content to a JSON object.

```liquid
{% json my_data %}
  {
    "name": "Example",
    "value": 123
  }
{% endjson %}

{% log my_data.name %}
```

#### Syntax

```liquid
{% json variable_name %}
  { JSON content }
{% endjson %}
```

#### Parameters

| Parameter       | Required | Description                                   |
| --------------- | -------- | --------------------------------------------- |
| `variable_name` | Yes      | Name of the variable to store the parsed JSON |

#### How It Works

1. Content between `{% json %}` and `{% endjson %}` is rendered (Liquid variables are interpolated)
2. The resulting string is parsed as JSON
3. The parsed object is assigned to the specified variable
4. Properties can be accessed immediately using dot notation

#### Examples

**Basic JSON object:**

```liquid
{% json config %}
  {
    "enabled": true,
    "max_items": 50,
    "api_version": "2024-01"
  }
{% endjson %}

{% if config.enabled %}
  {% log "Config loaded, max items: " | append: config.max_items %}
{% endif %}
```

**Dynamic JSON with Liquid variables:**

```liquid
{% json order_payload %}
  {
    "order_id": {{ order.id }},
    "customer": {
      "email": "{{ customer.email }}",
      "name": "{{ customer.first_name }} {{ customer.last_name }}"
    },
    "total": {{ order.total_price }},
    "currency": "{{ shop.currency }}"
  }
{% endjson %}

{% log "Payload created for order: " | append: order_payload.order_id %}
```

**Build request body for HTTP call:**

```liquid
{% json request_body %}
  {
    "event": "purchase",
    "properties": {
      "product_ids": {{ line_items | map: "product_id" | json }},
      "total_value": {{ order.total_price }},
      "currency": "{{ order.currency }}"
    },
    "user_id": "{{ customer.id }}"
  }
{% endjson %}

{% json headers %}
  {
    "Content-Type": "application/json",
    "Authorization": "Bearer {{ API_KEY }}"
  }
{% endjson %}

{% http url: "https://api.example.com/events", method: "POST", headers: headers, body: request_body as response %}
```

**Create array of objects:**

```liquid
{% json products_array %}
  [
    {% for item in order.line_items %}
      {
        "sku": "{{ item.sku }}",
        "quantity": {{ item.quantity }},
        "price": {{ item.price }}
      }{% unless forloop.last %},{% endunless %}
    {% endfor %}
  ]
{% endjson %}

{% log "Products count: " | append: products_array.size %}
```

**Nested JSON structure:**

```liquid
{% json webhook_payload %}
  {
    "type": "order.created",
    "timestamp": "{{ 'now' | date: '%Y-%m-%dT%H:%M:%SZ' }}",
    "data": {
      "order": {
        "id": {{ order.id }},
        "name": "{{ order.name }}",
        "line_items": {{ order.line_items | json }}
      },
      "customer": {
        "id": {{ customer.id }},
        "email": "{{ customer.email }}"
      }
    },
    "metadata": {
      "source": "datajet",
      "shop": "{{ shop.domain }}"
    }
  }
{% endjson %}

{% assign customer_email = webhook_payload.data.customer.email %}
```

**Build GraphQL variables:**

```liquid
{% capture query %}
  mutation updateProduct($input: ProductInput!) {
    productUpdate(input: $input) {
      product { id title }
      userErrors { field message }
    }
  }
{% endcapture %}

{% json variables %}
  {
    "input": {
      "id": "gid://shopify/Product/{{ product_id }}",
      "title": "{{ new_title }}",
      "tags": {{ new_tags | json }}
    }
  }
{% endjson %}

{% graphql query: query, variables: variables as result %}
```

**Set function return value:**

```liquid
{% comment %} Inside a function, use 'response' to set return value {% endcomment %}
{% json response %}
  {
    "success": true,
    "data": {
      "processed": {{ items_processed }},
      "errors": []
    }
  }
{% endjson %}

{% return response %}
```

**Conditional JSON content:**

```liquid
{% json notification %}
  {
    "channel": "email",
    "recipient": "{{ customer.email }}",
    "template": "{% if order.total_price > 100 %}vip_order{% else %}standard_order{% endif %}",
    "priority": {% if order.fulfillment_status == "unfulfilled" %}1{% else %}5{% endif %}
  }
{% endjson %}
```

#### Error Handling

If the content is not valid JSON, an error is thrown with the invalid content and parse error message:

```liquid
{% comment %} This will throw an error - missing quotes around value {% endcomment %}
{% json invalid %}
  {
    "key": invalid_value
  }
{% endjson %}
```

Error message will include the problematic JSON and the specific parse error.

#### Notes

* Content is first rendered as Liquid, then parsed as JSON
* Use `| json` filter to safely embed arrays or objects: `{{ my_array | json }}`
* Strings in JSON must be properly quoted with double quotes
* Special characters in string values should be escaped
* Variable names `response` and `return` have special meaning in functions
* Invalid JSON will throw an error - validate your JSON structure
* Use trailing comma handling with `{% unless forloop.last %},{% endunless %}` in loops


# log

Outputs a message to the Logs section of the Scripts Console. Useful for debugging, tracking script execution, and monitoring variable values.

```liquid
{% log "Hello world!" %}
{% log order.name %}
{% log "Order total: " | append: order.total_price %}
```

#### Syntax

```liquid
{% log value %}
{% log value | filter %}
{% log "string" | append: variable %}
```

#### Parameters

| Parameter | Required | Description                                                   |
| --------- | -------- | ------------------------------------------------------------- |
| `value`   | Yes      | Any value to log - string, number, object, array, or variable |

#### Output Formatting

The log tag automatically formats output based on the value type:

| Value Type  | Output                          |
| ----------- | ------------------------------- |
| String      | Displayed as-is                 |
| Number      | Displayed as-is                 |
| Object      | JSON-formatted with indentation |
| Array       | JSON-formatted with indentation |
| `null`      | Displayed as "null"             |
| `undefined` | Displayed as "undefined"        |

#### Examples

**Log a simple message:**

```liquid
{% log "Script started" %}
```

**Log a variable:**

```liquid
{% log order %}
{% log customer.email %}
{% log line_items %}
```

**Log with appended values:**

```liquid
{% log "Processing order: " | append: order.name %}
{% log "Customer email: " | append: customer.email %}
{% log "Total items: " | append: order.line_items.size %}
```

**Log calculation results:**

```liquid
{% assign discount = order.total_price | times: 0.1 %}
{% log "Discount amount: " | append: discount %}
```

**Debug conditional logic:**

```liquid
{% if customer.orders_count > 10 %}
  {% log "VIP customer detected" %}
  {% assign is_vip = true %}
{% else %}
  {% log "Regular customer" %}
  {% assign is_vip = false %}
{% endif %}
```

**Log loop progress:**

```liquid
{% for item in order.line_items %}
  {% log "Processing item: " | append: item.title %}

  {% comment %} Process item... {% endcomment %}

  {% log "Item " | append: forloop.index | append: " of " | append: forloop.length | append: " complete" %}
{% endfor %}
```

**Log object properties:**

```liquid
{% log "Order ID: " | append: order.id %}
{% log "Order name: " | append: order.name %}
{% log "Created at: " | append: order.created_at %}
{% log "Financial status: " | append: order.financial_status %}
```

**Log entire objects for debugging:**

```liquid
{% comment %} Log full order object {% endcomment %}
{% log order %}

{% comment %} Log API response {% endcomment %}
{% http url: api_endpoint, method: "GET" as response %}
{% log response %}

{% comment %} Log GraphQL result {% endcomment %}
{% graphql query: my_query, variables: vars as result %}
{% log result %}
```

**Log array contents:**

```liquid
{% assign tags = product.tags | split: ", " %}
{% log "Product tags:" %}
{% log tags %}
```

**Track script execution flow:**

```liquid
{% log "=== Starting inventory sync ===" %}

{% log "Step 1: Fetching products..." %}
{% comment %} ... fetch logic ... {% endcomment %}

{% log "Step 2: Processing updates..." %}
{% comment %} ... update logic ... {% endcomment %}

{% log "Step 3: Sending notifications..." %}
{% comment %} ... notification logic ... {% endcomment %}

{% log "=== Inventory sync complete ===" %}
```

**Log with JSON filter for complex objects:**

```liquid
{% json payload %}
  {
    "order_id": {{ order.id }},
    "items": {{ order.line_items | map: "sku" | json }}
  }
{% endjson %}

{% log "Payload created:" %}
{% log payload %}
```

**Conditional logging:**

```liquid
{% if response.status != 200 %}
  {% log "API Error - Status: " | append: response.status %}
  {% log "Error body:" %}
  {% log response.body %}
{% endif %}
```

#### Notes

* Logs appear in the Logs section of the Scripts Console
* Objects and arrays are automatically JSON-formatted with 2-space indentation
* Maximum log length is 20,000 characters - longer logs are truncated
* Supports Liquid filters for formatting (e.g., `| append:`, `| upcase`, `| date:`)
* No credit cost for logging
* Use logs liberally during development, consider reducing in production for cleaner output


# pop

Removes and returns the last element from an array. The original array is modified (mutated) and the removed element can be stored in a variable.

```liquid
{% json items %}[1, 2, 3, 4, 5]{% endjson %}
{% pop items as last_item %}

{% log last_item %}
{% comment %} Output: 5 {% endcomment %}

{% log items %}
{% comment %} Output: [1, 2, 3, 4] {% endcomment %}
```

#### Syntax

```liquid
{% pop array_name as variable_name %}
```

#### Parameters

| Parameter          | Required | Description                               |
| ------------------ | -------- | ----------------------------------------- |
| `array_name`       | Yes      | The array to remove the last element from |
| `as variable_name` | No       | Variable to store the removed element     |

#### Behavior

* Removes the **last** element from the array
* Modifies the original array (mutation)
* Returns `null` if the array is empty
* Supports nested array access with dot notation

#### Examples

**Basic pop operation:**

```liquid
{% json queue %}["task1", "task2", "task3"]{% endjson %}
{% pop queue as task %}

{% log "Processing: " | append: task %}
{% comment %} Output: Processing: task3 {% endcomment %}
```

**Pop without storing the value:**

```liquid
{% json numbers %}[1, 2, 3, 4, 5]{% endjson %}
{% pop numbers %}

{% log numbers %}
{% comment %} Output: [1, 2, 3, 4] {% endcomment %}
```

**Process array in reverse (LIFO):**

```liquid
{% json stack %}["first", "second", "third"]{% endjson %}

{% for i in (1..3) %}
  {% pop stack as item %}
  {% if item %}
    {% log "Popped: " | append: item %}
  {% endif %}
{% endfor %}

{% comment %}
Output:
Popped: third
Popped: second
Popped: first
{% endcomment %}
```

**Use with push for stack operations:**

```liquid
{% json undo_stack %}[]{% endjson %}

{% comment %} Add actions to stack {% endcomment %}
{% push undo_stack, "action1" %}
{% push undo_stack, "action2" %}
{% push undo_stack, "action3" %}

{% log "Stack: " %}
{% log undo_stack %}
{% comment %} Output: ["action1", "action2", "action3"] {% endcomment %}

{% comment %} Undo last action {% endcomment %}
{% pop undo_stack as last_action %}
{% log "Undoing: " | append: last_action %}
{% comment %} Output: Undoing: action3 {% endcomment %}
```

**Pop from nested array:**

```liquid
{% json data %}
  {
    "pending": ["order1", "order2", "order3"],
    "completed": []
  }
{% endjson %}

{% pop data.pending as order %}
{% log "Processing order: " | append: order %}
{% comment %} Output: Processing order: order3 {% endcomment %}
```

**Batch processing with pop:**

```liquid
{% json items_to_process %}
  [
    {"id": 1, "name": "Item A"},
    {"id": 2, "name": "Item B"},
    {"id": 3, "name": "Item C"}
  ]
{% endjson %}

{% assign processed_count = 0 %}

{% for i in (1..100) %}
  {% if items_to_process.size == 0 %}
    {% break %}
  {% endif %}

  {% pop items_to_process as item %}
  {% log "Processing: " | append: item.name %}

  {% comment %} Process item... {% endcomment %}

  {% assign processed_count = processed_count | plus: 1 %}
{% endfor %}

{% log "Total processed: " | append: processed_count %}
```

**Combine with conditionals:**

```liquid
{% json errors %}["Error 1", "Error 2"]{% endjson %}

{% if errors.size > 0 %}
  {% pop errors as latest_error %}
  {% log "Latest error: " | append: latest_error %}
{% else %}
  {% log "No errors" %}
{% endif %}
```

#### Related Tags

| Tag    | Description                            |
| ------ | -------------------------------------- |
| `push` | Adds an element to the end of an array |

#### Notes

* The array must exist and be a valid array, otherwise an error is thrown
* Pop modifies the original array - use with caution if you need to preserve the original
* Useful for implementing stack (LIFO - Last In, First Out) data structures
* When the array is empty, the popped value will be `null`/`undefined`
* Supports dot notation for nested arrays (e.g., `data.items`)


# push

Appends an element to the end of an array. The original array is modified (mutated) with the new element added.

```liquid
{% json items %}[1, 2, 3, 4]{% endjson %}
{% push items, 5 %}

{% log items %}
{% comment %} Output: [1, 2, 3, 4, 5] {% endcomment %}
```

#### Syntax

```liquid
{% push array_name, element %}
```

#### Parameters

| Parameter    | Required | Description                               |
| ------------ | -------- | ----------------------------------------- |
| `array_name` | Yes      | The array to add the element to           |
| `element`    | Yes      | The value to append (literal or variable) |

#### Behavior

* Adds the element to the **end** of the array
* Modifies the original array (mutation)
* Element can be any type: string, number, object, array, or variable
* Supports nested array access with dot notation

#### Examples

**Push a literal value:**

```liquid
{% json numbers %}[1, 2, 3]{% endjson %}
{% push numbers, 4 %}
{% push numbers, 5 %}

{% log numbers %}
{% comment %} Output: [1, 2, 3, 4, 5] {% endcomment %}
```

**Push a variable:**

```liquid
{% json cart_items %}[]{% endjson %}
{% assign new_item = "Product A" %}
{% push cart_items, new_item %}

{% log cart_items %}
{% comment %} Output: ["Product A"] {% endcomment %}
```

**Push an object:**

```liquid
{% json orders %}[]{% endjson %}

{% json new_order %}
  {
    "id": 12345,
    "total": 99.99,
    "status": "pending"
  }
{% endjson %}

{% push orders, new_order %}

{% log orders %}
{% comment %} Output: [{"id": 12345, "total": 99.99, "status": "pending"}] {% endcomment %}
```

**Build array in a loop:**

```liquid
{% json product_titles %}[]{% endjson %}

{% for item in order.line_items %}
  {% push product_titles, item.title %}
{% endfor %}

{% log "Products ordered:" %}
{% log product_titles %}
```

**Collect filtered items:**

```liquid
{% json high_value_orders %}[]{% endjson %}

{% for order in orders %}
  {% if order.total_price > 100 %}
    {% push high_value_orders, order %}
  {% endif %}
{% endfor %}

{% log "High value orders: " | append: high_value_orders.size %}
```

**Push to nested array:**

```liquid
{% json data %}
  {
    "pending": [],
    "completed": []
  }
{% endjson %}

{% push data.pending, "task1" %}
{% push data.pending, "task2" %}
{% push data.completed, "task0" %}

{% log data %}
{% comment %}
Output: {
  "pending": ["task1", "task2"],
  "completed": ["task0"]
}
{% endcomment %}
```

**Stack implementation with push and pop:**

```liquid
{% json history %}[]{% endjson %}

{% comment %} Add to history {% endcomment %}
{% push history, "page1" %}
{% push history, "page2" %}
{% push history, "page3" %}

{% log "History: " %}
{% log history %}
{% comment %} Output: ["page1", "page2", "page3"] {% endcomment %}

{% comment %} Go back {% endcomment %}
{% pop history as current %}
{% log "Current page: " | append: current %}
{% comment %} Output: Current page: page3 {% endcomment %}
```

**Collect errors during processing:**

```liquid
{% json errors %}[]{% endjson %}
{% json processed %}[]{% endjson %}

{% for item in items %}
  {% if item.valid %}
    {% push processed, item %}
  {% else %}
    {% json error %}
      {
        "item_id": {{ item.id }},
        "message": "Validation failed"
      }
    {% endjson %}
    {% push errors, error %}
  {% endif %}
{% endfor %}

{% if errors.size > 0 %}
  {% log "Errors encountered:" %}
  {% log errors %}
{% endif %}

{% log "Successfully processed: " | append: processed.size %}
```

**Build comma-separated string from array:**

```liquid
{% json tags %}[]{% endjson %}

{% push tags, "sale" %}
{% push tags, "featured" %}
{% push tags, "new" %}

{% assign tag_string = tags | join: ", " %}
{% log "Tags: " | append: tag_string %}
{% comment %} Output: Tags: sale, featured, new {% endcomment %}
```

**Aggregate data from API calls:**

```liquid
{% json all_results %}[]{% endjson %}

{% for page in (1..5) %}
  {% assign url = "https://api.example.com/data?page=" | append: page %}
  {% http url: url, method: "GET" as response %}

  {% if response.ok %}
    {% for item in response.body.items %}
      {% push all_results, item %}
    {% endfor %}
  {% endif %}
{% endfor %}

{% log "Total items collected: " | append: all_results.size %}
```

#### Related Tags

| Tag   | Description                                 |
| ----- | ------------------------------------------- |
| `pop` | Removes and returns the last element (LIFO) |

#### Notes

* The array must exist and be a valid array, otherwise an error is thrown
* Push modifies the original array in place
* Useful for building arrays dynamically during script execution
* Works with the Liquid `| push:` filter, but the tag version supports complex values
* Supports dot notation for nested arrays (e.g., `data.items`)
* No limit on the number of elements that can be pushed


# return

Returns a value from a function or sends a response from HTTP, shipping-rate and function scripts. Stops script execution immediately after returning.

```liquid
{% json response %}
  {
    "success": true,
    "message": "Operation completed"
  }
{% endjson %}

{% return response %}
```

#### Syntax

```liquid
{% return value %}
```

#### Parameters

| Parameter | Required | Description                                                  |
| --------- | -------- | ------------------------------------------------------------ |
| `value`   | Yes      | The value to return (variable, object, string, number, etc.) |

#### Behavior

* Immediately stops script execution
* Sets the return value accessible to the caller
* In functions: the returned value is output where the function was called
* In HTTP scripts: the returned value becomes the response body
* In shipping-rate scripts: the returned value defines the shipping rates
* Only one value can be returned

#### Examples

**Return from a function:**

```liquid
{% comment %} Function: calculate_discount {% endcomment %}
{% assign discount = total | times: 0.1 %}
{% return discount %}
```

```liquid
{% comment %} Calling the function {% endcomment %}
{% capture result %}{% function "calculate_discount", total: 100 %}{% endcapture %}
{% log "Discount: " | append: result %}
{% comment %} Output: Discount: 10 {% endcomment %}
```

**Return an object from a function:**

```liquid
{% comment %} Function: validate_order {% endcomment %}
{% if order.total > 0 and order.email != blank %}
  {% json result %}
    {
      "valid": true,
      "errors": []
    }
  {% endjson %}
{% else %}
  {% json result %}
    {
      "valid": false,
      "errors": ["Invalid order data"]
    }
  {% endjson %}
{% endif %}

{% return result %}
```

**Return simple values:**

```liquid
{% comment %} Return a string {% endcomment %}
{% return "success" %}

{% comment %} Return a number {% endcomment %}
{% assign total = items | size %}
{% return total %}

{% comment %} Return a boolean {% endcomment %}
{% if customer.vip %}
  {% return true %}
{% else %}
  {% return false %}
{% endif %}
```

**HTTP script response:**

```liquid
{% comment %} Return JSON response for HTTP endpoint {% endcomment %}
{% json response %}
  {
    "status": "ok",
    "data": {
      "order_id": {{ order.id }},
      "processed": true,
      "timestamp": "{{ 'now' | date: '%Y-%m-%dT%H:%M:%SZ' }}"
    }
  }
{% endjson %}

{% return response %}
```

**Shipping rates response:**

```liquid
{% json rates %}
  {
    "rates": [
      {% if cart.total_weight < 1000 %}
      {
        "service_name": "Standard Shipping",
        "service_code": "standard",
        "total_price": 500,
        "currency": "{{ shop.currency }}",
        "description": "5-7 business days"
      },
      {% endif %}
      {
        "service_name": "Express Shipping",
        "service_code": "express",
        "total_price": 1500,
        "currency": "{{ shop.currency }}",
        "description": "1-2 business days"
      }
    ]
  }
{% endjson %}

{% return rates %}
```

**Early return with validation:**

```liquid
{% comment %} Function: process_order {% endcomment %}

{% comment %} Validate input {% endcomment %}
{% if order == blank %}
  {% json error %}
    {"error": "Order is required"}
  {% endjson %}
  {% return error %}
{% endif %}

{% if order.line_items.size == 0 %}
  {% json error %}
    {"error": "Order has no items"}
  {% endjson %}
  {% return error %}
{% endif %}

{% comment %} Process the order... {% endcomment %}
{% assign processed = true %}

{% json success %}
  {
    "success": true,
    "order_id": {{ order.id }}
  }
{% endjson %}

{% return success %}
```

**Conditional return based on logic:**

```liquid
{% comment %} Function: get_customer_tier {% endcomment %}

{% if customer.total_spent > 10000 %}
  {% return "platinum" %}
{% elsif customer.total_spent > 5000 %}
  {% return "gold" %}
{% elsif customer.total_spent > 1000 %}
  {% return "silver" %}
{% else %}
  {% return "bronze" %}
{% endif %}
```

**Return array data:**

```liquid
{% comment %} Function: get_product_skus {% endcomment %}
{% json skus %}[]{% endjson %}

{% for item in order.line_items %}
  {% push skus, item.sku %}
{% endfor %}

{% return skus %}
```

**Return with computed values:**

```liquid
{% comment %} Function: calculate_order_summary {% endcomment %}
{% assign subtotal = 0 %}
{% assign item_count = 0 %}

{% for item in order.line_items %}
  {% assign line_total = item.price | times: item.quantity %}
  {% assign subtotal = subtotal | plus: line_total %}
  {% assign item_count = item_count | plus: item.quantity %}
{% endfor %}

{% assign tax = subtotal | times: 0.08 %}
{% assign total = subtotal | plus: tax %}

{% json summary %}
  {
    "subtotal": {{ subtotal }},
    "tax": {{ tax }},
    "total": {{ total }},
    "item_count": {{ item_count }}
  }
{% endjson %}

{% return summary %}
```

#### Use Cases

| Context               | Purpose                                      |
| --------------------- | -------------------------------------------- |
| Functions             | Return computed values to the calling script |
| HTTP Scripts          | Send response body to the HTTP caller        |
| Shipping Rate Scripts | Define available shipping rates and prices   |

#### Notes

* Only one argument is allowed - you cannot return multiple values directly
* To return multiple values, wrap them in an object using the `json` tag
* Script execution stops immediately after `return` - code after it won't run
* If no return is called in a function, the function returns `null`
* For complex responses, always use `json` tag to structure the data
* The returned value from functions is a string - use `| parse_json` if you need to access object properties in the caller


# run

The `run` tag executes another script **asynchronously**. Unlike `function`, `run` does not wait for the target script to finish and does not return a result. The calling script continues immediately after triggering the run.

This is useful for offloading work that doesn't need to happen inline — sending notifications, syncing data, processing queues, and other background tasks.

### Syntax

```liquid
{% run "script_handle", param1:value1, param2:value2 %}
```

* `"script_handle"` — the handle (or ID) of the script to run.
* `param1:value1, param2:value2` — named parameters. Each becomes a variable inside the target script.

### Parameters

Named parameters let you pass values into the target script. Each `key:value` pair becomes a variable accessible in the script's scope.

Values can be variables, string literals, or any Liquid expression:

```liquid
{% assign customer_email = "support@code57.pl" %}

{% run "send_welcome_email", email:customer_email, template:"onboarding" %}
```

Inside `send_welcome_email`, `email` and `template` are available as regular variables.

You can pass as many parameters as needed:

```liquid
{% run "sync_inventory", product_id:product.id, warehouse:"EU", quantity:new_qty %}
```

### No return value

Since `run` executes asynchronously, there is no `as result` clause. The calling script does not wait for the target script to finish and cannot access its result.

```liquid
{% run "process_order", order_id:order.id %}
{% log "Order processing triggered" %}
```

The log line executes immediately — it does not wait for `process_order` to complete.

If you need the result of another script, use `function` instead:

```liquid
{% function "process_order", order_id:order.id as result %}
```

### Delayed execution

Use the `delay` parameter to schedule a script to run after a specified number of seconds:

```liquid
{% run "send_followup_email", email:customer.email, delay:3600 %}
```

This triggers `send_followup_email` after 1 hour (3600 seconds).

The delay value can also be a variable:

```liquid
{% assign wait_time = 300 %}
{% run "retry_sync", product_id:product.id, delay:wait_time %}
```

### Dynamic handle

The script handle can be a variable instead of a string literal. This lets you decide which script to run at runtime:

```liquid
{% assign script_name = "process_returns" %}
{% run script_name, order_id:order.id %}
```

### Example: Trigger background sync for each order item

```liquid
{% for item in order.line_items %}
  {% run "sync_item_inventory", variant_id:item.variant_id, quantity:item.quantity %}
{% endfor %}
{% log "Inventory sync triggered for all items" %}
```

Each `run` call is dispatched asynchronously. The loop completes immediately without waiting for any of the sync scripts to finish.

### Example: Send notification with delay

```liquid
{% run "send_reminder", email:customer.email, order_name:order.name, delay:86400 %}
{% log "Reminder scheduled for 24h from now" %}
```

### Legacy format: JSON payload

The old format using a single JSON payload variable is still supported. All keys from the payload object are spread into the target script's scope.

```liquid
{% json run_input %}
{
  "email": "support@code57.pl",
  "template": "onboarding"
}
{% endjson %}

{% run "send_welcome_email", run_input %}
```

> **Note:** Named parameters and the JSON payload format cannot be mixed in a single call. Use one or the other.

### Limitations

* **No return value** — `run` is fire-and-forget. Use `function` if you need a result.
* **No recursive runs** — a script cannot run itself, directly or through a chain of other scripts.
* **Task variables are not available** inside the target script. Pass any values you need as parameters.
* **Global variables are accessible** inside the target script.


# storage\_read

### storage\_read

Reads a file from DataJet storage. The file content is returned in a result object for further processing.

```liquid
{% storage_read filename: "data.csv" as result %}

{% if result.ok %}
  {% log result.file.content %}
{% else %}
  {% log result.error %}
{% endif %}
```

#### Syntax

```liquid
{% storage_read filename: "filename" as result_variable %}
```

#### Parameters

| Parameter  | Required | Description                                   |
| ---------- | -------- | --------------------------------------------- |
| `filename` | Yes      | Name of the file to read from DataJet storage |

#### Result Object

| Property       | Type        | Description                                         |
| -------------- | ----------- | --------------------------------------------------- |
| `ok`           | boolean     | `true` if read succeeded                            |
| `error`        | string/null | Error message if `ok` is `false`, `null` on success |
| `file.name`    | string      | The filename                                        |
| `file.content` | string      | The file content as text                            |
| `file.size`    | number      | File size in bytes                                  |

Example result:

```json
{
  "ok": true,
  "error": null,
  "file": {
    "name": "data.csv",
    "content": "id,name,price\n1,Product A,29.99\n2,Product B,49.99",
    "size": 52
  }
}
```

#### Examples

**Read and log file content:**

```liquid
{% storage_read filename: "report.txt" as result %}

{% if result.ok %}
  {% log "File content:" %}
  {% log result.file.content %}
{% else %}
  {% log "Error reading file: " | append: result.error %}
{% endif %}
```

**Read and parse CSV file:**

```liquid
{% storage_read filename: "products.csv" as result %}

{% if result.ok %}
  {% assign rows = result.file.content | parse_csv %}

  {% for row in rows %}
    {% log "Product: " | append: row.name | append: " - $" | append: row.price %}
  {% endfor %}
{% endif %}
```

**Read and parse JSON file:**

```liquid
{% storage_read filename: "config.json" as result %}

{% if result.ok %}
  {% assign config = result.file.content | parse_json %}

  {% log "API Endpoint: " | append: config.api_endpoint %}
  {% log "Max retries: " | append: config.max_retries %}
{% endif %}
```

**Read file with dynamic filename:**

```liquid
{% assign today = "now" | date: "%Y-%m-%d" %}
{% assign filename = "export_" | append: today | append: ".csv" %}

{% storage_read filename: filename as result %}

{% if result.ok %}
  {% log "Successfully read: " | append: result.file.name %}
  {% log "File size: " | append: result.file.size | append: " bytes" %}
{% endif %}
```

**Process file line by line:**

```liquid
{% storage_read filename: "orders.txt" as result %}

{% if result.ok %}
  {% assign lines = result.file.content | split: "\n" %}

  {% for line in lines %}
    {% if line != blank %}
      {% log "Processing: " | append: line %}
    {% endif %}
  {% endfor %}
{% endif %}
```

**Read and send to external API:**

```liquid
{% storage_read filename: "payload.json" as result %}

{% if result.ok %}
  {% assign body = result.file.content | parse_json %}

  {% json headers %}
    {
      "Content-Type": "application/json",
      "Authorization": "Bearer {{ API_KEY }}"
    }
  {% endjson %}

  {% http url: "https://api.example.com/import", method: "POST", headers: headers, body: body as response %}

  {% if response.ok %}
    {% log "Data sent successfully" %}
  {% endif %}
{% endif %}
```

**Check file size before processing:**

```liquid
{% storage_read filename: "large_file.csv" as result %}

{% if result.ok %}
  {% if result.file.size > 1000000 %}
    {% log "Warning: Large file (" | append: result.file.size | append: " bytes)" %}
  {% endif %}

  {% comment %} Process file... {% endcomment %}
{% endif %}
```

**Read configuration and use in script:**

```liquid
{% storage_read filename: "settings.json" as settings_result %}

{% if settings_result.ok %}
  {% assign settings = settings_result.file.content | parse_json %}

  {% if settings.feature_enabled %}
    {% log "Feature is enabled, processing..." %}
    {% comment %} Feature logic here {% endcomment %}
  {% else %}
    {% log "Feature is disabled" %}
  {% endif %}
{% else %}
  {% log "Could not load settings, using defaults" %}
  {% assign settings = nil %}
{% endif %}
```

**Read and upload to FTP:**

```liquid
{% storage_read filename: "export.csv" as result %}

{% if result.ok %}
  {% ftp_session host: FTP_HOST, user: FTP_USER, password: FTP_PASSWORD %}
    {% ftp_upload to: "/imports/export.csv", content: result.file.content as upload %}

    {% if upload.ok %}
      {% log "File uploaded to FTP" %}
    {% endif %}
  {% endftp_session %}
{% endif %}
```

**Error handling pattern:**

```liquid
{% storage_read filename: "required_data.json" as result %}

{% unless result.ok %}
  {% log "CRITICAL: Failed to read required file" %}
  {% log result.error %}

  {% json error_payload %}
    {
      "error": "{{ result.error }}",
      "filename": "required_data.json",
      "timestamp": "{{ 'now' | date: '%Y-%m-%dT%H:%M:%SZ' }}"
    }
  {% endjson %}

  {% run "error_handler", error_payload %}
  {% exit %}
{% endunless %}

{% comment %} Continue processing... {% endcomment %}
{% assign data = result.file.content | parse_json %}
```

#### Related Tags

| Tag             | Description                       |
| --------------- | --------------------------------- |
| `storage_write` | Writes content to DataJet storage |

#### Notes

* Consumes 1 credit per read operation
* Maximum file size: 10MB.
* File content is returned as a string - use `| parse_csv` or `| parse_json` for structured data
* Use `storage_write` to create or update files in storage
* If the file doesn't exist, `ok` will be `false` with an error message


# storage\_write

Writes a file to DataJet storage. You can save content directly or download from a URL. Files can optionally be made publicly accessible.

```liquid
{% capture csv_content %}id,name,price
1,Product A,29.99
2,Product B,49.99{% endcapture %}

{% storage_write filename: "products.csv", content: csv_content as result %}

{% if result.ok %}
  {% log "File saved: " | append: result.file.name %}
{% endif %}
```

#### Storage Limits

Each installation comes with storage. The amount available depends on your plan:

* Trial - 5mb
* Basic - 20mb
* Advanced - 50mb
* Pro - 100mb

{% hint style="info" %}
Files are stored only temporarily and are automatically deleted after 48h.
{% endhint %}

Not all files can be stored. Storage is currently configured to accept the following file types:

```
jpg,
png,
gif,
txt,
log,
mp4,
csv,
tsv,
pdf
```

The file extension must be specified in the `filename` parameter. Writing a file that would exceed your plan's storage limit, or whose extension is not on the accepted list, will fail with an error in `result.error`.

#### Syntax

```liquid
{% storage_write filename: "filename", content: content_variable as result_variable %}
{% storage_write filename: "filename", url: "https://example.com/file.csv" as result_variable %}
{% storage_write filename: "filename", content: content_variable, public: true as result_variable %}
```

#### Parameters

| Parameter  | Required | Description                                            |
| ---------- | -------- | ------------------------------------------------------ |
| `filename` | Yes      | Name of the file to store in DataJet storage           |
| `content`  | Yes\*    | String content to save                                 |
| `url`      | Yes\*    | URL to download and save the file from                 |
| `public`   | No       | Set to `true` to make file publicly accessible via URL |

\*Either `content` or `url` is required, but not both.

#### Result Object

| Property      | Type        | Description                                         |
| ------------- | ----------- | --------------------------------------------------- |
| `ok`          | boolean     | `true` if write succeeded                           |
| `error`       | string/null | Error message if `ok` is `false`, `null` on success |
| `file.name`   | string      | The filename                                        |
| `file.public` | boolean     | Whether the file is publicly accessible             |
| `file.url`    | string/null | Public URL (only present if `public: true`)         |

Example result:

```json
{
  "ok": true,
  "error": null,
  "file": {
    "name": "export.csv",
    "public": false,
    "url": null
  }
}
```

Example result with public URL:

```json
{
  "ok": true,
  "error": null,
  "file": {
    "name": "report.pdf",
    "public": true,
    "url": "https://storage.datajet-app.com/..."
  }
}
```

#### Examples

**Save text content:**

```liquid
{% capture report %}
Order Report - {{ 'now' | date: "%Y-%m-%d" }}
================================
Total Orders: {{ orders.size }}
Total Revenue: {{ total_revenue }}
{% endcapture %}

{% storage_write filename: "daily_report.txt", content: report as result %}

{% if result.ok %}
  {% log "Report saved successfully" %}
{% else %}
  {% log "Error: " | append: result.error %}
{% endif %}
```

**Save CSV data:**

```liquid
{% capture csv_data %}sku,quantity,updated_at
{% for item in inventory %}{{ item.sku }},{{ item.quantity }},{{ 'now' | date: "%Y-%m-%dT%H:%M:%SZ" }}
{% endfor %}{% endcapture %}

{% storage_write filename: "inventory_export.csv", content: csv_data as result %}
```

**Save JSON data:**

```liquid
{% json export_data %}
  {
    "exported_at": "{{ 'now' | date: '%Y-%m-%dT%H:%M:%SZ' }}",
    "orders": {{ orders | json }},
    "total_count": {{ orders.size }}
  }
{% endjson %}

{% assign json_content = export_data | json %}
{% storage_write filename: "orders_backup.json", content: json_content as result %}
```

**Save with dynamic filename:**

```liquid
{% assign timestamp = "now" | date: "%Y%m%d_%H%M%S" %}
{% assign filename = "export_" | append: timestamp | append: ".csv" %}

{% storage_write filename: filename, content: csv_content as result %}

{% log "Saved as: " | append: result.file.name %}
```

**Download from URL and save:**

```liquid
{% assign file_url = "https://api.example.com/reports/latest.pdf" %}

{% storage_write filename: "latest_report.pdf", url: file_url as result %}

{% if result.ok %}
  {% log "File downloaded and saved" %}
{% else %}
  {% log "Download failed: " | append: result.error %}
{% endif %}
```

**Save as public file with shareable URL:**

```liquid
{% capture html_content %}
<!DOCTYPE html>
<html>
<head><title>Order Confirmation</title></head>
<body>
  <h1>Order {{ order.name }}</h1>
  <p>Thank you for your order!</p>
</body>
</html>
{% endcapture %}

{% storage_write filename: "confirmation.html", content: html_content, public: true as result %}

{% if result.ok %}
  {% log "Public URL: " | append: result.file.url %}

  {% comment %} Send URL in email or webhook {% endcomment %}
{% endif %}
```

**Save API response to storage:**

```liquid
{% http url: "https://api.example.com/data", method: "GET" as response %}

{% if response.ok %}
  {% assign content = response.body | json %}
  {% storage_write filename: "api_response.json", content: content as result %}
{% endif %}
```

**Save FTP download to storage:**

```liquid
{% ftp_session host: FTP_HOST, user: FTP_USER, password: FTP_PASSWORD %}
  {% ftp_download from: "/reports/data.csv" as download %}

  {% if download.ok %}
    {% assign file_content = download.file.name | content %}
    {% storage_write filename: "ftp_backup.csv", content: file_content as save_result %}

    {% if save_result.ok %}
      {% log "FTP file backed up to storage" %}
    {% endif %}
  {% endif %}
{% endftp_session %}
```

**Create public download link for customer:**

```liquid
{% capture invoice %}
INVOICE #{{ order.name }}
Date: {{ order.created_at | date: "%B %d, %Y" }}
-----------------------------------------
{% for item in order.line_items %}
{{ item.title }} x {{ item.quantity }} - {{ item.price | money }}
{% endfor %}
-----------------------------------------
Total: {{ order.total_price | money }}
{% endcapture %}

{% assign invoice_filename = "invoice_" | append: order.order_number | append: ".txt" %}
{% storage_write filename: invoice_filename, content: invoice, public: true as result %}

{% if result.ok %}
  {% comment %} Include download link in customer email {% endcomment %}
  {% assign download_url = result.file.url %}
{% endif %}
```

**Overwrite existing file:**

```liquid
{% comment %} storage_write overwrites if file exists {% endcomment %}
{% storage_read filename: "counter.txt" as read_result %}

{% if read_result.ok %}
  {% assign count = read_result.file.content | plus: 1 %}
{% else %}
  {% assign count = 1 %}
{% endif %}

{% storage_write filename: "counter.txt", content: count as write_result %}
{% log "Counter updated to: " | append: count %}
```

**Error handling:**

```liquid
{% storage_write filename: "important_data.json", content: json_data as result %}

{% unless result.ok %}
  {% log "CRITICAL: Failed to save file" %}
  {% log result.error %}

  {% comment %} Trigger error handler {% endcomment %}
  {% json error_payload %}
    {
      "operation": "storage_write",
      "filename": "important_data.json",
      "error": "{{ result.error }}",
      "timestamp": "{{ 'now' | date: '%Y-%m-%dT%H:%M:%SZ' }}"
    }
  {% endjson %}

  {% run "error_handler", error_payload %}
{% endunless %}
```

#### Related Tags

| Tag            | Description                        |
| -------------- | ---------------------------------- |
| `storage_read` | Reads content from DataJet storage |

#### Notes

* Consumes 3 credits per write operation
* Writing to an existing filename will overwrite the file
* Use `public: true` to generate a shareable URL
* Public URLs are permanent until the file is deleted or overwritten
* Use `content` for text/string data, use `url` to download and save remote files
* Maximum file size limits apply based on your plan


# storefront

Executes queries against Shopify's Storefront GraphQL API. Works the same way as the `graphql` tag, but targets the Storefront API instead of the Admin API. The response is stored in a variable for further processing.

The Storefront API provides access to public store data — products, collections, cart operations, and more — without requiring admin-level permissions.

```liquid
{% storefront query: my_query, variables: my_variables as result %}

{% if result.product %}
  {% log result.product.title %}
{% endif %}
```

#### Syntax

```liquid
{% storefront query: query_string, variables: variables_object as result_variable %}
```

#### Parameters

| Parameter   | Required | Description                                           |
| ----------- | -------- | ----------------------------------------------------- |
| `query`     | Yes      | Storefront GraphQL query string                       |
| `variables` | No       | Object containing variables for the GraphQL operation |

#### Result Object

The result contains the returned data directly accessible at the top level. Query fields are available as properties on the result variable.

If errors occur, they are available in the `errors` property:

| Property | Type       | Description                                    |
| -------- | ---------- | ---------------------------------------------- |
| `errors` | array/null | Array of error objects if the operation failed |

#### Examples

**Fetch a product by handle:**

```liquid
{% capture query %}
  query getProduct($handle: String!) {
    productByHandle(handle: $handle) {
      id
      title
      description
      priceRange {
        minVariantPrice {
          amount
          currencyCode
        }
      }
    }
  }
{% endcapture %}

{% json variables %}
  {
    "handle": "classic-tee"
  }
{% endjson %}

{% storefront query: query, variables: variables as result %}

{% if result.productByHandle %}
  {% log result.productByHandle.title %}
  {% log result.productByHandle.priceRange.minVariantPrice.amount %}
{% endif %}
```

**List products from a collection:**

```liquid
{% capture query %}
  query getCollection($handle: String!, $first: Int!) {
    collectionByHandle(handle: $handle) {
      title
      products(first: $first) {
        edges {
          node {
            id
            title
            handle
            availableForSale
          }
        }
      }
    }
  }
{% endcapture %}

{% json variables %}
  {
    "handle": "summer-sale",
    "first": 50
  }
{% endjson %}

{% storefront query: query, variables: variables as result %}

{% for edge in result.collectionByHandle.products.edges %}
  {% log edge.node.title | append: " - Available: " | append: edge.node.availableForSale %}
{% endfor %}
```

**Fetch product recommendations:**

```liquid
{% capture query %}
  query getRecommendations($productId: ID!) {
    productRecommendations(productId: $productId) {
      id
      title
      handle
      priceRange {
        minVariantPrice {
          amount
          currencyCode
        }
      }
    }
  }
{% endcapture %}

{% json variables %}
  {
    "productId": "gid://shopify/Product/{{ product_id }}"
  }
{% endjson %}

{% storefront query: query, variables: variables as result %}

{% for product in result.productRecommendations %}
  {% log product.title | append: " ($" | append: product.priceRange.minVariantPrice.amount | append: ")" %}
{% endfor %}
```

**Query with pagination:**

```liquid
{% capture query %}
  query getProducts($cursor: String) {
    products(first: 50, after: $cursor) {
      edges {
        node {
          id
          title
          availableForSale
        }
      }
      pageInfo {
        hasNextPage
        endCursor
      }
    }
  }
{% endcapture %}

{% assign cursor = null %}
{% assign all_products = "" | split: "" %}

{% for i in (1..100) %}
  {% json variables %}
    {
      "cursor": {{ cursor | json }}
    }
  {% endjson %}

  {% storefront query: query, variables: variables as result %}

  {% for edge in result.products.edges %}
    {% assign all_products = all_products | push: edge.node %}
  {% endfor %}

  {% if result.products.pageInfo.hasNextPage %}
    {% assign cursor = result.products.pageInfo.endCursor %}
  {% else %}
    {% break %}
  {% endif %}
{% endfor %}

{% log "Total products: " | append: all_products.size %}
```

**Handle errors:**

```liquid
{% storefront query: query, variables: variables as result %}

{% if result.errors %}
  {% log "Storefront API error:" %}
  {% for error in result.errors %}
    {% log error.message %}
  {% endfor %}
{% else %}
  {% log result %}
{% endif %}
```

#### Storefront vs Admin API

| Feature   | `storefront`                               | `graphql`                                    |
| --------- | ------------------------------------------ | -------------------------------------------- |
| API       | Storefront GraphQL API                     | Admin GraphQL API                            |
| Access    | Public store data                          | Full admin access                            |
| Products  | Read-only (public fields)                  | Read/write (all fields)                      |
| Orders    | No access                                  | Full access                                  |
| Customers | No access                                  | Full access                                  |
| Cart      | Yes                                        | No                                           |
| Use case  | Public data, recommendations, availability | Data management, mutations, admin operations |

#### Notes

* Consumes 1 credit per operation
* Uses the store's API version configured in DataJet
* Storefront API errors are automatically logged to the script logs
* Variables must be a valid object (use `json` tag to construct complex variables)
* Result properties are accessed directly (e.g., `result.products`, not `result.data.products`)
* Refer to [Shopify's Storefront API documentation](https://shopify.dev/docs/api/storefront) for available queries


# Filters

Coming soon


# array\_difference

Returns elements that exist in the first array but not in the second array. Similar to set subtraction (A - B).

```liquid
{% assign all_skus = "SKU1,SKU2,SKU3,SKU4,SKU5" | split: "," %}
{% assign processed_skus = "SKU2,SKU4" | split: "," %}

{% assign remaining = all_skus | array_difference: processed_skus %}
```

Output:

```
SKU1, SKU3, SKU5
```

#### Syntax

```liquid
{{ arrayA | array_difference: arrayB }}
```

| Parameter | Description                  |
| --------- | ---------------------------- |
| `arrayA`  | Source array                 |
| `arrayB`  | Array of elements to exclude |

#### Visual Representation

```
Array A: [1, 2, 3, 4, 5]
Array B: [3, 4, 5, 6, 7]

Result:  [1, 2]  (elements in A that are NOT in B)
```

#### Examples

**Find products not yet synced:**

```liquid
{% assign all_product_ids = shopify_products | map: "id" %}
{% assign synced_product_ids = sync_log | map: "product_id" %}

{% assign unsynced_ids = all_product_ids | array_difference: synced_product_ids %}

{% log "Products not yet synced: " | append: unsynced_ids.size %}

{% for product_id in unsynced_ids %}
  {% comment %} Sync this product {% endcomment %}
{% endfor %}
```

**Find removed tags:**

```liquid
{% assign old_tags = "sale,featured,new,bestseller" | split: "," %}
{% assign new_tags = "featured,bestseller" | split: "," %}

{% assign removed_tags = old_tags | array_difference: new_tags %}
{% log "Tags removed: " | append: removed_tags | join: ", " %}
```

Output:

```
Tags removed: sale, new
```

**Identify discontinued SKUs:**

```liquid
{% assign current_catalog_skus = current_products | map: "sku" %}
{% assign new_catalog_skus = import_data | map: "sku" %}

{% assign discontinued = current_catalog_skus | array_difference: new_catalog_skus %}

{% if discontinued.size > 0 %}
  {% log "Discontinued SKUs:" %}
  {% log discontinued %}

  {% for sku in discontinued %}
    {% comment %} Archive or mark as discontinued {% endcomment %}
  {% endfor %}
{% endif %}
```

**Filter out excluded customers:**

```liquid
{% assign all_customer_ids = customers | map: "id" %}
{% assign vip_customer_ids = vip_list | map: "customer_id" %}

{% assign non_vip_ids = all_customer_ids | array_difference: vip_customer_ids %}

{% log "Regular customers: " | append: non_vip_ids.size %}
{% log "VIP customers: " | append: vip_customer_ids.size %}
```

**Find orders missing from fulfillment report:**

```liquid
{% assign all_order_names = orders | map: "name" %}
{% assign fulfilled_order_names = fulfillment_report | map: "order_name" %}

{% assign unfulfilled = all_order_names | array_difference: fulfilled_order_names %}

{% if unfulfilled.size > 0 %}
  {% log "Orders not in fulfillment report:" %}
  {% for order_name in unfulfilled %}
    {% log order_name %}
  {% endfor %}
{% endif %}
```

#### Notes

* Both inputs must be arrays; throws error otherwise
* Preserves order of elements from the first array
* Duplicate elements in arrayA are preserved if they don't exist in arrayB
* Returns empty array if all elements of arrayA exist in arrayB
* Comparison uses strict equality (===)
* See also: `array_intersection`, `array_symmetric_difference`


# array\_equal

Checks if two arrays contain the same elements (regardless of order). Returns `true` if both arrays have identical elements, `false` otherwise.

```liquid
{% assign array_a = "apple,banana,cherry" | split: "," %}
{% assign array_b = "cherry,apple,banana" | split: "," %}
{% assign array_c = "apple,banana" | split: "," %}

{% if array_a | array_equal: array_b %}
  Arrays A and B are equal
{% endif %}

{% unless array_a | array_equal: array_c %}
  Arrays A and C are different
{% endunless %}
```

Output:

```
Arrays A and B are equal
Arrays A and C are different
```

#### Syntax

```liquid
{{ arrayA | array_equal: arrayB }}
```

| Parameter | Description             |
| --------- | ----------------------- |
| `arrayA`  | First array             |
| `arrayB`  | Second array to compare |

#### Return Value

| Condition                 | Result  |
| ------------------------- | ------- |
| Same elements (any order) | `true`  |
| Different lengths         | `false` |
| Different elements        | `false` |
| Either input not an array | Error   |

#### Examples

**Verify tag synchronization:**

```liquid
{% assign shopify_tags = product.tags | split: ", " | sort %}
{% assign expected_tags = erp_product.tags | sort %}

{% if shopify_tags | array_equal: expected_tags %}
  {% log "Tags are in sync" %}
{% else %}
  {% log "Tag mismatch detected - syncing..." %}
  {% comment %} Update tags {% endcomment %}
{% endif %}
```

**Check if permissions unchanged:**

```liquid
{% assign current_permissions = user.permissions %}
{% assign required_permissions = role.default_permissions %}

{% if current_permissions | array_equal: required_permissions %}
  {% log "User has standard permissions for role" %}
{% else %}
  {% log "User has custom permissions" %}
{% endif %}
```

**Validate order contents:**

```liquid
{% assign ordered_skus = order.line_items | map: "sku" | sort %}
{% assign expected_skus = subscription.items | map: "sku" | sort %}

{% if ordered_skus | array_equal: expected_skus %}
  {% log "Order matches subscription exactly" %}
{% else %}
  {% log "Order differs from subscription" %}

  {% assign missing = expected_skus | array_difference: ordered_skus %}
  {% assign extra = ordered_skus | array_difference: expected_skus %}

  {% if missing.size > 0 %}
    {% log "Missing items: " | append: missing | join: ", " %}
  {% endif %}

  {% if extra.size > 0 %}
    {% log "Extra items: " | append: extra | join: ", " %}
  {% endif %}
{% endif %}
```

**Compare collection contents:**

```liquid
{% assign collection_a_ids = collection_a.products | map: "id" | sort %}
{% assign collection_b_ids = collection_b.products | map: "id" | sort %}

{% if collection_a_ids | array_equal: collection_b_ids %}
  {% log "Collections contain the same products" %}
{% else %}
  {% log "Collections have different products" %}
{% endif %}
```

**Detect configuration changes:**

```liquid
{% assign current_config = "feature_a,feature_b,feature_c" | split: "," %}
{% assign saved_config = shop_metafield.value | split: "," %}

{% unless current_config | array_equal: saved_config %}
  {% log "Configuration has changed - saving..." %}

  {% comment %} Save new configuration {% endcomment %}
  {% json metafield_input %}
    {
      "metafields": [{
        "ownerId": "{{ shop.id }}",
        "namespace": "config",
        "key": "features",
        "value": "{{ current_config | join: "," }}",
        "type": "single_line_text_field"
      }]
    }
  {% endjson %}

  {% graphql query: metafields_set, variables: metafield_input as result %}
{% endunless %}
```

**Conditional processing based on array state:**

```liquid
{% assign empty_array = "[]" | parse %}
{% assign results = api_response.items | default: empty_array %}

{% if results | array_equal: empty_array %}
  {% log "No results returned from API" %}
{% else %}
  {% log "Processing " | append: results.size | append: " results" %}
  {% for item in results %}
    {% comment %} Process item {% endcomment %}
  {% endfor %}
{% endif %}
```

#### Important Notes on Comparison

**Order does not matter:**

```liquid
{% assign a = "1,2,3" | split: "," %}
{% assign b = "3,2,1" | split: "," %}
{{ a | array_equal: b }}  {% comment %} true {% endcomment %}
```

**Duplicates matter:**

```liquid
{% assign a = "1,2,2,3" | split: "," %}
{% assign b = "1,2,3" | split: "," %}
{{ a | array_equal: b }}  {% comment %} false (different lengths) {% endcomment %}
```

**Type matters (strict equality):**

```liquid
{% assign a = "[1, 2, 3]" | parse %}
{% assign b = "['1', '2', '3']" | parse %}
{{ a | array_equal: b }}  {% comment %} false (numbers vs strings) {% endcomment %}
```

#### Notes

* Both inputs must be arrays; throws error otherwise
* Comparison is order-independent (set equality)
* Uses strict equality (===) for element comparison
* Arrays with different lengths are immediately `false` (fast path)
* For order-sensitive comparison, convert to strings: `arrayA | join: "," == arrayB | join: ","`
* See also: `array_difference`, `array_intersection`, `array_symmetric_difference`


# array\_intersection

Returns elements that exist in both arrays. Similar to set intersection (A ∩ B).

```liquid
{% assign store_a_products = "SKU1,SKU2,SKU3,SKU4" | split: "," %}
{% assign store_b_products = "SKU3,SKU4,SKU5,SKU6" | split: "," %}

{% assign common_products = store_a_products | array_intersection: store_b_products %}
```

Output:

```
SKU3, SKU4
```

#### Syntax

```liquid
{{ arrayA | array_intersection: arrayB }}
```

| Parameter | Description  |
| --------- | ------------ |
| `arrayA`  | First array  |
| `arrayB`  | Second array |

#### Visual Representation

```
Array A: [1, 2, 3, 4, 5]
Array B: [3, 4, 5, 6, 7]

Result:  [3, 4, 5]  (elements in BOTH A and B)
```

#### Examples

**Find products in multiple collections:**

```liquid
{% assign sale_product_ids = sale_collection.products | map: "id" %}
{% assign featured_product_ids = featured_collection.products | map: "id" %}

{% assign featured_sale_items = sale_product_ids | array_intersection: featured_product_ids %}

{% log "Products that are both featured AND on sale: " | append: featured_sale_items.size %}
```

**Match import data with existing catalog:**

```liquid
{% assign existing_skus = shopify_variants | map: "sku" %}
{% assign import_skus = import_data | map: "sku" %}

{% assign matching_skus = existing_skus | array_intersection: import_skus %}
{% assign new_skus = import_skus | array_difference: existing_skus %}

{% log "SKUs to update: " | append: matching_skus.size %}
{% log "New SKUs to create: " | append: new_skus.size %}
```

**Find customers who purchased from both categories:**

```liquid
{% assign electronics_buyers = electronics_orders | map: "customer_id" | uniq %}
{% assign clothing_buyers = clothing_orders | map: "customer_id" | uniq %}

{% assign cross_category_customers = electronics_buyers | array_intersection: clothing_buyers %}

{% log "Customers who bought both electronics and clothing: " | append: cross_category_customers.size %}
```

**Validate allowed tags:**

```liquid
{% assign product_tags = product.tags | split: ", " %}
{% assign allowed_tags = "sale,featured,new,clearance,bestseller" | split: "," %}

{% assign valid_tags = product_tags | array_intersection: allowed_tags %}
{% assign invalid_tags = product_tags | array_difference: allowed_tags %}

{% if invalid_tags.size > 0 %}
  {% log "Warning: Invalid tags found: " | append: invalid_tags | join: ", " %}
{% endif %}
```

**Find overlapping inventory locations:**

```liquid
{% assign product_a_locations = product_a.inventory_levels | map: "location_id" %}
{% assign product_b_locations = product_b.inventory_levels | map: "location_id" %}

{% assign shared_locations = product_a_locations | array_intersection: product_b_locations %}

{% log "Both products available at " | append: shared_locations.size | append: " locations" %}
```

**Match webhook subscriptions:**

```liquid
{% assign required_topics = "orders/create,orders/updated,products/update" | split: "," %}
{% assign registered_topics = existing_webhooks | map: "topic" %}

{% assign covered_topics = required_topics | array_intersection: registered_topics %}
{% assign missing_topics = required_topics | array_difference: registered_topics %}

{% if missing_topics.size > 0 %}
  {% log "Missing webhook subscriptions:" %}
  {% log missing_topics %}
{% else %}
  {% log "All required webhooks are registered" %}
{% endif %}
```

#### Notes

* Both inputs must be arrays; throws error otherwise
* Preserves order of elements from the first array
* Duplicate elements in arrayA are preserved if they also exist in arrayB
* Returns empty array if no common elements exist
* Comparison uses strict equality (===)
* See also: `array_difference`, `array_symmetric_difference`


# array\_symmetric\_difference

Returns elements that exist in either array but not in both. Similar to set symmetric difference (A △ B) or XOR operation.

```liquid
{% assign old_tags = "sale,featured,new" | split: "," %}
{% assign new_tags = "featured,bestseller,clearance" | split: "," %}

{% assign changed_tags = old_tags | array_symmetric_difference: new_tags %}
{% log changed_tags | join: ", " %}
```

Output:

```
sale, new, bestseller, clearance
```

#### Syntax

```liquid
{{ arrayA | array_symmetric_difference: arrayB }}
```

| Parameter | Description  |
| --------- | ------------ |
| `arrayA`  | First array  |
| `arrayB`  | Second array |

#### Visual Representation

```
Array A: [1, 2, 3, 4, 5]
Array B: [3, 4, 5, 6, 7]

Result:  [1, 2, 6, 7]  (elements in A OR B, but NOT both)
```

This is equivalent to: `(A - B) + (B - A)`

#### Examples

**Detect all tag changes:**

```liquid
{% assign old_tags = product_before.tags | split: ", " %}
{% assign new_tags = product_after.tags | split: ", " %}

{% assign all_changes = old_tags | array_symmetric_difference: new_tags %}

{% if all_changes.size > 0 %}
  {% assign added_tags = new_tags | array_difference: old_tags %}
  {% assign removed_tags = old_tags | array_difference: new_tags %}

  {% log "Tags added: " | append: added_tags | join: ", " %}
  {% log "Tags removed: " | append: removed_tags | join: ", " %}
{% else %}
  {% log "No tag changes detected" %}
{% endif %}
```

**Find inventory discrepancies:**

```liquid
{% assign system_skus = inventory_system | map: "sku" %}
{% assign physical_skus = physical_count | map: "sku" %}

{% assign discrepancies = system_skus | array_symmetric_difference: physical_skus %}

{% if discrepancies.size > 0 %}
  {% log "Inventory discrepancies found: " | append: discrepancies.size %}

  {% assign missing_from_physical = system_skus | array_difference: physical_skus %}
  {% assign missing_from_system = physical_skus | array_difference: system_skus %}

  {% log "In system but not found physically:" %}
  {% log missing_from_physical %}

  {% log "Found physically but not in system:" %}
  {% log missing_from_system %}
{% endif %}
```

**Compare collection membership:**

```liquid
{% assign collection_a_ids = collection_a.products | map: "id" %}
{% assign collection_b_ids = collection_b.products | map: "id" %}

{% assign unique_to_either = collection_a_ids | array_symmetric_difference: collection_b_ids %}
{% assign in_both = collection_a_ids | array_intersection: collection_b_ids %}

{% log "Products unique to one collection: " | append: unique_to_either.size %}
{% log "Products in both collections: " | append: in_both.size %}
```

**Sync differences between systems:**

```liquid
{% assign shopify_product_ids = shopify_products | map: "external_id" %}
{% assign erp_product_ids = erp_products | map: "id" %}

{% assign out_of_sync = shopify_product_ids | array_symmetric_difference: erp_product_ids %}

{% if out_of_sync.size > 0 %}
  {% log "Products out of sync between systems:" %}

  {% comment %} Products in Shopify but not ERP {% endcomment %}
  {% assign shopify_only = shopify_product_ids | array_difference: erp_product_ids %}
  {% for id in shopify_only %}
    {% log "Shopify only: " | append: id %}
  {% endfor %}

  {% comment %} Products in ERP but not Shopify {% endcomment %}
  {% assign erp_only = erp_product_ids | array_difference: shopify_product_ids %}
  {% for id in erp_only %}
    {% log "ERP only: " | append: id %}
  {% endfor %}
{% endif %}
```

**Detect permission changes:**

```liquid
{% assign old_permissions = user_before.permissions %}
{% assign new_permissions = user_after.permissions %}

{% assign permission_changes = old_permissions | array_symmetric_difference: new_permissions %}

{% if permission_changes.size > 0 %}
  {% assign granted = new_permissions | array_difference: old_permissions %}
  {% assign revoked = old_permissions | array_difference: new_permissions %}

  {% if granted.size > 0 %}
    {% log "Permissions granted: " | append: granted | join: ", " %}
  {% endif %}

  {% if revoked.size > 0 %}
    {% log "Permissions revoked: " | append: revoked | join: ", " %}
  {% endif %}
{% endif %}
```

#### Performance

Uses Set-based lookup for O(n + m) time complexity instead of O(n × m + m × n).

| Array A Size | Array B Size | Operations |
| ------------ | ------------ | ---------- |
| 1,000        | 1,000        | \~4,000    |
| 10,000       | 10,000       | \~40,000   |
| 100,000      | 100,000      | \~400,000  |

#### Notes

* Both inputs must be arrays; throws error otherwise
* Result contains elements from arrayA first, then elements from arrayB
* Returns empty array if both arrays contain exactly the same elements
* Useful for detecting any kind of change between two states
* Comparison uses strict equality (===)
* See also: `array_difference`, `array_intersection`, `array_equal`


# base64\_decode

### base64\_decode

Decodes a Base64-encoded string back to its original text. Useful for reading encoded API responses, decoding authentication tokens, or processing encoded webhook payloads.

```liquid
{% assign decoded = "SGVsbG8sIFdvcmxkIQ==" | base64_decode %}
{% log decoded %}
```

Output:

```
Hello, World!
```

#### Syntax

```liquid
{{ base64_string | base64_decode }}
```

| Parameter       | Description                         |
| --------------- | ----------------------------------- |
| `base64_string` | The Base64-encoded string to decode |

#### Examples

**Decode API response data:**

```liquid
{% http options: api_request as response %}

{% if response.body.encoded_data %}
  {% assign decoded_data = response.body.encoded_data | base64_decode %}
  {% assign parsed_data = decoded_data | parse %}

  {% log "Decoded response:" %}
  {% log parsed_data %}
{% endif %}
```

**Decode webhook payload:**

```liquid
{% assign encoded_payload = webhook.body.data %}
{% assign decoded_payload = encoded_payload | base64_decode %}
{% assign payload = decoded_payload | parse %}

{% log "Order ID from webhook: " | append: payload.order_id %}
```

**Decode URL parameter data:**

```liquid
{% assign encoded_data = request.params.data %}
{% assign decoded_json = encoded_data | base64_decode %}
{% assign callback_data = decoded_json | parse %}

{% log "Callback received for order: " | append: callback_data.order_id %}
```

**Decode Basic Auth header (for debugging):**

```liquid
{% comment %} Extract Base64 part from "Basic <credentials>" {% endcomment %}
{% assign auth_header = request.headers.authorization %}
{% assign encoded_credentials = auth_header | remove: "Basic " %}
{% assign decoded_credentials = encoded_credentials | base64_decode %}

{% assign parts = decoded_credentials | split: ":" %}
{% assign username = parts[0] %}
{% assign password = parts[1] %}

{% log "Username: " | append: username %}
```

**Process encoded email content:**

```liquid
{% assign encoded_body = email_data.body_base64 %}
{% assign email_body = encoded_body | base64_decode %}

{% log "Email content:" %}
{% log email_body %}
```

**Decode international characters:**

```liquid
{% assign encoded = "44GT44KT44Gr44Gh44Gv5LiW55WM" %}
{% assign decoded = encoded | base64_decode %}
{% log decoded %}
```

Output:

```
こんにちは世界
```

**Decode and parse JSON from encoded parameter:**

```liquid
{% assign encoded_config = metafield.value %}
{% assign config_json = encoded_config | base64_decode %}
{% assign config = config_json | parse %}

{% log "Feature flags:" %}
{% for flag in config.features %}
  {% log flag.name | append: ": " | append: flag.enabled %}
{% endfor %}
```

**Handle URL-safe Base64:**

```liquid
{% comment %} Convert URL-safe Base64 to standard Base64 first {% endcomment %}
{% assign url_safe_encoded = request.params.token %}
{% assign standard_encoded = url_safe_encoded | replace: "-", "+" | replace: "_", "/" %}
{% assign decoded_token = standard_encoded | base64_decode %}

{% log "Decoded token: " | append: decoded_token %}
```

**Decode and validate signature data:**

```liquid
{% assign signature_data = webhook.headers.x-signature-data %}
{% assign decoded_sig_data = signature_data | base64_decode %}
{% assign sig_parts = decoded_sig_data | split: "." %}

{% assign timestamp = sig_parts[0] %}
{% assign payload_hash = sig_parts[1] %}

{% log "Signature timestamp: " | append: timestamp %}
{% log "Payload hash: " | append: payload_hash %}
```

**Decode embedded file content:**

```liquid
{% if attachment.content_base64 %}
  {% assign file_content = attachment.content_base64 | base64_decode %}

  {% comment %} If it's CSV data, parse it {% endcomment %}
  {% assign csv_data = file_content | parse_csv %}

  {% log "Parsed " | append: csv_data.size | append: " rows from attachment" %}
{% endif %}
```

#### Encoding Round-Trip

```liquid
{% assign original = "Hello, World! 你好世界" %}
{% assign encoded = original | base64_encode %}
{% assign decoded = encoded | base64_decode %}

{% if original == decoded %}
  {% log "Round-trip successful!" %}
{% endif %}

{% log "Original: " | append: original %}
{% log "Encoded:  " | append: encoded %}
{% log "Decoded:  " | append: decoded %}
```

Output:

```
Round-trip successful!
Original: Hello, World! 你好世界
Encoded:  SGVsbG8sIFdvcmxkISDkvaDlpb3kuJbnlYw=
Decoded:  Hello, World! 你好世界
```

#### Error Handling

```liquid
{% assign potentially_encoded = some_variable %}

{% comment %} Base64 strings typically end with = padding or have specific length {% endcomment %}
{% assign is_likely_base64 = false %}
{% if potentially_encoded contains "==" or potentially_encoded.size | modulo: 4 == 0 %}
  {% assign is_likely_base64 = true %}
{% endif %}

{% if is_likely_base64 %}
  {% assign decoded = potentially_encoded | base64_decode %}
  {% log decoded %}
{% else %}
  {% log "Value does not appear to be Base64 encoded" %}
{% endif %}
```

#### Notes

* Supports UTF-8 decoding (handles international characters correctly)
* Expects standard Base64 input (with `+`, `/`, and `=` padding)
* For URL-safe Base64, convert `-` to `+` and `_` to `/` before decoding
* Invalid Base64 input may produce unexpected results or empty output
* Decoding does not validate the content - always validate decoded data before use
* See also: `base64_encode` for encoding strings to Base64


# base64\_encode

Encodes a string to Base64 format. Useful for encoding data for APIs, creating basic authentication headers, or encoding binary data for transmission.

```liquid
{% assign encoded = "Hello, World!" | base64_encode %}
{% log encoded %}
```

Output:

```
SGVsbG8sIFdvcmxkIQ==
```

#### Syntax

```liquid
{{ string | base64_encode }}
```

| Parameter | Description        |
| --------- | ------------------ |
| `string`  | The text to encode |

#### Examples

**Create Basic Authentication header:**

```liquid
{% assign credentials = "username:password" | base64_encode %}
{% assign auth_header = "Basic " | append: credentials %}

{% json http_options %}
  {
    "url": "https://api.example.com/data",
    "method": "GET",
    "headers": {
      "Authorization": "{{ auth_header }}"
    }
  }
{% endjson %}

{% http options: http_options as response %}
```

**Encode API credentials:**

```liquid
{% assign api_key = "my_api_key" %}
{% assign api_secret = "my_api_secret" %}
{% assign credentials = api_key | append: ":" | append: api_secret | base64_encode %}

{% log "Encoded credentials: " | append: credentials %}
```

**Encode JSON payload for URL parameter:**

```liquid
{% json payload %}
  {
    "order_id": {{ order.id }},
    "customer_email": {{ customer.email | json }}
  }
{% endjson %}

{% assign encoded_payload = payload | json | base64_encode %}
{% assign callback_url = "https://myapp.com/callback?data=" | append: encoded_payload %}
```

**Encode data for webhook signature:**

```liquid
{% assign timestamp = "now" | date: "%s" %}
{% assign payload_string = timestamp | append: "." | append: request_body %}
{% assign encoded_data = payload_string | base64_encode %}

{% log "Encoded payload for signing: " | append: encoded_data %}
```

**Create encoded return URL:**

```liquid
{% assign return_url = shop.url | append: "/apps/myapp/callback" %}
{% assign encoded_return = return_url | base64_encode %}
{% assign oauth_url = "https://auth.example.com/authorize?return=" | append: encoded_return %}
```

**Encode file content reference:**

```liquid
{% assign file_path = "reports/daily_sales.csv" %}
{% assign encoded_path = file_path | base64_encode %}

{% json request %}
  {
    "file_reference": "{{ encoded_path }}",
    "action": "download"
  }
{% endjson %}
```

**Encode special characters for safe transmission:**

```liquid
{% assign message = "Price: $100 (50% off!) <limited>" %}
{% assign safe_message = message | base64_encode %}

{% log "Original: " | append: message %}
{% log "Encoded: " | append: safe_message %}
```

Output:

```
Original: Price: $100 (50% off!) <limited>
Encoded: UHJpY2U6ICQxMDAgKDUwJSBvZmYhKSA8bGltaXRlZD4=
```

**Encode Unicode/international characters:**

```liquid
{% assign greeting = "こんにちは世界" %}
{% assign encoded_greeting = greeting | base64_encode %}

{{ encoded_greeting }}
```

Output:

```
44GT44KT44Gr44Gh44Gv5LiW55WM
```

#### Common Use Cases

| Use Case           | Example                                |
| ------------------ | -------------------------------------- |
| HTTP Basic Auth    | \`"user:pass"                          |
| API tokens         | \`"api\_key:secret"                    |
| URL-safe data      | Encode JSON for query strings          |
| Webhook signatures | Encode payload before signing          |
| Data obfuscation   | Hide readable values (not encryption!) |

#### Notes

* Supports UTF-8 encoding (handles international characters correctly)
* Output is standard Base64 (uses `+`, `/`, and `=` padding)
* For URL-safe Base64, you may need to replace characters: `| replace: "+", "-" | replace: "/", "_"`
* Base64 encoding increases size by approximately 33%
* This is encoding, not encryption - data can be easily decoded
* See also: `base64_decode` for decoding Base64 strings


# concat

Concatenates two arrays together or appends elements to an array. Returns a new array without modifying the original arrays.

```liquid
{% assign fruits = "apple,banana" | split: "," %}
{% assign more_fruits = "cherry,date" | split: "," %}

{% assign all_fruits = fruits | concat: more_fruits %}
{% log all_fruits | join: ", " %}
```

Output:

```
apple, banana, cherry, date
```

#### Syntax

```liquid
{{ array | concat: other_array }}
{{ array | concat: element }}
```

| Parameter     | Description                               |
| ------------- | ----------------------------------------- |
| `array`       | The base array                            |
| `other_array` | Array to append, or single element to add |

#### Examples

**Combine two arrays:**

```liquid
{% assign domestic_orders = orders | where: "shipping_country", "US" %}
{% assign international_orders = orders | where: "shipping_country", "CA" %}

{% assign north_america_orders = domestic_orders | concat: international_orders %}

{% log "Total North America orders: " | append: north_america_orders.size %}
```

**Build array from multiple sources:**

```liquid
{% assign all_products = "[]" | parse %}

{% for collection in collections %}
  {% assign collection_products = collection.products %}
  {% assign all_products = all_products | concat: collection_products %}
{% endfor %}

{% log "Total products across all collections: " | append: all_products.size %}
```

**Merge GraphQL pagination results:**

```liquid
{% assign all_variants = "[]" | parse %}
{% assign cursor = "null" | parse %}

{% for n in (0..100) %}
  {% graphql query: variants_query, variables: vars as result %}

  {% assign page_variants = result.productVariants.edges | map: "node" %}
  {% assign all_variants = all_variants | concat: page_variants %}

  {% unless result.productVariants.pageInfo.hasNextPage %}
    {% break %}
  {% endunless %}

  {% assign cursor = result.productVariants.edges.last.cursor %}
{% endfor %}

{% log "Fetched " | append: all_variants.size | append: " variants" %}
```

**Combine tags from multiple products:**

```liquid
{% assign all_tags = "[]" | parse %}

{% for product in products %}
  {% assign product_tags = product.tags | split: ", " %}
  {% assign all_tags = all_tags | concat: product_tags %}
{% endfor %}

{% assign unique_tags = all_tags | uniq | sort %}
{% log "Unique tags: " | append: unique_tags | join: ", " %}
```

**Add single element to array:**

```liquid
{% assign items = "[]" | parse %}

{% if condition_a %}
  {% assign items = items | concat: "item_a" %}
{% endif %}

{% if condition_b %}
  {% assign items = items | concat: "item_b" %}
{% endif %}

{% log items %}
```

**Merge line items from multiple orders:**

```liquid
{% assign all_line_items = "[]" | parse %}

{% for order in orders %}
  {% assign all_line_items = all_line_items | concat: order.line_items %}
{% endfor %}

{% log "Total line items: " | append: all_line_items.size %}
```

**Combine API responses:**

```liquid
{% assign all_results = "[]" | parse %}

{% for endpoint in endpoints %}
  {% http url: endpoint as response %}
  {% if response.ok and response.body.items %}
    {% assign all_results = all_results | concat: response.body.items %}
  {% endif %}
{% endfor %}
```

**Build notification recipients:**

```liquid
{% assign recipients = "[]" | parse %}

{% comment %} Add order customer {% endcomment %}
{% assign recipients = recipients | concat: order.customer.email %}

{% comment %} Add store admins {% endcomment %}
{% assign admin_emails = "admin@store.com,manager@store.com" | split: "," %}
{% assign recipients = recipients | concat: admin_emails %}

{% comment %} Add vendor if applicable {% endcomment %}
{% if order.vendor_email %}
  {% assign recipients = recipients | concat: order.vendor_email %}
{% endif %}

{% assign unique_recipients = recipients | uniq %}
{% log "Sending to: " | append: unique_recipients | join: ", " %}
```

#### Comparison: concat vs push

| Feature           | `concat`                 | `push`                            |
| ----------------- | ------------------------ | --------------------------------- |
| Modifies original | No (returns new array)   | Yes (mutates in place)            |
| Can add arrays    | Yes (flattens one level) | No (adds array as single element) |
| Use case          | Merging arrays           | Adding single items               |

**Example difference:**

```liquid
{% assign arr = "[1, 2]" | parse %}
{% assign to_add = "[3, 4]" | parse %}

{% comment %} concat - merges arrays {% endcomment %}
{% assign result_concat = arr | concat: to_add %}
{{ result_concat | json }}
{% comment %} Output: [1, 2, 3, 4] {% endcomment %}

{% comment %} push - adds as single element {% endcomment %}
{% push arr, to_add %}
{{ arr | json }}
{% comment %} Output: [1, 2, [3, 4]] {% endcomment %}
```

#### Performance Tip

When building large arrays in a loop, `concat` creates a new array each time. For better performance with many iterations, consider using `push` which modifies in place:

```liquid
{% comment %} Less efficient (many array copies) {% endcomment %}
{% assign result = "[]" | parse %}
{% for item in large_array %}
  {% assign result = result | concat: item %}
{% endfor %}

{% comment %} More efficient (in-place modification) {% endcomment %}
{% assign result = "[]" | parse %}
{% for item in large_array %}
  {% push result, item %}
{% endfor %}
```

#### Notes

* Returns a new array; original arrays are not modified
* When concatenating arrays, elements are flattened one level deep
* When concatenating a single value, it's added as-is
* Works with any array types (strings, numbers, objects)
* For adding single items in a loop, prefer `push` for better performance
* See also: `push` for in-place array modification


# contains\_set

Checks if an array contains a value using optimized Set-based lookup. For large arrays (100+ items), this is significantly faster than the native `contains` operator, which uses O(n) linear search.

```liquid
{% assign all_skus = products | map: "sku" %}

{% if all_skus | contains_set: "ABC123" %}
  SKU exists in catalog
{% endif %}
```

#### Syntax

```liquid
{{ array | contains_set: value }}
```

| Parameter | Description         |
| --------- | ------------------- |
| `array`   | Array to search in  |
| `value`   | Value to search for |

#### Performance

| Array Size    | Native `contains`    | `contains_set` |
| ------------- | -------------------- | -------------- |
| 100 items     | O(100) per check     | O(1) per check |
| 10,000 items  | O(10,000) per check  | O(1) per check |
| 250,000 items | O(250,000) per check | O(1) per check |

The filter automatically caches the Set representation, so subsequent lookups on the same array are instant.

#### Examples

**Check if SKU exists in large inventory:**

```liquid
{% comment %} Fetch all existing SKUs from Shopify {% endcomment %}
{% assign all_skus = "[]" | parse %}
{% for n in (0..100) %}
  {% graphql query: sku_query, variables: vars as result %}
  {% for edge in result.productVariants.edges %}
    {% assign all_skus = all_skus | push: edge.node.sku %}
  {% endfor %}
  {% unless result.productVariants.pageInfo.hasNextPage %}{% break %}{% endunless %}
{% endfor %}

{% comment %} Check each import row against existing SKUs {% endcomment %}
{% for row in import_data %}
  {% if all_skus | contains_set: row.sku %}
    {% log "SKU already exists: " | append: row.sku %}
  {% else %}
    {% comment %} Create new product {% endcomment %}
  {% endif %}
{% endfor %}
```

**Filter items not in exclusion list:**

```liquid
{% assign excluded_tags = "sale,clearance,discontinued" | split: "," %}
{% assign products_to_process = "[]" | parse %}

{% for product in all_products %}
  {% assign dominated_value = false %}
  {% for tag in product.tags %}
    {% if excluded_tags | contains_set: tag %}
      {% assign dominated_value = true %}
      {% break %}
    {% endif %}
  {% endfor %}
  {% unless dominated_value %}
    {% assign products_to_process = products_to_process | push: product %}
  {% endunless %}
{% endfor %}
```

**Validate order items against allowed products:**

```liquid
{% assign allowed_product_ids = allowed_products | map: "id" %}

{% for line_item in order.line_items %}
  {% unless allowed_product_ids | contains_set: line_item.product_id %}
    {% log "Unauthorized product in order: " | append: line_item.title %}
    {% assign has_unauthorized = true %}
  {% endunless %}
{% endfor %}
```

#### When to Use

| Scenario                       | Recommended                                      |
| ------------------------------ | ------------------------------------------------ |
| Single lookup                  | Use native `contains`                            |
| Multiple lookups on same array | Use `contains_set`                               |
| Array with 100+ items          | Use `contains_set`                               |
| Small arrays (<100 items)      | Either works, `contains_set` has slight overhead |

#### Notes

* Works with strings too: `"hello world" | contains_set: "world"` returns `true`
* Returns `false` for `null` or `undefined` input arrays


# content

{% hint style="info" %}
Deprecated. Use [storage\_read](/liquid/tags/storage_read) tag instead.
{% endhint %}

Use this filter to get stringified content of file saved in store's DataJet storage. Useful with `file` `parse_csv` `ftp` filters. Used together it can transform file into array of objects.

When an error reading the file stored in storaged is encountered - an error object is returned instead of stringified file content e.g.

```json
{
    "error": true,
    "message": "The specified key does not exist."
}
```

To read stored file content:

```liquid
{% assign file_content = "file.csv" | content %}

{% if file_content.error %}
  {% comment %}Error while reading file from storage{% endcomment %}
  {{ file_content.message | log }}
{% else %}
  {% comment %}Proceed with processing the file. Content saved in file_content{% endcomment %}
  {{ file_content | log }}
{% endif %}
```

See more in [ftp filter](/liquid/filters/ftp) and[ file filter](/liquid/filters/file).


# email

{% hint style="info" %}
Deprecated. Use [email](/liquid/tags/email) tag instead.
{% endhint %}

`email` sends email notification. It accepts JSON object with all parameters required to send an email.

```javascript
{% json email_params %}
   {
        "to": "test@email.com",
        "subject": "Dummy",
        "replyTo": "test@email.com",
        "html": "<h2> Hello! </h2>",
        "attachments": [
          {
            "filename": "test.csv",
            "content": "name,last_name \n john,smith"
          }
        ]
    }
{% endjson %}


{{ email_params | email }}
```

Your email body can contain `html`. Also it is possible to send attachments of max size 10 MB.

{% hint style="warning" %}
In case of trial subscription, each email sent will use 50 credits. For subscribed users this action will use 1 credit.
{% endhint %}


# encode\_uri

Encodes a URI by replacing special characters with their UTF-8 percent-encoded equivalents. Preserves characters that are valid in URIs (like `:`, `/`, `?`, `#`, `&`, `=`).

```liquid
{% assign url = "https://example.com/search?q=hello world&category=shoes" %}
{% log url | encode_uri %}
```

Output:

```
https://example.com/search?q=hello%20world&category=shoes
```

#### Syntax

```liquid
{{ url | encode_uri }}
{{ string | encode_uri }}
```

| Parameter | Description                 |
| --------- | --------------------------- |
| `url`     | The URL or string to encode |

#### Characters Preserved vs Encoded

| Preserved (not encoded) | Encoded               |
| ----------------------- | --------------------- |
| `A-Z a-z 0-9`           | Spaces → `%20`        |
| `- _ . ! ~ * ' ( )`     | `"` → `%22`           |
| `; , / ? : @ & = + $ #` | `<` `>` → `%3C` `%3E` |
|                         | `{` `}` → `%7B` `%7D` |
|                         | Non-ASCII → `%XX`     |

#### Examples

**Encode URL with spaces:**

```liquid
{% assign product_name = "Blue Running Shoes" %}
{% assign search_url = "https://store.com/search?q=" | append: product_name | encode_uri %}

{% log search_url %}
```

Output:

```
https://store.com/search?q=Blue%20Running%20Shoes
```

**Build API URL with parameters:**

```liquid
{% assign base_url = "https://api.example.com/products" %}
{% assign query = "summer collection 2024" %}
{% assign full_url = base_url | append: "?search=" | append: query | encode_uri %}

{% http url: full_url as response %}
```

**Encode redirect URL:**

```liquid
{% assign return_url = shop.url | append: "/pages/thank-you?order=" | append: order.name %}
{% assign encoded_return = return_url | encode_uri %}
{% assign checkout_url = "https://checkout.example.com?return=" | append: encoded_return %}

{% log checkout_url %}
```

**Handle international characters:**

```liquid
{% assign city = "München" %}
{% assign url = "https://store.com/locations/" | append: city | encode_uri %}

{% log url %}
```

Output:

```
https://store.com/locations/M%C3%BCnchen
```

**Encode tracking URL:**

```liquid
{% assign tracking_url = "https://carrier.com/track?id=" | append: fulfillment.tracking_number | append: "&name=" | append: customer.name | encode_uri %}

{% log "Tracking URL: " | append: tracking_url %}
```

**Build webhook callback URL:**

```liquid
{% assign callback_data = order.id | append: "|" | append: order.email %}
{% assign callback_url = "https://myapp.com/webhook?data=" | append: callback_data | encode_uri %}

{% json webhook_payload %}
  {
    "callback_url": "{{ callback_url }}"
  }
{% endjson %}
```

**Encode file path in URL:**

```liquid
{% assign file_name = "Report Q1 2024 (Final).pdf" %}
{% assign download_url = "https://cdn.example.com/files/" | append: file_name | encode_uri %}

{% log download_url %}
```

Output:

```
https://cdn.example.com/files/Report%20Q1%202024%20(Final).pdf
```

**Create mailto link:**

```liquid
{% assign subject = "Order Inquiry: " | append: order.name %}
{% assign body = "Hi, I have a question about my order " | append: order.name %}
{% assign mailto = "mailto:support@store.com?subject=" | append: subject | append: "&body=" | append: body | encode_uri %}

<a href="{{ mailto }}">Contact Support</a>
```

#### encode\_uri vs url\_encode

| Filter       | Use Case               | Encodes                            |
| ------------ | ---------------------- | ---------------------------------- |
| `encode_uri` | Full URLs              | Preserves `: / ? # & =`            |
| `url_encode` | Query parameter values | Encodes everything including `& =` |

**Example difference:**

```liquid
{% assign value = "hello&world=test" %}

{{ value | encode_uri }}
{% comment %} Output: hello&world=test (& and = preserved) {% endcomment %}

{{ value | url_encode }}
{% comment %} Output: hello%26world%3Dtest (& and = encoded) {% endcomment %}
```

**When to use which:**

```liquid
{% comment %} Use encode_uri for complete URLs {% endcomment %}
{% assign full_url = "https://api.com/search?q=hello world" | encode_uri %}

{% comment %} Use url_encode for individual parameter values {% endcomment %}
{% assign param_value = "hello&world" | url_encode %}
{% assign url = "https://api.com/search?q=" | append: param_value %}
```

#### Common Use Cases

| Use Case       | Example                                     |
| -------------- | ------------------------------------------- |
| Search URLs    | \`"/search?q="                              |
| Redirect URLs  | Encode return URLs for OAuth flows          |
| API requests   | Encode URLs with dynamic parameters         |
| Tracking links | Encode customer/order data in URLs          |
| File downloads | Encode file names with spaces/special chars |

#### Notes

* Uses JavaScript's native `encodeURI()` function
* Preserves URI structure characters (`:`, `/`, `?`, `#`, `&`, `=`, etc.)
* For encoding individual query parameter values, consider `url_encode` instead
* Handles UTF-8 characters (international text) correctly
* Safe to call multiple times (already-encoded characters won't be double-encoded)
* See also: `url_encode` for encoding query parameter values


# file

File filter is used to manage your files. This might be useful if you need to upload a file to an FTP, share file url with another system, or accept files submitted by the users through the forms.

{% hint style="info" %}
Deprecated. Use [storage\_read](/liquid/tags/storage_read) and [storage\_write](/liquid/tags/storage_write) instead
{% endhint %}

Each installation comes with a storage. The amount of storage available depends from the plan you are on.

* Trial - 5mb
* Basic - 20mb
* Advanced - 50mb
* Pro - 100mb

{% hint style="info" %}
Files are stored only temporarily and after 48h are automatically deleted.
{% endhint %}

You can save your files based on following sources:

* content - string captured with `capture` tags
* multipart - file coming from a user form submission
* url - publicly accessible file hosted under specified url

Not all files can be uploaded to your storage. Currently storage is configured to accept following files:

```
jpg,
png,
gif,
txt,
log,
mp4,
csv,
tsv,
pdf
```

File extension needs to be specified in fileName parameter when uploading.


# multipart

{% hint style="info" %}
Deprecated. Use [storage\_write](/liquid/tags/storage_write) with multipart parameter.
{% endhint %}

With multipart paramter you can upload your form attachments to storage.

```javascript
{% json upload_options %}
  {
    "fileName": "user_provided_photo.png",
    "multipart": {{request.files.["user-file"] | default: "" | json }},
    "public": false
  }
{% endjson %}

{% assign file_result = upload_options | file %}

{% json ftp_params %}
  {
    "mode": "upload",
    "host": "datajet-app.com",
    "user": "user@datajet-app.com"",
    "password": "pass123",
    "file": {{file_result.fileName | json }},
    "fileName": "ftp_file.png",
    "path": "/",
    "port": 21
  }
{% endjson %}
{% assign ftp_result = ftp_params | ftp %}
```

In above code we define some upload\_options. Parameters are:

* `fileName` - under this name your file will be saved in the storage
* `multipart` - this parameter tells that the expected file source is multipart. For files with url source it would be replaced with url and for content files with `content`.
* `public` - optional parameter. When set to true your result object will contain an url to your file. With this url everyone can access your upload.

Our `file_result` will contain `fileName` parameter (if upload is successful). This is next used in `ftp` filter options to specify file in your storage and upload it to an FTP.


# url

{% hint style="info" %}
Deprecated. Use [storage\_write](/liquid/tags/storage_write) with url parameter.
{% endhint %}

Lets consider following code for HTTP task:

```javascript
{% assign file_url = "https://cdn.shopify.com/s/files/1/0062/1124/0007/files/paper-plane.png?v=1626373338" %}

{% json upload_options %}
  {
    "fileName": "user_provided_photo.png",
    "url": {{file_url | json }},
    "public": false
  }
{% endjson %}

{% assign file_result = upload_options | file %}

```

With following code your file will be transferred from url to your storage.


# content

{% hint style="info" %}
Deprecated. Use [storage\_write](/liquid/tags/storage_write) with content parameter.
{% endhint %}

Lets consider following code for HTTP task:

```javascript
{% capture csv_report %}
order_id, total
#121, 220
#122, 140
{% endcapture %}

{% json upload_options %}
  {
    "fileName": "report.csv",
    "content": {{csv_report | strip | json }},
    "public": false
  }
{% endjson %}

{% assign file_result = upload_options | file %}

```

With following code your file will be transferred from url to your storage. Additionally `file_result` will contain a url of the report.


# find

Finds the first item in an array where a property matches a given value. Returns `null` if no match is found.

```liquid
{% assign product = products | find: "sku", "ABC123" %}
{% log product.title %}
```

#### Syntax

```liquid
{{ array | find: property, value }}
```

| Parameter  | Description                                                                  |
| ---------- | ---------------------------------------------------------------------------- |
| `array`    | Array of objects to search                                                   |
| `property` | Property name to match against (supports nested properties via dot notation) |
| `value`    | Value to compare against                                                     |

#### Return Value

Returns the first matching item, or `null` if no item matches.

#### Examples

**Find a product by SKU:**

```liquid
{% assign product = products | find: "sku", "T-SHIRT-RED-M" %}
{% if product %}
  {% log "Found: " | append: product.title %}
{% else %}
  {% log "Product not found" %}
{% endif %}
```

**Find an order by name:**

```liquid
{% assign order = orders | find: "name", "#1042" %}
{% log order.total_price %}
```

**Find by nested property:**

```liquid
{% assign edge = result.products.edges | find: "node.handle", "classic-tee" %}
{% log edge.node.title %}
```

**Find a customer by email:**

```liquid
{% assign customer = customers | find: "email", target_email %}
{% if customer %}
  {% log "Customer found: " | append: customer.first_name %}
{% endif %}
```

#### Notes

* Returns the **first** matching item — if multiple items match, only the first is returned
* Uses loose equality (`==`) for comparison
* Supports dot notation for nested properties: `"node.title"`, `"address.city"`
* For more complex matching conditions, use `find_exp`
* For building a reusable lookup by property, consider `index_by` instead


# find\_exp

Finds the first item in an array that matches a Liquid expression. Unlike `find`, which compares a property to a value, `find_exp` lets you write arbitrary conditions using the full Liquid expression syntax.

```liquid
{% assign expensive = products | find_exp: "product", "product.price > 100" %}
{% log expensive.title %}
```

#### Syntax

```liquid
{{ array | find_exp: item_name, expression }}
```

| Parameter    | Description                                              |
| ------------ | -------------------------------------------------------- |
| `array`      | Array of objects to search                               |
| `item_name`  | Variable name for the current item inside the expression |
| `expression` | Liquid expression that evaluates to truthy/falsy         |

#### Return Value

Returns the first item for which the expression evaluates to a truthy value, or `null` if no item matches.

#### Examples

**Find first product above a price threshold:**

```liquid
{% assign expensive = products | find_exp: "p", "p.price > 50" %}
{% if expensive %}
  {% log "First expensive product: " | append: expensive.title %}
{% endif %}
```

**Find first order with a specific tag:**

```liquid
{% assign vip_order = orders | find_exp: "order", "order.tags contains 'VIP'" %}
{% log vip_order.name %}
```

**Find first out-of-stock variant:**

```liquid
{% assign out_of_stock = variants | find_exp: "v", "v.inventory_quantity <= 0" %}
{% if out_of_stock %}
  {% log "Out of stock: " | append: out_of_stock.sku %}
{% endif %}
```

**Find first item matching multiple conditions:**

```liquid
{% assign match = products | find_exp: "p", "p.vendor == 'Nike' and p.product_type == 'Shoes'" %}
{% if match %}
  {% log match.title %}
{% endif %}
```

#### Notes

* The `item_name` parameter defines how you reference each item inside the expression
* The expression supports all Liquid operators: `==`, `!=`, `>`, `<`, `>=`, `<=`, `contains`, `and`, `or`
* Returns the **first** matching item only
* For simple property-to-value comparisons, `find` is more concise
* See also: `where_exp` (returns all matching items, not just the first)


# flat

Flattens a nested array by a specified depth level. Useful when working with arrays of arrays, such as results from `map` on nested data.

```liquid
{% assign all_tags = products | map: "tags" | flat %}
{% log all_tags %}
```

#### Syntax

```liquid
{{ array | flat }}
{{ array | flat: depth }}
```

| Parameter | Description                                          |
| --------- | ---------------------------------------------------- |
| `array`   | Array to flatten                                     |
| `depth`   | How many levels of nesting to flatten (default: `1`) |

#### Return Value

Returns a new array with nested arrays flattened to the specified depth.

#### Examples

**Flatten one level (default):**

```liquid
{% assign nested = "1,2|3,4|5,6" | split: "|" | map: "split", "," %}
{% assign flattened = nested | flat %}
{% log flattened %}
```

**Collect all tags from multiple products:**

```liquid
{% assign all_tags = products | map: "tags" | flat %}
{% assign unique_tags = all_tags | uniq %}
{% log unique_tags %}
```

**Flatten deeply nested arrays:**

```liquid
{% assign deep_flat = nested_array | flat: 2 %}
{% log deep_flat %}
```

**Collect all variant SKUs across products:**

```liquid
{% assign all_variants = products | map: "variants" | flat %}
{% assign all_skus = all_variants | map: "sku" %}
{% log all_skus %}
```

#### Notes

* Default depth is `1` — only the first level of nesting is flattened
* Non-array values are wrapped in an array before flattening
* Use higher depth values for deeply nested structures


# flow

{% hint style="info" %}
Deprecated. Use [flow](/liquid/tags/flow) tag instead.
{% endhint %}

Use flow filter to send data to Shopify Flow. It expects JSON payload as parameter. Payload needs to contain one of the following keys:

* customer\_id
* product\_id
* order\_id

Sample usage could look like this:

```
{% json payload %}
	{"customer_id": 5287014039740}
{% endjson %}

{% assign result = payload | flow %}
```

Next custom with provided ID is automatically loaded in Shopify Flow:

<figure><img src="/files/93lZJnsZaWN4NA8g6krD" alt=""><figcaption></figcaption></figure>


# flow\_v2

{% hint style="info" %}
Deprecated. Use [flow](/liquid/tags/flow) tag instead
{% endhint %}

Triggers a flow configured in Shopify Flow app. Please refer to [Shopify Flow Integration V2](/integrations/shopify-flow/v2) for details.


# ftp

{% hint style="info" %}
Deprecated. Use [ftp\_session](/liquid/tags/ftp_session), [ftp\_list](/liquid/tags/ftp_list), [ftp\_upload](/liquid/tags/ftp_upload), [ftp\_download](/liquid/tags/ftp_download), [ftp\_delete](/liquid/tags/ftp_delete) tags instead
{% endhint %}

Set of utilities helping you to access, download, move or delete files in FTP or sFTP server.

{% hint style="info" %}
By default when an error is encountered in any of the ftp operations - a critical error is thrown and script terminates the execution. You can set the parameter exitOnError to false to continue executing the script anytime a FTP error is detected.
{% endhint %}

#### Download

```liquid
{% json ftp_params %}
  {
    "mode": "download",
    "host": "ftp.com",
    "user": "user@ftp.com",
    "password": "pass123",
    "path": "/",
    "port": 21,
    "fileName": "ftp_file.csv",
    "file": "local_file.csv",
    "exitOnError": false
  }
{% endjson %}

{% assign result = ftp_params | ftp %}
```

Above snippet downloads remove file called `ftp_file.csv` and saves it in your storage as local\_file.csv.

Next you can access this file by adding following commands:

```
{% assign file_content = "local_file.csv" | content %}
{% assign csv_parsed = file_content | parse_csv %}
```

Your `csv_parsed` variable would now contain an array of object with csv columns as property names.

#### List

```
{% json ftp_params %}
  {
    "mode": "list",
    "host": "ftp.com",
    "user": "user@ftp.com",
    "password": "pass123",
    "path": "/",
    "port": 21
  }
{% endjson %}

{% assign result = ftp_params | ftp %}
```

Gets all files from remote ftp server. You can next use `for` loop to iterate over available files.

```
{% for file in result.files %}
    {{file | log }}
{% endfor %}
```

Above snippet would log all files on FTP server under directory specified in `path` parameter.

#### Move

```
{% json ftp_params %}
  {
    "mode": "move",
    "host": "ftp.com",
    "user": "user@ftp.com",
    "password": "pass123",
    "path": "/",
    "port": 21,
    "fileName": "ftp_file.csv",
    "newPath": "/processed/ftp_file.csv"
  }
{% endjson %}

{% assign result = ftp_params | ftp %}
```

Moves file `ftp_file.csv` from `/` to `/processed/`

#### Delete

```
{% json ftp_params %}
  {
    "mode": "delete",
    "host": "ftp.com",
    "user": "user@ftp.com",
    "password": "pass123",
    "path": "/",
    "port": 21,
    "fileName": "ftp_file.csv"
  }
{% endjson %}

{% assign result = ftp_params | ftp %}
```

Deletes file `ftp_file.csv` from `/` directory.

#### Upload

```
{% capture sample_content %}
id,quantity
1234567,200
{% endcapture %}

{% json ftp_params %}
  {
    "mode": "upload",
    "host": "ftp.com",
    "user": "user@ftp.com",
    "password": "pass123",
    "path": "/",
    "port": 21,
    "content": {{ sample_content | json }},
    "fileName": "sample_upload.csv"
  }
{% endjson %}

{% assign result = ftp_params | ftp %}
```

Uploads file with defined content to ftp. File is saved as `sample_upload.txt`

#### sFTP servers

To access sFTP servers additional paramter sftp set to true is expected. To list files on sFTP server following snippet could be used.

```
{% json sftp_params %}
  {
    "mode": "list",
    "host": "ftp.com",
    "user": "user@ftp.com",
    "password": "pass123",
    "path": "/",
    "port": 21,
    "sftp": true
  }
{% endjson %}

{% assign result = sftp_params | ftp %}
```


# graphql

{% hint style="info" %}
Deprecated. Use [graphql](/liquid/tags/graphql) tag instead.
{% endhint %}

`graphql` is one of the most important filters. It calls Shopify GraphQL endpoint to fetch (query) or update (mutation) store data. Any Shopify GraphQL request can be used. Complete documentation of Shopify's GraphQL can be found here:

{% embed url="<https://shopify.dev/docs/admin-api/graphql/reference>" %}

#### Example

Let's add a tag to a customer.

First, using Shopify GraphQL reference, we need to find mutation that will allow us to do so. Use `capture` tag to define our mutation.

```javascript
{% capture mutation %} 
  mutation tagsAdd($id: ID!, $tags: [String!]!) {
    tagsAdd(id: $id, tags: $tags) {
      node {
        id
      }
      userErrors {
        field
        message
      }
    }
  }
{% endcapture %}
```

From the above we see that mutation needs an object id and tag we want to add. Let's use again `capture` tag to define our mutation variables.

```javascript
{% json variables %}
  {
    "id": "gid://shopify/Customer/123456",
    "tags": "datajet-tag"
  }
{% endjson %}
```

Before we execute above GraphQL mutation we need to tell compiler what to expect in order to evaluate required permission for executing this task. See more [here](/scripts/creating-a-task).

```
{%- comment -%} Feed compiler with dummy values to evaluate permissions required.{%- endcomment -%}
{% if mode.compiler %}
  {% json dummy_mutation_variables %}
    {
      "id": "gid://shopify/Customer/123456",
      "tags": "test"
    }
  {% endjson %}
  {% assign result = mutation | graphql: dummy_mutation_variables %}
{% endif %}
{%- comment -%}END REST and GraphQL definitions{%- endcomment -%}
```

\
Now we have everything. Last step is to use `graphql` filter to execute the query.

```javascript
{% assign result = mutation | graphql: variables %}
{{result | log }}
```


# group\_by

Groups an array of objects by a property value. Returns an array of groups, each with a `name` (the property value) and `items` (array of matching objects).

```liquid
{% assign grouped = products | group_by: "vendor" %}
{% for group in grouped %}
  {% log group.name | append: ": " | append: group.items.size | append: " products" %}
{% endfor %}
```

#### Syntax

```liquid
{{ array | group_by: property }}
```

| Parameter  | Description                                                             |
| ---------- | ----------------------------------------------------------------------- |
| `array`    | Array of objects to group                                               |
| `property` | Property name to group by (supports nested properties via dot notation) |

#### Return Value

Returns an array of group objects:

| Property | Type  | Description                                          |
| -------- | ----- | ---------------------------------------------------- |
| `name`   | any   | The property value shared by all items in this group |
| `items`  | Array | Array of items that have this property value         |

#### Examples

**Group orders by status:**

```liquid
{% assign grouped = orders | group_by: "financial_status" %}
{% for group in grouped %}
  {% log group.name | append: ": " | append: group.items.size | append: " orders" %}
{% endfor %}
```

Output:

```
paid: 42 orders
pending: 7 orders
refunded: 3 orders
```

**Group products by vendor and process each group:**

```liquid
{% assign by_vendor = products | group_by: "vendor" %}
{% for group in by_vendor %}
  {% log "Processing vendor: " | append: group.name %}
  {% for product in group.items %}
    {% log "  - " | append: product.title %}
  {% endfor %}
{% endfor %}
```

**Group by nested property:**

```liquid
{% assign by_country = orders | group_by: "shipping_address.country" %}
{% for group in by_country %}
  {% log group.name | append: ": " | append: group.items.size | append: " orders" %}
{% endfor %}
```

**Count items per group:**

```liquid
{% assign by_type = products | group_by: "product_type" %}
{% for group in by_type %}
  {% if group.items.size > 10 %}
    {% log group.name | append: " has " | append: group.items.size | append: " products" %}
  {% endif %}
{% endfor %}
```

#### Notes

* Groups are returned in the order their keys are first encountered
* Supports dot notation for nested properties: `"node.status"`, `"address.city"`
* For grouping with complex expressions, use `group_by_exp`
* For faster performance with direct key access, use `group_by_property` or `group_by_fast`
* See also: `group_by_exp`, `group_by_property`, `group_by_fast`


# group\_by\_property

Groups an array by a property value in a single O(n) pass. Returns an object with `groups` (lookup by key) and `keys` (list of unique keys in order). This is faster than combining `map`, `uniq`, and multiple `where` calls.

```liquid
{% assign grouped = products | group_by_property: "vendor" %}

{% for vendor in grouped.keys %}
  <h2>{{ vendor }} ({{ grouped.groups[vendor].size }} products)</h2>
  {% for product in grouped.groups[vendor] %}
    <p>{{ product.title }}</p>
  {% endfor %}
{% endfor %}
```

#### Syntax

```liquid
{{ array | group_by_property: property }}
{{ array | group_by_property: "nested.property" }}
```

| Parameter  | Description                                                             |
| ---------- | ----------------------------------------------------------------------- |
| `array`    | Array of objects to group                                               |
| `property` | Property name to group by (supports dot notation for nested properties) |

#### Return Value

Returns an object with two properties:

| Property | Type   | Description                                                              |
| -------- | ------ | ------------------------------------------------------------------------ |
| `groups` | Object | Lookup object where keys are property values, values are arrays of items |
| `keys`   | Array  | List of unique property values in the order they were first encountered  |

#### Variants

**group\_by\_property**

Returns `{ groups: { key: [items] }, keys: [unique_keys] }` format.

```liquid
{% assign grouped = orders | group_by_property: "status" %}
{% assign pending = grouped.groups["pending"] %}
{% assign fulfilled = grouped.groups["fulfilled"] %}
```

**group\_by\_fast**

Returns `[{ name, items }, ...]` format (same as native `group_by` but faster).

```liquid
{% assign grouped = orders | group_by_fast: "status" %}
{% for group in grouped %}
  {{ group.name }}: {{ group.items.size }} orders
{% endfor %}
```

#### Examples

**Process products by material (like the import script):**

```liquid
{% comment %} Old way - O(n) + O(n) + O(n*m) {% endcomment %}
{% assign materials = products | map: "material" %}
{% assign unique_materials = materials | uniq %}
{% for material in unique_materials %}
  {% assign group = products | where: "material", material %}
  {% comment %} process group {% endcomment %}
{% endfor %}

{% comment %} New way - single O(n) pass {% endcomment %}
{% assign grouped = products | group_by_property: "material" %}
{% for material in grouped.keys %}
  {% assign group = grouped.groups[material] %}
  {% comment %} process group {% endcomment %}
{% endfor %}
```

**Group GraphQL edges by nested property:**

```liquid
{% assign edges_by_status = result.products.edges | group_by_property: "node.status" %}

{% assign active_products = edges_by_status.groups["ACTIVE"] %}
{% assign draft_products = edges_by_status.groups["DRAFT"] %}
{% assign archived_products = edges_by_status.groups["ARCHIVED"] %}

{% log "Active: " | append: active_products.size %}
{% log "Draft: " | append: draft_products.size %}
{% log "Archived: " | append: archived_products.size %}
```

**Generate report by category:**

```liquid
{% assign orders_by_region = orders | group_by_property: "shipping_address.country" %}

{% log "=== Orders by Region ===" %}
{% for country in orders_by_region.keys %}
  {% assign country_orders = orders_by_region.groups[country] %}
  {% assign total = 0 %}
  {% for order in country_orders %}
    {% assign total = total | plus: order.total_price %}
  {% endfor %}
  {% log country | append: ": " | append: country_orders.size | append: " orders, $" | append: total %}
{% endfor %}
```

**Batch process variants by product:**

```liquid
{% assign variants_by_product = all_variants | group_by_property: "product_id" %}

{% for product_id in variants_by_product.keys %}
  {% assign product_variants = variants_by_product.groups[product_id] %}

  {% comment %} Update all variants for this product in one API call {% endcomment %}
  {% json mutation_input %}
    {
      "productId": "gid://shopify/Product/{{ product_id }}",
      "variants": [
        {% for variant in product_variants %}
          { "id": "{{ variant.id }}", "price": "{{ variant.new_price }}" }{% unless forloop.last %},{% endunless %}
        {% endfor %}
      ]
    }
  {% endjson %}

  {% graphql query: bulk_update_mutation, variables: mutation_input as result %}
{% endfor %}
```

**Using group\_by\_fast for simple iteration:**

```liquid
{% assign grouped = line_items | group_by_fast: "vendor" %}

{% for group in grouped %}
  <div class="vendor-section">
    <h3>{{ group.name }}</h3>
    <ul>
      {% for item in group.items %}
        <li>{{ item.title }} - {{ item.price }}</li>
      {% endfor %}
    </ul>
  </div>
{% endfor %}
```

#### Performance Comparison

For an array of 10,000 products with 500 unique materials:

| Approach                      | Operations                                            |
| ----------------------------- | ----------------------------------------------------- |
| `map` + `uniq` + `where` loop | O(10,000) + O(10,000) + O(10,000 × 500) = \~5,020,000 |
| `group_by_property`           | O(10,000) = 10,000                                    |

**\~500x faster for this use case.**

#### Comparison with group\_by

| Feature            | `group_by`               | `group_by_property` / `group_by_fast`     |
| ------------------ | ------------------------ | ----------------------------------------- |
| Expression support | Yes (`"price > 100"`)    | No, property names only                   |
| Nested properties  | Yes (via expression)     | Yes (via dot notation)                    |
| Performance        | Slower (expression eval) | Faster (direct access)                    |
| Output format      | `[{ name, items }]`      | `{ groups, keys }` or `[{ name, items }]` |

#### Notes

* Supports nested properties using dot notation: `"node.status"`, `"variant.sku"`
* Items with `null` or `undefined` property values are skipped
* Keys are returned in the order they were first encountered
* Returns `{ groups: {}, keys: [] }` for empty or invalid input
* Use `group_by_fast` if you need the same output format as native `group_by`
* Use native `group_by` if you need expression evaluation (e.g., `"price > 100"`)


# group\_by\_exp

Groups an array using a Liquid expression to determine the group key. Unlike `group_by`, which groups by a direct property value, `group_by_exp` lets you write arbitrary expressions to compute the grouping key.

```liquid
{% assign by_price_range = products | group_by_exp: "product", "product.price > 100" %}
{% for group in by_price_range %}
  {% log group.name | append: ": " | append: group.items.size | append: " products" %}
{% endfor %}
```

#### Syntax

```liquid
{{ array | group_by_exp: item_name, expression }}
```

| Parameter    | Description                                              |
| ------------ | -------------------------------------------------------- |
| `array`      | Array of objects to group                                |
| `item_name`  | Variable name for the current item inside the expression |
| `expression` | Liquid expression that produces the group key            |

#### Return Value

Returns an array of group objects:

| Property | Type  | Description                                              |
| -------- | ----- | -------------------------------------------------------- |
| `name`   | any   | The computed key value shared by all items in this group |
| `items`  | Array | Array of items that produced this key value              |

#### Examples

**Group products by price range:**

```liquid
{% assign by_range = products | group_by_exp: "p", "p.price > 50" %}
{% for group in by_range %}
  {% if group.name == true %}
    {% log "Premium products: " | append: group.items.size %}
  {% else %}
    {% log "Budget products: " | append: group.items.size %}
  {% endif %}
{% endfor %}
```

**Group by first letter of title:**

```liquid
{% assign by_letter = products | group_by_exp: "p", "p.title | slice: 0" %}
{% for group in by_letter %}
  {% log group.name | append: ": " | append: group.items.size | append: " products" %}
{% endfor %}
```

**Group orders by fulfillment state:**

```liquid
{% assign by_state = orders | group_by_exp: "o", "o.fulfillment_status | default: 'unfulfilled'" %}
{% for group in by_state %}
  {% log group.name | append: ": " | append: group.items.size %}
{% endfor %}
```

**Group by computed value using filters:**

```liquid
{% assign by_month = orders | group_by_exp: "o", "o.created_at | date: '%Y-%m'" %}
{% for group in by_month %}
  {% log group.name | append: ": " | append: group.items.size | append: " orders" %}
{% endfor %}
```

Output:

```
2025-01: 156 orders
2025-02: 203 orders
2025-03: 178 orders
```

**Group line items by quantity tier:**

```liquid
{% assign by_tier = line_items | group_by_exp: "item", "item.quantity > 10" %}
{% for group in by_tier %}
  {% if group.name == true %}
    {% log "Bulk orders: " | append: group.items.size %}
  {% else %}
    {% log "Regular orders: " | append: group.items.size %}
  {% endif %}
{% endfor %}
```

#### Notes

* The `item_name` parameter defines how you reference each item inside the expression
* The expression can use any Liquid filters and operators
* The expression result becomes the group `name` — it can be a string, number, boolean, etc.
* Groups are returned in the order their keys are first encountered
* For simple property-based grouping, `group_by` is more concise
* See also: `group_by`, `group_by_property`, `group_by_fast`


# hmac\_sha256

Generates an HMAC-SHA256 hash of a message using a secret key. Commonly used for webhook signature verification, API authentication, and secure token generation.

```liquid
{% assign signature = "message body" | hmac_sha256: "secret_key" %}
{% log signature %}
```

#### Syntax

```liquid
{{ message | hmac_sha256: key }}
{{ message | hmac_sha256: key, encoding }}
```

| Parameter  | Description                                                 |
| ---------- | ----------------------------------------------------------- |
| `message`  | The string to hash                                          |
| `key`      | The secret key used for hashing                             |
| `encoding` | Output encoding: `"hex"` (default), `"base64"`, or `"utf8"` |

#### Return Value

Returns the HMAC-SHA256 hash as a string in the specified encoding.

#### Examples

**Generate a hex-encoded signature (default):**

```liquid
{% assign signature = request.body | hmac_sha256: "my_secret" %}
{% log signature %}
```

Output:

```
a1b2c3d4e5f6...
```

**Generate a base64-encoded signature:**

```liquid
{% assign signature = request.body | hmac_sha256: "my_secret", "base64" %}
{% log signature %}
```

**Verify a Shopify webhook signature:**

```liquid
{% assign computed = request.raw_body | hmac_sha256: SHOPIFY_WEBHOOK_SECRET, "base64" %}
{% if computed == request.headers["X-Shopify-Hmac-SHA256"] %}
  {% log "Webhook signature valid" %}
{% else %}
  {% log "Invalid webhook signature!" %}
{% endif %}
```

**Sign an API request:**

```liquid
{% capture string_to_sign %}{{ timestamp }}{{ request_body }}{% endcapture %}
{% assign signature = string_to_sign | hmac_sha256: API_SECRET_KEY %}

{% json headers %}
{
  "X-Signature": "{{ signature }}",
  "X-Timestamp": "{{ timestamp }}"
}
{% endjson %}
```

#### Notes

* Default encoding is `hex`
* Supported encodings: `hex`, `base64`, `utf8`
* The secret key should be stored in global variables, not hardcoded in scripts
* See also: `sha1`, `base64_encode`


# http

{% hint style="info" %}
Deprecated. Use [http](/liquid/tags/http) tag instead.
{% endhint %}

`http` filter will call an endpoint of your choice and capture the response.

#### Example

We are going to call a dummy endpoint with following parameters:

```
POST https://dummy.api
Headers
 Content-Type: application/json
Body
{
 "foo": "bar"
}
```

Using `http` filter it will look like that:

{% tabs %}
{% tab title="Liquid" %}

```javascript
{% assign endpoint = "https://dummy.api" %}

{% json request_options %}
  { 
    "url": {{ endpoint | json }},
    "method": "POST",
    "headers": {
      "Content-Type": "application/json"
    },
    "body": {
      "foo": "bar"
    }
  }
{% endjson %}

{% assign response = request_options | http %}
{% if response.ok %}
  {{ "Success!" | log }}
  {{response | log }}
{% else %}
  {{ "Fail!" | log }}
{% endif %}
```

{% endtab %}
{% endtabs %}


# index\_by

Creates a lookup object from an array, indexed by a specified property. This enables O(1) direct access to items instead of O(n) array searching with `where` or `find`.

```liquid
{% assign products_by_sku = products | index_by: "sku" %}

{% comment %} O(1) direct access instead of O(n) search {% endcomment %}
{% assign product = products_by_sku["ABC123"] %}
```

#### Syntax

```liquid
{{ array | index_by: property }}
{{ array | index_by: "nested.property" }}
```

| Parameter  | Description                                                                         |
| ---------- | ----------------------------------------------------------------------------------- |
| `array`    | Array of objects to index                                                           |
| `property` | Property name to use as the index key (supports dot notation for nested properties) |

#### Variants

**index\_by**

Returns an object where each key maps to a single item. If multiple items share the same key, the last one wins.

```liquid
{% assign users_by_email = users | index_by: "email" %}
{% assign user = users_by_email["john@example.com"] %}
```

**index\_by\_all**

Returns an object where each key maps to an array of all items with that key. Useful when multiple items can share the same key.

```liquid
{% assign orders_by_status = orders | index_by_all: "status" %}
{% assign pending_orders = orders_by_status["pending"] %}
{% log "Pending orders: " | append: pending_orders.size %}
```

#### Examples

**Fast variant lookup by SKU:**

```liquid
{% comment %} Build index once {% endcomment %}
{% assign variants_by_sku = shopify_variants | index_by: "sku" %}

{% comment %} Process import file with O(1) lookups {% endcomment %}
{% for row in import_data %}
  {% assign existing_variant = variants_by_sku[row.sku] %}
  {% if existing_variant %}
    {% comment %} Update existing variant {% endcomment %}
    {% log "Updating: " | append: row.sku %}
  {% else %}
    {% comment %} Create new variant {% endcomment %}
    {% log "Creating: " | append: row.sku %}
  {% endif %}
{% endfor %}
```

**Index GraphQL edges by nested property:**

```liquid
{% comment %} GraphQL returns edges with node wrapper {% endcomment %}
{% assign edges_by_id = result.products.edges | index_by: "node.legacyResourceId" %}

{% comment %} Direct access using product ID {% endcomment %}
{% assign product_edge = edges_by_id["12345"] %}
```

**Group orders by customer for batch processing:**

```liquid
{% assign orders_by_customer = orders | index_by_all: "customer.id" %}

{% for customer_id in customer_ids %}
  {% assign customer_orders = orders_by_customer[customer_id] %}
  {% if customer_orders %}
    {% log "Customer " | append: customer_id | append: " has " | append: customer_orders.size | append: " orders" %}

    {% comment %} Process all orders for this customer {% endcomment %}
    {% for order in customer_orders %}
      {% comment %} ... {% endcomment %}
    {% endfor %}
  {% endif %}
{% endfor %}
```

**Match inventory levels to variants:**

```liquid
{% comment %} Index variants by barcode for inventory matching {% endcomment %}
{% assign variants_by_barcode = all_variants | index_by: "barcode" %}

{% for inventory_row in inventory_file %}
  {% assign variant = variants_by_barcode[inventory_row.ean] %}
  {% if variant %}
    {% comment %} Update inventory for this variant {% endcomment %}
    {% assign inventory_item_id = variant.inventory_item_id %}
    {% comment %} ... update logic ... {% endcomment %}
  {% else %}
    {% log "No variant found for barcode: " | append: inventory_row.ean %}
  {% endif %}
{% endfor %}
```

**Create lookup for metafield values:**

```liquid
{% assign metafields_by_key = product.metafields | index_by: "key" %}

{% assign color = metafields_by_key["color"].value %}
{% assign material = metafields_by_key["material"].value %}
{% assign care_instructions = metafields_by_key["care_instructions"].value %}
```

#### Performance Comparison

| Operation     | Without index\_by | With index\_by |
| ------------- | ----------------- | -------------- |
| Build index   | -                 | O(n) once      |
| Single lookup | O(n) with `where` | O(1)           |
| 100 lookups   | O(n × 100)        | O(n) + O(100)  |
| 1000 lookups  | O(n × 1000)       | O(n) + O(1000) |

For an array of 10,000 items with 500 lookups:

* Without index: 10,000 × 500 = 5,000,000 operations
* With index: 10,000 + 500 = 10,500 operations

#### Notes

* Supports nested properties using dot notation: `"node.sku"`, `"variant.barcode"`
* Returns empty object `{}` for `null`, `undefined`, or non-array input
* Items with `null` or `undefined` key values are skipped
* For `index_by`: if multiple items have the same key, the last item is kept
* For `index_by_all`: all items with the same key are collected into an array


# in\_groups\_of

Splits an array into smaller arrays (chunks) of a specified size. Useful for batching API calls, paginating data, or processing items in fixed-size groups.

```liquid
{% assign batches = products | in_groups_of: 50 %}
{% for batch in batches %}
  {% log "Batch size: " | append: batch.size %}
{% endfor %}
```

#### Syntax

```liquid
{{ array | in_groups_of: size }}
```

| Parameter | Description                          |
| --------- | ------------------------------------ |
| `array`   | Array to split into groups           |
| `size`    | Maximum number of elements per group |

#### Return Value

Returns an array of arrays. Each inner array contains at most `size` elements. The last group may contain fewer elements if the array length is not evenly divisible.

#### Examples

**Batch API calls in groups of 10:**

```liquid
{% assign batches = product_ids | in_groups_of: 10 %}
{% for batch in batches %}
  {% assign ids = batch | join: "," %}
  {% log "Processing batch: " | append: ids %}
{% endfor %}
```

**Process GraphQL mutations in batches of 250:**

```liquid
{% assign batches = variants | in_groups_of: 250 %}
{% for batch in batches %}
  {% log "Batch " | append: forloop.index | append: " of " | append: batches.size | append: ": " | append: batch.size | append: " variants" %}
  {% comment %} Build and execute mutation for this batch {% endcomment %}
{% endfor %}
```

**Split CSV rows for parallel processing:**

```liquid
{% assign rows = file.content | parse_csv %}
{% assign chunks = rows | in_groups_of: 100 %}
{% for chunk in chunks %}
  {% run "process_chunk", data:chunk %}
{% endfor %}
```

**Paginate results:**

```liquid
{% assign pages = all_items | in_groups_of: 20 %}
{% log "Total pages: " | append: pages.size %}
{% assign current_page = pages[0] %}
{% for item in current_page %}
  {% log item.title %}
{% endfor %}
```

#### Notes

* The last group may be smaller than the specified size
* Returns an empty array if the input is empty
* Useful in combination with `run` to dispatch batched work asynchronously


# in\_timezone

Converts a date to a specific timezone. Returns the date formatted as an ISO 8601 string (`YYYY-MM-DDTHH:mm:ss`) in the target timezone.

```liquid
{% assign local_time = "2025-01-15T10:00:00Z" | in_timezone: "America/New_York" %}
{% log local_time %}
```

Output:

```
2025-01-15T05:00:00
```

#### Syntax

```liquid
{{ date | in_timezone: timezone }}
```

| Parameter  | Description                                                                             |
| ---------- | --------------------------------------------------------------------------------------- |
| `date`     | Date to convert — can be a date string, Unix timestamp (seconds), `"now"`, or `"today"` |
| `timezone` | IANA timezone name (e.g. `"America/New_York"`, `"Europe/London"`, `"Asia/Tokyo"`)       |

#### Return Value

Returns a string in `YYYY-MM-DDTHH:mm:ss` format in the specified timezone.

#### Accepted Date Inputs

| Input                    | Description                        |
| ------------------------ | ---------------------------------- |
| `"now"` or `"today"`     | Current date and time              |
| `"2025-01-15T10:00:00Z"` | ISO 8601 date string               |
| `1705312800`             | Unix timestamp in seconds (number) |
| `"1705312800"`           | Unix timestamp in seconds (string) |

#### Examples

**Get current time in a specific timezone:**

```liquid
{% assign local_now = "now" | in_timezone: "Europe/Warsaw" %}
{% log local_now %}
```

**Convert order creation time to shop timezone:**

```liquid
{% assign local_created = order.created_at | in_timezone: shop.timezone %}
{% log "Order created at: " | append: local_created %}
```

**Format a date after timezone conversion:**

```liquid
{% assign local_time = order.created_at | in_timezone: "America/Los_Angeles" %}
{% assign formatted = local_time | date: "%B %d, %Y at %I:%M %p" %}
{% log formatted %}
```

Output:

```
January 15, 2025 at 02:00 AM
```

**Convert Unix timestamp:**

```liquid
{% assign local_time = 1705312800 | in_timezone: "Asia/Tokyo" %}
{% log local_time %}
```

**Use shop's timezone from global variables:**

```liquid
{% assign local_time = order.created_at | in_timezone: shop.timezone %}
{% log local_time %}
```

#### Notes

* Requires a valid IANA timezone name — throws an error if timezone is missing
* The output format is always `YYYY-MM-DDTHH:mm:ss` — use the `date` filter afterwards to reformat
* Chain with `date` for custom formatting: `| in_timezone: "US/Eastern" | date: "%H:%M"`
* See also: `time_add`, `time_subtract`


# left\_join

Merges two arrays of objects based on matching keys, similar to a SQL LEFT JOIN. Each item in the first array is enriched with properties from the matching item in the second array. Items without a match get `null` values for the second array's properties.

```liquid
{% assign enriched = orders | left_join: customers, "customer_id = id" %}
{% for order in enriched %}
  {% log order.name | append: " - " | append: order.email %}
{% endfor %}
```

#### Syntax

```liquid
{{ array1 | left_join: array2, expression }}
```

| Parameter    | Description                                                                                        |
| ------------ | -------------------------------------------------------------------------------------------------- |
| `array1`     | Primary array (left side) — all items are preserved                                                |
| `array2`     | Secondary array (right side) — properties are merged into matching items                           |
| `expression` | Join condition in the format `"key1 = key2"` where `key1` is from array1 and `key2` is from array2 |

#### Return Value

Returns the first array with properties from the second array merged into each item. Items in the first array that have no match in the second array receive `null` for the second array's properties.

#### Examples

**Enrich orders with customer data:**

```liquid
{% assign enriched_orders = orders | left_join: customers, "customer_id = id" %}
{% for order in enriched_orders %}
  {% log order.name | append: " | Customer: " | append: order.first_name | append: " " | append: order.last_name %}
{% endfor %}
```

**Match import data with existing products by SKU:**

```liquid
{% assign matched = import_rows | left_join: shopify_variants, "sku = sku" %}
{% for row in matched %}
  {% if row.inventory_item_id %}
    {% log "Matched: " | append: row.sku %}
  {% else %}
    {% log "No match: " | append: row.sku %}
  {% endif %}
{% endfor %}
```

**Combine inventory levels with variant data:**

```liquid
{% assign combined = inventory_levels | left_join: variants, "inventory_item_id = inventory_item_id" %}
{% for item in combined %}
  {% log item.sku | append: ": " | append: item.available | append: " in stock" %}
{% endfor %}
```

**Merge CSV data with API results:**

```liquid
{% assign csv_rows = file.content | parse_csv %}
{% assign api_products = result.data.products.edges | map: "node" %}

{% assign merged = csv_rows | left_join: api_products, "handle = handle" %}
{% for row in merged %}
  {% if row.id %}
    {% log "Update: " | append: row.handle %}
  {% else %}
    {% log "Create: " | append: row.handle %}
  {% endif %}
{% endfor %}
```

#### How it works

```
Array 1 (orders):
  [{ id: 1, customer_id: 10 }, { id: 2, customer_id: 20 }, { id: 3, customer_id: 99 }]

Array 2 (customers):
  [{ id: 10, email: "a@test.com" }, { id: 20, email: "b@test.com" }]

Expression: "customer_id = id"

Result:
  [
    { id: 1, customer_id: 10, email: "a@test.com" },
    { id: 2, customer_id: 20, email: "b@test.com" },
    { id: 3, customer_id: 99, email: null }        ← no match, null values
  ]
```

#### Notes

* The expression format is `"key_from_array1 = key_from_array2"` — spaces around `=` are required
* Both inputs must be arrays — throws an error otherwise
* Modifies the first array in place — properties from matching items in the second array are merged directly
* Uses O(n + m) performance via internal Map lookup, not O(n \* m)
* Items without a match receive `null` for all properties from the second array (except the join key)
* If multiple items in the second array share the same key, the last one is used
* See also: `right_join`, `index_by`


# log

{% hint style="info" %}
Deprecated. Use [log](/liquid/tags/log) tag instead.
{% endhint %}

`log` is a simple tag that will log anything to task logs section.

It can optionally accept a parameter. This parameter must be one of the following:

* CRITICAL
* ACTION
* WARNING
* INFO

Parameters will define the severity of the log.

#### Example

```
{{ "Hello world" | log: "INFO" }}
```

This will output *Hello world* in the task logs section. Same could be done with:

```
{{ "Hello world" | log }}
```

{% hint style="info" %}
When creating task, all logs will be displayed in browser console.
{% endhint %}

{% hint style="warning" %}
Number of logs that can be created during 24h period is limited to 1 million
{% endhint %}


# parse\_csv

Parses a CSV string into an array of objects. The first row is used as headers — each subsequent row becomes an object with header names as keys.

```liquid
{% assign rows = file.content | parse_csv %}
{% for row in rows %}
  {% log row.sku | append: ": " | append: row.title %}
{% endfor %}
```

#### Syntax

```liquid
{{ csv_string | parse_csv }}
```

| Parameter    | Description                            |
| ------------ | -------------------------------------- |
| `csv_string` | A string containing CSV-formatted data |

#### Return Value

Returns an array of objects. Each object represents a row, with keys taken from the header row.

#### Examples

**Parse a CSV file from file storage:**

```liquid
{% storage_read filename:"products.csv" as csv_content %}
{% assign rows = csv_content | parse_csv %}
{% log "Rows: " | append: rows.size %}
{% for row in rows %}
  {% log row %}
{% endfor %}
```

**Parse an uploaded input file:**

```liquid
{% assign rows = file.content | parse_csv %}
{% for row in rows %}
  {% log row.sku | append: " - " | append: row.price %}
{% endfor %}
```

**Parse CSV from an HTTP response:**

```liquid
{% http url:"https://example.com/export.csv" method:"GET" as response %}
{% assign rows = response.body | parse_csv %}
{% for row in rows %}
  {% log row %}
{% endfor %}
```

**Process CSV rows and update products:**

```liquid
{% assign rows = file.content | parse_csv %}
{% for row in rows %}
  {% if row.sku and row.price %}
    {% log "Updating " | append: row.sku | append: " to $" | append: row.price %}
    {% comment %} Update logic here {% endcomment %}
  {% endif %}
{% endfor %}
```

**Combine with left\_join to match import data:**

```liquid
{% assign import_rows = file.content | parse_csv %}
{% assign matched = import_rows | left_join: existing_variants, "sku = sku" %}
{% for row in matched %}
  {% if row.inventory_item_id %}
    {% log "Update: " | append: row.sku %}
  {% else %}
    {% log "Create: " | append: row.sku %}
  {% endif %}
{% endfor %}
```

#### Notes

* The first row is always treated as the header row
* Auto-detects the delimiter (comma, semicolon, tab, etc.)
* Returns an empty array if the input is empty or not a string
* Throws an error on malformed CSV data
* See also: `parse_json`, `parse_xml`


# parse\_json

Parses a JSON string into a Liquid object. Useful when working with API responses, stored data, or metafield values that are JSON-encoded strings.

```liquid
{% assign data = '{"name": "John", "age": 30}' | parse_json %}
{% log data.name %}
```

Output:

```
John
```

#### Syntax

```liquid
{{ json_string | parse_json }}
```

| Parameter     | Description                  |
| ------------- | ---------------------------- |
| `json_string` | A valid JSON string to parse |

#### Return Value

Returns a Liquid object (hash, array, string, number, or boolean) depending on the JSON content.

#### Examples

**Parse a JSON string:**

```liquid
{% assign product_data = '{"title": "T-Shirt", "price": 29.99}' | parse_json %}
{% log product_data.title %}
{% log product_data.price %}
```

**Parse a metafield value:**

```liquid
{% assign config = product.metafields.custom.settings.value | parse_json %}
{% log config.color %}
```

**Parse an API response body:**

```liquid
{% assign response_data = http_result.body | parse_json %}
{% for item in response_data.results %}
  {% log item.name %}
{% endfor %}
```

**Parse stored JSON from file storage:**

```liquid
{% storage_read filename:"config.json" as file_content %}
{% assign config = file_content | parse_json %}
{% log config.api_key %}
```

**Parse a JSON array:**

```liquid
{% assign items = '[1, 2, 3, 4, 5]' | parse_json %}
{% for item in items %}
  {% log item %}
{% endfor %}
```

#### Notes

* Throws an error if the input is not valid JSON
* For building JSON objects, use the `{% json %}` tag instead
* See also: `parse_csv`, `parse_xml`


# parse\_xml

Coming soon

Parses an XML string into a Liquid object. Useful for processing XML feeds, API responses, and file imports.

```liquid
{% assign data = xml_string | parse_xml %}
{% log data %}
```

#### Syntax

```liquid
{{ xml_string | parse_xml }}
```

| Parameter    | Description                        |
| ------------ | ---------------------------------- |
| `xml_string` | A string containing valid XML data |

#### Return Value

Returns a Liquid object representing the XML structure. Elements become object keys, text content becomes values, and repeated elements become arrays.

#### Examples

**Parse an XML API response:**

```liquid
{% http url:"https://api.example.com/products.xml" method:"GET" as response %}
{% assign data = response.body | parse_xml %}
{% for product in data.products.product %}
  {% log product.title %}
{% endfor %}
```

**Parse an uploaded XML file:**

```liquid
{% assign data = file.content | parse_xml %}
{% log data %}
```

**Parse an XML feed and extract items:**

```liquid
{% assign feed = xml_content | parse_xml %}
{% assign items = feed.rss.channel.item %}
{% for item in items %}
  {% log item.title | append: " - " | append: item.link %}
{% endfor %}
```

**Process XML product feed:**

```liquid
{% assign catalog = file.content | parse_xml %}
{% assign products = catalog.catalog.products.product %}
{% for product in products %}
  {% log product.sku | append: ": " | append: product.name | append: " ($" | append: product.price | append: ")" %}
{% endfor %}
```

#### Notes

* Elements with text content are converted to their text value directly
* Repeated sibling elements with the same name become arrays
* Nested elements are preserved in the object structure
* Element attributes are not included; use `parse_xml_attrs` to keep them
* See also: `parse_xml_attrs`, `parse_json`, `parse_csv`


# parse\_xml\_attrs

Coming soon

Parses an XML string into a Liquid object, **including element attributes**. Works exactly like `parse_xml` but keeps attributes instead of stripping them.

```liquid
{% assign data = xml_string | parse_xml_attrs %}
{% log data %}
```

#### Syntax

```liquid
{{ xml_string | parse_xml_attrs }}
```

| Parameter    | Description                        |
| ------------ | ---------------------------------- |
| `xml_string` | A string containing valid XML data |

#### Return Value

Returns a Liquid object representing the XML structure. Each attribute becomes a plain key on its element (e.g. `node.currency`). If an element has both attributes and text content, the text is available under the `#text` key.

#### Examples

**Read attributes from an API response:**

```liquid
{% assign xml = '<rate currency="USD" code="STD">9.99</rate>' %}
{% assign data = xml | parse_xml_attrs %}
{% log data.rate.currency %}     {# USD #}
{% log data.rate.code %}         {# STD #}
{% log data.rate['#text'] %}     {# 9.99 #}
```

**Iterate elements that carry attributes:**

```liquid
{% assign data = response.body | parse_xml_attrs %}
{% for line in data.order.line %}
  {% log line.sku | append: ": " | append: line['#text'] %}
{% endfor %}
```

#### Notes

* Attributes are exposed as plain keys, so they're dot-accessible like any element
* Text content of an element that also has attributes lives under `#text`
* If you don't need attributes, use `parse_xml` instead
* See also: `parse_xml`, `parse_json`, `parse_csv`


# pop

Removes and returns the last element from an array. Modifies the original array in place.

```liquid
{% assign last_item = my_array | pop %}
{% log last_item %}
```

#### Syntax

```liquid
{{ array | pop }}
```

| Parameter | Description                           |
| --------- | ------------------------------------- |
| `array`   | Array to remove the last element from |

#### Return Value

Returns the removed element. The original array is shortened by one element.

#### Examples

**Pop the last element:**

```liquid
{% assign colors = "red,green,blue" | split: "," %}
{% assign last = colors | pop %}
{% log last %}
{% log colors %}
```

Output:

```
blue
["red", "green"]
```

**Process items from the end:**

```liquid
{% assign stack = "first,second,third" | split: "," %}
{% assign item = stack | pop %}
{% log "Processing: " | append: item %}
{% log "Remaining: " | append: stack.size %}
```

**Use with the pop tag for named assignment:**

```liquid
{% pop my_array as last_element %}
{% log last_element %}
```

#### Notes

* Modifies the original array — the popped element is removed permanently
* Returns the input unchanged if it is not an array
* There is also a `{% pop array as variable %}` tag that does the same thing with a clearer syntax
* See also: `push`


# push

{% hint style="info" %}
Deprecated. Use [push](/liquid/tags/push) tag instead.
{% endhint %}

push adds an element to an array.

#### Example

```javascript
{% json sample_array %}
    [
        "element1",
        "element2",
        "element3"
    ]
{% endjson $}
{% assign sample_array = sample_array | push: "element4" %}
```

Now the content of `sample_array` is: `["element1", "element2", "element3", "element4"]`

{% hint style="info" %}
Using `push` with `assign` might significantly slow down your script. Each time new element is added - new array variable is created.\
\
It is recommended to use [`push`](/liquid/tags/push) tag when adding items to an array.
{% endhint %}


# random

Generates random values — strings, numbers, alphanumeric codes, or floating-point numbers. Useful for creating unique identifiers, random codes, and test data.

```liquid
{% assign code = "alphanumeric" | random: 8 %}
{% log code %}
```

#### Syntax

```liquid
{{ type | random }}
{{ type | random: length }}
{{ type | random: length, downcase }}
```

#### Modes

**Alphanumeric string**

Generates a random string of letters and digits.

```liquid
{{ "alphanumeric" | random: 8 }}
```

| Parameter  | Description                             |
| ---------- | --------------------------------------- |
| `length`   | Number of characters (1–20, default: 5) |
| `downcase` | Set to `true` for lowercase only        |

```liquid
{% assign code = "alphanumeric" | random: 10 %}
{% log code %}
```

Output: `aB3xK9mP2n`

```liquid
{% assign code = "alphanumeric" | random: 6, true %}
{% log code %}
```

Output: `k3m8p2`

**Number**

Generates a random integer with the specified number of digits.

```liquid
{{ "number" | random: 6 }}
```

| Parameter | Description                         |
| --------- | ----------------------------------- |
| `length`  | Number of digits (1–20, default: 5) |

```liquid
{% assign pin = "number" | random: 4 %}
{% log pin %}
```

Output: `7284`

**String (letters only)**

Generates a random string of letters only (no digits).

```liquid
{{ "string" | random: 5 }}
```

| Parameter  | Description                             |
| ---------- | --------------------------------------- |
| `length`   | Number of characters (1–20, default: 5) |
| `downcase` | Set to `true` for lowercase only        |

```liquid
{% assign token = "string" | random: 8 %}
{% log token %}
```

Output: `KmPxBnTr`

**Float / Decimal**

Generates a random floating-point number within a range.

```liquid
{{ "float" | random: min, max }}
{{ "decimal" | random: min, max }}
```

| Parameter | Description                |
| --------- | -------------------------- |
| `min`     | Minimum value (default: 0) |
| `max`     | Maximum value (default: 1) |

```liquid
{% assign price = "float" | random: 10, 100 %}
{% log price %}
```

Output: `47.382917`

#### Examples

**Generate a unique order reference:**

```liquid
{% assign ref = "alphanumeric" | random: 12 %}
{% log "REF-" | append: ref %}
```

**Generate a discount code:**

```liquid
{% assign discount_code = "alphanumeric" | random: 8, true %}
{% log "SAVE-" | append: discount_code | upcase %}
```

**Generate a random verification PIN:**

```liquid
{% assign pin = "number" | random: 6 %}
{% log "Your PIN: " | append: pin %}
```

**Generate a random price for testing:**

```liquid
{% assign test_price = "float" | random: 5, 200 %}
{% assign test_price = test_price | round: 2 %}
{% log "Test price: $" | append: test_price %}
```

#### Notes

* Maximum length for strings and numbers is 20 characters/digits
* `"float"` and `"decimal"` are interchangeable — both generate floating-point numbers
* Random values are not cryptographically secure — do not use for passwords or security tokens
* For secure hashing, use `hmac_sha256` or `sha1` instead


# remove\_prop

Removes a property from an object. Modifies the object in place and returns it.

```liquid
{% assign cleaned = product_data | remove_prop: "internal_notes" %}
{% log cleaned %}
```

#### Syntax

```liquid
{{ object | remove_prop: property_name }}
```

| Parameter       | Description                        |
| --------------- | ---------------------------------- |
| `object`        | Object to remove the property from |
| `property_name` | Name of the property to delete     |

#### Return Value

Returns the object with the specified property removed.

#### Examples

**Remove sensitive data before logging:**

```liquid
{% assign safe_data = api_response | remove_prop: "api_key" %}
{% log safe_data %}
```

**Clean up an object before sending to an API:**

```liquid
{% assign payload = order_data | remove_prop: "internal_id" %}
{% assign payload = payload | remove_prop: "debug_info" %}
{% log payload %}
```

**Remove a temporary property:**

```liquid
{% assign row = row | remove_prop: "_processed" %}
{% log row %}
```

**Strip metadata before returning from a function:**

```liquid
{% assign result = data | remove_prop: "_metadata" %}
{% assign result = result | remove_prop: "_timestamp" %}
{% return result %}
```

#### Notes

* Modifies the object in place — the property is permanently deleted
* Returns the object unchanged if the property does not exist
* Returns the input unchanged if it is not an object
* Only removes top-level properties — does not support dot notation for nested properties


# rest

{% hint style="info" %}
Shopify REST Admin API is deprecated. You should use Shopify GraphQL Admin API and [graphql](/liquid/tags/graphql) tag.
{% endhint %}

`rest` will allow you to perform Shopify actions with use of Shopify REST API.

It accepts a JSON object as a parameter.

Here is an example input object that would be used to create fulfillment via REST API.

```javascript
{% json rest_input %}
  {
    "path": "/orders/1234567/fulfillments.json",
    "method": "POST",
    "body": {
      "fulfillment": {
        "location_id": "98765431",
        "notify_customer": false,
        "status": "success"
      }
    }
  }
{% endjson %}
```

Above can be used together with `rest` filter to create fulfillment for order: 1234567.

```javascript
{% assign fulfillment_result = rest_input | rest %}
```

`fulfillment_result` is going to store response returned from shopify. If we want to access newly created fulfillment we would do with with following syntax: `fulfillment_result.body.fulfillment`

`rest` input object has one required parameter which is `path`. You can always look up path for every Shopify resource in official Shopify REST documentation:

{% embed url="<https://shopify.dev/docs/admin-api/rest/reference>" %}


# right\_join

Joins two arrays based on matching keys, similar to SQL RIGHT JOIN. All items from the second array are kept, with matching properties merged from the first array. Non-matching items get `null` values for the first array's properties.

```liquid
{% json employees %}
  [
    { "id": 1, "name": "Ann", "department_id": 10 },
    { "id": 2, "name": "Adam", "department_id": 20 }
  ]
{% endjson %}

{% json departments %}
  [
    { "dept_id": 10, "dept_name": "Accounting", "floor": 3 },
    { "dept_id": 20, "dept_name": "Sales", "floor": 5 },
    { "dept_id": 30, "dept_name": "Marketing", "floor": 7 }
  ]
{% endjson %}

{% assign result = employees | right_join: departments, "department_id = dept_id" %}
{% log result %}
```

Results in following output:

```json
[
  {
    "dept_id": 10,
    "dept_name": "Accounting",
    "floor": 3,
    "id": 1,
    "name": "Ann",
    "department_id": 10
  },
  {
    "dept_id": 20,
    "dept_name": "Sales",
    "floor": 5,
    "id": 2,
    "name": "Adam",
    "department_id": 20
  },
  {
    "dept_id": 30,
    "dept_name": "Marketing",
    "floor": 7,
    "id": null,
    "name": null
  }
]
```

#### Syntax

```liquid
{{ array1 | right_join: array2, "key1 = key2" }}
```

| Parameter | Description                      |
| --------- | -------------------------------- |
| `array1`  | Array to join from               |
| `array2`  | Primary array (all items kept)   |
| `key1`    | Property name in array1 to match |
| `key2`    | Property name in array2 to match |

#### Notes

* Modifies `array2` in place
* Unmatched items receive `null` for properties from `array1` (excluding the join key)
* See also: `left_join` for keeping all items from the first array instead


# run

{% hint style="info" %}
Deprecated. Use [run](/liquid/filters/run) tag instead.
{% endhint %}

With the use of this filter you can run any other scripts that you have set up. Additionally you can pass a JSON objects with parameters. These parameters will be preloaded to task you would like to run.

{% hint style="info" %}
Script will be executed asynchronously.
{% endhint %}

This filter is particularly useful when using [Blank HTTP](/scripts/blank/http) task. You can run any other task and respond with 200 code to acknowledge receipt of a message.

```javascript
{% if request == blank %}
    {% json request %}
        {
            "body": {}
        }
    {% endjson %}
{% endif %}

{% capture task_input %}
    { "recieved_inventories": {{request.body | json }} }
{% endcapture %}

{{ task_input | run: "update_inventory_levels" }}
{% json response %}
    {
        "status": "ok"
    }
{% endjson %}
```

Placing above snippet in any Blank HTTP task would trigger a task identified with handle: `update_inventory_level`

Instead of script handle you can also use Script ID


# sha1

Generates a SHA1 hash of a string. Returns a hex-encoded hash string.

```liquid
{% assign hash = "hello world" | sha1 %}
{% log hash %}
```

Output:

```
2aae6c35c94fcfb415dbe95f408b9ce91ee846ed
```

#### Syntax

```liquid
{{ value | sha1 }}
```

| Parameter | Description        |
| --------- | ------------------ |
| `value`   | The string to hash |

#### Return Value

Returns a hex-encoded SHA1 hash string (40 characters).

#### Examples

**Generate a hash for deduplication:**

```liquid
{% assign content_hash = email.body.text | sha1 %}
{% log "Content hash: " | append: content_hash %}
```

**Create a cache key:**

```liquid
{% capture cache_key_input %}{{ product.id }}-{{ product.updated_at }}{% endcapture %}
{% assign cache_key = cache_key_input | sha1 %}
{% log cache_key %}
```

**Generate a unique identifier from combined fields:**

```liquid
{% capture combined %}{{ order.name }}{{ order.email }}{{ order.created_at }}{% endcapture %}
{% assign unique_id = combined | sha1 %}
{% log unique_id %}
```

#### Notes

* Always returns a hex-encoded string (lowercase, 40 characters)
* SHA1 is suitable for checksums and deduplication, but not for security-sensitive operations
* For HMAC-based authentication, use `hmac_sha256` instead
* See also: `hmac_sha256`, `base64_encode`


# sum

Calculates the sum of all numeric values in an array. Non-numeric values are ignored.

```liquid
{% assign total = prices | sum %}
{% log total %}
```

#### Syntax

```liquid
{{ array | sum }}
```

| Parameter | Description            |
| --------- | ---------------------- |
| `array`   | Array of values to sum |

#### Return Value

Returns a number — the sum of all numeric values in the array. Non-numeric values are skipped.

#### Examples

**Sum order line item prices:**

```liquid
{% assign prices = order.line_items | map: "price" %}
{% assign total = prices | sum %}
{% log "Total: $" | append: total %}
```

**Sum quantities:**

```liquid
{% assign quantities = line_items | map: "quantity" %}
{% assign total_items = quantities | sum %}
{% log "Total items: " | append: total_items %}
```

**Sum values from a CSV import:**

```liquid
{% assign rows = file.content | parse_csv %}
{% assign amounts = rows | map: "amount" %}
{% assign total = amounts | sum %}
{% log "Import total: " | append: total %}
```

**Calculate total weight:**

```liquid
{% assign weights = order.line_items | map: "grams" %}
{% assign total_grams = weights | sum %}
{% assign total_kg = total_grams | divided_by: 1000.0 %}
{% log "Total weight: " | append: total_kg | append: " kg" %}
```

**Sum with mixed data (non-numeric values are ignored):**

```liquid
{% assign values = "10,abc,20,30,n/a" | split: "," %}
{% assign total = values | sum %}
{% log total %}
```

Output:

```
60
```

#### Notes

* Non-numeric values (strings, nulls, etc.) are silently skipped
* Values are parsed as floats, so decimal numbers are supported
* Combine with `map` to sum a specific property from an array of objects
* Returns `0` for an empty array


# time\_add

Adds a specified amount of time to a date. Returns the result as an ISO 8601 string.

```liquid
{% assign tomorrow = "now" | time_add: 1, "days" %}
{% log tomorrow %}
```

#### Syntax

```liquid
{{ date | time_add: amount, unit }}
```

| Parameter | Description                                                                                |
| --------- | ------------------------------------------------------------------------------------------ |
| `date`    | Starting date — a date string, `"now"`, or `"today"`                                       |
| `amount`  | Number of units to add                                                                     |
| `unit`    | Time unit: `"years"`, `"months"`, `"weeks"`, `"days"`, `"hours"`, `"minutes"`, `"seconds"` |

#### Return Value

Returns a string in `YYYY-MM-DDTHH:mm:ss` format.

#### Examples

**Add days:**

```liquid
{% assign delivery_date = order.created_at | time_add: 5, "days" %}
{% log "Expected delivery: " | append: delivery_date %}
```

**Add hours from now:**

```liquid
{% assign expires_at = "now" | time_add: 24, "hours" %}
{% log "Expires at: " | append: expires_at %}
```

**Add months:**

```liquid
{% assign renewal_date = subscription.start_date | time_add: 1, "months" %}
{% log "Renewal: " | append: renewal_date %}
```

**Schedule a follow-up:**

```liquid
{% assign followup_date = order.created_at | time_add: 2, "weeks" %}
{% log "Follow-up scheduled for: " | append: followup_date %}
```

**Calculate an expiration date and format it:**

```liquid
{% assign expiry = "now" | time_add: 30, "days" %}
{% assign formatted = expiry | date: "%B %d, %Y" %}
{% log "Offer expires: " | append: formatted %}
```

Output:

```
Offer expires: March 23, 2025
```

**Combine with in\_timezone:**

```liquid
{% assign future = "now" | time_add: 3, "hours" | in_timezone: shop.timezone %}
{% log future %}
```

#### Notes

* Accepts `"now"` or `"today"` as the current date/time
* The output format is always `YYYY-MM-DDTHH:mm:ss` — use the `date` filter to reformat
* See also: `time_subtract`, `in_timezone`


# time\_subtract

Subtracts a specified amount of time from a date. Returns the result as an ISO 8601 string.

```liquid
{% assign yesterday = "now" | time_subtract: 1, "days" %}
{% log yesterday %}
```

#### Syntax

```liquid
{{ date | time_subtract: amount, unit }}
```

| Parameter | Description                                                                                |
| --------- | ------------------------------------------------------------------------------------------ |
| `date`    | Starting date — a date string, `"now"`, or `"today"`                                       |
| `amount`  | Number of units to subtract                                                                |
| `unit`    | Time unit: `"years"`, `"months"`, `"weeks"`, `"days"`, `"hours"`, `"minutes"`, `"seconds"` |

#### Return Value

Returns a string in `YYYY-MM-DDTHH:mm:ss` format.

#### Examples

**Get yesterday's date:**

```liquid
{% assign yesterday = "now" | time_subtract: 1, "days" %}
{% log yesterday %}
```

**Get orders from the last 7 days:**

```liquid
{% assign week_ago = "now" | time_subtract: 7, "days" %}
{% log "Fetching orders since: " | append: week_ago %}
```

**Calculate a refund deadline:**

```liquid
{% assign refund_cutoff = order.created_at | time_subtract: 0, "days" | time_add: 30, "days" %}
{% assign now = "now" | time_add: 0, "seconds" %}
{% log "Refund deadline: " | append: refund_cutoff %}
```

**Look back by hours:**

```liquid
{% assign two_hours_ago = "now" | time_subtract: 2, "hours" %}
{% log "Since: " | append: two_hours_ago %}
```

**Filter by date range:**

```liquid
{% assign start_date = "now" | time_subtract: 30, "days" %}
{% assign end_date = "now" | time_add: 0, "seconds" %}
{% log "Date range: " | append: start_date | append: " to " | append: end_date %}
```

**Combine with in\_timezone:**

```liquid
{% assign past = "now" | time_subtract: 1, "weeks" | in_timezone: shop.timezone %}
{% log past %}
```

#### Notes

* Accepts `"now"` or `"today"` as the current date/time
* The output format is always `YYYY-MM-DDTHH:mm:ss` — use the `date` filter to reformat
* See also: `time_add`, `in_timezone`


# type

Returns the JavaScript type of a value as a string. Useful for debugging and conditional logic based on data types.

```liquid
{% assign t = my_var | type %}
{% log t %}
```

#### Syntax

```liquid
{{ value | type }}
```

| Parameter | Description                    |
| --------- | ------------------------------ |
| `value`   | Any value to check the type of |

#### Return Value

Returns a string — one of: `"string"`, `"number"`, `"boolean"`, `"object"`, `"undefined"`.

#### Examples

**Check the type of a variable:**

```liquid
{% assign t = product.price | type %}
{% log "Price type: " | append: t %}
```

Output:

```
Price type: string
```

**Conditional logic based on type:**

```liquid
{% assign t = input_value | type %}
{% if t == "string" %}
  {% assign parsed = input_value | parse_json %}
{% elsif t == "object" %}
  {% assign parsed = input_value %}
{% endif %}
{% log parsed %}
```

**Debug unknown data:**

```liquid
{% log "Value: " | append: my_var %}
{% log "Type: " | append: my_var | type %}
```

**Validate function parameters:**

```liquid
{% assign t = email | type %}
{% if t == "undefined" %}
  {% log "Error: email parameter is required" %}
  {% return nil %}
{% endif %}
```

#### Notes

* Returns JavaScript `typeof` values: `"string"`, `"number"`, `"boolean"`, `"object"`, `"undefined"`
* Arrays return `"object"` — use the `size` filter to check if a value is array-like
* `null` returns `"object"` (standard JavaScript behavior)
* Useful for debugging when you're unsure what type a variable holds


# where\_exp

Filters an array using a Liquid expression. Unlike `where`, which matches a property to a value, `where_exp` lets you write arbitrary conditions using the full Liquid expression syntax.

```liquid
{% assign expensive = products | where_exp: "p", "p.price > 100" %}
{% log "Expensive products: " | append: expensive.size %}
```

#### Syntax

```liquid
{{ array | where_exp: item_name, expression }}
```

| Parameter    | Description                                              |
| ------------ | -------------------------------------------------------- |
| `array`      | Array of objects to filter                               |
| `item_name`  | Variable name for the current item inside the expression |
| `expression` | Liquid expression that evaluates to truthy/falsy         |

#### Return Value

Returns an array containing only the items for which the expression evaluated to a truthy value.

#### Examples

**Filter products above a price threshold:**

```liquid
{% assign expensive = products | where_exp: "p", "p.price > 50" %}
{% for product in expensive %}
  {% log product.title | append: ": $" | append: product.price %}
{% endfor %}
```

**Filter orders with a specific tag:**

```liquid
{% assign vip_orders = orders | where_exp: "o", "o.tags contains 'VIP'" %}
{% log "VIP orders: " | append: vip_orders.size %}
```

**Filter with multiple conditions:**

```liquid
{% assign matches = products | where_exp: "p", "p.vendor == 'Nike' and p.product_type == 'Shoes'" %}
{% log "Nike shoes: " | append: matches.size %}
```

**Filter out empty values:**

```liquid
{% assign with_sku = variants | where_exp: "v", "v.sku != blank" %}
{% log "Variants with SKU: " | append: with_sku.size %}
```

**Filter by numeric comparison:**

```liquid
{% assign low_stock = variants | where_exp: "v", "v.inventory_quantity < 10" %}
{% for variant in low_stock %}
  {% log variant.sku | append: ": " | append: variant.inventory_quantity | append: " remaining" %}
{% endfor %}
```

**Filter by negation:**

```liquid
{% assign not_archived = products | where_exp: "p", "p.status != 'ARCHIVED'" %}
{% log "Active products: " | append: not_archived.size %}
```

**Combine with other filters:**

```liquid
{% assign active_expensive = products | where_exp: "p", "p.status == 'ACTIVE'" | where_exp: "p", "p.price > 100" %}
{% assign total = active_expensive | map: "price" | sum %}
{% log "Total value of active expensive products: $" | append: total %}
```

#### Comparison with `where`

| Feature             | `where`                     | `where_exp`                                           |
| ------------------- | --------------------------- | ----------------------------------------------------- |
| Syntax              | `where: "status", "active"` | `where_exp: "p", "p.status == 'active'"`              |
| Equality only       | Yes                         | No — supports `>`, `<`, `contains`, `and`, `or`, etc. |
| Multiple conditions | No (chain multiple `where`) | Yes (use `and` / `or` in expression)                  |
| Negation            | No                          | Yes (`!=`, `unless`-style logic)                      |

#### Notes

* The `item_name` parameter defines how you reference each item inside the expression
* The expression supports all Liquid operators: `==`, `!=`, `>`, `<`, `>=`, `<=`, `contains`, `and`, `or`
* Returns all matching items — to get only the first match, use `find_exp` instead
* Can be chained: `| where_exp: "p", "..." | where_exp: "p", "..."`
* See also: `where`, `find_exp`, `group_by_exp`


# Shopify Flow


# V1

{% hint style="info" %}
Below documentation describes first version of DataJet / Shopify Flow Integration. As of 1st October 2024 V1 is deprecated. There is no need to migrate to [V2](/integrations/shopify-flow/v2).
{% endhint %}

DataJet is able to interact with flow either by sending data or receiving it from [Shopify Flow](https://help.shopify.com/en/manual/shopify-flow).

### Sending data to flow

To send data to Shopify Flow first you would need to create Flow with DataJet trigger.<br>

<figure><img src="/files/4oetKOV8dzYBAigihy5D" alt=""><figcaption></figcaption></figure>

Next you can proceed with creating a DataJet task that would use flow filter to send data to your flow. Your task can be any of the following:

* Blank HTTP (useful when you want to process user submitted form from the frontend)
* Blank Scheduled (when you need to perform any repetitive actions in your store)
* Blank Input (when task is triggered by uploading CSV file to process)
* Blank Event (triggered by any of the shops webhooks)

For the purpose of this example we are going to hardcode payload with customer id property.

Task could look like this:

```
{% json payload %}
	{"customer_id": 5287014039740}
{% endjson %}

{% assign result = payload | flow %}
```

Now lets add add a tag through flow to that customer. This is how our flow would look like:

<figure><img src="/files/bwQukvdbREPBaTP0VBfI" alt=""><figcaption></figcaption></figure>

When our task code is executed - customer with provided ID would be tagged with: `test-flow-tag`

### Receiving data from flow

#### Action Trigger

{% hint style="info" %}
Action Trigger is deprecated. Use Action Trigger V2 instead.
{% endhint %}

DataJet task can also be triggered by configured flow.

In such case first we need to create Blank HTTP task. This task has task id attached as last part of url.

<figure><img src="/files/2GcdJhtAqiNbgNjZ0FNM" alt=""><figcaption></figcaption></figure>

In our case task id is: `61631513777e665dc57343db`<br>

Now we can create trigger in Shopify Flow. For that we are going to use flow we configured in previous example:

<figure><img src="/files/nrZGNS9OidCVia48KeMw" alt=""><figcaption></figcaption></figure>

In Task ID field we need to provide DataJet task id. The complete flow would work as following:

* First DataJet triggers Flow with customer id as payload
* Flow adds customer tag
* Flow triggers DataJet task
* DataJet processes HTTP task

Here is sample code for the Blank HTTP task. This code only logs incoming Flow request to console. However we could for example further modify customer here by adding another tags.

```
{{request.body | log }}

{% json response %}
  {
    "body": {
      "status": "ok"
    },
    "status": 200
  }
{% endjson %}
```

`request` is global object in all HTTP tasks. It contains incoming request data.


# V2

The second version of the DataJet / Shopify integration leverages the latest updates to the Shopify Flow integration, such as:

* Structured data in event triggers
* Processing responses returned to action triggers

Additionally, DataJet flow event triggers now include context information, such as:

* Script ID
* Run ID
* Script handle

### Event trigger

To start a flow from DataJet, you will need to complete two steps:

1. In DataJet, create a script that triggers the flow using the `flow_v2` filter.
2. In Shopify Flow, create a flow with the `Event Payload Trigger V2`

A sample script to trigger the flow is:

```
{% json payload %}
  {
    "name": "Mat",
    "age": 32
  }
{% endjson %}
{% flow payload:payload as result %}
{% log result %}
```

To create a Shopify Flow start by adding `Event Payload Trigger V2`:

<figure><img src="/files/fffnvlkchzcATkPL9JpX" alt=""><figcaption></figcaption></figure>

{% hint style="info" %}
If you have multiple flows responding to the same event - all flows are triggered.
{% endhint %}

After the event trigger block, you might want to add an "if" block to check the Script ID and ensure you only respond to events coming from a specific script:

<figure><img src="/files/ECT0tC1kS76pBcOTEWKd" alt=""><figcaption></figcaption></figure>

In the example above, we also log the payload passed to the flow from DataJet. This payload will be passed as stringified JSON. This is why we use a "Run code" block next to parse the stringified JSON into JSON:

<figure><img src="/files/XTX2uvpJXCVtZ5GuYiEA" alt=""><figcaption></figcaption></figure>

After this, we can access our payload properties in the next blocks using the following syntax:`{{runCode.payload.name}}`.

The flow above can be easily imported using this file (after importing, update the Script ID in the second block to match your script ID).

{% file src="/files/sL4D75hw5CddJFfElkdq" %}

### Action trigger

With Flow action triggers, you can trigger any script in DataJet. Depending on the outcome of the script execution, you can return either a success or error response to Shopify Flow and take corresponding actions.

The following two steps need to be completed:

1. In DataJet, create an HTTP script
2. In Shopify Flow - add the `Action Trigger V2` trigger

To create an HTTP script, select the "Add new HTTP script" button from the left-hand side navigation in DataJet. Next, you can use the following sample code:

```
{% assign flow_payload = request.body.properties.payload | default: "null" | parse %}

{{ flow_payload.email | log }}

{% json response %}
  {
    "body": {
      "return_value": {
        "status": "SUCCESS",
        "message": "OK"
      }
    }
  }
{% endjson %}
```

The code above reads the `email` property sent through Action Trigger V2 and logs it. After that, we return a success response to Shopify Flow. If you want to return an error response, replace `SUCCESS` with `ERROR`. Use the `message` field to send additional information.\
\
Your sample flow could look like this:

<figure><img src="/files/UNohuANhtWeijTR773Zb" alt=""><figcaption></figcaption></figure>

{% hint style="info" %}
Replace Task ID with your script id from DataJet dashboard.
{% endhint %}

The flow above monitors changes to customer tags. When a tag is added, it sends a payload to DataJet with the customer's email. Next, if it receives a success response, it logs a success message; otherwise, it logs an error message.\
\
Above flow can be imported with this file:

{% file src="/files/8XEUobi3VXzTk2xMIpfp" %}

{% hint style="info" %}
The flow waits 10 seconds for an action response. After this time, the action resends the request. This is why it is important to ensure that your DataJet script processes the request in less than 10 seconds. You can use the run filter to delegate the logic to another task.
{% endhint %}


# GitHub

#### Introduction

You can connect DataJet to your GitHub account. This integration can be used, for example, to:

* Track changes in a script's source code
* Connect development stores with development branches
* Use one codebase across multiple Shopify stores

The integration works both ways. Any change made in the DataJet script editor is automatically pushed to your repository, and any change pushed to the repository is automatically reflected in your DataJet scripts.

#### Prerequisites

Before connecting a GitHub repository with DataJet, you need to have your repository created, ensure that your GitHub user has full permissions to the repository, and install DataJet on your Shopify store.

#### Setup

Following steps are requiored to complete the setup:

1. Create [fine-grained access token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-fine-grained-personal-access-token) to your Github repository
2. Connect repository via DataJet settings
3. Connect script with a file from repository

**Create fine-grained access token**

This API access token is used to push any changes from DataJet to your repository. To create the token, follow the instructions on the GitHub [website](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-fine-grained-personal-access-token).\
\
Under the ***Repository Access*** section - ensure that your token has access only to the repository you created for DataJet scripts:

<figure><img src="/files/LSD4LvQKMCCG4lpyKUQp" alt=""><figcaption></figcaption></figure>

Under ***Permissions,*** select *Read and write access* for Webhooks and Contents<br>

<figure><img src="/files/ujcwfbA2ImdHkVcbxYWM" alt=""><figcaption></figcaption></figure>

After creating the token, copy it. We will use it in the next step.

**Connect repository via DataJet settings**

Open the DataJet app and go to Settings. At very bottom, select the *Connect GitHub* checkbox and paste your token. Click the Connect button. You should now see repository for which you created the token. Select it and click *Save connection*. If the connection is successful, a green confirmation message will appear at the bottom.

You can now close settings and go to Scripts Console to connect your first script.

**Connect script with a file from repository**

Before connecting the script with a file from the repository, ensure that the file is present in your repository. If it isn't, create it before proceeding to the next step.\
\
For the purpose of this tutorial, we will create an `order-created.dj` file inside the `webhooks` directory.<br>

Now we can create a new script in the DataJet Scripts Console and connect it to a file created in our repository. After creating the file select cogwheel next to the script name, then choose GitHub. In the modal, select the branch and file.

Connect and close the modal. Your script is now connected to a file in your repository. Any changes to the file will be reflected in the script code, and vice versa.

{% hint style="info" %}
Use refresh button next to repository file name to resync code from the repo file with current script code:\
You can also fully refresh the page to fetch latest script version to the code editor.
{% endhint %}


# Track changes in a script's source code

After completing the [integration setup](/integrations/github), any changes to your scripts are tracked in your GitHub repository.


# Connect development stores with development branches

You can use GitHub integration to create development environments for your scripts.

A single GitHub repository might be connected to multiple DataJet apps across multiple stores. Additionally, a single script might be connected to a different branch. This allows you to connect your production store scripts with *main* branch in your repository and your development store scripts with any other branch (e.g. *development*).

We will reuse the repository we created as part of the [setup](/integrations/github). Start by creating development branch in your GitHub repository. With two branches in your repository, the *main* branch will be connected to production Shopify store, while *development* branch can be connected to the development store.

<figure><img src="/files/fCmsCvhR3TH9T53I1YWw" alt=""><figcaption></figcaption></figure>

Now you can work on any script updates in your development store. When the changes are ready to be deployed, simply merge your *development* branch to your *main* branch. The production store will be automatically updated with the latest script version.




---

[Next Page](/llms-full.txt/1)

