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" }
  }
}
FieldMeaning
name, description, versionShown on the Plugins page and in the library
mainEntry file (default index.js)
scopesExtra Shopify permissions; you grant them with one click. See Shopify API.
optionalScopesPermissions only some features need; the plugin runs without them
httpOutside services the plugin may call with sp.http.fetch (you approve each host)
settingsBuilds 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 with sp.doAction(name, payload). Lower priority runs first (default 10).
  • Filters: sp.filter(name, fn, priority) and await 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 http hosts

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.log lines; 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: true tool'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)

APIUse
sp.shopify.graphql(query, vars)Shopify Admin API for your store (guide)
sp.on · sp.filter · sp.doAction · sp.applyFiltersHooks and filters
sp.kv.get/set/delete/listPlugin 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.shopWhat started the run, store info (sp.shop.plus for Shopify Plus)
sp.utilsCSV, dates, chunk, slug, escapeHtml, cleanHtml (safe HTML for descriptions and pages)
sp.log(...)Log console

Full details: Plugins · Shopify API · Theme blocks · Shopify Flow