Skip to main content

Copy a flow with the CLI

The CLI can serialize a flow into a portable bundle and recreate it somewhere else. dci export-cloudflow-flow writes the flow — plus every flow it reaches through subflow nodes — as a single JSON document, and dci import-cloudflow-flow creates those flows in the tenant you're authenticated against.

The bundle is tenant-neutral and credential-free. Connections, Datastore tables, and global variables travel as named requirements that you rebind to real resources at import time. Nothing else about the source tenant comes along: no credentials, no tenant identifiers, no schedules, and no execution history.

This tutorial uses a flow named After-Hours Instance Sweep — a scheduled flow that stops non-production EC2 instances — but every command works the same for any flow.

Note

Export and import need a recent CLI. Run dci update first, and see Keep it up to date if the commands don't appear in dci --help.

Goal and objectives

  • Goal: Move a working flow from one tenant to another without rebuilding it node by node in the console.

  • Objectives: In this tutorial, you'll learn how to:

    • Export a flow to a bundle file and read what's inside it.

    • Validate an import before it writes anything, using a dry run.

    • Bind the bundle's requirements to connections and tables in the target tenant.

    • Import the flow and finish it in the console.

Before you begin

  1. Install the CLI and sign in. Confirm both with dci validate.

  2. Make sure your DoiT account has the CloudFlow Editor or CloudFlow Manager permission in each tenant you export from or import into. See CloudFlow permissions.

  3. Optionally install jq, used below to read parts of a bundle and to add import options.

Find the flow ID

Export takes a flow ID, not a flow name. The flow list shows IDs when you ask for them:

dci list-cloudflows --fields id,name
╔══════════════════════╤══════════════════════════════════════════════════════╗
║ id │ name ║
╟━━━━━━━━━━━━━━━━━━━━━━┼━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╢
║ q8RR0Ea9KqbMtgAxKWiW │ Datastore SQL Testing ║
║ G2zdE9inbvwxBc38FCVc │ Draft Delete unused Oracle Cloud reserved public IPs ║
║ EBnC9oHaZpcH3hgWGoSp │ After-Hours Instance Sweep ║
╚══════════════════════╧══════════════════════════════════════════════════════╝

The ID is also the last segment of the flow's URL when you open it in the console.

Export the flow

Export writes the bundle to standard output, so redirect it to a file:

dci export-cloudflow-flow EBnC9oHaZpcH3hgWGoSp > after-hours-sweep.json

The result is one JSON document describing the whole flow. Its top level tells you what you got:

jq '{kind, schemaVersion, rootFlow, requirements, flows: [.flows[] | {key, name, nodes: (.nodes | length)}]}' after-hours-sweep.json
{
"kind": "cloudflow.doit.com/FlowBundle",
"schemaVersion": 1,
"rootFlow": "EBnC9oHaZpcH3hgWGoSp",
"requirements": {},
"flows": [
{
"key": "EBnC9oHaZpcH3hgWGoSp",
"name": "After-Hours Instance Sweep",
"nodes": 7
}
]
}
  • rootFlow: The flow you exported. Any subflows it calls appear alongside it in flows.

  • requirements: The tenant-scoped resources the flow needs. This flow declares none; Bind requirements covers a flow that does.

  • flows: The flows themselves — nodes, transitions, parameters, and variable definitions.

Variable values travel by default. To keep them out of the bundle while preserving each variable's name, type, and required-ness, export with --include-variable-values=false.

Policy and Slack-channel references can't travel between tenants. Export records them as unsupported references, and import flags the nodes that use them as incomplete so you can repoint them in the console.

Read the bundle as YAML

Bundles are meant for machines, but YAML is easier to skim when you want to see how a flow is put together. Page through it with less, or take the first lines to get your bearings:

dci export-cloudflow-flow EBnC9oHaZpcH3hgWGoSp --output yaml | head -13
exportedAt: "2026-08-25T17:58:31.696566271Z"
flows:
- description: Stops non-prod instances on weekday evenings. Skips production. Flags
untagged instances to their owners.
firstNode: 4e10b33c-5ff9-4667-b8d9-594317c55e7d
key: EBnC9oHaZpcH3hgWGoSp
name: After-Hours Instance Sweep
nodes:
- key: 0421485f-0a3a-4f64-a587-fe1b0fb361e0
name: 'Production: skip'
parameters:
outputs: []
type: flowOutput

Every node in the flow appears under nodes, keyed by the internal IDs that firstNode and each transition point to. The same view works in any output format, and --fields narrows it further.

Note

Use YAML for reading only. Import accepts JSON, so keep the file you plan to import in the default format.

Dry-run the import

Always validate before importing. A dry run writes nothing and returns the import plan:

dci import-cloudflow-flow --dry-run < after-hours-sweep.json
{
"action": {
"command": "import-cloudflow-flow",
"status": "simulated",
"dry_run": true
},
"result": {
"errors": [],
"flowsToCreate": [
{
"incompleteNodeCount": 0,
"key": "EBnC9oHaZpcH3hgWGoSp",
"name": "After-Hours Instance Sweep",
"nodeCount": 7
}
],
"requirements": [],
"valid": true
}
}

