Skip to main content

connect-flow

A minimal IaC toolkit for Amazon Connect contact flows: promote a flow between environments, verify it, and keep its layout readable.

A CLI built around one idea: a contact flow JSON file in your repo should be an environment-neutral source of truth, and each environment (dev, test, prod) a rendering of it. copy/normalize translate the environment-specific IDs and ARNs baked into a flow so one file can be promoted anywhere; diff/lint/digest verify it along the way; prettify auto-arranges its block layout without changing its behaviour.


Why this exists

The problem. A contact flow exported from Amazon Connect is full of environment-specific identifiers baked directly into the JSON — Lex bot alias ARNs, Wisdom AI-agent ARNs, Lambda ARNs, sibling contact-flow/ flow-module/queue/prompt ARNs. Promoting a flow from dev to test to prod means finding and swapping every one of them, by hand, in a multi-thousand- line file — a process that's slow, error-prone, and leaves no record of which identifiers changed or why. And once a flow is edited in three places, there's no cheap way to tell whether the copy in git still matches what's actually deployed.

The solution. connect-flow copy/normalize translate a flow's identifiers against a per-environment map file, so one JSON file in git becomes the environment-neutral template and each environment a rendering of it — see Promoting a flow between environments below. diff/lint/digest answer "has this drifted" and "is this well-formed" without a console round trip. And because flows in git get edited by several people, their layout rots too — crossing edges, error paths visually competing with the happy path, blocks wherever they happened to land mid-edit — which is what prettify is for.

Every rewrite this tool makes, whether it's an identifier or a coordinate, goes through the same discipline: patch only the literals that are supposed to change, in place, with a targeted regex — never json.dumps over the whole document, which would rewrite every line and bury any accidental behavioural change inside an unreadable diff. A semantic digest (a hash of everything behavioural) is taken before and after every write; if it doesn't match what the operation is supposed to produce, nothing is written and the tool reports a safety violation instead. See docs/safety.md for the full mechanism.

