{"title":"Liquid controller actions guide","slug":"liquid-controller-actions","url":"https://support.storeconnect.com/articles/liquid-controller-actions","url_markdown":"https://support.storeconnect.com/articles/liquid-controller-actions.md","subtitle":null,"summary":"Build Liquid controllers that handle requests, validate input, execute actions, and control responses. Comprehensive guide to params, variables, respond, redirect, and action tags.","type":"Developer_Documentation","video_url":"","keywords":"liquid controller, params tag, variables tag, respond tag, redirect tag, action tag, request handling, form submission, custom response, storeconnect liquid","last_modified":"2026-09-15T02:32:04+0000","body_markdown":"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.\n\n## Before you start\n\n- Controller templates live at `controllers/\u003ccontroller\u003e/\u003caction\u003e.liquid`\n- Controllers run in three phases: `{% before %}`, **standard action**, then `{% after %}`\n- All controller tags work only inside a controller template; they are silent no-ops elsewhere\n- Read [Liquid controller lifecycle](liquid-controllers-guide) for the full execution model\n\n## Request parameter flow\n\nThe typical controller flow:\n\n1. **Extract parameters** — read URL and form input with `{% params %}`\n2. **Validate** — check that inputs are safe and sensible\n3. **Execute action** — use `{% action %}` to run built-in operations (add to cart, etc.)\n4. **Respond or redirect** — send a response or redirect the browser\n\n## Extracting parameters\n\nUse `{% params %}` in the `{% before %}` phase to extract query strings and form POST parameters.\n\n### Basic parameter extraction\n\n\n```liquid\n\n{% before %}\n  {% params product_id: current_request.params.product_id, quantity: current_request.params.quantity %}\n{% endbefore %}\n```\n\n\nAfter this tag, `params.product_id` and `params.quantity` are available to use in the controller.\n\n### With defaults\n\nUse the Liquid `default` filter to provide fallback values:\n\n\n```liquid\n\n{% before %}\n  {% params quantity: current_request.params.quantity | default: 1 %}\n{% endbefore %}\n```\n\n\n### Converting types\n\nParameters arrive as strings. Convert them as needed:\n\n\n```liquid\n\n{% before %}\n  {% params count: current_request.params.count | default: 1 | times: 1 %}\n  {%- comment -%} times: 1 converts string to number {%- endcomment -%}\n{% endbefore %}\n```\n\n\n## Validating input\n\nAlways validate parameters before using them. Invalid input is a security risk and can cause confusing errors downstream.\n\n### Check for presence\n\n\n```liquid\n\n{% before %}\n  {% assign product_id = current_request.params.product_id %}\n\n  {% params product_id: product_id %}\n\n  {% if product_id == blank %}\n    {% respond body: '{\"error\": \"product_id is required\"}', status: 400, layout: false %}\n  {% endif %}\n{% endbefore %}\n```\n\n\n### Check length or range\n\n\n```liquid\n\n{% before %}\n  {% assign quantity = current_request.params.quantity | default: 1 | times: 1 %}\n\n  {% params quantity: quantity %}\n\n  {% if quantity \u003c 1 or quantity \u003e 100 %}\n    {% redirect to: current_store.product_path, alert: \"Quantity must be between 1 and 100\" %}\n  {% endif %}\n{% endbefore %}\n```\n\n\n### Check allowed values\n\n\n```liquid\n\n{% before %}\n  {% assign view_mode = current_request.params.mode %}\n\n  {% params view_mode: view_mode %}\n\n  {% if view_mode != \"grid\" and view_mode != \"list\" %}\n    {% redirect to: current_store.products_path %}\n  {% endif %}\n{% endbefore %}\n```\n\n\n## Setting variables for rendering\n\nUse `{% variables %}` to make data available to the page template and all snippets.\n\n\n```liquid\n\n{% before %}\n  {% variables page_title: \"My Custom Title\", show_sidebar: true %}\n{% endbefore %}\n```\n\n\nInside the page template, reference these as top-level variables:\n\n\n```liquid\n\n\u003ch1\u003e{{ page_title }}\u003c/h1\u003e\n{% if show_sidebar %}\n  {% render \"sidebar\" %}\n{% endif %}\n```\n\n\nThis is useful for:\n- Setting page titles dynamically\n- Controlling which sections render\n- Passing controller context to snippets\n\n## Executing built-in actions\n\nUse `{% action %}` to execute pre-built controller operations. Available actions:\n\n| Action | Purpose | Parameters |\n|--------|---------|------------|\n| `cart.add` | Add item to cart | `product_identifier`, `quantity` |\n| `cart.update` | Update item quantity | `cart_item`, `quantity` |\n| `cart.remove` | Remove item from cart | `cart_item` |\n| `cart.empty` | Clear entire cart | (none) |\n| `cart.select` | Select a specific cart | `cart` |\n| `cart.create` | Create a new cart | (none) |\n| `cart.clone` | Duplicate a cart | `cart` |\n| `shipping.set` | Set shipping method | `shipping_method` |\n| `pricebook.set` | Set customer price book | `pricebook` |\n| `pricebook.clear` | Clear custom price book | (none) |\n| `outlet.set` | Set fulfillment outlet | `outlet` |\n| `outlet.clear` | Clear outlet selection | (none) |\n| `promotion.apply` | Apply promotion code | `code` |\n| `promotion.remove` | Remove a promotion | `promotion` |\n| `promotion.clear` | Clear all promotions | (none) |\n\n### Example: Add to cart\n\n\n```liquid\n\n{% before %}\n  {% assign product_id = current_request.params.product_id %}\n  {% assign quantity = current_request.params.quantity | default: 1 | times: 1 %}\n\n  {% params product_id: product_id, quantity: quantity %}\n\n  {% if product_id == blank or quantity \u003c 1 %}\n    {% respond body: '{\"error\": \"invalid input\"}', status: 400, layout: false %}\n  {% endif %}\n  \n  {% action \"cart.add\", product_identifier: product_id, quantity: quantity %}\n{% endbefore %}\n\n{% after %}\n  {% redirect to: current_store.cart_path, notice: \"Item added to cart\" %}\n{% endafter %}\n```\n\n\n### Example: Apply promotion\n\n\n```liquid\n\n{% before %}\n  {% assign code = current_request.params.promo_code %}\n\n  {% params code: code %}\n\n  {% if code != blank %}\n    {% action \"promotion.apply\", code: code %}\n  {% endif %}\n{% endbefore %}\n\n{% after %}\n  {%- new Map result -%}\n  {%- assign result = result | set_key: \"cart_total\", current_cart.totals.subtotal -%}\n  {%- assign result = result | set_key: \"promotion_count\", current_cart.promotions.size -%}\n  {% respond body: result | json, status: 200, layout: false %}\n{% endafter %}\n```\n\n\n## Sending responses\n\nUse `{% respond %}` to send a custom response (instead of rendering the page template).\n\n### HTML response\n\n\n```liquid\n\n{% respond body: \"\u003ch1\u003eSuccess!\u003c/h1\u003e\", status: 200 %}\n```\n\n\n### JSON response\n\n\n```liquid\n\n{% before %}\n  {% params product_id: current_request.params.product_id %}\n{% endbefore %}\n\n{% after %}\n  {%- new Map result -%}\n  {%- assign result = result | set_key: \"success\", true -%}\n  {%- assign result = result | set_key: \"product_id\", current_request.params.product_id -%}\n  {%- assign response_body = result | json -%}\n  {% respond body: response_body, status: 200, layout: false %}\n{% endafter %}\n```\n\n\n**Key options:**\n- `body:` — the response content (string)\n- `status:` — HTTP status code. Defaults to `302`, so always set it explicitly when returning a body\n- `layout:` — include theme layout? (default true; use false for JSON/API responses)\n- `notice:` — flash message (positive)\n- `alert:` — flash message (negative)\n\n### Error response\n\n\n```liquid\n\n{% before %}\n  {% assign quantity = current_request.params.quantity | default: 1 | times: 1 %}\n\n  {% params quantity: quantity %}\n\n  {% if quantity \u003e 50 %}\n    {%- new Map error -%}\n    {%- assign error = error | set_key: \"error\", \"Quantity cannot exceed 50\" -%}\n    {% respond body: error | json, status: 422, layout: false %}\n  {% endif %}\n{% endbefore %}\n```\n\n\n## Redirects\n\nUse `{% redirect %}` to send the browser to a different URL.\n\n### Simple redirect\n\n\n```liquid\n\n{% after %}\n  {% redirect to: current_store.cart_path %}\n{% endafter %}\n```\n\n\n### Redirect with flash message\n\n\n```liquid\n\n{% after %}\n  {% redirect to: current_store.account_path, notice: \"Changes saved successfully\" %}\n{% endafter %}\n```\n\n\n### Conditional redirect\n\n\n```liquid\n\n{% before %}\n  {% params customer_id: current_request.params.customer_id %}\n{% endbefore %}\n\n{% after %}\n  {% if current_request.params.customer_id == blank %}\n    {% redirect to: current_store.account_login_path, alert: \"Please log in first\" %}\n  {% else %}\n    {% redirect to: current_store.account_path %}\n  {% endif %}\n{% endafter %}\n```\n\n\n### Difference from respond\n\n- **`{% redirect %}`** — sends a 302 redirect; the browser fetches a new page\n- **`{% respond %}`** — returns content directly; the page does not change in the browser's address bar\n\nUse redirects for state changes (after adding to cart, updating profile). Use responses for API endpoints that return data.\n\n## Complete examples\n\n### Add-to-cart form handler\n\n\n```liquid\n\n{% before %}\n  {%- comment -%}\n    Extract and validate input\n  {%- endcomment -%}\n  {% assign product_id = current_request.params.product_id %}\n  {% assign quantity = current_request.params.quantity | default: 1 | times: 1 %}\n\n  {% params product_id: product_id, quantity: quantity %}\n\n  {% if product_id == blank %}\n    {% redirect to: current_store.products_path, alert: \"Product not found\" %}\n  {% endif %}\n  \n  {% if quantity \u003c 1 or quantity \u003e 100 %}\n    {% redirect to: current_store.product_path, alert: \"Invalid quantity\" %}\n  {% endif %}\n  \n  {%- comment -%}\n    Execute the action\n  {%- endcomment -%}\n  {% action \"cart.add\", product_identifier: product_id, quantity: quantity %}\n{% endbefore %}\n\n{%- comment -%}\n  Redirect after success\n{%- endcomment -%}\n{% after %}\n  {% redirect to: current_store.cart_path, notice: \"Item added to cart\" %}\n{% endafter %}\n```\n\n\n### AJAX endpoint for cart status\n\n\n```liquid\n\n{% before %}\n  {%- comment -%}\n    No parameters needed; just return cart state\n  {%- endcomment -%}\n{% endbefore %}\n\n{% after %}\n  {%- new Map response -%}\n  {%- assign response = response | set_key: \"item_count\", current_cart.items.size -%}\n  {%- assign response = response | set_key: \"subtotal\", current_cart.totals.subtotal -%}\n  {%- assign response = response | set_key: \"currency\", current_store.currency -%}\n  {% respond body: response | json, status: 200, layout: false %}\n{% endafter %}\n```\n\n\n### Promotion code validator\n\n\n```liquid\n\n{% before %}\n  {% assign code = current_request.params.code %}\n\n  {% params code: code %}\n\n  {% if code == blank %}\n    {% respond body: '{\"valid\": false, \"message\": \"Code required\"}', status: 400, layout: false %}\n  {% endif %}\n  \n  {% action \"promotion.apply\", code: code %}\n{% endbefore %}\n\n{% after %}\n  {%- new Map result -%}\n  {%- assign promos_applied = current_cart.promotions.size -%}\n  {%- if promos_applied \u003e 0 -%}\n    {%- assign result = result | set_key: \"valid\", true -%}\n    {%- assign result = result | set_key: \"discount\", current_cart.totals.discount_amount -%}\n  {%- else -%}\n    {%- assign result = result | set_key: \"valid\", false -%}\n    {%- assign result = result | set_key: \"message\", \"Promotion code not found or expired\" -%}\n  {%- endif -%}\n  {% respond body: result | json, status: 200, layout: false %}\n{% endafter %}\n```\n\n\n## Common patterns\n\n### Guard clause for authentication\n\n\n```liquid\n\n{% before %}\n  {% if current_customer == blank %}\n    {% redirect to: current_store.account_login_path %}\n  {% endif %}\n{% endbefore %}\n```\n\n\n### Type conversion\n\n\n```liquid\n\n{% before %}\n  {% params id: current_request.params.id | times: 1 %}\n  {%- comment -%} string → number {%- endcomment -%}\n{% endbefore %}\n```\n\n\n### Default values\n\n\n```liquid\n\n{% before %}\n  {% params sort: current_request.params.sort | default: \"name\" %}\n  {% params limit: current_request.params.limit | default: 20 | times: 1 %}\n{% endbefore %}\n```\n\n\n### Whitelist allowed values\n\n\n```liquid\n\n{% before %}\n  {% params mode: current_request.params.mode %}\n  \n  {% unless mode == \"grid\" or mode == \"list\" %}\n    {% assign mode = \"grid\" %}\n  {% endunless %}\n  \n  {% variables view_mode: mode %}\n{% endbefore %}\n```\n\n\n## Security considerations\n\n- **Always validate** — treat all user input (query strings, form posts, request headers) as untrusted\n- **Use built-in actions** — don't construct custom business logic if a built-in action exists\n- **Scope to customer** — when reading/writing customer data, filter by `current_customer`\n- **Never expose IDs in responses** — don't leak record IDs, counts, or internal structure to the client unless intentional\n- **Use HTTPS only** — controllers over HTTP expose parameters in logs and network traces"}