valid: true with an empty errors array and incompleteNodeCount: 0 means the import would land complete. Validation reports every problem at once, so a rejected bundle tells you everything to fix in one pass rather than one error per attempt.

The bundle goes in on standard input exactly as exported — the CLI wraps it in the request shape the API expects.

Bind requirements

A flow that uses a connection, a Datastore table, or a global variable declares it as a requirement, because those resources exist in one tenant only. Here are the requirements of a flow that deletes unused Oracle Cloud reserved public IPs:

dci export-cloudflow-flow G2zdE9inbvwxBc38FCVc > oci-flow.json
jq '.requirements' oci-flow.json
{
"connections": [
{
"key": "omni-oci",
"name": "omni-oci",
"provider": "Oracle",
"usedByNodes": [
"G2zdE9inbvwxBc38FCVc/delete-resources",
"G2zdE9inbvwxBc38FCVc/list-resources"
]
}
]
}

The dry run resolves each requirement against the target tenant and proposes candidates:

dci import-cloudflow-flow --dry-run < oci-flow.json | jq '.result'
{
"errors": [],
"flowsToCreate": [
{
"incompleteNodeCount": 2,
"key": "G2zdE9inbvwxBc38FCVc",
"name": "Draft Delete unused Oracle Cloud reserved public IPs",
"nodeCount": 4
}
],
"requirements": [
{
"candidates": [
{
"id": "CWGMz3ndg9WihPZnUXAi",
"name": "omni-oci",
"provider": "Oracle"
}
],
"consequenceIfUnbound": "referencing nodes are imported with the connection cleared and flagged incomplete",
"key": "omni-oci",
"resolution": "suggested",
"section": "connections"
}
],
"valid": true
}

Each requirement carries a resolution:

  • bound: You supplied a binding, and it checks out.

  • suggested: The target tenant has a plausible match, listed under candidates. A suggestion is not applied automatically — bind it explicitly.

  • willCreate: Import creates the resource. Global variables resolve this way, as do Datastore tables when you pass options.createMissingTables.

  • unbound: Nothing matched. The import still succeeds, but consequenceIfUnbound tells you what you'll have to fix afterwards — here, two nodes arriving with their connection cleared.

Bind a requirement by sending the full request shape instead of a bare bundle. The keys come from the plan, and the IDs from the target tenant (dci list-cloudflow-connections):

jq '{bundle: ., bindings: {connections: {"omni-oci": "CWGMz3ndg9WihPZnUXAi"}}}' oci-flow.json \
| dci import-cloudflow-flow --dry-run | jq '.result.requirements[0]'
{
"boundTo": "CWGMz3ndg9WihPZnUXAi",
"consequenceIfUnbound": "referencing nodes are imported with the connection cleared and flagged incomplete",
"key": "omni-oci",
"resolution": "bound",
"section": "connections"
}

The same request shape carries import options. options.createMissingTables: true creates missing Datastore tables from the schemas in the bundle (structure only, never row data), and options.namePrefix prefixes the name of every flow the import creates:

jq '{bundle: ., options: {namePrefix: "Copy of "}}' after-hours-sweep.json \
| dci import-cloudflow-flow --dry-run | jq '.result.flowsToCreate[0]'
{
"incompleteNodeCount": 0,
"key": "EBnC9oHaZpcH3hgWGoSp",
"name": "Copy of After-Hours Instance Sweep",
"nodeCount": 7
}

Import the flow

When the plan looks right, drop --dry-run and add an idempotency key. Generate a fresh one per import:

dci import-cloudflow-flow --idempotency-key "$(uuidgen)" < after-hours-sweep.json
{
"flows": [
{
"id": "YT57P7OhP2nLwZMo9QSw",
"key": "EBnC9oHaZpcH3hgWGoSp",
"name": "After-Hours Instance Sweep",
"status": "draft"
}
]
}

key is the flow's ID in the bundle, and id is the new flow in the target tenant. Import is create-only: every call creates new draft flows with new IDs, and it never updates an existing flow. Resubmitting the same idempotency key returns this same response instead of creating a second copy, which makes an interrupted import safe to retry.

Imported flows arrive as drafts. Nothing runs, and no schedule activates, until someone publishes them in the console.

Import into another tenant

Copying between tenants is the same two commands with a customer context on each one — export from the source, import into the target:

dci export-cloudflow-flow EBnC9oHaZpcH3hgWGoSp -D <source-customer-id> > after-hours-sweep.json
dci import-cloudflow-flow --idempotency-key "$(uuidgen)" -D <target-customer-id> < after-hours-sweep.json

Requirements are where cross-tenant copies need attention: a connection that resolved to suggested at home may be unbound in the target tenant. Dry-run against the target before the real import, and bind what the plan lists.

Note

CloudFlow commands need the customer ID in -D, not a domain or URL display name. Other commands accept all three.

What's next

  1. Open the imported flow in the console and confirm every node is complete. Nodes whose requirements stayed unbound are flagged for you.

  2. Reattach anything that can't travel in a bundle: connections you left unbound, Slack channels, and policy references.

  3. Set the schedule or trigger you want in the target tenant, then publish the flow. See Manage a flow.

See also