So a layout rewrite's diff is exactly the numbers that moved:

       "ActionMetadata": {
         "560e67f4-4322-4788-831e-e49c2a0e7dd2": {
-          "position": { "x": 1284, "y": 61 },
+          "position": { "x": 240, "y": 120 },
           "children": ["d3ee852c-65de-4180-95a6-c8bd1f10b5bf"]
         },

And the layout itself goes from this to this:

   BEFORE — edited by hand over a year    AFTER — one command

   [Play prompt]        [Disconnect]      [Entry]
        │   ┌────────────────┘  ▲            │
   [Entry]  │   [Error handler] │            ▼
        └───┼────┐      │  ▲    │         [Get input] ──► [Compare] ──► [Play prompt]
   [Compare]│  [Get input]─┘    │              ╷              ╷               │
        │   └──────┘     └──────┘              └┄┄┄┄┄┬┄┄┄┄┄┄┄┄┘               ▼
        └───────────────────────┘              [Error handler]          [Disconnect]

   happy path zig-zags, error paths       happy path straight and left-to-right,
   cross it and each other                error paths pushed below, out of the way

What you get

connect-flow uses git-style subcommands. Every one of them takes a TARGET that is either a local path or a live flow / flow-module ARN, interchangeably — so diff-ing a file in your repo against what's actually running in production is one command, not a download step plus a comparison step.

subcommand purpose
connect-flow copy --from SOURCE --to DEST [--from-map FILE] [--to-map FILE] [--apply] promote a flow 1:1, translating identifiers through an intermediate <<...>> token form; a live --to is previewed unless --apply
connect-flow normalize TARGET... [--reverse] [--map FILE] the strictly offline form of the same translation — local files only, no AWS
connect-flow prettify TARGET... rewrite block positions into a readable layout
connect-flow diff TARGET_A [TARGET_B] semantic diff between two targets, local and/or remote
connect-flow lint TARGET... structural checks: dangling transitions, unreachable actions, empty branches
connect-flow digest TARGET... print each target's semantic digest — "is my file still what's live?"
connect-flow list [INSTANCE[/KIND[/NAME-OR-ID]]] list instances, or flows/flow modules/views within one, or describe one resource

Without --from-map/--to-map, copy is a byte-verbatim transfer — fetch a remote flow to disk, or upload a local file to a remote flow, with no identifier translation at all.

--profile/--region/--json/--quiet are accepted either before or after the subcommand, aws-cli style: connect-flow --profile lab --region us-east-1 list and connect-flow list --profile lab --region us-east-1 are equivalent.

How a run works

  SOURCE (--from)                                            DEST (--to)
  ───────────────                                             ───────────
                                                            ┌─► local file  (+ .bak)
  local file ─┐                                             │
              ├─► parse ─► translate/layout ─► patch text ──┼─► live ARN, preview only
  live ARN  ──┘     │      (from-map/to-map,   (regex,      │   (--apply to write)
                    │       dot)                in place)   │
                    └──► digest ── compare ──┘               └─► stdout (--emit-map only)
                                      │
                                   mismatch ───► refuse to write, exit 2

prettify is a different translate/layout step over the same pipeline — geometry only, no identifier maps — with the same digest safety gate.

Promoting a flow between environments

copy and normalize are two thin argument shapes over one engine: read a flow, substitute identifiers against a map file, optionally re-layout, write it somewhere. They differ only in which sources/destinations are legal:

verb source destination touches AWS
copy local, live ARN, or INSTANCE/NAME (--from) local file, or live (--to, previewed unless --apply) reads (+ writes with --apply)
normalize local file only local only never

copy translates in one direction per map: --from-map normalises --from's real identifiers to <<...>> placeholders, --to-map resolves those placeholders to --to's real identifiers — both are optional and independent, so a single copy can do either half, both, or neither (a verbatim transfer). normalize instead takes one map and a --reverse flag, since it always has exactly one local file on both ends.

The map file

One JSON file per environment, in the shape {"<<ARN:Some Name>>": "arn:aws:...", "<<ID:Some Name>>": "22222222-..."} — flat, or grouped into named sections ({"lexv2": {...}, "others": {...}}); both are accepted, and a grouped file's section names are preserved when a skeleton is emitted. A plain, non-<<...>> key such as IVR_BOT_AGENT_ID works too — a placeholder is whatever the map says it is, not only a <<...>> token.

Bootstrap a map for a flow that doesn't have one yet

--emit-map discovers every ARN-shaped literal and documented bare-id field in a flow and writes them as a map — nothing is written to the flow itself. Each entry is named automatically wherever a name is available: a Lambda ARN's function name is parsed straight out of the ARN (works even from normalize, which never touches AWS); a ContactFlowId/FlowModuleId/QueueId is resolved to its live Name via a Describe call against copy's --from instance (turned off with --no-auto-resolve, same flag as live-to-live copy). Anything with no name available — like the Lex bot-alias ARN below — falls back to a <<ARN:RENAME-ME-HASH>>/<<ID:RENAME-ME-HASH>> key, HASH a short hex digest of the value itself (so the key a given value gets doesn't depend on the order flows/modules were fed into --emit-map).

--to is still required but never written to in this mode:

$ connect-flow copy --from contact-flows/main-ivr.json --to flows/main-ivr.json \
    --emit-map maps/dev.json
$ cat maps/dev.json
{
  "discovered": {
    "<<ARN:RENAME-ME-8ed6922a>>": "arn:aws:lex:ap-southeast-2:000000000000:bot-alias/22222222-2222-2222-2222-222222222222/33333333-3333-3333-3333-333333333333"
  }
}

Rename any remaining RENAME-ME keys by hand to something meaningful — the edited file is that environment's map. Running --emit-map again against the same FILE — the natural shape of a loop, once per downloaded flow or module — merges in, rather than overwrites: existing entries and groups are kept as-is, and only genuinely new values are appended, so the file accumulates into one shared map across the loop. The downloading half of such a loop stays readable on its own: a local --to reports CHANGED/OK without a diff (a flow arrives from Connect as one line of JSON, so its diff is the whole file), and --diff asks for one — see docs/adr/0021-copy-diff-defaults.md.

dev → committed template → test

# dev's live flow -> an environment-neutral file, committed to the repo
connect-flow copy --from arn:aws:connect:ap-southeast-2:000000000000:instance/1111.../contact-flow/2222... \
    --from-map maps/dev.json --prettify --to flows/main-ivr.json --profile dev

# template -> test instance (maps/test.json holds test's real values for the same keys);
# previewed first, applied once the diff looks right
connect-flow copy --from flows/main-ivr.json --to-map maps/test.json \
    --to arn:aws:connect:ap-southeast-2:000000000000:instance/1111.../contact-flow/2222... \
    --profile test
connect-flow copy --from flows/main-ivr.json --to-map maps/test.json \
    --to arn:aws:connect:ap-southeast-2:000000000000:instance/1111.../contact-flow/2222... \
    --profile test --apply --publish --yes

copy --to, whether previewing or applying, refuses (exit 2) if the destination's live content no longer normalises back to the source's template form through --to-map — i.e. someone edited it outside this pipeline since it was last copied — unless --force.

The preview itself is the same block-level comparison the diff verb runs: a summary of which blocks were added, removed or changed, then a unified diff of just those blocks, with each resolved identifier annotated by the <<Token>> it came from. A write that changes bytes without changing any block — a re-indented template, a console re-save — previews as exactly that, in one line. Add --functional to drop the designer layer (ActionMetadata, annotations, snapToGrid) from the comparison and see only what the flow does at runtime; it changes the preview, never what is written. See docs/adr/0022-copy-block-level-preview.md.

Offline: re-point or CI-gate a local file, no credentials needed

connect-flow normalize flows/main-ivr.json --reverse --map maps/test.json -o /tmp/rendered.json
connect-flow normalize contact-flows/*.json --map maps/dev.json --check    # CI gate

normalize never touches AWS on its own code path — no credentials, no network calls — which is what makes it the right verb for a pre-commit hook or a CI job that has no AWS credentials at all.

Unresolved identifiers are a hard error

If a template still contains a <<...>> placeholder — or the resolve/ normalise direction leaves any of the map's own literals in place — after translation, that's refused (exit 2), not silently written: a template with a leftover placeholder is exactly the failure mode this feature exists to catch. --force downgrades it to a warning.

See docs/adr/0001-cross-environment-id-mapping.md for the design decisions behind this (placeholder style, map format, the duplicate-value and substring-shadowing rules), docs/adr/0009-copy-verb.md for copy's two-pass design, and docs/safety.md for how the rewrite itself is proven safe.

prettify

Run connect-flow prettify --help for all options.

Example uses:

Reformat all contact-flows in a directory in-place

connect-flow prettify contact-flows/*/*.json --in-place

What a reformat actually looks like

Here's tests/fixtures/messy.json in the Connect designer, before and after connect-flow prettify — same flow, same behaviour, same digest, only the block positions rewritten:

Before — the happy path doubles back on itself and the error branches cross it:

before

After — the happy path runs left to right along the top, error branches drop below it:

after

Use --render to see that before/after without opening the console

--render writes a STEM.before.png / STEM.after.png pair — connect-flow's own sketch of the graph, drawing the blocks where the rewrite will put them. It combines with --check/--dry-run, which write nothing at all, so you can look before letting the tool near the file:

connect-flow prettify contact-flows/my-flow.json --dry-run --render --render-dir /tmp/renders

The report says whether the layout actually got better, in numbers:

$ connect-flow prettify tests/fixtures/messy.json --dry-run --render --render-dir /tmp/renders
CHANGED tests/fixtures/messy.json
    read: tests/fixtures/messy.json
    wrote: rendered
    before: crossings=2 backward_happy_edges=4 overlaps=0 canvas=1670x1020
    after : crossings=0 backward_happy_edges=0 overlaps=0 canvas=1050x680

Edge crossings and backward happy-path edges both to zero, on the same flow pictured above.

diff

Run connect-flow diff --help for all options.

diff compares two targets semantically — it answers "does this behave differently", not "are these files byte-identical". Layout, key order and designer churn are normalised away first (see Flow normalisation explained below). Either side can be a local path, a live ARN, or INSTANCE/[KIND/]NAME-OR-ID addressing (see Addressing a remote resource), so file↔file, file↔live and live↔live all work the same way. It exits 1 when the two differ, which makes it usable as a CI check.

Example uses:

See what actually changed between two flows

Here tiny-edited.json is a copy of the fixture with one prompt reworded:

$ connect-flow diff tests/fixtures/tiny.json tiny-edited.json
A: tests/fixtures/tiny.json
B: tiny-edited.json
  ~ e5565ba7-ebc9-43da-8d9e-94412c33ab4b

--- e5565ba7-ebc9-43da-8d9e-94412c33ab4b (tests/fixtures/tiny.json)
+++ e5565ba7-ebc9-43da-8d9e-94412c33ab4b (tiny-edited.json)
@@ -1,7 +1,7 @@
 {
   "Identifier": "e5565ba7-ebc9-43da-8d9e-94412c33ab4b",
   "Parameters": {
-    "Text": "Hello!"
+    "Text": "Hello, welcome to support!"
   },
   "Transitions": {
     "Conditions": [],

A per-Identifier summary (+ added, - removed, ~ changed) followed by a unified diff of just the affected blocks — rather than of the whole file, where one edited prompt would be buried among thousands of unchanged lines. --json emits the same structurally.

See what a commit actually changed in a flow

A flow committed the way the console exports it is one line of JSON, so git show reports the whole file as one - line and one + line whatever changed. --git-show and --git-diff take the two sides from git instead, with every other diff option unchanged:

connect-flow diff --git-show abc1234                    # what commit abc1234 changed (abc1234^ vs abc1234)
connect-flow diff --git-show HEAD:contact-flows/main-flow.json  # one file only
connect-flow diff --git-diff                            # HEAD vs the working tree, staged or not
connect-flow diff --git-diff v1.2..HEAD --functional    # between two revisions

Without a :PATH every changed *.json flow gets its own report (added or deleted files are just named); the exit code is 1 if any differ. Unlike plain git diff, a bare --git-diff compares against HEAD, not the index — see docs/adr/0023-diff-git-sources.md.

Confirm a reformat changed nothing

Two files with different layouts and identical behaviour are identical to diff, which is the whole point:

$ connect-flow diff tests/fixtures/messy.json tests/fixtures/messy.pretty.json
identical: tests/fixtures/messy.json == tests/fixtures/messy.pretty.json

Has my repo drifted from what's really deployed?

connect-flow diff contact-flows/main-flow.json \
    arn:aws:connect:ap-southeast-2:000000000000:instance/1111.../contact-flow/2222... \
    --profile prod

Compare the same flow across two environments by name

INSTANCE/NAME-OR-ID addressing (ADR 0008) works on either side, avoiding the need to look up ARNs by hand:

connect-flow diff \
    dev-instance/"CSC Support Flow" \
    prod-instance/"CSC Support Flow" \
    --profile prod

Did someone leave an unpublished draft in the console?

--both-stages compares one flow's saved draft against its own published content — a single ARN, no second target:

connect-flow diff arn:aws:connect:ap-southeast-2:000000000000:instance/1111.../contact-flow/2222... \
    --both-stages --profile prod

Compare a Terraform template against the deployed flow

A .json.tftpl is full of ${...} placeholders that will never match the resolved ARNs in the live flow. --normalize-opaque replaces every placeholder and every ARN-shaped literal with one <opaque> token before comparing, so the structural comparison still works:

connect-flow diff contact-flows/main-flow.json.tftpl \
    arn:aws:connect:ap-southeast-2:000000000000:instance/1111.../contact-flow/2222... \
    --normalize-opaque --profile prod

It's off by default because it deliberately hides a real class of bug — the wrong ARN wired into a block — so reach for it only when comparing a template to its rendered result.

Compare the same flow lineage across two instances

A flow copied from one instance to another keeps its own identity, but every ContactFlowId/FlowModuleId/QueueId it references — an "Invoke Flow Module" block, for example — gets a fresh id in the new instance, even when the referenced flow/module/queue is genuinely "the same one" by name. --normalize-opaque also resolves these against each target's own instance (a live Describe* call), replacing the id with <<FlowModule:Name>> (etc.) — so two references that both resolve to the same Name stop looking like a diff, while a reference that resolves to a different Name still is one:

connect-flow diff arn:aws:connect:ap-southeast-2:000000000000:instance/1111.../contact-flow/2222... \
    arn:aws:connect:ap-southeast-2:000000000000:instance/3333.../contact-flow/4444... \
    --normalize-opaque --profile prod

This only applies to a remote (ARN) target — there's no instance to resolve a bare id against for a local file. A reference id that can't be resolved (deleted resource, no permission) is left as-is and a warning is printed, rather than silently hidden.

lint and digest

Run connect-flow lint --help / connect-flow digest --help for all options. Both are strictly read-only — there is no writer path in either, so neither can ever modify a flow, local or live.

Example uses:

Catch broken wiring before it reaches Connect

lint reports dangling transitions (a NextAction/Conditions[]/Errors[] entry pointing at an Identifier that doesn't exist), blocks unreachable from StartAction, a missing StartAction, and Compare/DistributeByPercentage blocks with no conditions at all. It exits 1 if any target has findings:

$ connect-flow lint tests/fixtures/unreachable.json
ISSUES tests/fixtures/unreachable.json
    unreachable: '4008265c-66ba-4495-b465-9c586352ff0b' is not reachable from StartAction
    unreachable: '41b9ba6c-fef3-48dd-849c-425afcd58a7c' is not reachable from StartAction

$ connect-flow lint tests/fixtures/messy.json
OK     tests/fixtures/messy.json

These are the mistakes a hand-edited or templated flow makes and the console never shows you, because you only find out when a call falls off the end of a branch at 2am.

Answer "is my file still what's live?" in one line

digest prints the semantic hash of each target — the same normalised form diff compares — so a whole flow reduces to one comparable string:

$ connect-flow digest tests/fixtures/messy.json tests/fixtures/messy.pretty.json tests/fixtures/tiny.json
2afe14e5f2b6ec64122ab9e03e866d343a96ce43ff4f259ece5d166b988cf1b1  tests/fixtures/messy.json
2afe14e5f2b6ec64122ab9e03e866d343a96ce43ff4f259ece5d166b988cf1b1  tests/fixtures/messy.pretty.json
50c2bbe077f06890311f33d13d9ece8d39290b957d685eeb83c52d8fddfae72d  tests/fixtures/tiny.json

The first two are the messy and prettified versions of one flow: different files, different layouts, one digest. That's the property the whole tool rests on.

Mixing local and remote targets in a single invocation is the quick "did anything drift" sweep — no diff to read, just two strings to compare:

connect-flow digest contact-flows/main-flow.json \
    arn:aws:connect:ap-southeast-2:000000000000:instance/1111.../contact-flow/2222... \
    --profile prod

Both accept --stage {published,saved} (default: published) for remote targets, and the same INSTANCE/[KIND/]NAME-OR-ID addressing as prettify.

Views

A view - the agent-facing UI a ShowView block renders - is handled by the same verbs as a flow: copy, diff, digest, lint, normalize and list all accept one, local or remote (INSTANCE/view/NAME-OR-ID, or INSTANCE/view/aws/NAME for an AWS-managed view). prettify refuses a view (there's no layout to rewrite), and a flow and a view can't be copied onto or diffed against each other (exit 3).

A local view file holds the view's content with Template expanded to an object, so it diffs line by line; a Template still encoded as a string (saved straight from aws connect describe-view) loads too. diff reports view changes per component, and flags as breaking a removed action (a ShowView branch the flow can no longer take) or a renamed/removed component _id/Props.Name (a $.Views.ViewResultData.<Name> the flow reads). AWS-managed views and numbered view versions are read-only.

# pull a view, edit it, check it against the flow that shows it, deploy it
connect-flow copy --from lab-instance/view/Disposition --to views/disposition.json \
    --region us-east-1 --profile lab
connect-flow lint flows/wrap-up.json --view-dir views/
connect-flow copy --from views/disposition.json --to lab-instance/view/Disposition \
    --region us-east-1 --profile lab --publish

lint FLOW --view-dir DIR (or --with-view FILE) matches each ShowView block to a view file by name (Disposition → disposition.json) and checks that every ViewData key is an input the view reads, every branch the block takes is one of the view's Actions, and every $.Views.ViewResultData.<Name> the flow reads is some component's Props.Name. See docs/adr/0031-view-lint.md.

Deploy order matters, because a flow and its view are deployed separately. Publish a view change before the flow that depends on it (new action, new field); remove an action or field from a view only after no published flow still uses it. copy between instances maps a ShowView's view ARN by name as <<View:Name>> (<<View:Name:N>> for a pinned version, only carried over when version N has the same content in both instances) - see docs/adr/0029-view-reference-tokens.md.

A view's copy snapshots work as for flows, with one limit of the Connect API: CreateViewVersion can only version the published content, so a draft-only write can't snapshot the draft it replaces (keep one with --local-backup DIR) - see docs/adr/0030-view-remote-writes.md.

Flow normalisation explained

diff and digest both work on one canonical form of a flow rather than on its raw JSON, and prettify uses the digest of that same form as its safety check. Normalisation is what makes all three trustworthy: without it, a flow re-saved in the console with no edit at all would look "changed" every time, and a tool that cries wolf on every save gets ignored.

What gets normalised away, and why none of it is behavioural:

normalised why it isn't a behavioural difference
all geometry — position, entryPointPosition, annotation size pure layout; the whole point of prettify is to change exactly this and nothing else
Actions[] array order Connect dispatches by Identifier reference, not array position, so the order blocks appear in the file means nothing
JSON key order and formatting whitespace and key ordering carry no meaning to Connect
conditionMetadata[].id on Compare/DistributeByPercentage designer display decoration with no counterpart in Transitions.Conditions[], and Connect regenerates it on every save even with no edit
a compound block's hidden child Identifier (diff only) e.g. inside a "Create Wisdom Session" block; re-randomised on every save, so diff aliases it to <parent-identifier>#<index> and compares that
${...}/<<...>> placeholders and ARN literals (diff --normalize-opaque, opt-in) lets a Terraform template, or a copy/normalize token-form template, be compared to its rendered result; off by default because it also hides a genuinely wrong ARN
ContactFlowId/FlowModuleId/QueueId referenced from inside a flow, resolved to the referenced resource's Name (diff --normalize-opaque, remote targets only) these ids aren't stable across instances, only the Name is — see ADR 0007
the whole designer layer — ActionMetadata, its key order, Annotations, snapToGrid (diff --functional, opt-in) affects only how the console draws the flow; it's rewritten on every save, so two snapshots of an unedited flow always differ here — see ADR 0016

The last two rows are orthogonal axes and compose — --functional chooses which fields are compared, --normalize-opaque chooses how values inside the surviving fields are matched:

# same instance: is the saved draft functionally the published flow?
connect-flow diff 'arn:...:contact-flow/2222...' 'arn:...:contact-flow/2222...:$SAVED' --functional

# across instances: same logic, different queue/flow ids either side
connect-flow diff dev.json 'arn:...:contact-flow/3333...' --functional --normalize-opaque

What is deliberately not normalised, because it is behavioural:

  • Transitions.Conditions[] order — Connect evaluates conditions first-match, so reordering two branches changes which one runs.
  • ActionMetadata key order — not behavioural at runtime, but it's tracked anyway in the digest (as a separate ActionMetadataOrder field), because the writer must prove its own rewrite didn't disturb it. Pass diff --functional to leave it, and the rest of the designer layer, out of a comparison between two independently-saved flows.
  • Everything else — Parameters, block Type, StartAction, Settings, annotations' text, and any unrecognised top-level Metadata key are all carried into the canonical form verbatim. The digest is sensitive to every one of them by design; see docs/safety.md for how that's tested rather than merely intended.

Working against live Amazon Connect instances

connect-flow extends the same "provably no behavioural change" guarantee across the network: it can fetch a flow or flow module straight from a live Connect instance, prettify it, and push it back, with a rollback point and post-write verification.

AWS credentials

Use the usual --profile and/or --region cli parameters, or AWS_* environment variables.

Addressing a remote resource

Anywhere a TARGET (or copy's --from/--to, or diff's two sides) can be remote, it is either a full ARN or INSTANCE/[KIND/]NAME-OR-ID:

  • INSTANCE is an instance id or alias (exact, case-sensitive match).
  • KIND is the ARN's own resource type — contact-flow (the default when omitted; flow is accepted too), flow-module or view — so it can be copy-pasted from an ARN.
  • NAME-OR-ID is the resource's id, or its name resolved via List*. INSTANCE/view/aws/NAME is an AWS-managed view.
  • A trailing :$SAVED (or :SAVED) means the unpublished draft.

--region is needed with this form (your profile's default region is used if you don't give one). A value is a local file instead when it starts with /, ./, ../ or ~/, or when its last path segment contains .json (main.json, main.json.tftpl). See docs/adr/0028-kind-in-address.md.

connect-flow list lists the Connect instances visible to your credentials; give it an instance to list its contact flows, INSTANCE/flow-module for its flow modules, or INSTANCE/view (INSTANCE/view/aws for the AWS-managed ones) for its views:

connect-flow list --region us-east-1 --profile lab

connect-flow list 11111111-1111-1111-1111-111111111111/flow-module \
    --region us-east-1 --profile lab

Give list a single resource instead to describe it — its ARN, description, version and last-modified time (including the instance it lives in):

connect-flow list "lab-instance/Main Menu" --region us-east-1 --profile lab

Every verb takes the same form, and kinds can be mixed in one call:

connect-flow prettify "lab-instance/flow-module/Main Menu" --region us-east-1 \
    --dry-run --diff --profile lab

A remote write always defaults to the unpublished draft (:$SAVED), never the live content — pass --publish to write live instead:

# writes a draft only, with a rollback snapshot; live content is untouched
connect-flow prettify arn:aws:connect:us-east-1:000000000000:instance/1111.../flow-module/2222... \
    --in-place --profile prod

# writes the live content, unattended
connect-flow prettify 1111.../flow-module/2222... --region us-east-1 \
    --in-place --publish --yes --profile prod

Every remote write is guarded four ways:

  1. version snapshot   immutable pre-change copy   (--snapshot before|after|both|none;
                                                     copy also snapshots after)
  2. sha256 check       taken immediately before the write — refuses on conflict
  3. read-back verify   re-reads what Connect stored, recomputes its digest
  4. confirmation       mandatory (--yes) rather than skippable off a TTY

copy --to additionally refuses to upload a file whose digest has drifted from the destination's live content — a straight digest compare for a map-less (byte-verbatim) copy, or the reverse-normalisation drift check described above when --to-map is given — unless --force. For a local --from that check is made against the version you started editing, not the edited file: live content still matching one of the file's last 20 committed versions (or --base FILE|git:REV) passes, so pull → edit → copy needs no --force. See docs/safety.md for the full detail, including the residual race the sha check narrows but can't close.

Snapshots are Connect flow/module versions (CreateContactFlow{,Module}Version), listed and restorable in the console. copy defaults to --snapshot both, so what it deployed is a version too; prettify defaults to before. The default descriptions read connect-flow <version> <command>: before change / after change; --snapshot-description TEXT replaces them (prefixed [before]/[after] under both, (before)/(after) for a view). If the pre-change snapshot fails (e.g. no connect:CreateContactFlowVersion permission), the write is refused with a pointer to --local-backup DIR (writes the pre-change content to DIR/STEM.json first, no dependency on that API) or --force (proceeds with no rollback point at all).

The remote-write guardrail flags (--yes, --force, --publish, --no-verify, --snapshot, --local-backup) have no short forms — a remote write is never one keystroke. Plain I/O-destination flags like --output/-o aren't subject to that: they don't touch a live instance by themselves, so there's no safety reason to make them harder to type.

Install

pipx install connect-flow

Requires Python 3.11+. The base install only needs boto3, for live-instance support — boto3 needs no configuration to run normalize/digest/lint on local files or diff on two local files, since those never make an AWS call.

prettify, copy --prettify/normalize --prettify, and --render also need pygraphviz to compute a layout; install it with the graphviz extra:

pipx install "connect-flow[graphviz]"
# or, if connect-flow is already installed:
pipx inject connect-flow pygraphviz

pygraphviz's wheel bundles the Graphviz libraries, so no system Graphviz install is needed either way. Running a layout command without the extra installed fails cleanly with an error telling you to install it.

Use it as a pre-commit hook in your flows repo

If you keep exported contact flow JSON in a git repo, connect-flow can run as a pre-commit hook and fail the commit whenever a flow's layout has drifted, so the tidy layout is the state the repo is always in, rather than something someone remembers to run.

This is set up in the repo that holds the flow JSON, not here. connect-flow ships a .pre-commit-hooks.yaml, which is the manifest offering the hook to other repos to install.

Add this to your flows repo's own .pre-commit-config.yaml:

- repo: https://gitlab.com/mludvig/connect-flow
  rev: master
  hooks:
    - id: connect-flow
      files: ^contact-flows/.*\.json$
      ## Uncomment to enable in-place re-formatting
      # args: [prettify, --in-place]

Set files: to wherever your flow JSON actually lives. The hook's default pattern matches every .json in the repo, which will also feed it package.json and friends.

By default the hook runs connect-flow prettify --check: it writes nothing and fails the commit if a flow file would be reformatted — well suited to a CI gate too.

Exit codes

code meaning
0 success — nothing changed, or a write succeeded
1 --check/diff/lint only: at least one target would change, differs, or has findings
2 flow safety violation (would-be behavioural change), or a target failed to parse / isn't a contact flow
3 bad usage (bad flags, missing args, a directory passed as a target, a view where a flow is expected)
4 an AWS/remote failure — credentials, access denied, a sha256 conflict, or a resource that doesn't exist

When targets fail differently, the most serious code wins: 2, then 3, 4, 1.

Going deeper

  • docs/design.md — why Graphviz dot rather than a hand-rolled layout engine, with the edge-crossing measurements that settled it.
  • docs/data-model.md — how a flow's ports and happy/error paths are modelled.
  • docs/safety.md — exactly what guarantees "never changes behaviour", and how each one is enforced rather than merely asserted.

License

MIT — see LICENSE.

Metadata

Release files for connect-flow 0.3.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for connect-flow 0.3.1
File Size Uploaded Provenance
connect_flow-0.3.1.tar.gz 251.5 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for connect-flow 0.3.1
File Interpreter ABI Platform
connect_flow-0.3.1-py3-none-any.whl Python 3 none any Details

Total release size: 360.3 kB

Release files / connect_flow-0.3.1.tar.gz

Download URL connect_flow-0.3.1.tar.gz
Size 251.5 kB
Tags Source
SHA-256 checksum
How to use checksums
6886a9d44a8bda841e6cd022846573830920464e9663a09bf0ab3c097c8d3326
BLAKE2b-256 checksum
How to use checksums
5c2048bd0ad4ee326fe55978a36579866ce81c3f101562d405ac752de023d033
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.12.12

Release files / connect_flow-0.3.1-py3-none-any.whl

Download URL connect_flow-0.3.1-py3-none-any.whl
Size 108.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
95ac7009614c099089e1fec2f583da413f534c1609e890503cdb7ee912f4ed6f
BLAKE2b-256 checksum
How to use checksums
e039348943ba61dc748ff7ee15923d27a3563716480657e9e461df2838ce9c5b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.12.12

Security for connect-flow 0.3.1

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.3.1 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page