Backup Script - All flows and split them

Since I’m not really happy with the backup function in homey, I did ask Gemini to help create a script to get all flows and split them up, in separated folders/files as backup.

Just run the “api script” in the api-playground:
Homey Developer Tools.

Then run the local “split script” file in cli/terminal, like this: “python3 split_flows.py” to split them in to standard and advanced separated flows: folders/files.

Update: 4
Made the api script output flow names more unique, in the script only.
So the split script is change aswell

This Api Script will generate a file that saves to your computer, with all the flow output in a .txt file named “all_flows.txt”

Now, all you need to do is run the split script file in the same folder as the saved file “all_flows.txt”
There is a long name version, and a short name version

Long name: name + id in the flow name
Short name: name only in the flow name

Api Script

async function exportHomeyFlows() {
    try {
        let output = "";
        
        // Fetch folders and flows
        const flowFolders = await Homey.flow.getFlowFolders().catch(() => ({}));
        const flows = await Homey.flow.getFlows();
        const advancedFlows = await Homey.flow.getAdvancedFlows();
        
        // Helper function to robustly resolve nested folder paths
        function getFolderPath(folderId) {
            if (!folderId) return "Root";
            
            let path = [];
            let currentId = folderId;
            let safetyCounter = 0; // Prevent infinite loops if data is circular
            
            while (currentId && flowFolders[currentId] && safetyCounter < 10) {
                const folder = flowFolders[currentId];
                path.unshift(folder.name);
                
                // Check various possible parent property names used in different Homey API versions
                currentId = folder.folder || folder.parent || folder.parentFolder;
                safetyCounter++;
            }
            
            // If the folder ID exists in our list but wasn't caught above, or couldn't resolve
            if (path.length === 0 && flowFolders[folderId]) {
                return flowFolders[folderId].name;
            }
            
            return path.length > 0 ? path.join(" / ") : "Root";
        }
        
        output += `--- FOUND ${Object.keys(flows).length} STANDARD FLOWS ---\n\n`;
        
        // Output Standard Flows
        for (const id in flows) {
            const flow = flows[id];
            
            const folderName = getFolderPath(flow.folder);
            const orderedFlow = { type: "standard", folderName: folderName, ...flow };
            
            const safeFolderName = folderName.replace(/[/\\?%*:|"<>]/g, '-');
            const safeFlowName = flow.name.replace(/[/\\?%*:|"<>]/g, '-');
            
            output += `_.:._ START_FLOW: [${safeFolderName}] ${safeFlowName} (${flow.id}) _.:._\n`;
            output += JSON.stringify(orderedFlow, null, 2) + "\n";
            output += `_.:._ END_FLOW _.:._\n\n`;
        }

        output += `--- FOUND ${Object.keys(advancedFlows).length} ADVANCED FLOWS ---\n\n`;

        // Output Advanced Flows
        for (const id in advancedFlows) {
            const aflow = advancedFlows[id];
            
            const folderName = getFolderPath(aflow.folder);
            const orderedAflow = { type: "advanced", folderName: folderName, ...aflow };
            
            const safeFolderName = folderName.replace(/[/\\?%*:|"<>]/g, '-');
            const safeFlowName = aflow.name.replace(/[/\\?%*:|"<>]/g, '-');
            
            output += `_.:._ START_ADVANCED_FLOW: [${safeFolderName}] ${safeFlowName} (${aflow.id}) _.:._\n`;
            output += JSON.stringify(orderedAflow, null, 2) + "\n";
            output += `_.:._ END_ADVANCED_FLOW _.:._\n\n`;
        }
        
        output += "--- EXPORT COMPLETE ---";
        
        // --- AUTO DOWNLOAD MECHANISM ---
        const blob = new Blob([output], { type: 'text/plain;charset=utf-8' });
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        a.download = `all_flows.txt`;
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
        URL.revokeObjectURL(url);
        
        return "File download triggered successfully!";
        
    } catch (error) {
        return "Error fetching flows: " + error.message;
    }
}

exportHomeyFlows();

Split Script : Long Name

import json
import os
import re

input_file = "all_flows.txt"

if not os.path.exists(input_file):
    print(f"Error: {input_file} not found.")
    exit(1)

STD_DIR = "Standard_Flows"
ADV_DIR = "Advanced_Flows"

os.makedirs(STD_DIR, exist_ok=True)
os.makedirs(ADV_DIR, exist_ok=True)

with open(input_file, "r", encoding="utf-8") as f:
    content = f.read()

content = content.replace("_.:._ START_ADVANCED_FLOW: ", "_.:._ START_FLOW: ")
blocks = content.split("_.:._ START_FLOW: ")
count = 0

# Matches: [Folder] Name (ID)
header_pattern = re.compile(r"^\[(.*?)\]\s+(.*?)\s+\(([0-9a-fA-F-]+)\)$")


for block in blocks:
    if "_.:._" not in block:
        continue
        
    header, _, body = block.partition("\n")
    raw_header = header.replace("_.:._", "").strip()
    
    json_str, _, _ = body.partition("_.:._")
    json_str = json_str.strip()
    
    if not json_str:
        continue
        
    try:
        flow_data = json.loads(json_str)
        flow_type = flow_data.get("type", "unknown")
        
        # Use the uncorrupted hierarchical path stored inside the JSON object
        folder_name = flow_data.get("folderName", "Root")
        
        match = header_pattern.match(raw_header)
        if match:
            _, flow_name, flow_id = match.groups()
        else:
            flow_name = raw_header
            flow_id = flow_data.get("id", "unknown")
        
        if flow_type == "standard":
            base_dir = STD_DIR
            extension = ".homeystdflow"
        elif flow_type == "advanced":
            base_dir = ADV_DIR
            extension = ".homeyadvflow"
        else:
            base_dir = "."
            extension = ".json"
            
        # Split the hierarchical path by " / " to build true nested subfolders
        folder_parts = folder_name.split(" / ")
        clean_parts = [
            "".join(c if c.isalnum() or c in "._-" else "_" for c in part).strip("_")
            for part in folder_parts
        ]
        clean_parts = [p if p else "Root" for p in clean_parts]
        
        # Build the nested directory path safely
        target_dir = os.path.join(base_dir, *clean_parts)
        os.makedirs(target_dir, exist_ok=True)
        
        clean_name = "".join(c if c.isalnum() or c in "._-" else "_" for c in flow_name).strip("_")
        
        # Base filename using name
        filename = os.path.join(target_dir, f"{clean_name}_{flow_id}{extension}")
        
        # Fallback conflict resolution: if filename exists, append incremental counter
        counter = 1
        base_filename = filename
        while os.path.exists(filename):
            counter += 1
            name_part, ext_part = os.path.splitext(base_filename)
            filename = f"{name_part}_{counter}{ext_part}"
        
        with open(filename, "w", encoding="utf-8") as out_f:
            json.dump(flow_data, out_f, indent=2, ensure_ascii=False)
            
        print(f"Saved: {filename}")
        count += 1
    except json.JSONDecodeError as e:
        print(f"Failed to parse flow '{raw_header}': {e}")
    except Exception as e:
        continue

print(f"\nDone! Successfully processed {count} flows.")

Split Script : Short Name

import json
import os
import re

input_file = "all_flows.txt"

if not os.path.exists(input_file):
    print(f"Error: {input_file} not found.")
    exit(1)

STD_DIR = "Standard_Flows"
ADV_DIR = "Advanced_Flows"

os.makedirs(STD_DIR, exist_ok=True)
os.makedirs(ADV_DIR, exist_ok=True)

with open(input_file, "r", encoding="utf-8") as f:
    content = f.read()

content = content.replace("_.:._ START_ADVANCED_FLOW: ", "_.:._ START_FLOW: ")
blocks = content.split("_.:._ START_FLOW: ")
count = 0

# Matches: [Folder] Name (ID)
header_pattern = re.compile(r"^\[(.*?)\]\s+(.*?)\s+\(([0-9a-fA-F-]+)\)$")


for block in blocks:
    if "_.:._" not in block:
        continue
        
    header, _, body = block.partition("\n")
    raw_header = header.replace("_.:._", "").strip()
    
    json_str, _, _ = body.partition("_.:._")
    json_str = json_str.strip()
    
    if not json_str:
        continue
        
    try:
        flow_data = json.loads(json_str)
        flow_type = flow_data.get("type", "unknown")
        
        # Use the uncorrupted hierarchical path stored inside the JSON object
        folder_name = flow_data.get("folderName", "Root")
        
        match = header_pattern.match(raw_header)
        if match:
            _, flow_name, flow_id = match.groups()
        else:
            flow_name = raw_header
            flow_id = flow_data.get("id", "unknown")
        
        if flow_type == "standard":
            base_dir = STD_DIR
            extension = ".homeystdflow"
        elif flow_type == "advanced":
            base_dir = ADV_DIR
            extension = ".homeyadvflow"
        else:
            base_dir = "."
            extension = ".json"
            
        # Split the hierarchical path by " / " to build true nested subfolders
        folder_parts = folder_name.split(" / ")
        clean_parts = [
            "".join(c if c.isalnum() or c in "._-" else "_" for c in part).strip("_")
            for part in folder_parts
        ]
        clean_parts = [p if p else "Root" for p in clean_parts]
        
        # Build the nested directory path safely
        target_dir = os.path.join(base_dir, *clean_parts)
        os.makedirs(target_dir, exist_ok=True)
        
        clean_name = "".join(c if c.isalnum() or c in "._-" else "_" for c in flow_name).strip("_")
        
        # Base filename using name
        filename = os.path.join(target_dir, f"{clean_name}{extension}")
        
        # Fallback conflict resolution: if filename exists, append incremental counter
        counter = 1
        base_filename = filename
        while os.path.exists(filename):
            counter += 1
            name_part, ext_part = os.path.splitext(base_filename)
            filename = f"{name_part}_{counter}{ext_part}"
        
        with open(filename, "w", encoding="utf-8") as out_f:
            json.dump(flow_data, out_f, indent=2, ensure_ascii=False)
            
        print(f"Saved: {filename}")
        count += 1
    except json.JSONDecodeError as e:
        print(f"Failed to parse flow '{raw_header}': {e}")
    except Exception as e:
        continue

print(f"\nDone! Successfully processed {count} flows.")

I do not understand much of scripts so i have ChatGPT checked it. And she thinks it should work.
But is it that flows with the same name overwrite each other the way this script works?
Her idea was to add the flow ID to the flow name so the same flow names is no problem.

Thanks for your work.

This will overwrite if you have a file with the same name yes.

The output file will be like this.

Name of the flow in homey ex:
An advanced flow named “Adv_Sensors” then the file will be named “Adv_Sensors.homeyadvflow”

The app Flow Version History might be useful as well, it does this automatically and keeps multiple revisions

But this script get all flows at the same time, and split them in to different folders/files. You app is savin one by one. Just like a normal export.

But please use this to make an app that get all flows and split them and save them

So if i have multiple flows with the same name, let’s say “turn on light”, in different folders, at the end of the backup i only have one flow left with the name “turn on light”.

It seems like that yes. I did not think of that.
I will check that later. Or if someone like like to

(post deleted by author)

(post deleted by author)

Update: 3
This Api Script will generate a file that downloads to your computer with all the flow output in a .txt file named “all_flows.txt”

Now, all you need to do is run the split script.

https://community.homey.app/t/backup-script-all-flows-and-split-them

@Mike-Nono: I would love to have a script that can save all my scripts as javascript text files. Think it’s possible?

Homey-Script Backup: It has it’s own thread

https://community.homey.app/t/backup-script-for-homey-script/159157

Tried a restore of some flows: working. :+1:

FlowSplit skips some flows because of error: failed to parse flow: … : Unterminated string starting at: …
Happens a few times. Have to dig into it.

Cause is: in some code I use === or === END too.

What do you mean?
Are you using === and === END in the flows in homey?

Can you send me the saved “all_flows.txt” you get from the api script?
You should not edit the “all_flows.txt” file. just run " python3 split.py " file in the terminal/cli

@SingKT
Did some change to the script, so it will not collide if using “===” in the flow like you do. Test it please

https://community.homey.app/t/backup-script-all-flows-and-split-them

Looks good. Error is gone.

Great.
If all is running ok and no error.

That’s what we all like.

Thanks :slight_smile: