Skip to content

Capabilities

llms.txt lists every page for an agent

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
plan?(ctx: DeployContext, input: In): Promise<CapabilityPlan>; // what run would change
}

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

plan is optional. A capability that has it can say what run would change before it changes anything. It answers with a member name, that member’s plan digest and an artifact for run. A gated-wave fan-out calls it for each wave, binds the wave’s gate to the set digest of the answers, and hands the artifact back to run as ctx.plans[member] so the approved plan is the one applied. The terraform lexicon’s terraform-apply implements it.

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.

chant