{"title":"POS views","slug":"pos-views","url":"https://support.storeconnect.com/articles/pos-views","url_markdown":"https://support.storeconnect.com/articles/pos-views.md","subtitle":null,"summary":"Create and update POS views, the HTML templates that replace part of a POS screen with your own code. Covers the steps to add or change a view record, every place a view can be used, the Liquid context and globals available on a register, and where JavaScript runs and where it does not.","type":"Help_Documentation","video_url":"","keywords":"pos view, custom view, create pos view, edit pos view, pos liquid, liquidjs, html template, scaction, pos globals, current_register, current_staff, current_cart, query tag, pos javascript, custom screen, layout field view, modal view, nav view, product_action_card, identifier","last_modified":"2026-10-07T05:47:34+0000","body_markdown":"A **POS View** is a single HTML template that takes over part of a POS screen. It can contain\nHTML, CSS, JavaScript, and Liquid, so it's the option to reach for when action groups and\nlayouts can't produce the screen you need.\n\nEach view is one record with two fields that matter: an **Identifier** you refer to it by, and\nthe **HTML Template** itself.\n\n## Where you can use a view\n\nA view is either attached to a record through that record's **POS View** lookup field, or named\nby an action's parameters. What you attach it to decides how much of the screen it replaces and\nwhich Liquid variables it receives.\n\n| Attached to | What it replaces | What you get in Liquid |\n| --- | --- | --- |\n| **POS Layout** (`list` or `grid`) | The records list. The search bar, filters, filter chips, and action buttons still render around it | `records`, the record drops for the current page |\n| **POS Layout** (`record`) | The headline and field list inside the record card. The back button and actions still render | `record` |\n| **POS Layout Field** | How that one field renders, on a card or in a labeled row | `record` |\n| **POS Action Item** | The contents of the button, not what tapping it does | The item's params, plus `action_item`, plus `product` on a `cart:add_product` item |\n\nA layout view renders once for the whole screen, so on a `grid` layout your template draws the\nentire grid rather than one card. To render markup per record, attach the view to a **POS\nLayout Field** instead.\n\nOn a form layout, such as the customer form, a field view has to include a form element\n(`\u003cinput\u003e`, `\u003cselect\u003e`, `\u003ctextarea\u003e`) whose `name` attribute matches the field name. Without it\nthe value is not captured when the form is submitted.\n\nTwo actions take a view by identifier rather than through a lookup field:\n\n-   **`modal:open:view`** opens a view as a modal. Pass the identifier as the `view_identifier`\n    param. Any other params become Liquid variables, alongside the context carried over from an\n    earlier action in the chain.\n-   **`nav:view`** opens a view as a full-page screen. Pass `view` (the identifier) or\n    `view_sc_id`. Supply `record_id` and `object_name` together to get a `record` variable.\n\nThe order confirmation screen and the quick search panel are layouts too, so you customize\nthem by attaching a view to those layouts rather than through any separate mechanism. The\norder confirmation view also gets an `outcome_message` variable alongside `record`.\n\nLayout views and view pages don't scroll on their own, so wrap your content in an overflow\ncontainer.\n\n### Reserved identifiers\n\nOne identifier changes behavior by name alone. A view saved with the **Identifier**\n`product_action_card` replaces the default card for every `cart:add_product` action item that\nhas no view of its own, and receives the item's params, `action_item`, and `product`.\n\n## Create a view\n\n:::tip\nIf you are not familiar with using frontend code, you can ask an AI agent to do this work for you, and request design changes in plain language. To use this method for creating views, see [build in POS using AI agents](pos-with-ai-agents).\n:::\n\nThe following describes how to manually create a POS view record and attach it to one of the records in the table above.\n\n### Before you start\n\n- You need Salesforce admin access to create **POS View** records, and to edit the **POS\n  Layout**, **POS Layout Field**, or **POS Action Item** record you will attach it to.\n- Decide where the view will be used. That choice sets which Liquid variables your template\n  receives, so it's worth settling before you write any markup.\n\n1.  Go to the **POS Views** list and select **New**.\n2.  Enter a **POS View Name**. This is the internal name administrators see in Salesforce, not\n    anything staff see on a register.\n3.  Enter an **Identifier** if you will refer to the view by name: from another view with\n    `{% render %}`, from a `modal:open:view` or `nav:view` action, or to claim a reserved identifier\n    such as `product_action_card`. A view attached only through a lookup field doesn't need one.\n4.  Enter the **HTML Template**. The record won't save while this is empty, and the field holds\n    up to 131,072 characters.\n5.  Save the record.\n6.  Open the **POS Layout**, **POS Layout Field**, or **POS Action Item** record the view\n    belongs to, and set its **POS View** lookup field. Skip this step for a view you open by\n    identifier from a `modal:open:view` or `nav:view` action.\n7.  On a register, reload the POS.\n\n:::warning\nSalesforce does not enforce uniqueness on **Identifier**. If two views share one, the POS takes\nwhichever it finds first, and which that is isn't predictable. Search the **POS Views** list for\nan identifier before reusing it.\n:::\n\nThe screen you attached the view to now renders your template in place of its default. If it\nstill shows the default, the register hasn't picked up the new record yet. Sync the device from\n**Settings** then **Manage data**, described in [Manage POS data](manage-pos-data-sync).\n\n## Update a view\n\nUse this process to change a view that is already attached. Editing the **HTML Template** takes\neffect wherever the view is used, so a shared view changes every screen that renders it.\n\n1.  Go to the **POS Views** list and open the view you want to change. The list shows\n    **Identifier** alongside the name, and you can search on either.\n2.  Edit the **HTML Template**.\n3.  Save the record.\n4.  On a register, reload the POS.\n\nThe register now renders your edited template.\n\nTo see what a view is attached to before you change it, open the view and check its **POS Layout\nFields** and **POS Action Items** related lists. Layouts that use the view are not among them,\nso check those by filtering the **POS Layouts** list on its **POS View** field.\n\nFor a worked example that builds a view and wires it to a layout field from scratch, see\n[Add custom fields to the End Shift form](end-shift-form-layout).\n\n## Liquid on a register\n\nThe POS runs LiquidJS in the register's browser, not the Liquid engine your storefront theme\nuses. The syntax is the same and standard filters like `upcase` and `date` work, but the\nStoreConnect-specific set is much smaller.\n\n### Globals that work\n\nThese twelve globals are available in any view:\n\n| Global | What it gives you |\n| --- | --- |\n| `current_store` | The store |\n| `current_outlet` | The outlet the register belongs to |\n| `current_register` | The register |\n| `current_staff` | The signed-in POS user |\n| `current_cart` | The cart open on the register |\n| `current_customer` | The customer on the current cart |\n| `current_account` | The account on the current cart |\n| `current_order` | The current order |\n| `current_product` | The product in context |\n| `current_pricebook` | The price book in use |\n| `all_products` | The purchasable products synced to this device |\n| `store_variables` | Declared, but currently raises. See the note below |\n\n### Globals that raise an error\n\nThirty storefront globals are declared in the POS but not implemented, and calling one throws\nrather than rendering empty. The ones most likely to be reached for out of habit are\n`current_request`, `current_page`, `current_search`, `current_product_category`,\n`theme_variables`, `session_variables`, `all_pages`, `all_menus`, `all_media`, and\n`all_content_blocks`.\n\nIf a view renders blank or the screen errors, an unimplemented global is the first thing to\ncheck.\n\n:::warning\n`store_variables` is declared as a global but does not work in a POS view. Reading it raises, and a\nview whose template raises renders as an empty box with no error on screen. Put the values your\ntemplate needs in `{% assign %}` statements at the top of it instead. Store variables that configure\nPOS behavior, such as the `pos.payment_options` keys, are unaffected: the app reads those directly\nrather than through Liquid.\n:::\n\n### Extra filters and tags\n\nOn top of the standard LiquidJS set, the POS registers these:\n\n-   **`money`** formats a number as currency.\n-   **`keys`, `merge`, `set_key`, `unset_key`, `collect_keys`, `rename_keys`** work on maps,\n    and mirror the map filters on the web storefront.\n-   **`serialize` and `deserialize`** convert between a value and a string, which is how you\n    pass structured data into a `data-` attribute for your JavaScript to read.\n-   **`{% query %}`** reads records from the device's local database.\n-   **`{% new %}`** creates a `Map` or a `List` in the template, optionally seeded from JSON:\n    `{% new Map totals %}` or `{% new List codes = '[\"a\", \"b\"]' %}`.\n\nStorefront filters that aren't in that list are not available on a register.\n\n## Reusing a view inside another view\n\nViews resolve each other by **Identifier**, so a shared piece of markup can live in its own\nview and be pulled into others. A rendered view gets its own scope, so pass in anything it\nneeds by name:\n\n\n```liquid\n\n{% render 'product_card_body', product: record, show_price: true %}\n```\n\n\nGlobals still reach the rendered view, but the calling template's own variables do not.\nThere's no folder structure, so use the identifier exactly as it appears on the record.\n\n## JavaScript in a view\n\nScript tags in a view do run, but not the way they would in an ordinary page. The POS renders\nyour template, then finds each `\u003cscript\u003e` and executes it. Inline code and `src` scripts both\nwork, and three consequences are worth knowing:\n\n-   **Scripts run after the view is on the page.** `DOMContentLoaded` has already fired, so\n    don't wait for it. Your elements exist by the time your code runs.\n-   **Scripts run in global scope.** They're not scoped to the view.\n-   **Scripts run again when the view re-renders.** A top-level `const` or `let` throws on the\n    second run because the name is already declared. Wrap your code in an immediately invoked\n    function, or hang what you need off `window`.\n\n```html\n\n\u003cscript\u003e\n  (function () {\n    const total = document.querySelector('#my-total')\n    // your code here\n  })()\n\u003c/script\u003e\n```\n\nThe exception is a field view on a `list` or `grid` layout. Those templates are rendered in one\nbatch for every visible record and injected as HTML, and injected HTML never runs its scripts.\nTo get code onto a list or grid screen, put the script in a view attached to the layout itself\nrather than to one of its fields.\n\nYour JavaScript can drive the POS through the `scAction` function, which calls any built-in\naction such as adding to the cart, starting checkout, printing a receipt, or saving a record.\nSee [scAction](pos-sc-action) for the syntax and the\n[POS actions reference](pos-actions-reference) for every action and its parameters.\n\nTo call something outside Salesforce from inside a view, see\n[Bring external data into POS](bring-external-data-into-pos).\n\n## What a view can't do\n\n-   **It can't reach data that hasn't synced.** A view runs against the device's local\n    database, so it sees what the POS has already downloaded, not whatever is in Salesforce\n    right now.\n-   **It can't run Apex.** Server-side logic has to be reached over HTTP like any other\n    external call.\n-   **It can't be scoped to one store.** Like every POS configuration record, a view is\n    org-wide, so two stores in the same Salesforce org share it. Branch inside the template\n    on `current_store` or `current_outlet` where behavior needs to differ.\n\nIf a view renders blank after a reload, open the browser console on the register before changing\nanything else. An unimplemented global or a Liquid syntax error reports there, and names the\nline it failed on."}