Extension points
Extension points are the places in Stok.ly's applications where your app can add its own UI, hide Stok.ly's, or hook into an existing action. Each one is defined around a concept, such as the tabs on an item's page or the bulk actions on the items list, rather than a pixel position. That lets Stok.ly evolve the surrounding page without breaking the apps built against it.
Extension points become part of the public API as soon as an app is built against them. New ones are added over time, but treat the keys of published ones as stable.
How extension points work
- Your manifest names the extension points your app uses: what it adds, what it hides, and which actions it hooks.
- When a user opens a page, Stok.ly adds your extensions to the matching places. Pages, tabs, sections and dialogs render your URL in a sandboxed frame. Sidebar items, chips, actions and columns are drawn by Stok.ly itself. When a user clicks one of your actions, it either opens your
content_urlin a dialog or, without one, is handled by your worker. - Your frames, and your app's hidden worker, talk to Stok.ly through the App Bridge SDK. They receive events (an action clicked, column values needed, a hook running) and send commands back: show a toast, open a dialog, navigate, refresh.
Keys
Every extension point has a key such as web.items.view.tabs. It starts with the application (web for the webapp), followed by the area (usually the entity, such as items), then usually the page (list, view or create), and then the place on it. Segments are lowercase letters, digits and hyphens. Elements inside an extension point have keys one segment longer, for example the item's Inventory tab is web.items.view.tabs.inventory. You use those child keys in three ways:
- as
beforeandafteranchors when you position your extension (you give just the last segment, e.g.after: "inventory"). Set at most one of the two. With no anchor, or an anchor that isn't on the page (another app may have hidden it), your extension goes at the end. Extensions sharing an anchor keep their manifest order. Table columns take no anchor and are always appended; - in
removeElements, to hide the element, if the reference marks it removable. Create-page steps, a table's bulk actions and columns containers, and a stepped create page'sback,nextandcreateactions never are; - as a hook
target, for page actions and bulk actions marked hookable (phasesbeforeandafter). See Hooks.
The extension point reference lists every published key, its type, and whether each element is removable or hookable. It's generated from the webapp itself on each release, so it's always current.
Types of extension point
| Type | What your app can add | Example key |
|---|---|---|
| Sidebar and groups | A navigation item, at the top level or in a group | web.sidebar, web.sidebar.stock-inventory |
| Chips | A chip in a page's links row, linking to one of your pages | web.items.list.links |
| Page actions | A button among a page's actions | web.items.view.actions |
| Sub-actions | An entry in an action's menu, such as the items list's Import menu | web.items.list.actions.import |
| Bulk actions | An action on a table's selected rows | web.items.list.table.bulk-actions |
| Table columns | A column whose values your worker provides | web.items.list.table.columns |
| Tabs | A tab in a page's tab set | web.items.view.tabs |
| Sections | A section on a page, alongside Stok.ly's own | web.items.sections |
Apps can't add sidebar groups, and can't remove the sidebar itself or its Account Management group (which holds Users, Apps and Roles). Tab sets are currently on the item page and on your app's own page. Create-page steps (web.<entity>.create.steps.<step>) can't be added or removed, but you can place sections in them with parent.
Sections are keyed by the entity, not by a tab: web.items.sections (sections such as details, selling and images), web.item-types.sections (details) and web.item-attributes.sections (details, basic-configuration). Customers (web.customers.sections), customer groups (web.customer-groups.sections), sale orders (web.sale-orders.sections, plus web.sale-orders.sidebar for the narrow column beside it, with blocks summary, invoices, shipments, returns, notes and details), sale order invoices (web.sale-order-invoices.sections) and channels (web.channels.sections) work the same way; the reference lists every container and section key. Where an entity's create page has sections, the same container serves both its view and create pages. To choose which tab or step a section goes in, use the manifest's parent field; the reference lists each entity's tabs and steps, and not every entity has them. The old tab-scoped section keys have no alias, so manifests using them must be updated and republished.
Create pages have their own actions container, web.<entity>.create.actions. Stepped create pages have back, next and create (for example web.item-types.create.actions); single-page ones, such as items and sale orders, have only create. A stepped page's actions can't be removed, but a single-page create can (the reference marks which). create is hookable.
The sale order view, in both its classic and its newer layout, has the same web.sale-orders.view.actions, web.sale-orders.sections and web.sale-orders.sidebar points. Its items table has a bulk actions container, web.sale-orders.view.items.bulk-actions, and a columns container, web.sale-orders.view.items.columns. A bulk action's recordIds are the order lines' ids (and its context is { saleOrderId }), and the core actions cancel, process, collect, add-to-purchase-order and manufacture can be removed. Items cancel is hookable: hook.before gets the selected recordIds, and hook.after gets recordIds and failedRecordIds.
The sale order view's Save is hooked like a create action: its before hook gets state and, if your release has sales.orders.read, a draft of the unsaved changes:
{
state: object; // what your frames published with setCreateState
draft: {
items: { lineId: string, referenceId: string, quantity: number, status: string }[];
removedLineIds: string[];
};
}Hold, unhold and accept quote are hookable too, as are Submit on a purchase order, Dispatch on a goods out note and Create on a supplier (whose after context has the new supplierId). The goods receipt create page's sections, and its create hooks, receive purchaseOrderIds: the purchase orders the receipt is being created against.
Your app can also add full pages at /apps/<appId>/<route>, which don't belong to any extension point.
Your app's own page
Once an account installs your app, it appears under Account Management > Apps, and its page there has a Details tab and a Marketplace tab showing your listing. This is the natural home for your app's settings. You can add:
- tabs, with
web.passport.apps.view.tabs(for example a Settings tab,after: "details"); - sections, with
web.passport.apps.sections(the coredetailssection is locked); - page actions, with
web.passport.apps.view.actions(for example "Test connection", handled by your worker).
These extensions only ever show on your own app's page, never on another app's. Nothing on this page can be removed, so a manifest that lists one of its elements in removeElements (such as web.passport.apps.view.actions.uninstall) is rejected. Frames here receive no record context: your app already knows its own id, and the account comes from the session token. The Save and Delete actions are hookable before and after, and Uninstall after only, so nothing can block an uninstall. Their hooks receive { appId }; check it matches your app before acting. The app being uninstalled is removed before the after hook runs, so it isn't called itself.
Hooks
A hook calls your worker before an action runs, or after it succeeds. A before hook can cancel the action, and the user sees your message; on a bulk action it can skip individual rows instead. An after hook isn't awaited. Hooks can only read and cancel: they can't change what's being saved. Hooks need a worker_url in your manifest. They fail open: if your worker doesn't answer within 5 seconds (before) or 30 seconds (after), the action goes ahead and the user sees a warning. See hooks in the manifest and in the App Bridge.
Hooks only run in the Stok.ly web UI. Changes made through the API, CSV imports, integrations or scheduled rules never trigger them, so don't rely on abeforehook as your only validation.
What isn't extendable
- Users and roles pages, by policy: no extension points and no hooks.
- The Apps list and create pages under Account Management (only an installed app's own page is extendable).
- Legacy pages, such as the invoice create pages opened from a single purchase order, manufacturing run or sale order.
- Dialogs, per-row actions (such as Quick View), components that hold credentials (channel, courier and payment settings), and shared editors inside a page, such as an order's payments or the rows of its items (the items table only offers bulk actions and columns).
Data your app receives
Frames and actions only receive references, never record data. For example, a tab on the item page receives the item's itemId, and a bulk action receives the selected rows' ids. The exceptions are hooks on create actions and on the sale order view's Save: if your app has the entity's read scope, the before hook includes the draft and a create's after hook includes the created record, with secrets left out. Your app fetches the data it needs through the Stok.ly API with the access it was granted at install, and scopes are enforced there. See How your frames get their data.
Third Party locations are hidden from location pickers and lists by default.
What your app can do
Through the App Bridge, your frames and worker can:
| Command | Description |
|---|---|
toast | Show a short notification. |
openDialog | Open one of your URLs in a dialog. |
closeDialog | Close the dialog your frame is shown in. |
navigate | Go to one of your pages, or to a Stok.ly page. |
refresh | Refresh the table an action came from, or reload the current page. |
setQuery | Update the query parameters in the address bar from one of your pages. |
setTitle | Set the heading and browser tab title of one of your pages. |
choose | Let the user pick sale orders, items, purchase orders, suppliers, locations or customers, and get their ids. |
setState | Publish your frame's current state, so your worker's hooks can read it. |
setCreateState | Publish state for a create page, so hooks on its create action can read it. |
setDirty | Tell Stok.ly your frame has unsaved changes, so leaving the page prompts. |
getSessionToken | Get a signed token your backend can verify to know the user and account. |
resize | Set your frame's height. The SDK does this for you by default. |
See the App Bridge page for details, events and limits.
Platform support
| Platform | Key prefix | Status |
|---|---|---|
| Web | web. | Available |
| Stock app | warehouse. | Planned |
| ePOS | epos. | Planned |
Updated about 7 hours ago
