@flow-as-code/cdk
npm i @flow-as-code/cdk
Apache-2.0, Node 22.12 or newer. Peers on aws-cdk-lib and constructs, both
optional so a CLI-only install does not fetch them.
Site and docs: https://flow-as-code.dev/. This package on npm: https://www.npmjs.com/package/@flow-as-code/cdk.
CDK binding for @flow-as-code/core. Two exports, plus a scaffold generator on a subpath:
TokenBinder: an interface you implement, mapping reference names to construct attributes (queue.attrQueueArn,fn.functionArn, a Lex alias ARN).@flow-as-code/core'smaterializeWithBinderinserts the returned strings byte-for-byte, so CDK tokens pass through and CloudFormation resolves them per account at deploy time. No resource map, no literal ARNs in this path.FlowSet: a construct consuming a directory of*.flowdoc.jsonfiles (orFlowDoc[]) plus aTokenBinder. Flows becomeAWS::Connect::ContactFlow(type fromconnectType), modules becomeAWS::Connect::ContactFlowModule, created in dependency order. References between documents in the set resolve without the binder (see below).
Usage
import { FlowSet } from "@flow-as-code/cdk";
new FlowSet(stack, "Flows", {
instanceArn: instance.attrArn,
source: "flows/", // or FlowDoc[]
binder: {
queue: (name) => queues[name].attrQueueArn,
hours: (name) => hours[name].attrHoursOfOperationArn,
lambda: (name) => fns[name].functionArn,
lex: (name) => lexAliases[name].attrBotAliasArn,
prompt: (name) => prompts[name].attrPromptArn,
// flow?(name) and module?(name, alias): only for a flow or module that is
// not in this set, see "References inside and outside the set".
},
});
References inside and outside the set
A reference that names a document in the set is resolved by FlowSet itself, the way @flow-as-code/tf and @flow-as-code/hcl resolve their own documents; the binder is not consulted for it:
${cdref:module:name@alias}whose module is in the set becomes the alias ARN of theAWS::Connect::ContactFlowModuleAliasthe construct manages (see the versioning model below).${cdref:flow:name}whose flow is in the set becomes that flow's ARN (Fn::GetAttat itsAWS::Connect::ContactFlow), with an explicit dependency. This is what a main line that pointsUpdateContactEventHooksat a whisper or hold flow in the same directory needs, andTransferToFlowto a flow in the set works the same way.
Documents are created in dependency order over both kinds of reference, and the order is stable, so the template is byte-identical however the files were read. A set whose flow references form a cycle (two flows transferring to each other, a hook pointing back at its caller) fails at synth with the cycle spelled out, Flow reference cycle: day-line -> night-line -> day-line, because each flow would need the other's ARN and one CloudFormation template cannot create that; at deploy it would surface as a Circular dependency naming logical IDs. Deploy one side from another stack and bind it through flow().
A reference that names a document outside the set goes to the binder's optional method:
flow(name)returns the ARN of a contact flow managed elsewhere.module(name, alias)returns the alias ARN of a module managed elsewhere, at the alias the token pins (livewhen it pins none), so the same document binds to a different module alias per environment, from a map or anFn.importValue.
Without the method, synth fails naming the token, the document and both remedies (add the document to the set, or implement the method). A Lazy.string is a fine return value: FlowSet materializes content in its constructor, so a binder pointing at a flow another FlowSet in the same stack manages defers the lookup with Lazy.string({ produce: () => other.flows.get(name)!.attrContactFlowArn }), and CDK resolves it at synth, when both constructs exist.
The scaffold writes flow and module methods only for names the set does not hold.
The scaffold (@flow-as-code/cdk/scaffold)
Writing that stack by hand starts with the same file every time, so cdkScaffold({ docs, source }) generates it: a FlowStack constructing a FlowSet over the FlowDoc directory, and a TokenBinder carrying one TODO per reference type the documents actually use, with the referenced names listed. Types the set does not use still get a method, because the interface requires them, but theirs throws instead of carrying a TODO.
The subpath exists because two tools emit this file and must emit it identically: flow-cli emit --target cdk and the studio's CDK export button. It is pure and imports no node builtin, so the studio can bundle it for a browser; source is the FLOW_DOCS expression (., ..), which is the one thing that needs a path calculation, and sourceForDepth computes it where node's path.relative is unavailable. The emitted file resolves that expression against itself with new URL(source, import.meta.url), so the stack reads the same directory whatever the CDK app's working directory is. Output is byte-stable and independent of the order the documents were read.
Peer dependencies
aws-cdk-lib ^2.267.0 and constructs ^10.8.1, both optional. A project that
has them pinned below that floor gets an ERESOLVE from npm 7 and later rather
than a silent mismatch; a project that has neither installs neither. The floor
is deliberate: FlowSet builds CfnContactFlowModuleVersion and
CfnContactFlowModuleAlias (see the versioning model below), which are recent
additions to aws-cdk-lib/aws-connect, and 2.267 is the version this package is
developed and tested against. Lowering it means finding the release that
introduced both L1s and running the aws-cdk-lib/assertions tests against it,
not just widening the range.
They are optional because the CDK entry is not the only entry. @flow-as-code/cli and
the studio reach this package for @flow-as-code/cdk/scaffold alone,
which imports nothing but @flow-as-code/core: aws-cdk-lib appears in that module as a
line of the stack source it generates, not as an import. Non-optional peers are
auto-installed, so with these declared as ordinary peers every
npm install @flow-as-code/cli would fetch 166 MB of CDK that nothing in that
install loads. Importing FlowSet or the
binder still needs both packages present, and tests/packaging.test.ts holds
the scaffold subpath to its one bare import.
The cost of optional is that npm says nothing at install time, so the package
says it itself. Importing @flow-as-code/cdk with aws-cdk-lib
absent throws:
@flow-as-code/cdk needs the CDK at runtime, but aws-cdk-lib is not
installed. It and constructs are optional peers so that a CLI-only install
stays small, which means npm does not warn about them at install time. Install
them with: npm install aws-cdk-lib@^2.267.0 constructs@^10.8.1
The entry checks for the peer before it loads FlowSet, so this replaces the
bare ERR_MODULE_NOT_FOUND that used to come out of dist/flow-set.js. The
./scaffold subpath is unaffected and imports with no CDK installed.
src/index.test.ts runs the entry in a directory with no node_modules and
asserts the message, including the version floor read from package.json.
Versioning model
- Modules are versioned. Each module gets an
AWS::Connect::ContactFlowModuleVersionwhose logical ID embeds a hash of the module's canonical content, so a content change publishes a new immutable version, plus anAWS::Connect::ContactFlowModuleAliasper alias name the doc set pins (defaultlive) with a stable logical ID that repoints to the new version. Flows reference the alias ARN, so repointing changes no flow content. - Full flows are not versioned; content updates in place. Connect's
CreateContactFlowVersionstates "This API only supports creating versions for flows of type Campaign." (https://docs.aws.amazon.com/connect/latest/APIReference/API_CreateContactFlowVersion.html, verified 2026-08-31)
Validation
FlowSet refuses, at synth time: duplicate document names, kind/connectType mismatches, flow and module reference cycles (named), and any document failing a hard @flow-as-code/core lint rule (no-literal-arn, no-unresolved-token). Binder gaps, including a module or flow that is neither in the set nor bound, fail with an error naming the token, the document, and the missing method.
No instance/queue provisioning constructs here; users bring their own or use L1s.
Testing
Offline tests assert on synthesized templates with aws-cdk-lib/assertions. The live-deploy test in src/integration.test.ts runs only when both FLOW_TEST_INSTANCE_ARN and FLOW_TEST_DEPLOY=1 are set, deploys the demo flow plus a versioned module to that instance through the AWS CLI, and tears the stack down. It writes to the account, so naming an instance is not enough on its own: use an empty sandbox.
That test has run green against a live Connect instance (2026-09-02). The test asserts only that the deploy exited zero, and it deletes the stack in its finally, so the content check was a separate step: the same synthesized template was deployed again and left up, and its flows were read back with DescribeContactFlow and DescribeContactFlowModule and compared against what the FlowDocs materialize to. That comparison is structural rather than byte-for-byte, because deploying resolves each ${cdref:...} reference to a real ARN: every other position matched exactly, and the reference positions were checked against the stack's own resources instead, each resolving to a resource of the right kind and the module reference resolving to the alias rather than the version. The stack needs no CDK bootstrap: it is asset-free and uses CliCredentialsStackSynthesizer. tasks/A07-flow-cdk.md records both.