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:
- Extract parameters — read URL and form input with
{% params %} - Validate — check that inputs are safe and sensible
- Execute action — use
{% action %}to run built-in operations (add to cart, etc.) - 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?
Thanks for your feedback! It helps us improve our docs.