Composition & Wiring
A component’s deploy is a composition over capabilities, expressed in the same grammar as Ops: phases run in order, steps in a phase run sequentially unless parallel, gate pauses for a signal, and onFailure compensates. Steps wire output to input so lint can check the graph before anything runs.
deploy: [ phase("Publish", [publishImage({ from: "archive", to: "$env.registry" })]), // → @Publish.digest phase("Apply", [cfnDeploy({ template: "archive:t.json", imageRef: "@Publish.digest" })]), phase("Verify", [waitSteadyState({ service: "api" })]),]Fan-out is composition, not orchestrator knowledge
Section titled “Fan-out is composition, not orchestrator knowledge”A single component can expand to many units — one CloudFormation stack per instance in a cluster, one host in a fleet. The fan-out is authored in the composition, never known by the driver.
A fan-out unit is itself a small composition. One EC2 instance is provision (cfn-deploy) + code deploy (code-deploy) + health. A component that fans out over N instances composes N such mini-compositions, sequenced seed-first then rolling with a health gate between:
deploy: [ phase("Seed", [cfnDeploy({ stack: "neo4j-az0-0", template: "archive:neo4j.template.json" }), codeDeploy({ instance: 0, revision: "$env.appBundle" }), waitClusterHealthy({ size: 1 })]), ...followers.map((n) => // Array.map over instance configs phase(`Node ${n}`, [cfnDeploy({ stack: `neo4j-${n}`, template: "archive:neo4j.template.json" }), codeDeploy({ instance: n, revision: "$env.appBundle" }), waitClusterHealthy({ quorum: true })]), ),]The driver never learns that a Neo4j cluster seeds first. If a composition ever needs an if component.name === … in the driver, the contract or a capability is missing expressiveness — fix it there, never in the driver.
Cross-stack outputs
Section titled “Cross-stack outputs”When one component’s stack exports a value another needs, the consumer imports it. The graph resolves it, replacing the describe-stacks | jq glue a pipeline would otherwise carry.
export const searchService: Component = { name: "search-service", dependsOn: ["shared-alb"], deploy: [ phase("Apply", [cfnDeploy({ template: "archive:search.template.json", inputs: { ...sharedAlb.outputs } })]), // listener ARN, cluster ARN, subnets, … ],};chant graph --stacks orders shared-alb before the service and wires its outputs. See Multi-Stack Projects for the underlying stackOutput mechanism.
examples/adopt-alb-services is the runnable version of this exact pattern: a bespoke two-service ALB pipeline — describe-stacks | jq glue and all, kept as before/.gitlab-ci.yml — adopted as three build.json components a single driver runs.
Cross-component outputs
Section titled “Cross-component outputs”Components also wire by artifact, not just by stack output. A producer publishes an artifact; a consumer references it as @<component>.publish.uri (or .digest / .key). dependsOn orders the producer first; the graph resolves the reference into the consumer’s step.
The example illustrates the wiring — its jvm-build and publish-artifact verbs are not yet implemented (the cross-component reference resolution is what ships today):
// producer — a JAR published to S3, publish-only deployexport const jarLib: Component = { name: "jar-lib", build: jvmBuild({ tool: "maven", path: ".", into: "archive" }), deploy: [phase("Publish", [publishArtifact({ from: "archive", to: "$env.s3" })])], // → @jar-lib.publish.uri};
// consumer — an EMR job that reads the JARexport const emrJob: Component = { name: "emr-job", dependsOn: ["jar-lib"], deploy: [ phase("Submit", [emrStartJobRun({ jar: "@jar-lib.publish.uri", args: [/* … */] })]), phase("Verify", [waitJob({ runId: "@Submit.runId" })]), ],};Read next
Section titled “Read next”- examples/adopt-alb-services — a bespoke multi-service ALB pipeline adopted onto the component model, before and after, runnable end to end.
- Build Archive — where a producer’s artifact lives before it is published.
- Orchestration — how the graph order becomes a run.