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

  1. Your manifest names the extension points your app uses: what it adds, what it hides, and which actions it hooks.
  2. 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_url in a dialog or, without one, is handled by your worker.
  3. 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 before and after anchors 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's back, next and create actions never are;
  • as a hook target, for page actions and bulk actions marked hookable (phases before and after). 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

TypeWhat your app can addExample key
Sidebar and groupsA navigation item, at the top level or in a groupweb.sidebar, web.sidebar.stock-inventory
ChipsA chip in a page's links row, linking to one of your pagesweb.items.list.links
Page actionsA button among a page's actionsweb.items.view.actions
Sub-actionsAn entry in an action's menu, such as the items list's Import menuweb.items.list.actions.import
Bulk actionsAn action on a table's selected rowsweb.items.list.table.bulk-actions
Table columnsA column whose values your worker providesweb.items.list.table.columns
TabsA tab in a page's tab setweb.items.view.tabs
SectionsA section on a page, alongside Stok.ly's ownweb.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 core details section 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 a before hook 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:

CommandDescription
toastShow a short notification.
openDialogOpen one of your URLs in a dialog.
closeDialogClose the dialog your frame is shown in.
navigateGo to one of your pages, or to a Stok.ly page.
refreshRefresh the table an action came from, or reload the current page.
setQueryUpdate the query parameters in the address bar from one of your pages.
setTitleSet the heading and browser tab title of one of your pages.
chooseLet the user pick sale orders, items, purchase orders, suppliers, locations or customers, and get their ids.
setStatePublish your frame's current state, so your worker's hooks can read it.
setCreateStatePublish state for a create page, so hooks on its create action can read it.
setDirtyTell Stok.ly your frame has unsaved changes, so leaving the page prompts.
getSessionTokenGet a signed token your backend can verify to know the user and account.
resizeSet your frame's height. The SDK does this for you by default.

See the App Bridge page for details, events and limits.

Platform support

PlatformKey prefixStatus
Webweb.Available
Stock appwarehouse.Planned
ePOSepos.Planned


Did this page help you?