Skip to content
Log in

Liquid controller actions guide

On this page

A Liquid controller handles HTTP requests to your storefront. This guide covers extracting parameters, validating input, executing actions (cart operations, shipping, promotions), and controlling the response, all without writing Apex.

Before you start

  • Controller templates live at controllers/<controller>/<action>.liquid
  • Controllers run in three phases: {% before %}, standard action, then {% after %}
  • All controller tags work only inside a controller template; they are silent no-ops elsewhere
  • Read Liquid controller lifecycle for the full execution model

Request parameter flow

The typical controller flow:

  1. Extract parameters — read URL and form input with {% params %}
  2. Validate — check that inputs are safe and sensible
  3. Execute action — use {% action %} to run built-in operations (add to cart, etc.)
  4. Respond or redirect — send a response or redirect the browser

Extracting parameters

Use {% params %} in the {% before %} phase to extract query strings and form POST parameters.

Basic parameter extraction

```liquid

{% before %} {% params product_id: current_request.params.product_id, quantity: current_request.params.quantity %} {% endbefore %} ```

After this tag, params.product_id and params.quantity are available to use in the controller.

With defaults

Use the Liquid default filter to provide fallback values:

```liquid

{% before %} {% params quantity: current_request.params.quantity | default: 1 %} {% endbefore %} ```

Converting types

Parameters arrive as strings. Convert them as needed:

```liquid

{% before %} {% params count: current_request.params.count | default: 1 | times: 1 %} {%- comment -%} times: 1 converts string to number {%- endcomment -%} {% endbefore %} ```

Validating input

Always validate parameters before using them. Invalid input is a security risk and can cause confusing errors downstream.

Check for presence

```liquid

{% before %} {% assign product_id = current_request.params.product_id %}

{% params product_id: product_id %}

{% if product_id == blank %} {% respond body: ‘{“error”: “product_id is required”}’, status: 400, layout: false %} {% endif %} {% endbefore %} ```

Check length or range

```liquid

{% before %} {% assign quantity = current_request.params.quantity | default: 1 | times: 1 %}

{% params quantity: quantity %}

{% if quantity < 1 or quantity > 100 %} {% redirect to: current_store.product_path, alert: “Quantity must be between 1 and 100” %} {% endif %} {% endbefore %} ```

Check allowed values

```liquid

{% before %} {% assign view_mode = current_request.params.mode %}

{% params view_mode: view_mode %}

{% if view_mode != “grid” and view_mode != “list” %} {% redirect to: current_store.products_path %} {% endif %} {% endbefore %} ```

Setting variables for rendering

Use {% variables %} to make data available to the page template and all snippets.

```liquid

{% before %} {% variables page_title: “My Custom Title”, show_sidebar: true %} {% endbefore %} ```

Inside the page template, reference these as top-level variables:

```liquid

{{ page_title }}

{% if show_sidebar %} {% render “sidebar” %} {% endif %} ```

This is useful for: - Setting page titles dynamically - Controlling which sections render - Passing controller context to snippets

Executing built-in actions

Use {% action %} to execute pre-built controller operations. Available actions:

Action Purpose Parameters
cart.add Add item to cart product_identifier, quantity
cart.update Update item quantity cart_item, quantity
cart.remove Remove item from cart cart_item
cart.empty Clear entire cart (none)
cart.select Select a specific cart cart
cart.create Create a new cart (none)
cart.clone Duplicate a cart cart
shipping.set Set shipping method shipping_method
pricebook.set Set customer price book pricebook
pricebook.clear Clear custom price book (none)
outlet.set Set fulfillment outlet outlet
outlet.clear Clear outlet selection (none)
promotion.apply Apply promotion code code
promotion.remove Remove a promotion promotion
promotion.clear Clear all promotions (none)

Example: Add to cart

```liquid

{% before %} {% assign product_id = current_request.params.product_id %} {% assign quantity = current_request.params.quantity | default: 1 | times: 1 %}

{% params product_id: product_id, quantity: quantity %}

{% if product_id == blank or quantity < 1 %} {% respond body: ‘{“error”: “invalid input”}’, status: 400, layout: false %} {% endif %}

{% action “cart.add”, product_identifier: product_id, quantity: quantity %} {% endbefore %}

{% after %} {% redirect to: current_store.cart_path, notice: “Item added to cart” %} {% endafter %} ```

Example: Apply promotion

```liquid

{% before %} {% assign code = current_request.params.promo_code %}

{% params code: code %}

{% if code != blank %} {% action “promotion.apply”, code: code %} {% endif %} {% endbefore %}

{% after %} {%- new Map result -%} {%- assign result = result | set_key: “cart_total”, current_cart.totals.subtotal -%} {%- assign result = result | set_key: “promotion_count”, current_cart.promotions.size -%} {% respond body: result | json, status: 200, layout: false %} {% endafter %} ```

Sending responses

Use {% respond %} to send a custom response (instead of rendering the page template).

HTML response

```liquid

{% respond body: “<h1>Success!</h1>”, status: 200 %} ```

JSON response

```liquid

{% before %} {% params product_id: current_request.params.product_id %} {% endbefore %}

{% after %} {%- new Map result -%} {%- assign result = result | set_key: “success”, true -%} {%- assign result = result | set_key: “product_id”, current_request.params.product_id -%} {%- assign response_body = result | json -%} {% respond body: response_body, status: 200, layout: false %} {% endafter %} ```

