A package is the community-node equivalent: one JSON file that adds ready-made pieces to a connector without a rebuild. It can bundle any of these:
| Content | What the install does |
|---|---|
| Sub-flows | Reusable flow tabs. They appear in the editor palette under Packages and run through a subflow node (calls: "pkg:<name>/<id>"). |
| Examples | Flows that join the Examples gallery, with their inputs, mock services and sample record. |
| Apps | App definitions saved as drafts (templates), never published for you. |
| OpenAPI specs | Registered as <package name>.<id>, for an http node’s openapi: "<package name>.<id>#operationId". |
| Event types | Added to the business-event catalogue. |
Packages are listed at flow.opexcg.com/packages. Open Packages in the editor’s menu (or Ctrl/Cmd+K, Packages…) to browse what is installed, fetch from the catalogue, or install a downloaded file.
Trust
Every package carries an Ed25519 signature over its contents. A connector decides what to do with it:
| Trust | Meaning | Installs |
|---|---|---|
official |
Signed by the OpexCG publisher key built into the connector. | Always |
trusted |
Signed by a key in packages.trusted_publishers. |
Always |
unsigned |
No signature. | Only with packages.allow_unsigned: true, shown with a warning |
unknown_signer |
Signed with a key this connector does not know, so the signature was not checked. | Only with packages.allow_unsigned: true, shown with a warning |
A signature that does not match the contents is always refused, with or without allow_unsigned: the file was changed
after it was signed. A verified package is only upgraded by a package signed with the same key.
A package can carry scripts (JavaScript transforms), HTTP and SQL calls and printer jobs, and they run inside your flows. Read an unsigned or third-party package before you install it. Trust is checked again every time the connector starts: remove a publisher key from the config and its packages become inactive (still listed, their sub-flows stop resolving, and a deploy that calls them says so).
packages:
allow_unsigned: false # default
catalogue_url: https://flow.opexcg.com/api/packages # "" = offline: install from a file only
trusted_publishers: # extra signers besides OpexCG
- id: acme
public_key: <base64 raw 32-byte Ed25519 public key>Install, upgrade, uninstall
Installing needs the admin role and is audited (package.install, package.upgrade, package.uninstall).
Everything that can fail is checked before anything changes: signature and trust, requires (the lowest opex-flow that
runs the package; a newer requirement is refused with both versions), the contents, and conflicts. A package never
overwrites what exists: an event type name, an app id or slug, an example id or a loaded OpenAPI spec that is already taken
refuses the install and names it.
- Install adds the contents. The same package twice is refused: upgrade it instead.
- Upgrade replaces the contents with a newer version (same or older versions are refused). Sub-flows, specs and event
types the new version drops are removed, so an upgrade is refused while a deployed flow uses one of them. A flow that calls
the package runs the new sub-flow after its next deploy (the response says
redeploy: true). An app you edited since it was installed is kept as it is. - Uninstall removes everything the package added. It is refused (409
in_use) while a deployed flow calls one of its sub-flows, uses one of its OpenAPI specs or one of its event types; the answer lists the nodes. Apps you edited stay.
Installing from the catalogue downloads the file and checks its SHA-256 against the listing before it reads it; a download
must come from the catalogue’s own host. For a machine with no internet access, download the .opexpkg.json file elsewhere,
copy it over and use Packages > From file (or POST /api/packages/install {bundle}).
State lives in the data directory: packages.json (the index) and packages/<name>.json (the verified bundle).
Using what a package adds
A sub-flow shows up in the palette under Packages; dropping it creates a subflow node whose calls is
pkg:<package name>/<sub-flow id>. It behaves exactly like a sub-flow tab of your own flow (see
Flows): each call site runs its own copy with its own counters, and an error inside follows the call node’s
error wire. The package’s own nodes are not on your canvas; a problem inside one is reported on the call node.
REST and MCP
| Method | Path | |
|---|---|---|
| GET | /api/packages |
Installed packages, the palette sub-flows and the policy (viewer) |
| GET | /api/packages/catalogue |
The catalogue with installed_version and update_available (viewer) |
| GET | /api/packages/{name} |
One installed package (viewer) |
| POST | /api/packages/inspect |
{name, version?} or {bundle}: trust, warnings, install or upgrade, changes nothing (admin) |
| POST | /api/packages/install |
Same body: installs (admin, audited) |
| POST | /api/packages/{name}/upgrade |
{version?} or {bundle} (admin, audited) |
| DELETE | /api/packages/{name} |
Uninstall (admin, audited) |
Errors carry code: bad_signature and requires and invalid (422), unsigned_refused (403), already_installed,
not_newer, conflict and in_use (409), catalogue_unavailable (502). MCP tools: list_packages (with catalogue: true
to fetch the catalogue), install_package (with upgrade: true to upgrade) and uninstall_package; list_examples and
get_example include the examples of installed packages.
Build and sign your own
A package is a directory:
manifest.json name, version, title, description, author, license, requires, homepage?
subflows/<id>.json { title, description?, nodes, edges }: one subflow_in, one subflow_out, no sources
examples/<id>.json an example file, as in apps/connector/examples/
apps/<id>.json an app definition
openapi/<id>.json an OpenAPI 3.x document
events/<name>.json an event type: { description, owner, schema, ... }{
"name": "com.acme.payments",
"version": "1.0.0",
"title": "ACME payments",
"description": "What it does, in a sentence or two.",
"author": "ACME",
"license": "MIT",
"requires": "2.1.0"
}The name is reverse-DNS (lower-case words and dots, at least two parts), the version semver. Inside a package, a
subflow node calls a sibling by its local id ("calls": "other"). Node ids must not contain :: or #.
opex-flow package keygen --out ~/.config/opex-flow/acme.key # once; keep it out of git
opex-flow package pack ./my-package -o my-package.opexpkg.json # validates, writes an UNSIGNED bundle
opex-flow package sign my-package.opexpkg.json --key ~/.config/opex-flow/acme.key --key-id acme
opex-flow package verify my-package.opexpkg.json --public-key <b64> --key-id acme # exit 0 signed, 3 unsigned/unknown, 1 altered
opex-flow package info my-package.opexpkg.json # one line of JSON: manifest, counts, sha256pack runs the install-time checks (shapes, ids, every sub-flow called once by the real expander, the examples rendered with
their defaults, specs indexed), so a package that packs installs. scripts/sign-package.sh DIR does pack, sign and verify in
one go; the key comes from OPEX_PACKAGE_KEY_FILE or OPEX_PACKAGE_KEY (base64 PKCS#8), never from the repository.
Give a customer the public key from opex-flow package pubkey --key FILE for their trusted_publishers.
The OpexCG catalogue
flow.opexcg.com serves GET /api/packages (every package, newest version plus versions, each with sha256, size,
signed, key_id, requires and a download path) and GET /api/packages/{name}/{version} (the bundle, with its SHA-256
in X-Checksum-Sha256). Packages live in R2 with a row in D1; scripts/publish-package.sh FILE verifies the signature
against the OpexCG key, uploads the file and registers it, and --withdraw NAME VERSION hides a version. The signed
packages of this repository are in packages/dist/ with their sources in packages/src/.