Skip to content

Plugin System Guide

This guide explains how to extend Business M by creating and integrating new domain plugins.

A backend plugin is a standard Framework M App defined as a library.

libs/my_plugin/
├── src/
│ └── my_plugin/
│ ├── __init__.py # App definition
│ ├── doctypes/ # Domain entities
│ ├── services/ # Business logic
│ └── bootstrap.py # DI wiring & extensions
├── pyproject.toml
└── README.md

In pyproject.toml, register the app and optional bootstrap/container:

[project.entry-points."framework_m.apps"]
my_plugin = "my_plugin:app"
[project.entry-points."framework_m.bootstrap"]
my_plugin_init = "my_plugin.bootstrap:MyPluginBootstrap"

Use the Bounded Context pattern. Instead of adding fields directly to core Item, create a MyPluginItem in your library:

class MyPluginItem(BaseDocType):
item: str = Field(description="Link to core Item")
special_field: str = Field(...)
class Meta:
table_name = "my_plugin_item"

Frontend plugins are integrated into the Desk UI at build-time.

libs/my_plugin/frontend/
├── src/
│ ├── pages/
│ ├── components/
│ └── plugin.config.ts # Plugin Manifest
├── package.json
└── vite.config.ts

The manifest defines how your plugin integrates into the UI:

import type { FrameworkMPlugin } from "@framework-m/plugin-sdk";
const plugin: FrameworkMPlugin = {
name: "my_plugin",
version: "0.1.0",
manifests: [
{
app_id: "my_plugin",
label: "My Plugin",
icon: "box",
resources: [
{
name: "my_plugin.dashboard",
label: "Dashboard",
route: "/my-plugin/dashboard",
}
]
}
],
routes: [
{
path: "/my-plugin/dashboard",
element: () => import("./pages/Dashboard"),
}
]
};
export default plugin;

To enable your plugin in the main application:

Add the library to apps/business-m/pyproject.toml or install it in the environment.

Terminal window
cd apps/business-m
uv add --path ../../libs/my_plugin

Add the plugin path to apps/business-m/frontend/plugin.config.ts (or the discovery configuration). Ensure the library is linked via pnpm:

Terminal window
cd apps/business-m/frontend
pnpm add ../../../libs/my_plugin/frontend

5. Role-Based Menu Visibility (visibility_policy)

Section titled “5. Role-Based Menu Visibility (visibility_policy)”

Every resource in your plugin manifest can be restricted to specific user roles using visibility_policy. The framework evaluates these on the frontend and shows or hides menu items accordingly.

Always import role constants from the module’s roles file — never use raw string literals in visibility_policy. This ensures a CI contract test can validate that every referenced role is a registered canonical role.

import type { FrameworkMPlugin } from "@framework-m/plugin-sdk";
import { WMSRoles } from "./roles";
// roles.ts — defines canonical string constants
export const WMSRoles = {
WAREHOUSE_MANAGER: "Warehouse Manager",
WAREHOUSE_OPERATOR: "Warehouse Operator",
INVENTORY_AUDITOR: "Inventory Auditor",
} as const;
const plugin: FrameworkMPlugin = {
name: "my_plugin",
version: "0.1.0",
manifests: [{
app_id: "my_plugin",
resources: [
{
name: "my_plugin.dashboard",
label: "Dashboard",
route: "/my-plugin/dashboard",
// ✅ Correct — use constants from roles file
visibility_policy: {
roles: [WMSRoles.WAREHOUSE_MANAGER, WMSRoles.WAREHOUSE_OPERATOR]
}
},
{
name: "my_plugin.admin",
label: "Admin Setup",
route: "/my-plugin/admin",
visibility_policy: {
// ✅ Also valid — string literals from the canonical class are checked
roles: ["Warehouse Manager"]
}
}
]
}]
};

A CI test (tests/test_frontend_roles_contract.py) automatically parses all plugin.config.ts files and asserts that every role string in visibility_policy.roles[] exists in the canonical RoleRegistry. This means:

  • You cannot add a new role to a menu without first registering it in the Python RoleRegistry
  • Typos or renamed roles are caught at CI time, not at runtime

The frontend evaluates visibility_policy by calling checkUserRole() for each menu resource. This uses a DataLoader pattern that coalesces all checks within a 20ms window into a single POST /authz/evaluate/bulk request, and caches results for the entire session. Adding many menu items to your plugin has zero performance cost after the first page load.

  • Namespacing: Always prefix your DocTypes and menu items to avoid collisions (e.g., my_plugin.settings).
  • Isolation: Use m_protocols to communicate with other libraries. Avoid direct imports of other domain logic.
  • Seeding: Use m seed to populate initial data required by your plugin.
  • Roles: Define your role constants in libs/my_plugin/src/my_plugin/roles.py and register a RoleDefinition in your module’s bootstrap.py before any DocType Meta class is loaded.