{ "kind": "open-flow-workflow", "version": 1, "name": "Back Up n8n Workflows to GitHub", "content": { "modelVersion": 14, "document": { "bindings": {}, "graph": { "nodes": { "n_b83a4a145b748d06": { "name": "Run on demand", "kind": "start", "inputDefs": [] }, "n_82d7b83ed7055507": { "name": "Every day at 20:11 UTC", "kind": "cron", "cronTimes": [ { "type": "cron", "expression": "11 20 * * *", "timezone": "UTC" } ] }, "n_67261ecca7a485de": { "inputs": { "owner": { "kind": "value", "value": "YOUR_GITHUB_OWNER" }, "repo": { "kind": "value", "value": "YOUR_GITHUB_REPO" }, "path": { "kind": "value", "value": "YOUR_EXISTING_BACKUP_DIRECTORY" } }, "name": "Choose the backup repository", "kind": "task", "task": { "inputs": [ { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "owner" }, { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "repo" }, { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "path" } ], "outputs": [ { "jsonSchema": { "type": "string", "minLength": 1, "description": "The repository owner." }, "nullable": false, "handle": "owner" }, { "jsonSchema": { "type": "string", "minLength": 1, "description": "The repository name." }, "nullable": false, "handle": "repo" }, { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "path" } ], "name": "Choose the backup repository", "moduleId": "n_9d12cfd2f310fe17", "capabilities": [ { "kind": "connector", "mode": "independent", "actions": [] } ] } }, "n_feca466208e618d9": { "inputs": { "previous": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_feca466208e618d9", "output": "state" } ] } }, "name": "Collect n8n workflow pages", "kind": "task", "task": { "inputs": [ { "jsonSchema": { "type": "object", "additionalProperties": true }, "nullable": true, "handle": "previous" } ], "outputs": [ { "jsonSchema": { "type": "object", "additionalProperties": true }, "nullable": false, "handle": "state" }, { "jsonSchema": { "type": "boolean" }, "nullable": false, "handle": "more" } ], "name": "Collect n8n workflow pages", "moduleId": "n_c81cab905d3de8aa", "capabilities": [ { "kind": "connector", "mode": "independent", "actions": [ { "action": "n8n.list_workflows" } ] } ] } }, "n_de4ba23e5107ba2a": { "inputs": {}, "name": "More workflow pages?", "kind": "condition", "matchMode": "first", "cases": [ { "output": "more", "groups": [ { "expressions": [ { "left": { "kind": "source", "source": { "kind": "node", "nodeId": "n_feca466208e618d9", "output": "more" } }, "operator": "isTrue" } ] } ] } ] }, "n_e6651b3e191f5263": { "inputs": { "owner": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_67261ecca7a485de", "output": "owner" } ] }, "repo": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_67261ecca7a485de", "output": "repo" } ] }, "path": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_67261ecca7a485de", "output": "path" } ] } }, "name": "List the backup directory", "kind": "task", "task": { "inputs": [ { "description": "The repository owner.", "jsonSchema": { "type": "string", "minLength": 1, "description": "The repository owner." }, "nullable": false, "handle": "owner" }, { "description": "The repository name.", "jsonSchema": { "type": "string", "minLength": 1, "description": "The repository name." }, "nullable": false, "handle": "repo" }, { "description": "The directory path.", "jsonSchema": { "type": "string", "description": "The directory path." }, "nullable": true, "handle": "path", "value": null }, { "description": "The branch, tag, or commit SHA.", "jsonSchema": { "type": "string", "description": "The branch, tag, or commit SHA." }, "nullable": true, "handle": "ref", "value": null } ], "outputs": [ { "description": "The directory entries.", "jsonSchema": { "type": "array", "items": { "type": "object", "properties": { "type": { "type": "string", "enum": [ "file", "dir", "symlink", "submodule" ], "description": "The content entry type." }, "name": { "type": "string", "description": "The file or directory name." }, "path": { "type": "string", "description": "The file path." }, "sha": { "type": "string", "description": "The content SHA." }, "size": { "type": "integer", "description": "The file size in bytes." }, "html_url": { "type": [ "string", "null" ], "description": "The content URL." }, "download_url": { "type": [ "string", "null" ], "description": "The download URL." } }, "required": [ "type", "name", "path", "sha" ], "additionalProperties": true, "description": "A GitHub content entry record." }, "description": "The directory entries." }, "nullable": false, "handle": "entries" } ], "name": "List the backup directory", "executor": { "kind": "connector", "action": "github.list_directory_contents" } } }, "n_18646e09eecfe2ea": { "inputs": { "state": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_feca466208e618d9", "output": "state" } ] }, "entries": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_e6651b3e191f5263", "output": "entries" } ] }, "owner": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_67261ecca7a485de", "output": "owner" } ] }, "repo": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_67261ecca7a485de", "output": "repo" } ] }, "path": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_67261ecca7a485de", "output": "path" } ] } }, "name": "Check backup paths", "kind": "task", "task": { "inputs": [ { "jsonSchema": { "type": "object", "additionalProperties": true }, "nullable": false, "handle": "state" }, { "jsonSchema": { "type": "array", "items": { "type": "object", "properties": { "type": { "type": "string", "enum": [ "file", "dir", "symlink", "submodule" ], "description": "The content entry type." }, "name": { "type": "string", "description": "The file or directory name." }, "path": { "type": "string", "description": "The file path." }, "sha": { "type": "string", "description": "The content SHA." }, "size": { "type": "integer", "description": "The file size in bytes." }, "html_url": { "type": [ "string", "null" ], "description": "The content URL." }, "download_url": { "type": [ "string", "null" ], "description": "The download URL." } }, "required": [ "type", "name", "path", "sha" ], "additionalProperties": true, "description": "A GitHub content entry record." }, "description": "The directory entries." }, "nullable": false, "handle": "entries" }, { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "owner" }, { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "repo" }, { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "path" } ], "outputs": [ { "jsonSchema": { "type": "array", "items": { "type": "object", "additionalProperties": true } }, "nullable": false, "handle": "items" } ], "name": "Check backup paths", "moduleId": "n_cddeeccc3fd9dfa1", "capabilities": [ { "kind": "connector", "mode": "independent", "actions": [] } ] } }, "n_5ce9c952cfa924ef": { "inputs": { "items": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_18646e09eecfe2ea", "output": "items" } ] }, "previous": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_5ce9c952cfa924ef", "output": "state" } ] } }, "name": "Prepare the next workflow", "kind": "task", "task": { "inputs": [ { "jsonSchema": { "type": "array", "items": { "type": "object", "additionalProperties": true } }, "nullable": false, "handle": "items" }, { "jsonSchema": { "type": "object", "additionalProperties": true }, "nullable": true, "handle": "previous" } ], "outputs": [ { "jsonSchema": { "type": "object", "additionalProperties": true }, "nullable": false, "handle": "state" }, { "jsonSchema": { "type": "boolean" }, "nullable": false, "handle": "more" }, { "jsonSchema": { "type": "object", "additionalProperties": true }, "nullable": false, "handle": "item" }, { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "id" }, { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "path" }, { "jsonSchema": { "type": "boolean" }, "nullable": false, "handle": "exists" } ], "name": "Prepare the next workflow", "moduleId": "n_258796b30312984e", "capabilities": [ { "kind": "connector", "mode": "independent", "actions": [] } ] } }, "n_e96f71a88d3c74f3": { "inputs": {}, "name": "More workflows?", "kind": "condition", "matchMode": "first", "cases": [ { "output": "yes", "groups": [ { "expressions": [ { "left": { "kind": "source", "source": { "kind": "node", "nodeId": "n_5ce9c952cfa924ef", "output": "more" } }, "operator": "isTrue" } ] } ] } ] }, "n_40aea1c9f044dd07": { "inputs": { "id": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_5ce9c952cfa924ef", "output": "id" } ] } }, "name": "Read the n8n workflow", "kind": "task", "task": { "inputs": [ { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "id" } ], "outputs": [ { "jsonSchema": { "type": "object", "additionalProperties": true }, "nullable": false, "handle": "workflow" } ], "name": "Read the n8n workflow", "moduleId": "n_e90aaa1a06d990f2", "capabilities": [ { "kind": "connector", "mode": "independent", "actions": [ { "action": "n8n.get_workflow" } ] } ] } }, "n_c659263f218404ec": { "inputs": { "workflow": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_40aea1c9f044dd07", "output": "workflow" } ] }, "item": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_5ce9c952cfa924ef", "output": "item" } ] }, "owner": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_67261ecca7a485de", "output": "owner" } ] }, "repo": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_67261ecca7a485de", "output": "repo" } ] } }, "name": "Compare the existing backup", "kind": "task", "task": { "inputs": [ { "jsonSchema": { "type": "object", "additionalProperties": true }, "nullable": false, "handle": "workflow" }, { "jsonSchema": { "type": "object", "additionalProperties": true }, "nullable": false, "handle": "item" }, { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "owner" }, { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "repo" } ], "outputs": [ { "jsonSchema": { "type": "boolean" }, "nullable": false, "handle": "changed" }, { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "content" }, { "jsonSchema": { "type": [ "string", "null" ] }, "nullable": true, "handle": "sha" } ], "name": "Compare the existing backup", "moduleId": "n_27fb6e170ffcba3e", "capabilities": [ { "kind": "connector", "mode": "independent", "actions": [ { "action": "github.get_file_contents" } ] } ] } }, "n_d89c5e9145ba7af5": { "inputs": {}, "name": "Is the workflow new or changed?", "kind": "condition", "matchMode": "first", "cases": [ { "output": "yes", "groups": [ { "expressions": [ { "left": { "kind": "source", "source": { "kind": "node", "nodeId": "n_c659263f218404ec", "output": "changed" } }, "operator": "isTrue" } ] } ] } ] }, "n_c86a3d3df12c6915": { "inputs": { "owner": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_67261ecca7a485de", "output": "owner" } ] }, "repo": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_67261ecca7a485de", "output": "repo" } ] }, "path": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_5ce9c952cfa924ef", "output": "path" } ] }, "content": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_c659263f218404ec", "output": "content" } ] }, "sha": { "kind": "sources", "sources": [ { "kind": "node", "nodeId": "n_c659263f218404ec", "output": "sha" } ] } }, "name": "Save the GitHub backup", "kind": "task", "task": { "inputs": [ { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "owner" }, { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "repo" }, { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "path" }, { "jsonSchema": { "type": "string" }, "nullable": false, "handle": "content" }, { "jsonSchema": { "type": [ "string", "null" ] }, "nullable": true, "handle": "sha" } ], "outputs": [ { "jsonSchema": { "type": "object", "additionalProperties": true }, "nullable": false, "handle": "result" } ], "name": "Save the GitHub backup", "moduleId": "n_210cf4e0b16aec4e", "capabilities": [ { "kind": "connector", "mode": "independent", "actions": [ { "action": "github.create_or_update_file" } ] } ] } } }, "edges": [ { "source": "n_b83a4a145b748d06", "target": "n_67261ecca7a485de" }, { "source": "n_82d7b83ed7055507", "target": "n_67261ecca7a485de" }, { "source": "n_67261ecca7a485de", "target": "n_feca466208e618d9" }, { "source": "n_feca466208e618d9", "target": "n_de4ba23e5107ba2a" }, { "source": "n_de4ba23e5107ba2a", "target": "n_feca466208e618d9", "sourceHandle": "more" }, { "source": "n_de4ba23e5107ba2a", "target": "n_e6651b3e191f5263", "sourceHandle": "otherwise" }, { "source": "n_e6651b3e191f5263", "target": "n_18646e09eecfe2ea" }, { "source": "n_18646e09eecfe2ea", "target": "n_5ce9c952cfa924ef" }, { "source": "n_5ce9c952cfa924ef", "target": "n_e96f71a88d3c74f3" }, { "source": "n_e96f71a88d3c74f3", "target": "n_40aea1c9f044dd07", "sourceHandle": "yes" }, { "source": "n_40aea1c9f044dd07", "target": "n_c659263f218404ec" }, { "source": "n_c659263f218404ec", "target": "n_d89c5e9145ba7af5" }, { "source": "n_d89c5e9145ba7af5", "target": "n_c86a3d3df12c6915", "sourceHandle": "yes" }, { "source": "n_c86a3d3df12c6915", "target": "n_5ce9c952cfa924ef" }, { "source": "n_d89c5e9145ba7af5", "target": "n_5ce9c952cfa924ef", "sourceHandle": "otherwise" } ] } }, "modules": { "n_9d12cfd2f310fe17": { "name": "Choose the backup repository", "imports": [], "source": "export default function config({owner,repo,path}){for(const [k,v] of Object.entries({owner,repo,path}))if(typeof v!=='string'||!v.trim()||v.startsWith('YOUR_'))throw Error('Configure GitHub '+k);if(path.startsWith('/')||path.endsWith('/')||path.split('/').some(x=>!x||x==='.'||x==='..'))throw Error('Invalid existing directory');return {owner,repo,path}}\n" }, "n_c81cab905d3de8aa": { "name": "Collect n8n workflow pages", "imports": [], "source": "export default async function pages({previous},context){const s=previous??{items:[],cursor:null,seen:[]};const r=await context.actions.call('n8n.list_workflows',{limit:100,...(s.cursor?{cursor:s.cursor}:{})});if(!Array.isArray(r?.data))throw Error('Invalid n8n workflow page');const c=r.nextCursor;if(c!==null&&(typeof c!=='string'||!c||s.seen.includes(c)))throw Error('Invalid or repeated workflow cursor');return {state:{items:s.items.concat(r.data),cursor:c,seen:c?[...s.seen,c]:s.seen},more:c!==null}}\n" }, "n_cddeeccc3fd9dfa1": { "name": "Check backup paths", "imports": [], "source": "export default function plan({state,entries,owner,repo,path}){for(const [k,v] of Object.entries({owner,repo,path}))if(typeof v!=='string'||!v.trim()||v.startsWith('YOUR_'))throw Error('Configure GitHub '+k);if(!Array.isArray(state?.items)||!Array.isArray(entries))throw Error('Invalid workflow or directory list');if(entries.length>=1000)throw Error('Directory may exceed the GitHub listing limit');if(path.startsWith('/')||path.endsWith('/')||path.split('/').some(x=>!x||x==='.'||x==='..'))throw Error('Invalid existing directory');const seen=new Set(),items=[];for(const w of state.items){if(typeof w?.id!=='string'||!w.id||typeof w.name!=='string'||!w.name||/[\\/\\\\\\u0000-\\u001f]/.test(w.name)||w.name==='.'||w.name==='..')throw Error('Workflow name cannot be used as a file name');const name=w.name+'.json';if(seen.has(name))throw Error('Workflow names would collide');seen.add(name);const matches=entries.filter(e=>e.name===name);if(matches.length>1||matches.some(e=>e.type!=='file'||typeof e.sha!=='string'||!e.sha))throw Error('Invalid GitHub directory entry');items.push({workflowId:w.id,name,path:path+'/'+name,exists:matches.length===1})}return {items}}\n" }, "n_258796b30312984e": { "name": "Prepare the next workflow", "imports": [], "source": "export default function next({items,previous}){const i=previous?.index??0;if(i>=items.length)return {state:{index:i},more:false,item:{},id:'-',path:'-',exists:false};const item=items[i];return {state:{index:i+1},more:true,item,id:item.workflowId,path:item.path,exists:item.exists}}\n" }, "n_e90aaa1a06d990f2": { "name": "Read the n8n workflow", "imports": [], "source": "export default async function workflow({id},context){const w=await context.actions.call('n8n.get_workflow',{workflowId:id});if(!w||w.id!==id||typeof w.name!=='string'||!Array.isArray(w.nodes)||typeof w.connections!=='object'||!w.connections)throw Error('Invalid n8n workflow');return {workflow:w}}\n" }, "n_27fb6e170ffcba3e": { "name": "Compare the existing backup", "imports": [], "source": "export default async function compare({workflow,item,owner,repo},context){if(workflow.id!==item.workflowId||workflow.name+'.json'!==item.name)throw Error('Workflow changed during backup');let sha=null,changed=true;if(item.exists){const file=await context.actions.call('github.get_file_contents',{owner,repo,path:item.path});if(!file||file.type!=='file'||file.path!==item.path||typeof file.sha!=='string'||!file.sha||typeof file.decoded_content!=='string')throw Error('Invalid existing backup');let old;try{old=JSON.parse(file.decoded_content)}catch{throw Error('Existing backup is not JSON')}if(!old||typeof old!=='object'||Array.isArray(old))throw Error('Existing backup is not a workflow');const order=x=>Object.fromEntries(Object.keys(x).sort().map(k=>[k,x[k]]));changed=JSON.stringify(order(old))!==JSON.stringify(order(workflow));sha=file.sha}return {changed,content:JSON.stringify(workflow,null,2),sha}}\n" }, "n_210cf4e0b16aec4e": { "name": "Save the GitHub backup", "imports": [], "source": "export default async function write({owner,repo,path,content,sha},context){const r=await context.actions.call('github.create_or_update_file',{owner,repo,path,message:'[n8n backup] '+path,content,...(sha?{sha}:{})});if(typeof r?.content?.sha!=='string'||!r.content.sha||typeof r?.commit?.sha!=='string'||!r.commit.sha)throw Error('GitHub did not confirm the backup commit');return {result:r}}\n" } } }, "presentation": { "designer": { "version": 1, "flow": { "direction": "vertical" } } } }