Module package format
Module package format
For integrators who turn a working folder of tables, views, dashboards and automations into a reusable module, and install it into other projects. This page is the reference for the package file, the export and import rules, and what travels and what does not. Step-by-step tasks are in . The built-in modules are in .
What a package is
A module package is a single JSON file that carries structure, not data: table definitions, status workflows, saved views, dashboards and automations. It does not carry rows. It is different from a project copy or transfer, which moves live data (see ).
A package is installable by module id, in the same way as a built-in module. Its tables link to other tables by purpose, so they bind to the customers, catalogue or invoice tables of the target project instead of to ids from the project it came from.
Packages are stored per project. Importing a package into one project does not make it available in another. No packages ship with the product today.
Make a package
In the product. Open a folder's menu in the navigator and choose Export as module…. The browser downloads <folder-name>.module.json. If something could not be made portable, a dialog titled Some configuration won't be exported lists it, and you choose Export anyway to continue. You need the Owner right.
From a script. Call POST /api/projects/{projectId}/modules/export as a signed-in Owner:
{
"folderId": 1234,
"moduleId": "custom_acme_workshop",
"name": "Acme workshop",
"tier": "domain",
"version": "1.0.0"
}
Field | Required | Default | Rule |
|---|---|---|---|
| Yes | none | Document id of the folder to export. Everything inside it, at any depth, is included |
| No |
| Lower-case letters, digits and underscores only. Anything else is ignored and the default is used |
| No |
| Becomes the module's display name |
| No |
|
|
| No |
| Free text |
The response is { "package": …, "warnings": [ … ] }. The route is not part of the @juxtaboard/portal-client OpenAPI spec.
What is included
Included | How it travels |
|---|---|
Data tables in the folder | As table templates. Reference columns keep the purpose of their target and drop the concrete table id |
Status workflows | Inside the table templates, on the status column |
Automations on those tables | As |
Saved views on those tables | As |
Dashboards in the folder | As a |
The folder tree | As an optional |
What is left out, with a warning
A table with no purpose: its views and automations are skipped, and references to it install as unlinked columns. The table itself is exported under the id tbl_<document id>.
Dashboard panels and slicers that read a spreadsheet or a table with no purpose. A dashboard with no portable panel is skipped entirely.
Run another automation steps. A package has no portable way to name another rule. An automation with only such steps is skipped.
Reference columns whose target has no purpose: they install unlinked, and the person installing must choose the target.
Documents other than tables and dashboards (text documents, forms, spreadsheets, sites) are not exported.
Careful: the export rewrites table links only. A person, group, webhook address or other id inside an automation travels as it is. Review automations before you hand a package to someone else, and remove anything that names your own people or systems.
Careful: the file records where it came from: the source project's name, slug and id. The picker shows the project name as a tooltip on the Community badge. Replace the values in the
sourceblock before sharing, but keep the block itself, because the importer reads it.
File layout
{
"version": 1,
"exportedAt": "2026-11-03T09:00:00.000Z",
"module": { },
"dataTableTemplates": [ ],
"stateTemplates": [ ],
"wiringSteps": [ ],
"layout": { },
"source": { "projectId": 12, "projectName": "Acme", "projectSlug": "acme" },
"counts": { "tables": 3, "automations": 1, "views": 4, "dashboards": 1 }
}
Field | Meaning |
|---|---|
| Must be |
| ISO timestamp |
| The manifest (below) |
| One template per table (below) |
| Reserved. An export writes an empty list, because status workflows are inlined on the status columns, and the installer does not read this list |
| Steps replayed after the tables exist (below) |
| Optional. If absent, all new tables go flat into the install folder |
| Where it was exported from. Informational |
| Totals for display |
The manifest
module has the same fields as a built-in module:
Field | Meaning |
|---|---|
| Stable module id. This is what you pass to the installer |
|
|
| Display name. For an export it is the plain name you gave, not a translation key |
| Description. Empty on export |
| Icon name. |
| Purposes of the package's tables |
| Purposes the tables link to that the package does not provide itself. Filled in on export |
| Soft dependencies. Empty on export; add them by hand if you want opportunistic links |
| The template ids to create, in order |
| Your version string |
Table templates
Each entry of dataTableTemplates carries id, purpose, optional behaviour, columns, requiredColumnIds and two lists of reference columns that let the person installing choose a target. Reference columns have referenceTargetPurpose and no referenceDocumentId. A behaviour set on the source table (for example exclusive_scheduling) is kept.
Packages exported before the list was renamed may call requiredColumnIds by the old name protectedColumnIds. The importer accepts both and stores the current name.
Wiring steps
wiringSteps is a list of { "tool": …, "args": { … }, "bind": … } steps. They use ordinary MCP tool names and arguments. Three placeholders let a step point at things that do not exist yet:
Placeholder | Replaced at install with |
|---|---|
| The document id of the table that was created or reused for that purpose |
| The document id of the recreated sub-folder. |
| The id returned by an earlier step with |
An unknown $purpose: placeholder is left as it is, so the step fails with a visible warning instead of pointing at the wrong table.
Layout
{
"folders": [
{ "key": "custom_acme_workshop/f31", "name": "Reports", "parentKey": null }
],
"tableFolder": { "core_work_order": "custom_acme_workshop/f31" }
}
folders lists the sub-folders to recreate, parents first (parentKey: null means directly under the install folder). tableFolder maps a table template id to a folder key. Keys carry the module id as a prefix so layouts from several packages never collide.
Import a package
In the product. In the Set up from modules dialog choose Import module… and pick the .json file. The message Module imported confirms it, and the module appears in the list with a Community badge. On failure you see Could not import this module package. To remove it, hover over the module and use the bin icon (tooltip Remove imported module).
From a script. POST /api/projects/{projectId}/modules/import with the package as the body, as a signed-in Owner.
Result | Status | Body |
|---|---|---|
Stored |
|
|
Not a package |
|
|
Failed validation |
|
|
Server error |
|
|
Importing the same moduleId again replaces the stored package (and its version) for that project.
What the importer checks
Check | Failure |
|---|---|
|
|
Every |
|
Every id in |
|
Column validation patterns compile and have no nested repetition that could hang the server |
|
Outgoing automation actions ( |
|
And one change the importer makes for you: portal exposure is stripped. Any step with portalExposed: true is stored as false, and the import returns a warning. Importing a package never gives anyone portal access. Share the new tables with portal users yourself after you have checked them.
Careful: the importer checks structure and obvious abuse. It does not review what an automation does. Read the automations of a package from a source you do not control before you install it.
Install a package
An imported package installs like a built-in module: tick it in By module, or pass its id in moduleIds to POST /api/projects/{projectId}/modules/apply (see ). The MCP tool install_modules does not see imported packages.
What happens, beyond the normal install:
The package's module and templates are added to the built-in catalogue for that project. Its requires are resolved against both, so a package can depend on a built-in module such as foundation_customers, and the installer adds it.
Tables are created in the recreated folder tree from layout, or flat if there is none. If two packages are installed together, their layouts are merged.
After the tables exist, the wiringSteps run, as the installing user, with $purpose:, $folder: and $bindings resolved. Each step runs on its own; a failure becomes a warning and the rest continue.
If an imported package uses the id of a built-in module, installs in that project use your package for that id. Give custom modules ids that cannot clash; the export default custom_<name> does.
Careful: removing an imported module only deletes the stored package. Tables already installed from it stay and are ordinary tables from then on.
Other routes
Route | Method | Purpose |
|---|---|---|
|
| Lists installable packages: |
|
| Removes an imported package. Returns |
All routes in this chapter need the Owner right and the signed-in session. None of them is in the @juxtaboard/portal-client OpenAPI spec, which covers the portal routes only.
Limits
Structure only: no rows, files, text documents, forms or sites.
Packages are per project. To use one in several projects, import it into each.
Table names in a new project come from the template ids, so rename tables after installing if you want different names.