Skip to content

Package authoring

This guide covers everything an external author needs to ship a Muoto extension package without reading kernel source. It follows the same public contract the official packages use.

KindPurposeOfficial example
IntegrationMechanics of one external system (API clients, projections of external state)@muoto/github, @muoto/gitlab
Reusable workflowWorkflow, lane, rule, check, gate, reaction definitions@muoto/issue-to-pr
PresetOpinionated composition: bundles + config + workspace/execution strategies@muoto/solo-github
UtilityPlain library code without extension declarationsa GitHubClient class

All of them are ordinary npm/TypeScript packages. There is no Muoto-specific distribution protocol: users install them with their normal package manager, and the AppRevision records the resolved dependency lock.

A minimal integration package (this is the actual shape of the official packages):

packages/acme-github/src/mod.ts
import type {
ExtensionBundle,
PackageMetadata,
} from "@muoto/sdk/extensions.ts";
import type { CapabilityDeclaration } from "@muoto/sdk/sdk.ts";
const CAPABILITIES: readonly CapabilityDeclaration[] = [
{ operation: "network.github", scope: "api.github.com" },
];
export const acmeGitHubBundle: ExtensionBundle = {
id: "@acme/github",
version: "1.0.0",
packageMetadata: {
id: "@acme/github",
version: "1.0.0",
sdkCompatibility: "1.x",
capabilities: CAPABILITIES,
migrations: [],
dependencies: [
{ name: "@muoto/sdk", source: "registry", requested: "^1.0.0" },
],
},
requiredCapabilities: CAPABILITIES,
projections: [
{
kind: "projection",
id: "acme.github.records",
reduce: (state, event) => state,
},
],
};
{
"name": "@acme/github",
"version": "1.0.0",
"type": "module",
"exports": { ".": "./src/mod.ts" },
"types": "./src/mod.ts",
"repository": {
"type": "git",
"url": "git+https://example.com/acme/github.git"
},
"homepage": "https://example.com/acme/github",
"bugs": "https://example.com/acme/github/issues",
"files": ["src"],
"peerDependencies": { "@muoto/sdk": "^1.0.0" }
}

The @muoto/sdk import specifiers resolve against the SDK source checkout (sdk/ + the repository deno.json import map) during development; at runtime the running Muoto install injects the versioned SDK modules into the AppHost, so consumers never install the SDK themselves. The peerDependencies entry declares the SDK generation the package is written against.

Contract rules:

  • Bundle id, bundle version, and packageMetadata identity must agree, and the published npm version must equal them.
  • Bundle IDs use npm conventions (@scope/name or name); extension IDs are lowercase with dashes and dots, never slashes.
  • Declare repository, homepage, and bugs so the catalog can link back to source.
  • The bundle declares what the application consumes: workflows, lanes, rules, checks, gates, reactions, policies, agents, workspaces, executions, projections, frontends, migrations.

packageMetadata.sdkCompatibility declares which SDK major versions the package supports, using range tokens (1, 1.x, >=1 <2, ^1). The kernel rejects a candidate whose packages are incompatible with the revision’s SDK version.

packageMetadata.dependencies lists package-level inputs (workspace, registry, or Git sources); packageLock entries in the AppRevision resolve them to immutable versions, and package revisions require a bundled deno.lock so the AppHost boots frozen. Commit lock files.

Declare every capability your extensions need in requiredCapabilities and mirror it in packageMetadata.capabilities. Adding capabilities is an escalation: the candidate is still packaged, but activation requires explicit approval before the revision pointer moves. Removing a capability never escalates. The conformance kit rejects unknown capability operations and empty scopes.

Runnable escalation flow (same path the kernel exercises in its package lifecycle tests):

Terminal window
muoto mold validate <session> # compatibility checks pass
muoto mold capability-diff <session> # shows the added capability grant
muoto mold candidate <session> # kernel mandatory CI runs
muoto mold activate <session> # requires the escalation to be approved

Extensions get namespaced persistent state through the AppHost context (state.get / state.put with a schema version). Migrations are declared per package with fromVersion/toVersion; the kernel enforces that migration targets advance and that a migration is owned by its declaring extension.

Run the same checks the official packages run, in your own CI:

import { assertExtensionBundleConforms } from "@muoto/sdk/conformance.ts";
import { acmeGitHubBundle } from "./src/mod.ts";
assertExtensionBundleConforms(acmeGitHubBundle, {
sdkVersion: 1,
existingExtensions: [], // extension IDs already in the revision
availableServices: [], // service contracts other packages provide
});

The kit validates bundle identity, semver, SDK compatibility, capabilities, migrations, extension conflicts, command conflicts, service dependency graphs, and idempotency. It throws a structured report on any violation, so CI fails deterministically for broken cleanup, invalid dependency graphs, and side-effect ownership violations.

Reusable extensions compose through abstract service contracts, not concrete packages. A provider declares services.provides, a consumer declares services.requires (optionally optional: true), and the AppHost reconciles activation: providers activate before dependents, dependents deactivate through their lifecycle scopes when a provider is lost, and recovery reactivates exactly once.

Every extension’s register hook receives a LifecycleScope: own reversible AppHost-local effects (subscriptions, timers, watchers) and provide service instances on it. The scope is disposed in reverse registration order on deactivation.

Read Side-effect ownership before shipping: kernel resources (workspaces, executions, ACP sessions) go through kernel RPC, and externally visible operations (messages, pushes, merges) are proposed as idempotent, capability-checked commands with audit provenance — never represented as undo.

The official workspace (packages/) publishes with npm provenance:

  • Release tags: packages/<package-name>@<version>.
  • Each package owns a CHANGELOG.md; entries describe contract, SDK compatibility, capability, and migration changes.
  • CI runs npm run check --workspaces --include-workspace-root, npm test, and node packages/validate-release.mjs before any publish job.
  • Separate repositories may host packages, but they must run the same conformance kit and publish the same metadata fields.

A preset composes bundles and defaults into one opinionated application. Mirror the @muoto/solo-github shape: export a config, a bundles list, a packageLock, and default workspace/execution strategy definitions. Consumers fork it, adjust the pieces they disagree with, and register it through defineApplication({ ...preset }) — the same mechanism official packages use.