POS views
On this page
A POS View is a single HTML template that takes over part of a POS screen. It can contain HTML, CSS, JavaScript, and Liquid, so it’s the option to reach for when action groups and layouts can’t produce the screen you need.
Each view is one record with two fields that matter: an Identifier you refer to it by, and the HTML Template itself.
Where you can use a view
A view is either attached to a record through that record’s POS View lookup field, or named by an action’s parameters. What you attach it to decides how much of the screen it replaces and which Liquid variables it receives.
| Attached to | What it replaces | What you get in Liquid |
|---|---|---|
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 |
POS Layout (record) |
The headline and field list inside the record card. The back button and actions still render | record |
| POS Layout Field | How that one field renders, on a card or in a labeled row | record |
| 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 |
A layout view renders once for the whole screen, so on a grid layout your template draws the
entire grid rather than one card. To render markup per record, attach the view to a POS
Layout Field instead.
On a form layout, such as the customer form, a field view has to include a form element
(<input>, <select>, <textarea>) whose name attribute matches the field name. Without it
the value is not captured when the form is submitted.
Two actions take a view by identifier rather than through a lookup field:
modal:open:viewopens a view as a modal. Pass the identifier as theview_identifierparam. Any other params become Liquid variables, alongside the context carried over from an earlier action in the chain.nav:viewopens a view as a full-page screen. Passview(the identifier) orview_sc_id. Supplyrecord_idandobject_nametogether to get arecordvariable.
The order confirmation screen and the quick search panel are layouts too, so you customize
them by attaching a view to those layouts rather than through any separate mechanism. The
order confirmation view also gets an outcome_message variable alongside record.
Layout views and view pages don’t scroll on their own, so wrap your content in an overflow container.
Reserved identifiers
One identifier changes behavior by name alone. A view saved with the Identifier
product_action_card replaces the default card for every cart:add_product action item that
has no view of its own, and receives the item’s params, action_item, and product.
Create a view
:::tip If 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. :::
The following describes how to manually create a POS view record and attach it to one of the records in the table above.
Before you start
- You need Salesforce admin access to create POS View records, and to edit the POS Layout, POS Layout Field, or POS Action Item record you will attach it to.
- Decide where the view will be used. That choice sets which Liquid variables your template receives, so it’s worth settling before you write any markup.
- Go to the POS Views list and select New.
- Enter a POS View Name. This is the internal name administrators see in Salesforce, not anything staff see on a register.
- Enter an Identifier if you will refer to the view by name: from another view with
{% render %}, from amodal:open:viewornav:viewaction, or to claim a reserved identifier such asproduct_action_card. A view attached only through a lookup field doesn’t need one. - Enter the HTML Template. The record won’t save while this is empty, and the field holds up to 131,072 characters.
- Save the record.
- Open the POS Layout, POS Layout Field, or POS Action Item record the view
belongs to, and set its POS View lookup field. Skip this step for a view you open by
identifier from a
modal:open:viewornav:viewaction. - On a register, reload the POS.
:::warning Salesforce does not enforce uniqueness on Identifier. If two views share one, the POS takes whichever it finds first, and which that is isn’t predictable. Search the POS Views list for an identifier before reusing it. :::
The screen you attached the view to now renders your template in place of its default. If it still shows the default, the register hasn’t picked up the new record yet. Sync the device from Settings then Manage data, described in Manage POS data.
Update a view
Use this process to change a view that is already attached. Editing the HTML Template takes effect wherever the view is used, so a shared view changes every screen that renders it.
- Go to the POS Views list and open the view you want to change. The list shows Identifier alongside the name, and you can search on either.
- Edit the HTML Template.
- Save the record.
- On a register, reload the POS.
The register now renders your edited template.
To see what a view is attached to before you change it, open the view and check its POS Layout Fields and POS Action Items related lists. Layouts that use the view are not among them, so check those by filtering the POS Layouts list on its POS View field.
For a worked example that builds a view and wires it to a layout field from scratch, see Add custom fields to the End Shift form.
Liquid on a register
The POS runs LiquidJS in the register’s browser, not the Liquid engine your storefront theme
uses. The syntax is the same and standard filters like upcase and date work, but the
StoreConnect-specific set is much smaller.
Globals that work
These twelve globals are available in any view:
| Global | What it gives you |
|---|---|
current_store |
The store |
current_outlet |
The outlet the register belongs to |
current_register |
The register |
current_staff |
The signed-in POS user |
current_cart |
The cart open on the register |
current_customer |
The customer on the current cart |
current_account |
The account on the current cart |
current_order |
The current order |
current_product |
The product in context |
current_pricebook |
The price book in use |
all_products |
The purchasable products synced to this device |
store_variables |
Declared, but currently raises. See the note below |
Globals that raise an error
Thirty storefront globals are declared in the POS but not implemented, and calling one throws
rather than rendering empty. The ones most likely to be reached for out of habit are
current_request, current_page, current_search, current_product_category,
theme_variables, session_variables, all_pages, all_menus, all_media, and
all_content_blocks.
If a view renders blank or the screen errors, an unimplemented global is the first thing to check.
:::warning
store_variables is declared as a global but does not work in a POS view. Reading it raises, and a
view whose template raises renders as an empty box with no error on screen. Put the values your
template needs in {% assign %} statements at the top of it instead. Store variables that configure
POS behavior, such as the pos.payment_options keys, are unaffected: the app reads those directly
rather than through Liquid.
:::
Extra filters and tags
On top of the standard LiquidJS set, the POS registers these:
moneyformats a number as currency.keys,merge,set_key,unset_key,collect_keys,rename_keyswork on maps, and mirror the map filters on the web storefront.serializeanddeserializeconvert between a value and a string, which is how you pass structured data into adata-attribute for your JavaScript to read.{% query %}reads records from the device’s local database.{% new %}creates aMapor aListin the template, optionally seeded from JSON:{% new Map totals %}or{% new List codes = '["a", "b"]' %}.
Storefront filters that aren’t in that list are not available on a register.
Reusing a view inside another view
Views resolve each other by Identifier, so a shared piece of markup can live in its own view and be pulled into others. A rendered view gets its own scope, so pass in anything it needs by name:
```liquid
{% render ‘product_card_body’, product: record, show_price: true %} ```
Globals still reach the rendered view, but the calling template’s own variables do not. There’s no folder structure, so use the identifier exactly as it appears on the record.
JavaScript in a view
Script tags in a view do run, but not the way they would in an ordinary page. The POS renders
your template, then finds each <script> and executes it. Inline code and src scripts both
work, and three consequences are worth knowing:
- Scripts run after the view is on the page.
DOMContentLoadedhas already fired, so don’t wait for it. Your elements exist by the time your code runs. - Scripts run in global scope. They’re not scoped to the view.
- Scripts run again when the view re-renders. A top-level
constorletthrows on the second run because the name is already declared. Wrap your code in an immediately invoked function, or hang what you need offwindow.
```html
```
The exception is a field view on a list or grid layout. Those templates are rendered in one
batch for every visible record and injected as HTML, and injected HTML never runs its scripts.
To get code onto a list or grid screen, put the script in a view attached to the layout itself
rather than to one of its fields.
Your JavaScript can drive the POS through the scAction function, which calls any built-in
action such as adding to the cart, starting checkout, printing a receipt, or saving a record.
See scAction for the syntax and the
POS actions reference for every action and its parameters.
To call something outside Salesforce from inside a view, see Bring external data into POS.
What a view can’t do
- It can’t reach data that hasn’t synced. A view runs against the device’s local database, so it sees what the POS has already downloaded, not whatever is in Salesforce right now.
- It can’t run Apex. Server-side logic has to be reached over HTTP like any other external call.
- It can’t be scoped to one store. Like every POS configuration record, a view is
org-wide, so two stores in the same Salesforce org share it. Branch inside the template
on
current_storeorcurrent_outletwhere behavior needs to differ.
If a view renders blank after a reload, open the browser console on the register before changing anything else. An unimplemented global or a Liquid syntax error reports there, and names the line it failed on.
Was this article helpful?
Thanks for your feedback! It helps us improve our docs.