Key options: - body: — the response content (string) - status: — HTTP status code. Defaults to 302, so always set it explicitly when returning a body - layout: — include theme layout? (default true; use false for JSON/API responses) - notice: — flash message (positive) - alert: — flash message (negative)

Error response

```liquid

{% before %} {% assign quantity = current_request.params.quantity | default: 1 | times: 1 %}

{% params quantity: quantity %}

{% if quantity > 50 %} {%- new Map error -%} {%- assign error = error | set_key: “error”, “Quantity cannot exceed 50” -%} {% respond body: error | json, status: 422, layout: false %} {% endif %} {% endbefore %} ```

Redirects

Use {% redirect %} to send the browser to a different URL.

Simple redirect

```liquid

{% after %} {% redirect to: current_store.cart_path %} {% endafter %} ```

Redirect with flash message

```liquid

{% after %} {% redirect to: current_store.account_path, notice: “Changes saved successfully” %} {% endafter %} ```

Conditional redirect

```liquid

{% before %} {% params customer_id: current_request.params.customer_id %} {% endbefore %}

{% after %} {% if current_request.params.customer_id == blank %} {% redirect to: current_store.account_login_path, alert: “Please log in first” %} {% else %} {% redirect to: current_store.account_path %} {% endif %} {% endafter %} ```

Difference from respond

  • {% redirect %} — sends a 302 redirect; the browser fetches a new page
  • {% respond %} — returns content directly; the page does not change in the browser’s address bar

Use redirects for state changes (after adding to cart, updating profile). Use responses for API endpoints that return data.

Complete examples

Add-to-cart form handler

```liquid

{% before %} {%- comment -%} Extract and validate input {%- endcomment -%} {% assign product_id = current_request.params.product_id %} {% assign quantity = current_request.params.quantity | default: 1 | times: 1 %}

{% params product_id: product_id, quantity: quantity %}

{% if product_id == blank %} {% redirect to: current_store.products_path, alert: “Product not found” %} {% endif %}

{% if quantity < 1 or quantity > 100 %} {% redirect to: current_store.product_path, alert: “Invalid quantity” %} {% endif %}

{%- comment -%} Execute the action {%- endcomment -%} {% action “cart.add”, product_identifier: product_id, quantity: quantity %} {% endbefore %}

{%- comment -%} Redirect after success {%- endcomment -%} {% after %} {% redirect to: current_store.cart_path, notice: “Item added to cart” %} {% endafter %} ```

AJAX endpoint for cart status

```liquid

{% before %} {%- comment -%} No parameters needed; just return cart state {%- endcomment -%} {% endbefore %}

{% after %} {%- new Map response -%} {%- assign response = response | set_key: “item_count”, current_cart.items.size -%} {%- assign response = response | set_key: “subtotal”, current_cart.totals.subtotal -%} {%- assign response = response | set_key: “currency”, current_store.currency -%} {% respond body: response | json, status: 200, layout: false %} {% endafter %} ```

Promotion code validator

```liquid

{% before %} {% assign code = current_request.params.code %}

{% params code: code %}

{% if code == blank %} {% respond body: ‘{“valid”: false, “message”: “Code required”}’, status: 400, layout: false %} {% endif %}

{% action “promotion.apply”, code: code %} {% endbefore %}

{% after %} {%- new Map result -%} {%- assign promos_applied = current_cart.promotions.size -%} {%- if promos_applied > 0 -%} {%- assign result = result | set_key: “valid”, true -%} {%- assign result = result | set_key: “discount”, current_cart.totals.discount_amount -%} {%- else -%} {%- assign result = result | set_key: “valid”, false -%} {%- assign result = result | set_key: “message”, “Promotion code not found or expired” -%} {%- endif -%} {% respond body: result | json, status: 200, layout: false %} {% endafter %} ```

Common patterns

Guard clause for authentication

```liquid

{% before %} {% if current_customer == blank %} {% redirect to: current_store.account_login_path %} {% endif %} {% endbefore %} ```

Type conversion

```liquid

{% before %} {% params id: current_request.params.id | times: 1 %} {%- comment -%} string → number {%- endcomment -%} {% endbefore %} ```

Default values

```liquid

{% before %} {% params sort: current_request.params.sort | default: “name” %} {% params limit: current_request.params.limit | default: 20 | times: 1 %} {% endbefore %} ```

Whitelist allowed values

```liquid

{% before %} {% params mode: current_request.params.mode %}

{% unless mode == “grid” or mode == “list” %} {% assign mode = “grid” %} {% endunless %}

{% variables view_mode: mode %} {% endbefore %} ```

Security considerations

  • Always validate — treat all user input (query strings, form posts, request headers) as untrusted
  • Use built-in actions — don’t construct custom business logic if a built-in action exists
  • Scope to customer — when reading/writing customer data, filter by current_customer
  • Never expose IDs in responses — don’t leak record IDs, counts, or internal structure to the client unless intentional
  • Use HTTPS only — controllers over HTTP expose parameters in logs and network traces

Was this article helpful?

Was this article helpful?