FlowDoc: the interchange format
A FlowDoc is one JSON file per flow or module. It is Amazon Connect Flow language, extended with tokens and tool metadata that Connect ignores-by-removal at materialization time.
Structure
{
"flowdoc": "0.3",
"kind": "flow | module",
"name": "inbound-main",
"displayName": "optional, the name Connect shows when it is not name",
"description": "optional, as Connect shows it",
"connectType": "CONTACT_FLOW | CUSTOMER_QUEUE | ... | MODULE",
"content": { "Version": "2019-10-30", "StartAction": "...", "Actions": [ ... ] },
"layout": { "<actionId>": { "x": 0, "y": 0 } },
"refs": [ { "token": "${cdref:queue:front-desk}", "type": "queue", "name": "front-desk" } ],
"meta": { "generator": "core@0.3", "sourceHash": "sha256:...", "sourceKind": "ts | tf" }
}
contentis verbatim Flow language except that every ARN position holds a${cdref:type:name}token. Ref types: queue, hours, lambda, lex, prompt, flow, module, view, and since 0.3 tasktemplate, casetemplate, casefield, assistant, phonenumber. The token grammar (TOKEN_PATTERNinpackages/core/src/refs.ts) is${cdref:<type>:<slug>}with an optional@<slug>alias suffix on any type; the builder'sRefs.module(name, alias)always emits one (${cdref:module:recording-consent@prod}), and@flow-as-code/cdkand@flow-as-code/tfread it as the module alias to publish and point at. A module ref without an alias is bound by@flow-as-code/cdkto its default aliasliveand by@flow-as-code/tfto an address you supply, like any other external reference. Aviewref carries the view's version in the same slot (${cdref:view:after-contact-work@1}), because the console writes the version into the ARN;Refs.view(name, version?)builds it, and the version is optional. The five 0.3 types carry no alias (the grammar admits one on them as on every type, and onlymoduleandviewgive it a meaning;@flow-as-code/cdkbinds the others by name alone):tasktemplateisCreateTask.TaskTemplateId,casetemplateisCreateCase.CaseTemplateId,casefieldis a Cases field id, which stands as a whole map key ofCaseRequestFieldsor a whole item ofCaseResponseFields(docs/adr/0008-case-field-ids.md, the only token position that is not a field value),assistantisCreateWisdomSession.WisdomAssistantArn(anarn:aws:wisdom:ARN, not a Connect one) andphonenumberisStartOutboundChatContact.SourceEndpoint.Address(arn:aws:connect:<region>:<account>:phone-number/<id>, which nests under no instance).Refs.tasktemplate(name)and its four siblings build them. A${cdref:user:...}forRouteContactToAgentand a${cdref:segment:...}forCheckSegmentMembershipwere considered for 0.3 and not added: both Types were refused by the service on 2026-10-05 and their parameter keys are unknown until a console export (tasks/D00, "Pending"); if D09 models either with a resource id, that is a 0.4 bump, at the cost 0.3 paid (a migration case, every reader in both repositories, and a provider minor before the npm one).displayNameis the name Connect shows when that is not the slug inname, optional, 1 to 127 characters with a non-space character (the limitsCreateContactFlowModulestates: https://docs.aws.amazon.com/connect/latest/APIReference/API_CreateContactFlowModule.html).namestays a slug, because it is the file name and the key other documents refer to;displayNameis what a deploy names the Connect resource, through every path (the CDK construct, both Terraform emitters, the flowascode provider). Export writes it whenever the instance's name is not the slug it assigns, so adopting a console flow called "Main Line" keeps that name instead of renaming itmain-lineon the first deploy. Added 2026-09-29, before 0.2 had shipped in a release; absent means Connect's name isname.descriptionis the flow's description as Connect shows it, optional, at most 500 characters. It is authored data likename: the builder'sFlowConfig.descriptioncarries it,synth()writes it, export reads it back fromDescribeContactFlow, and codegen emits it, so the three views agree on one fact. Added in 0.2.- A module's
contentalso carries a top-levelSettingsobject; a flow's does not. Connect requires it and rejectsCreateContactFlowModulewithout one (see theSettingscomment inpackages/core/src/flowdoc.ts); it may be empty, andsynth()emits{}by default. Connect returns it inside the module's content, and export carries it into the FlowDoc (conformance/export/module-settings). The schema documents the property but does not yet enforce its presence on a module. layoutholds canvas positions keyed by action id. Synth assigns deterministic auto-layout when absent (the owned layered algorithm specified inconformance/layout/README.md, so a second implementation reaches the same integers); the studio persists user positions. Layout differences never affect flow behavior.layoutis the single source of truth for position. Flow language carries positions of its own incontent.Metadata.ActionMetadata.<id>.Position, so FlowDoc deliberately does not authorcontent.Metadata; materialization projectslayoutinto it. Two stores of the same fact would drift. Recorded 2026-08-31 from the Flow language example, https://docs.aws.amazon.com/connect/latest/devguide/flow-language-example.htmlrefsis a derived index of every token incontent; regenerated on save. The studio's ref pickers, the@flow-as-code/cdkscaffold, and theno-unresolved-tokenlint rule read it; materialization and the emitters walkcontentdirectly.meta.generatornames the tool that wrote the file (core@0.3fromsynth(),cli@<version>fromflow-cli synthand the studio bridge).meta.sourceHashissha256:<hex>of the paired companion file's bytes, andmeta.sourceKind(tsortf, added in 0.2) says which companion that is:<name>.flow.tsor<name>.flow.tf. Absent meansts, the only companion before 0.2.- The two stamps deliberately count different things and neither is written by hand.
core@0.3is the FlowDoc format version:synth()andexport()are pure functions of the format, their output is byte-identical across@flow-as-code/corereleases, and the conformance goldens underconformance/export/hold those bytes, so a package version there would churn every golden at every release for no provenance gained.cli@<version>is the package version of@flow-as-code/cli, read from its manifest at runtime (packages/cli/src/version.ts), because flow-cli is the distributed tool and the question a committed document has to answer is which build wrote it. Do not read a package version out ofcore@0.3, and do not read a format version out ofcli@<version>; the format version is the top-levelflowdockey in every case. - Materialization (map or binder) replaces tokens, projects
layoutintocontent.Metadataso the flow lays out correctly in the Connect console, and drops thelayout,refs, andmetakeys themselves from deployable content.
Invariants
- Deterministic: same inputs, byte-identical FlowDoc (stable key order, stable action ordering).
- Lossless for unknown blocks: any Action not modeled by the builder is preserved verbatim (GenericBlock) through studio edits and codegen.
sourceHashties the FlowDoc to the builder file it was generated from, for the watch-mode dirty guard and the studio's conflict check.- A reference token occupies an entire field value, or, for a
casefield, an entire map key or list item, and is never interpolated into a longer string. Connect requires the fields that hold references to be "either fully static or a single valid JSONPath identifier", so a partially substituted value is invalid. See conformance/flow-language/actions.md. (Restated to cover a key in 0.3, docs/adr/0008-case-field-ids.md.) - No more than 250 Actions per flow, and Identifiers are unique, at most 50 characters, and exclude the characters Connect reserves. Enforced by conformance/schema/flowdoc-0.3.schema.json.
Versioning
flowdoc: "0.3" since 2026-10-05; 0.2 was the format from 2026-09-11 and 0.1 from 2026-08-30. Every version bump ships a migration in @flow-as-code/core plus fixtures: migrateFlowDoc reads any supported version and returns the current one, conformance/migrate/<case>/ holds an input at each older version and the exact bytes it becomes, and every reader (the CLI, the studio, the CDK construct, both Terraform emitters) migrates on the way in, so the rest of the code sees one version. 0.1 to 0.2 was a version bump alone: 0.2 added the view reference type, meta.sourceKind, and the optional description, none of which a 0.1 document could carry. 0.2 to 0.3 is the same: 0.3 added the tasktemplate, casetemplate, casefield, assistant and phonenumber reference types, all at once, because an older reader (npm 0.2.x, provider 0.1.x) refuses a document carrying a token it does not know, so each type added on its own would have been a release both repositories make in step (tasks/README.md, "Phase D", "Considered and not taken"). Adding a reference type is a version change by this precedent. The older schemas stay byte-identical, held by a hash in packages/core/src/conformance.test.ts, so a 0.1 or 0.2 file is still validated against the rules it was written to, and a per-type clause for a type modeled after 0.2 shipped goes into 0.3 only, encoding what the service enforces at create (owner decision 3 in tasks/README.md), so a 0.2 document the service accepted still validates after migration.
Contract artifacts
conformance/schema/flowdoc-0.3.schema.jsonis the machine-readable form of this document;flowdoc-0.2.schema.jsonandflowdoc-0.1.schema.jsonare the frozen previous versions. A second implementation validates against the schema of the version a document names, as this one does. 0.3 differs from 0.2 in the token pattern, therefs[].typeenum and thedescriptionstrings alone; the per-type clauses the Phase D group tasks add are the rest of the difference.conformance/migrate/holds the migration cases: an input at each older version and the bytes it becomes.conformance/flow-language/actions.mdrecords the Connect action types, parameter shapes, and reference-bearing fields this format wraps, with a doc URL per entry.conformance/flow-language/catalog.jsonis the machine-readable twin of that document: every documented type, and for each modeled one its HCL block and attribute names, kinds, reference paths, restrictions, and transitions. A second implementation generates its schema from it;packages/core/src/catalog.test.tsholds the TypeScript tables to it.conformance/demo/appointment-line.flowdoc.jsonis the canonical demo used by synth, round-trip, emitter goldens, the studio, and the hosted demo.