{"title":"Liquid controllers guide","slug":"liquid-controllers-guide","url":"https://support.storeconnect.com/articles/liquid-controllers-guide","url_markdown":"https://support.storeconnect.com/articles/liquid-controllers-guide.md","subtitle":null,"summary":"How to use Liquid controllers in StoreConnect themes, covering the three execution phases (before, after, final), the action tags available inside each phase, and common patterns for cart operations, redirects, custom responses, and async API calls.","type":"Developer_Documentation","video_url":"","keywords":"liquid controllers, before phase, after phase, final phase, controller lifecycle, params tag, variables tag, redirect tag, respond tag, update tag, action tag, cart actions, controller patterns, theme controllers","last_modified":"2026-08-21T07:12:35+0000","body_markdown":"Liquid controllers add server-side logic to your StoreConnect theme pages. They live in the `controllers/` directory of your theme and run in three ordered phases around the normal page request. This article explains those phases and the action tags you use inside them.\n\nFor a list of all controller routes (HTTP verbs and URL paths), see [Liquid controllers](liquid-controllers).\n\n## How controllers work\n\nWhen a page is requested, StoreConnect matches the URL to a controller and action. The system runs the corresponding controller template in three phases:\n\n1. **`{% before %}`** — runs before the page renders. Use for data preparation, input validation, cart operations, and early redirects.\n2. **`{% after %}`** — runs after the main action. Use for redirects based on results, custom JSON responses, and post-processing.\n3. **`{% final %}`** — runs after the HTTP response is sent to the browser. Use for fire-and-forget work like analytics calls and logging. Any `{% api %}` call inside `{% final %}` is automatically asynchronous.\n\nNot every controller needs all three phases — include only the ones you need.\n\n\n```liquid\n\n{% before %}\n  {% params product_id: current_request.params.id %}\n  {% variables page_title: \"Product detail\" %}\n{% endbefore %}\n\n{% after %}\n  {% if some_condition %}\n    {% redirect to: \"/cart\" %}\n  {% endif %}\n{% endafter %}\n\n{% final %}\n  {%- new Map payload -%}\n  {%- assign payload = payload | set_key: \"event\", \"page_view\" -%}\n  {% api url: \"https://analytics.example.com/track\", method: \"post\", data: payload %}\n  {% endapi %}\n{% endfinal %}\n```\n\n\n## Action tags\n\nAction tags are only valid inside controller phase blocks. Using them outside a phase block has no effect.\n\n### `{% params %}`\n\nReads values from the incoming request and makes them available as controller parameters for the current action.\n\n\n```liquid\n\n{% before %}\n  {% params\n    product_id: current_request.params.product_id,\n    quantity: current_request.params.qty | default: 1\n  %}\n{% endbefore %}\n```\n\n\n### `{% variables %}`\n\nSets template variables that are accessible in the page template.\n\n\n```liquid\n\n{% before %}\n  {% variables\n    page_title: \"Featured products\",\n    show_sidebar: true,\n    max_items: 12\n  %}\n{% endbefore %}\n```\n\n\n### `{% redirect %}`\n\nRedirects to a different URL and stops further controller execution. All subsequent phases are skipped.\n\n\n```liquid\n\n{% before %}\n  {% unless current_customer %}\n    {% redirect to: \"/auth/sign_in\", alert: \"Please log in to continue\" %}\n  {% endunless %}\n{% endbefore %}\n\n{% after %}\n  {% redirect to: \"/cart\", notice: \"Item added to your cart\" %}\n{% endafter %}\n```\n\n\n| Option | Description |\n|--------|-------------|\n| `to` | URL path to redirect to (required) |\n| `notice` | Flash notice message displayed after redirect |\n| `alert` | Flash alert message displayed after redirect |\n| `status` | HTTP status code (default: `302`) |\n\n### `{% respond %}`\n\nSends a custom HTTP response and stops normal page rendering. Used for AJAX endpoints and API-style responses.\n\n\n```liquid\n\n{% after %}\n  {%- new Map result -%}\n  {%- assign result = result | set_key: \"success\", true | set_key: \"count\", current_cart.item_count -%}\n  {%- assign body = result | json -%}\n  {% respond status: 200, body: body %}\n{% endafter %}\n```\n\n\n| Option | Description |\n|--------|-------------|\n| `status` | HTTP status code (required) |\n| `body` | Response body as a string |\n| `json` | Response body as a JSON object (alternative to `body`) |\n\n### `{% update %}`\n\nUpdates a custom data field on a database object. The field name uses the platform's internal identifier format.\n\n\n```liquid\n\n{% before %}\n  {%- assign new_count = current_product.data.view_count__c | plus: 1 -%}\n  {% update current_product, field: \"view_count__c\", value: new_count %}\n{% endbefore %}\n```\n\n\n### `{% action %}`\n\nExecutes a named platform action. Actions cover cart management, shipping, pricebooks, and promotions.\n\n#### Cart actions\n\n| Action | Parameters | Description |\n|--------|-----------|-------------|\n| `cart.add` | `product_identifier`, `quantity`, `variant_id` | Add a product to the cart |\n| `cart.update` | `cart_item_id`, `quantity` | Update an item's quantity |\n| `cart.remove` | `cart_item_id` | Remove an item from the cart |\n| `cart.empty` | (none) | Clear all items from the cart |\n| `cart.select` | `identifier` | Switch to a specific cart |\n| `cart.clone` | `cart_id` | Clone an existing cart |\n\n#### Shipping actions\n\n| Action | Parameters | Description |\n|--------|-----------|-------------|\n| `shipping.set` | `address_id`, `method` | Set the shipping address and method |\n\n#### Pricebook actions\n\n| Action | Parameters | Description |\n|--------|-----------|-------------|\n| `pricebook.set` | `pricebook_id` | Activate a specific pricebook |\n| `pricebook.clear` | (none) | Clear the active pricebook |\n\n#### Promotion actions\n\n| Action | Parameters | Description |\n|--------|-----------|-------------|\n| `promotion.apply` | `code` | Apply a promotion code |\n| `promotion.remove` | `code` | Remove a promotion code |\n| `promotion.clear` | (none) | Remove all promotions |\n\n## Common patterns\n\n### Add to cart\n\n\n```liquid\n\n{% before %}\n  {% action \"cart.add\",\n    product_identifier: current_request.params.product_id,\n    quantity: current_request.params.quantity | default: 1,\n    variant_id: current_request.params.variant_id\n  %}\n{% endbefore %}\n\n{% after %}\n  {% redirect to: \"/cart\", notice: \"Added to cart\" %}\n{% endafter %}\n```\n\n\n### Apply a promotion code\n\n\n```liquid\n\n{% before %}\n  {% action \"promotion.apply\", code: current_request.params.promo_code %}\n{% endbefore %}\n\n{% after %}\n  {% redirect to: \"/cart\" %}\n{% endafter %}\n```\n\n\n### Return JSON from a controller\n\n\n```liquid\n\n{% after %}\n  {%- new Map response -%}\n  {%- assign response = response | set_key: \"cart_count\", current_cart.item_count -%}\n  {%- assign json_body = response | json -%}\n  {% respond status: 200, body: json_body %}\n{% endafter %}\n```\n\n\n### Async analytics in `{% final %}`\n\nAPI calls inside `{% final %}` are always asynchronous — the response is sent to the browser before the call completes, so no `response` data is available.\n\n\n```liquid\n\n{% final %}\n  {%- new Map event -%}\n  {%- assign event = event | set_key: \"type\", \"add_to_cart\" | set_key: \"product\", current_product.id -%}\n  {% api url: \"https://analytics.example.com/events\", method: \"post\", data: event %}\n  {% endapi %}\n{% endfinal %}\n```\n\n\n## Controller helpers\n\nThe `helpers/` directory contains shared Liquid logic for controllers. Include a helper with `{% render %}` inside a phase block:\n\n\n```liquid\n\n{% before %}\n  {% render \"helpers/delivery_options\" %}\n{% endbefore %}\n```\n\n\nHelpers work like snippets but are intended for controller-level logic rather than HTML output."}