ADR-0006: A Terraform provider of managed resources that take action blocks

Status: accepted, 2026-09-11

Context

Phase A deploys flows through Terraform one way: @flow-as-code/tf turns a FlowDoc set into aws_connect_contact_flow resources whose content is a templatefile over a rendered .flow.tftpl, with the reference ARNs bound in flow_refs.tf. A flow is authored in TypeScript or on the studio canvas, and Terraform sees only the rendered JSON. Four documents recorded the reverse direction, HCL to FlowDoc, as deferred to a "provider era".

The aim of Phase B is to author and manage flows natively in Terraform: the actions written as HCL, validated at plan time, and applied through the Connect API with no rendering step in between. Two shapes were considered. A data source over the existing hashicorp/aws resource would turn HCL action blocks into the content string and leave the create, update and delete calls to aws_connect_contact_flow. A set of managed resources would own the whole lifecycle and call the Connect API directly.

Decision

Managed resources: flowascode_contact_flow, flowascode_contact_flow_module, flowascode_contact_flow_module_version, flowascode_contact_flow_module_alias, and a read-only flowascode_view data source, in a separate repository, flow-as-code/terraform-provider-flowascode, written in Go on the plugin framework and published as flow-as-code/flowascode on the Terraform Registry and the OpenTofu registry from its first tag.

A resource takes one repeated action block: id, next, exactly one typed sub-block named after the action type in snake_case (or generic {} for a type the catalog does not model), then condition and error blocks, then an optional position. Reference fields hold the key form of a reference ("queue:front-desk"), and one refs map on the resource binds each key to a Terraform expression; a literal ARN in a field or a refs value is a plan-time error. Blocks were chosen over nested attributes against the plugin framework's own advice ("Use nested attribute types instead of block types for new schema implementations", https://developer.hashicorp.com/terraform/plugin/framework/handling-data/blocks): a flow is read top to bottom as a sequence of steps, which repeated blocks show and a list attribute of objects hides behind brackets and commas.

The provider's schema is generated from conformance/flow-language/catalog.json, the file that also holds @flow-as-code/core's tables, so the two languages cannot disagree about a parameter's name or kind. The provider vendors conformance/ at a pinned commit and passes the families conformance/README.md names; its plan-time flowdoc attribute, the canonical FlowDoc of the configuration with reference tokens in place, is what the cross-language cases compare. Lint runs at plan time with the eleven rules ported to Go; hard rules are errors and cannot be disabled, and the rest are warnings that lint { disable = [...] } may silence.

The emitter keeps its --target tf. The provider is a second target, flowascode, not a replacement, and docs/06-terraform-provider.md says when to pick which.

Consequences

Two implementations of one contract are held together by fixtures rather than shared code, which is what conformance/hcl/ is for (ADR-0007). The provider uses the Connect API's own module version and alias operations, so it needs no awscc resource; the emitter still writes those for the tf target. Migration from hashicorp/aws is by import and moved blocks (the state move needs Terraform 1.8 or OpenTofu 1.10 and later), so the provider's floor is higher than the emitter's OpenTofu 1.7.0.

Live acceptance tests gate every provider pull request, a choice taken against the recommendation: an unreachable sandbox account blocks every merge, and a pull request from a fork cannot pass the gate.