Skip to content

Capabilities

A capability is a typed leaf behavior, registered once and named after an operation. Components compose capabilities; the orchestrator dispatches to them by kind. This is the unit of reuse — smaller than a pipeline, smaller than a “kind” of component.

interface Capability<In, Out> {
kind: string; // "publish-image", "cfn-deploy", ...
run(ctx: DeployContext, input: In): Promise<Out>;
rollback?(ctx: DeployContext, input: In): Promise<void>; // paired compensation
}

Typed In/Out let compositions wire one step’s output into the next and let lint check the wiring before anything runs.

Many distinct components on the left, one generic orchestrator in the middle, and a small shared set of capabilities on the right that every component draws from. Many distinct components on the left, one generic orchestrator in the middle, and a small shared set of capabilities on the right that every component draws from.
The sprawl fix: unbounded components, one generic orchestrator, a bounded capability set.

The set is a plugin registry, not a closed enum. Verbs group into families. The Plugin column says who contributes each: core ships the cloud-agnostic verbs in its always-loaded starter set; a cloud lexicon contributes its own leaves through the same capability-plugin seam, loaded when a project’s chant.config.ts lists that lexicon (lexicons: ["aws"]). So a component’s cfn-deploy step resolves only when the aws lexicon is active — core itself has no AWS in it. See the cloud boundary.

FamilyVerbsPluginNotes
builddocker-build, zip-package, jvm-buildcoresource → archive, keyed by artifact type
sbomgenerate-sbomcoreartifact-type-keyed software SBOM (native SPDX/CycloneDX, hermetic lockfile backend) into the build archive — see Build Archive
supply chainsign, attest-provenance, verify, scan-vulnerabilities, vuln-gatecorekeyless cosign signing/attestation + a deploy-time verify gate and vulnerability policy gate
wait / verify (agnostic)wait-cluster-healthy, wait-endpoint, health-gatecorebolt-port cluster probe + HTTP readiness/health gates
secretsensure-secretcoregenerated-once materialization (#1365): read-then-write — present means done, never mints over an existing value, never rotates implicitly; a presence / declared key-set / metadata mismatch stops the apply naming key names only; needs-opt-out rollback (COMP003); store adapters come from provider lexicons
escape hatchshellcoretyped, lint-flagged
apply (Cloudflare Workers)wrangler-deploy, wrangler-versions-promote, r2-synccorewraps the wrangler/rclone CLIs directly — the Workers plane is deliberately ceded to wrangler (no cloudflare lexicon owns these; #1293); r2-sync mirrors s3-sync’s input shape
sbom (config)extract-config-bomawsIaC config-BOM (declared resources / nested stacks / external refs) from the synthesized CloudFormation template into the build archive
publishpublish-image, load-image-on-host, publish-asset / publish-artifactawsdeploy-time, promote by identity; an interface with backends
applycfn-deploy, ecs-update-service, lambda-deploy, s3-sync, cdn-invalidate, run-migrationawscfn-deploy carries safety options (changeset preview, onReplace, stageGsi)
job submissionemr-start-job-run, emr-submit-stepawspoint a running compute service at an artifact; reusable for Glue / Batch / Step Functions
host / code deliverycode-deploy, copy-to-host, remote-execawscode-deploy = AWS CodeDeploy; the others are SSM host verbs (SSH transport pending)
wait / verify (cloud)wait-for-stack, wait-steady-state, wait-jobawsCloudFormation / ECS / EMR terminal-state polls
safety / rollbacksnapshot-before, rollback-previousawsDynamoDB/RDS/EBS snapshot + restore
applykubectl-apply, kustomize-applyk8sserver-side apply as chant:<stack> with the marker-scoped prune; kustomize renders in front of the same pipeline
gitopsargo-app, flux-reconcilek8sapply the controller’s CR through the same stack-labelled pipeline, then wait for convergence — Healthy+Synced for Argo, Ready (terminal on wedge reasons) for Flux

A genuinely new apply family (job submission was one) is written once and then reused across every component of that shape. A new component is a declaration only. This is why the family count grows sub-linearly against components — see the cloud boundary for how each family’s leaf maps per cloud.

Some applies are sticky. A DynamoDB GSI updates one at a time; a key-schema change forces replacement; RDS and OpenSearch changes can trigger blue/green or replacement that loses data. That knowledge belongs inside the capability, configured by declarative options, not scripted per component:

phase("Apply", [
cfnDeploy({ stack: "ddb", template: "archive:ddb.template.json", onReplace: "block", stageGsi: true }), // sticky knowledge, captured once
]);

Every sticky resource reuses that one implementation. The alternative — each component scripting its own workaround — is exactly the sprawl the model exists to remove. Capabilities are allowed to be smart (preflight, guard, stage), not thin shells.

Cloudflare’s Worker versions are the same discipline paying off with a whole new cloud attached: wrangler-deploy returns the version id it just published, and wrangler-versions-promote moves live traffic to any version by id. Cloudflare’s own gradual-deployment rollback is exactly “promote to a prior version” — no bespoke undo to write — so wrangler-deploy declares a rollback that re-promotes to whatever version was live before it ran, landing on the same RollbackPolicy: "native" (../orchestration/#rollback-comes-free) every other native-rollback verb uses. r2-sync has no such native undo (an overwritten/deleted object is gone), so it carries needs-opt-out — the same disposition s3-sync does.

The pattern holds only if variance stays behind the capability interface. Three guardrails, enforced by lint:

  • Verbs, not nouns. A capability describes an operation (cfn-deploy), never a component (deploy-search-service). A capability used by exactly one component is a smell.
  • No orchestrator special-casing. The driver takes only the contract. A shell escape hatch is allowed but flagged, and must declare a reason.
  • No composition copy-paste. Identical compositions repeated across components are a declaration-sprawl signal — reach for a preset.

Capabilities give own-destiny, but the common shapes should not be retyped. Presets are named compositions, shipped like the ApplyOp composite:

EcsFargateComponent({ service: "search", image: "search", healthPath: "/healthz" });

A component uses a preset, or starts from one and drops to raw capabilities where it is special. Both produce the same contract and run through the same orchestrator, so presets remove copy-paste without creating a closed strategy set.