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

folderId

Yes

none

Document id of the folder to export. Everything inside it, at any depth, is included

moduleId

No

custom_ plus the name in lower case with other characters turned into _

Lower-case letters, digits and underscores only. Anything else is ignored and the default is used

name

No

Custom module

Becomes the module's display name

tier

No

domain

foundation, domain or extension

version

No

1.0.0

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 automation_set_meta steps, with the whole rule definition

Saved views on those tables

As datatable_create_view steps

Dashboards in the folder

As a doc_upsert step plus a dashboard_set_meta step each

The folder tree

As an optional layout, so tables and dashboards land in the same sub-folders

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 source block 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

version

Must be 1. Any other value is rejected

exportedAt

ISO timestamp

module

The manifest (below)

dataTableTemplates

One template per table (below)

stateTemplates

Reserved. An export writes an empty list, because status workflows are inlined on the status columns, and the installer does not read this list

wiringSteps

Steps replayed after the tables exist (below)

layout

Optional. If absent, all new tables go flat into the install folder

source

Where it was exported from. Informational

counts

Totals for display

The manifest

module has the same fields as a built-in module:

Field

Meaning

id

Stable module id. This is what you pass to the installer

tier

foundation, domain or extension

nameKey

Display name. For an export it is the plain name you gave, not a translation key

descriptionKey

Description. Empty on export

icon

Icon name. package on export

provides

Purposes of the package's tables

requires

Purposes the tables link to that the package does not provide itself. Filled in on export

enhances

Soft dependencies. Empty on export; add them by hand if you want opportunistic links

tableTemplateIds

The template ids to create, in order

version

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

$purpose:<purpose>

The document id of the table that was created or reused for that purpose

$folder:<key>

The document id of the recreated sub-folder. $folder: with an empty key means the install folder

$bindings.<name>.documentId

The id returned by an earlier step with "bind": "<name>". Dashboards use it: doc_upsert creates the dashboard, then dashboard_set_meta fills it

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

200

{ "ok": true, "moduleId": …, "validation": { "valid": true, "errors": [], "warnings": [ … ] } }

Not a package

400

{ "error": "invalid_package", … }

Failed validation

400

{ "error": "validation_failed", "moduleId": …, "validation": { "errors": [ … ] } }

Server error

500

{ "error": "import_failed", … }

Importing the same moduleId again replaces the stored package (and its version) for that project.

What the importer checks

Check

Failure

version is 1 and the module has an id

Unsupported package version… or Package module has no id.

Every requires purpose is provided by a built-in module or by the package itself

Requires "<purpose>" which no installed module provides. Another imported package does not count

Every id in tableTemplateIds has a template in the package

Manifest references template "<id>" not in the package.

Column validation patterns compile and have no nested repetition that could hang the server

Column "<table>.<column>" has an unsafe/invalid validation pattern.

Outgoing automation actions (webhook_out, fetch_url) use https and a public host. localhost, private ranges and .local or .internal names are refused

Automation <action> action has an unsafe URL (must be https + public host).

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:

1.

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.

2.

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.

3.

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

/api/projects/{projectId}/modules

GET

Lists installable packages: imported (each with module, templates, validated, sourceProjectName) and official (bundled packages, none today)

/api/projects/{projectId}/modules/{moduleId}

DELETE

Removes an imported package. Returns { "ok": true, "deleted": 1 }

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.

•

Only tables with a purpose travel completely. Give your tables a purpose before you export (see , section "Table purposes and behaviours").

•

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.

•

•

•

•