0054 Lmcp A4 Workflow Manifest Schema
ADR-0054: LMCP-A4 — Workflow manifest schema and workflow-defs normalization
- Date: 2026-07-03
- Status: accepted
- Deciders: Gerald Dagher (owner), Construct maintainers (cx-architect)
- Relates to: ADR-0020 (local orchestration runtime), ADR-0021 (provider worker backend), ADR-0033 (platform capability registry), ADR-0046 (modular org runtime merge)
- Tracking: LMCP-A4 (epic: Living Modular Control Plane)
Problem
Eleven workflows are hardcoded in lib/embedded-contract/workflow-defs.mjs:13-84. The
catalog encodes tier, defaultApprovalMode, role chain, and output schema as static
JavaScript object literals. Teams and pack authors cannot add, swap, or override
workflows without forking the core package. Two downstream maps compound the rigidity:
- The intake-to-workflow routing table at
:90-107is also a hardcoded literal — adding a workflow type requires editing two locations in the same file. workflow-defs.mjsis consumed directly by graph build, the capability map, andworkflow-invoke. Any schema change requires coordinated edits across all consumers with no version signal.
There is no declared schema, no versioning, no pack contribution point, and no override mechanism. This is the root cause blocking pack extensibility (LMCP-E1), workflow graph generation (LMCP-D1..D4), and any team-specific workflow customization.
Decision
Manifest schema
Define a workflow manifest as a JSON document with the following fields:
{
"id": "string", // stable slug, e.g. "prd-draft"
"version": "string", // semver of this manifest revision
"type": "linear|routing|orchestrator-worker|evaluator-loop|pipeline",
"modes": ["string"], // e.g. ["solo", "team", "enterprise"]
"surfaces": ["string"], // e.g. ["mcp", "cli", "api"]
"inputSchema": {}, // JSON Schema for workflow input
"outputSchema": {}, // JSON Schema for workflow output
"roleChain": ["string"], // ordered specialist ids
"packRequirements": ["string"], // pack ids that must be installed
"providerRequirements": {}, // e.g. { "minTier": "standard" }
"toolGrants": ["string"], // tools available to this workflow
"policyGates": ["string"], // named gates that must pass
"defaultApprovalMode": "string", // proposal-only | requires-human-approval | allow-durable-write
"durableStateModel": "string", // none | git-queue | in-process
"telemetryEvents": ["string"], // event names emitted by this workflow
"tests": ["string"], // paths to integration test fixtures
"docs": "string", // path or URL to workflow documentation
"examples": [{}], // illustrative input/output pairs
"degradation": {}, // degraded-mode behavior spec
"owner": "string", // team or pack id that owns this workflow
"compatVersion": "string" // minimum construct core version required
}
All fields are optional except id, version, type, and defaultApprovalMode.
Unrecognized fields are ignored by the loader (forward-compat), and missing optional
fields use documented defaults.
Storage locations and merge precedence
Manifests are discovered from three locations and merged by id with explicit
precedence (higher wins):
- Project overrides —
.cx/workflows/*.manifest.json(highest) - Pack contributions —
{pack_root}/workflows/*.manifest.json - Built-in catalog —
lib/embedded-contract/workflows/*.manifest.json(lowest)
When two sources declare the same id, the higher-precedence source wins in full
(field-level merging is not supported; an override must be a complete manifest). The
loader records which source won for each id in the resolved catalog and surfaces
conflicts as warnings when CONSTRUCT_DEBUG is set.
Disposition of workflow-defs.mjs
lib/embedded-contract/workflow-defs.mjs becomes a generated artifact:
construct graph buildreads all manifests from the three locations, validates them against the manifest schema, and writes the merged catalog toworkflow-defs.mjs(or a build-time JSON equivalent consumed by the runtime).- A thin compat shim re-exports the generated catalog at the existing module path
so all current consumers (
graph build, capability map,workflow-invoke) continue to import without change during the migration window. - Hand-editing
workflow-defs.mjsis deprecated once LMCP-D2 lands; the file carries a generated header warning. Direct edits will be overwritten on the nextconstruct graph buildrun. - The compat shim and
workflow-defs.mjsas a hand-editable file are removed in LMCP-D4 once all consumers import from the manifest-aware loader directly. - The intake-to-workflow routing table (
:90-107) is replaced by asurfaces+modesindex derived from manifests at build time; no separate routing table is maintained by hand.
Validation
The loader validates each manifest against the schema on load:
- Unknown
typevalues → hard error (fail-closed). compatVersionoutside the running core version range → hard error with a clear message naming the manifest file and the version mismatch.roleChainreferences a specialist id that is not installed → warning in solo mode, hard error in team/enterprise.- Schema validation errors → hard error; malformed manifests are never silently skipped.
Rejected alternatives
-
Keep the hardcoded catalog and add a plugin registration function. A JS
registerWorkflow(def)call at import time avoids files on disk. Rejected: it requires pack authors to ship JS that runs in the core process, raises security concerns, makes order-of-import significant, and prevents static analysis of what workflows are installed. File-based manifests are inspectable, diffable, and auditable without executing code. -
Single merged JSON file written by pack install. One
workflows.jsonfile that pack install rewrites on each install. Rejected: concurrent installs race, git diffs are noisy, and the file cannot be overridden per-project without replacing the entire catalog. The three-location precedence model composes cleanly. -
Extend the existing
workflow-defs.mjsschema in place with a version field. Addversionandownerto the current static objects. Rejected: it does not solve the extensibility problem (still requires a fork to add workflows), does not provide a pack contribution point, and does not enable the graph-build pipeline that LMCP-D1..D4 depends on.
Consequences
- Pack authors can contribute new workflow types by shipping a
workflows/*.manifest.jsonunder their pack root — no core fork required. - Project teams can override any built-in workflow's defaults (approval mode, role chain,
policy gates) without touching the package by placing a manifest in
.cx/workflows/. workflow-defs.mjsis regenerated onconstruct graph build; CI should run graph build and assert the committed file matches the generated output (drift detection).- The compat shim means zero consumer changes during the migration window; LMCP-D1..D4 can land incrementally.
- Removing the shim in LMCP-D4 is a breaking internal API change — consumers must migrate to the manifest-aware loader before D4 lands.
- The intake-to-workflow routing table becomes a derived index; removing it from
workflow-defs.mjsis part of the LMCP-D2 scope.
Unblocks: LMCP-D1, LMCP-D2, LMCP-D3, LMCP-D4, LMCP-E1