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:
After — the happy path runs left to right along the top, error branches drop below it:
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.ActionMetadatakey order — not behavioural at runtime, but it's tracked anyway in the digest (as a separateActionMetadataOrderfield), because the writer must prove its own rewrite didn't disturb it. Passdiff --functionalto leave it, and the rest of the designer layer, out of a comparison between two independently-saved flows.- Everything else —
Parameters, blockType,StartAction,Settings, annotations' text, and any unrecognised top-levelMetadatakey are all carried into the canonical form verbatim. The digest is sensitive to every one of them by design; seedocs/safety.mdfor 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:
INSTANCEis an instance id or alias (exact, case-sensitive match).KINDis the ARN's own resource type —contact-flow(the default when omitted;flowis accepted too),flow-moduleorview— so it can be copy-pasted from an ARN.NAME-OR-IDis the resource's id, or its name resolved viaList*.INSTANCE/view/aws/NAMEis 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 Graphvizdotrather 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)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| connect_flow-0.3.1.tar.gz | 251.5 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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.