Build a plugin, step by step
A ShopPress plugin works like a WordPress plugin: a folder of files you install, activate and deactivate. It can do anything Shopify's Admin API allows for your store: add features, react to events, show widgets in your theme, add discounts, talk to other services. This guide builds one from scratch.
You need: ShopPress on the Builder or Studio plan, and the Plugins module switched on (Modules page).
1. Create the plugin
Open Plugins and scroll to New plugin. You get a starter package with every part already in place. Or upload a .zip (a folder with the files below). Your AI can do the same through MCP (plugin_save with files).
my-plugin/
plugin.json name, description, version, permissions, settings
index.js the entry module (tools, hooks, widgets, tests)
lib/products.js more code; import it from index.js
config.json data you can import
widgets/notice.html storefront widget HTML
widgets/notice.css storefront widget CSS
templates/*.liquid Liquid templates
README.md notes for you and other users
Allowed files are .js .mjs .ts .json .css .html .liquid .md .txt .svg. Limits: 100 files, 400 KB in total.
2. plugin.json: the plugin's header
{
"name": "Stock tools",
"description": "Low-stock report and alerts.",
"version": "1.0.0",
"main": "index.js",
"scopes": ["read_inventory"],
"http": ["hooks.slack.com"],
"settings": {
"threshold": { "type": "number", "label": "Alert at or below", "default": 3 },
"slackUrl": { "type": "secret", "label": "Slack webhook URL" }
}
}
| Field | Meaning |
|---|---|
name, description, version | Shown on the Plugins page and in the library |
main | Entry file (default index.js) |
scopes | Extra Shopify permissions; you grant them with one click. See Shopify API. |
optionalScopes | Permissions only some features need; the plugin runs without them |
http | Outside services the plugin may call with sp.http.fetch (you approve each host) |
settings | Builds the settings form. Types: string, number, boolean, select (with options), secret (encrypted) |
3. index.js: tools, hooks, widgets, tests
import { lowStock } from "./lib/stock.js";
import cfg from "./config.json";
// Hooks (like WordPress add_action): run when something happens
sp.on("inventory_levels/update", async (event, sp) => {
sp.log("stock changed for item", event.data.inventory_item_id);
});
// Filters (like add_filter): change a value
sp.filter("alert_text", (text) => "⚠️ " + text);
export default {
tools: {
low_stock: {
description: "Variants at or below the threshold.",
input: { limit: "number?" },
schedule: 60, // also run every 60 minutes
async run(input, sp) {
const items = lowStock(sp, sp.settings.threshold, input.limit || 50);
const text = await sp.applyFilters("alert_text", items.length + " items are low");
if (items.length) sp.flow.trigger("low stock", { text, items });
return items;
},
},
},
widgets: {
notice: { label: "Notice", html: "file:widgets/notice.html", css: "file:widgets/notice.css", js: "" },
},
tests: {
returns_list: { tool: "low_stock", input: { limit: 5 }, check: (r) => Array.isArray(r) || "expected a list" },
},
};
- Tools are actions: run by hand, by your AI, on a
schedule, on Shopify events (on: [...]), from Flow (flow: true) or the storefront (public: true, read-only). - Hooks:
sp.on(event, fn, priority). Shopify events (products/create,orders/paid, …) are delivered automatically. Your own names work withsp.doAction(name, payload). Lower priority runs first (default 10). - Filters:
sp.filter(name, fn, priority)andawait sp.applyFilters(name, value, ...args). - Widgets can point at files:
"file:widgets/notice.html". - Tests run in dry-run mode before activation.
- Runs started in the background (schedule, events, Run, your AI) may take up to 2 minutes, enough for AI services; storefront and admin-button runs stop after 20 seconds. Outside requests wait up to 60 seconds.
A complete real example is the AI Rewriter in the plugin library (several files, settings, a secret API key, a queue, review and restore).
Writing plugin = { tools: {...} } instead of export default {...} works too (one-file plugins).
TypeScript
Any code file can be .ts (for example index.ts and lib/stock.ts). The editor checks types and suggests the sp API as you type. On save, types are removed (they are not checked at run time) and every line stays where it was, so errors still show the right file.ts:line. Import other TypeScript files as ./lib/stock.js or ./lib/stock.
import { lowStock, type Row } from "./lib/stock.js";
export default {
tools: {
low_stock: {
description: "Variants at or below the threshold.",
input: { limit: "number?" },
async run(input: { limit?: number }, sp: any): Promise<Row[]> {
return lowStock(sp, sp.settings.threshold, input.limit ?? 50);
},
},
},
};
4. Save: verification and activation
Limits keep every store fast: up to 2 plugin runs at once per store (others wait a few seconds), 20 saves and 30 verifications per minute.
Save checks the package and runs safe-mode verification: manifest, limits, tests, and a dry run of every tool that needs no input. A new plugin is activated only if it passes. If code is broken, it doesn't run, and the error shows the file and line:
Plugin code error: ReferenceError: 'undefinedThing' is not defined
at lib/stock.js:12:10
12 | return undefinedThing.x;
The same file:line appears in the Log console for errors during a run.
5. Settings, permissions, websites
After saving, the plugin page shows:
- Settings: the form from
plugin.json - Grant permissions: Shopify's permission window, for
scopes - Outside websites: approve the
httphosts
6. Run and watch
- Test (dry run): reads your store, changes nothing.
- Run: for real.
- Log console: every run, its trigger, time, result or error with file:line, and your
sp.loglines; tick Live to follow it. - Run tests: your
tests.
7. Show it in your theme
Online Store → Themes → Customize → Add block → Apps:
- ShopPress widget: a widget from
widgets(plugin slug + widget name) - ShopPress plugin content: HTML saved with
sp.theme.setBlock(key, html) - ShopPress live plugin: a
public: truetool's result, per product or collection
In Custom Liquid sections: <div data-shoppress-widget="<plugin>/<widget>"></div> (with the ShopPress app embed on). More in Theme blocks.
8. Versions, export, share
Every save is a version. Roll back puts an earlier one back. Download .zip gives the whole folder; Upload installs a zip on any store. Publish for review shares it in the library.
The sp toolbox (quick reference)
| API | Use |
|---|---|
sp.shopify.graphql(query, vars) | Shopify Admin API for your store (guide) |
sp.on · sp.filter · sp.doAction · sp.applyFilters | Hooks and filters |
sp.kv.get/set/delete/list | Plugin storage |
sp.settings · sp.secrets.get(name) | Settings and secrets |
sp.files.get(path) · sp.files.list() | The plugin's other files |
sp.http.fetch(url, opts) | Approved outside services |
sp.theme.setBlock(key, html) | Theme block content |
sp.flow.trigger(event, details) | Start Shopify Flow workflows |
sp.event · sp.trigger · sp.shop | What started the run, store info (sp.shop.plus for Shopify Plus) |
sp.utils | CSV, dates, chunk, slug, escapeHtml, cleanHtml (safe HTML for descriptions and pages) |
sp.log(...) | Log console |
Full details: Plugins · Shopify API · Theme blocks · Shopify Flow