Bring existing flows under Terraform
Most instances already have flows, built in the console or deployed some other way. This tutorial takes them over without recreating them: a flow keeps its id, its ARN and anything that points at it (phone numbers, other flows' transfers), and from then on changes go through a plan.
There are three ways in, depending on where the flows are managed today:
| Today | Way in |
|---|---|
| In the console, a few flows | an import block per flow |
| In the console, many flows | flow-cli export --author tf for the whole instance, then import blocks |
As aws_connect_contact_flow in Terraform |
a moved block per flow |
Outputs below are from a sandbox instance on 2026-09-29 (Terraform 1.8.5,
provider v0.1.0, @flow-as-code/cli 0.2.0), identifiers masked.
A few flows: import blocks
Write an import block with the flow's id, <instance id>:<flow id> (both
are in the flow's ARN), and let Terraform generate the configuration:
import {
to = flowascode_contact_flow.appointment_line
id = "<instance id>:<flow id>"
}
terraform plan -generate-config-out=generated.tf
Plan: 1 to import, 0 to add, 0 to change, 0 to destroy.
generated.tf holds the flow as action blocks:
resource "flowascode_contact_flow" "appointment_line" {
description = null
display_name = null
instance_id = "<instance-id>"
name = "appointment-line"
refs = {
"hours:main-line" = "arn:aws:connect:us-west-2:<account>:instance/<instance-id>/operating-hours/<id>"
"lambda:appointment-lookup" = "arn:aws:lambda:us-west-2:<account>:function:appointment-lookup"
"queue:appointments" = "arn:aws:connect:us-west-2:<account>:instance/<instance-id>/queue/<id>"
}
...
action {
id = "welcome"
next = "check-hours"
error {
next = "apologize"
type = "NoMatchingError"
}
message_participant {
media = null
prompt_id = null
ssml = null
text = "Thanks for calling. Let's get you to the right place."
}
}
The provider read each ARN in the flow, found what it names in the instance's
inventory, and wrote a reference key in its place: the actions say
"queue:appointments", not the queue's ARN. The ARNs are left in refs,
because Terraform needs a value there and the provider cannot know which of
your resources each one is. Three things to tidy before you commit:
- Replace each
refsvalue with the address that manages it. An ARN left inrefsworks, but it is the per-environment table this tooling exists to delete, and the studio refuses it. Writeaws_connect_queue.appointments.arn, adatasource, remote state, or a module variable, as the promotion tutorial does. - Replace
instance_idwith a variable (var.connect_instance_id). - Leave the rest as it is. The
nullattributes (settings = null,next = nulland the others) read as unset, in Terraform and in the studio andflow-clialike, and the alphabetical order of the blocks is only order. To edit the flow in the studio too, move its resource into a file of its own named after it,appointment-line.flow.tf(one flow per file); with the two edits above made,npx flow-cli synth appointment-line.flow.tfreads it to its FlowDoc, and a save in the studio rewrites it in the canonical form. Export (below) writes the canonical form directly.
Then plan again. When the configuration describes the flow as it is, the plan is empty and nothing in Connect changes.
A flow named in the console as "Main Line" keeps that name: name becomes the
slug main-line, which identifies the flow to the tooling, and
display_name = "Main Line" is what Connect shows. Import writes both; the
first apply renames nothing.
Many flows: export the instance
flow-cli export reads every flow and module in an instance and writes a
FlowDoc and a companion for each. With --author tf the companion is a
.flow.tf in the provider's canonical form, references already keys:
npx flow-cli export --instance <instance ARN> --out flows/ --author tf
default-agent-transfer (flow): default-agent-transfer.flowdoc.json, default-agent-transfer.flow.tf
default-outbound (flow): default-outbound.flowdoc.json, default-outbound.flow.tf
sample-queue-configurations-flow (flow): sample-queue-configurations-flow.flowdoc.json, sample-queue-configurations-flow.flow.tf
sample-ab-test (flow): sample-ab-test.flowdoc.json, sample-ab-test.flow.tf
sample-note-for-screenpop (flow): sample-note-for-screenpop.flowdoc.json, sample-note-for-screenpop.flow.tf
failed: Sample Lambda integration (arn:aws:connect:us-west-2:<account>:instance/<instance-id>/contact-flow/<id>): Cannot export "sample-lambda-integration": 1 ARN(s) not found in the instance inventory: arn:aws:lambda:us-east-1:<account>:function:state-lookup
Exported 19 of 20 to flows/
1 flow(s) could not be exported
The failure is the tool refusing to guess. That flow invokes a Lambda function the instance's inventory does not list (it points at another region), and rather than write its ARN into the document, export names it and moves on. Associate the function with the instance, or fix the flow, and export again.
One exported file, abridged:
resource "flowascode_contact_flow" "sample_note_for_screenpop" {
instance_id = var.connect_instance_id
name = "sample-note-for-screenpop"
display_name = "Sample note for screenpop"
type = "CONTACT_FLOW"
description = "Screenpop is a Contact control pannel feature that allows loading a web page optionally with parameters based on attributes. Refer to the screenpop documentation for more information."
start = "2b6ee9f2-0f1a-417c-91c7-bdc12cfc6a07"
refs = {
# TODO: no terraform address for ${cdref:queue:basicqueue}.
"queue:basicqueue" = null
}
action {
id = "35dde84a-f902-4a2b-9e0d-b6079d2a5260"
next = "e8f664fd-3e86-4aaa-915f-c1fa4a4d4dd0"
transfer_contact_to_queue {}
error {
type = "NoMatchingError"
next = "e8f664fd-3e86-4aaa-915f-c1fa4a4d4dd0"
}
error {
type = "QueueAtCapacity"
next = "e8f664fd-3e86-4aaa-915f-c1fa4a4d4dd0"
}
position {
x = 928
y = 85
}
}
...
}
The console's action ids and block positions are kept, so the studio shows
the flow as the console drew it. Each refs entry arrives as null under a
TODO; the provider refuses a plan with a null binding and names the key,
so none can be forgotten. Fill them in, add an import block per flow, and
plan until it is empty.
flow-cli lint flows/ runs the same rules the provider runs at plan time,
without a plan. On a freshly exported instance it is a quick way to see which
console flows would not survive a redeploy as they are.
From hashicorp/aws: moved blocks
A flow managed today as aws_connect_contact_flow moves to the provider
without being recreated. Replace the resource with a flowascode_contact_flow
describing the same flow, and add:
moved {
from = aws_connect_contact_flow.appointment_line
to = flowascode_contact_flow.appointment_line
}
Terraform 1.8 and OpenTofu 1.10 carry the state across resource types. The
move copies the flow's identity and name; the next plan writes the actions
from your configuration, and when they describe the flow as it is, nothing
changes in Connect. aws_connect_contact_flow_module moves to
flowascode_contact_flow_module the same way.
To write the new resource from the old one's content, run the content through
flow-cli: flow-cli export --author tf reads the live flow, or, for a
.tftpl file the tf emitter wrote, the FlowDoc it was emitted from is
already in your repository and flow-cli codegen <doc> --to tf writes the
companion.
Afterwards
- Promote the flows from a module, one root per environment.
- Edit them in the studio: a
.flow.tfand its FlowDoc are a pair the studio keeps in step. - The provider's registry guide
migrate-from-aws-providercovers the same ground from the provider's side.