diff --git a/bin/deepforge b/bin/deepforge
index aa2466592..745eb0fdc 100755
--- a/bin/deepforge
+++ b/bin/deepforge
@@ -102,13 +102,6 @@ var storeConfig = function(id, value) {
         var cKey = envToConf[env];
         process.env[env] = process.env[env] || p(getConfigValue(cKey));
     });
-
-    // Special cases
-    if (process.env.DEEPFORGE_WORKER_USE_BLOB === 'true' &&
-        exists.sync(process.env.DEEPFORGE_BLOB_DIR)) {
-
-        process.env.DEEPFORGE_WORKER_CACHE = process.env.DEEPFORGE_BLOB_DIR + '/wg-content';
-    }
 })();
 
 program
diff --git a/bin/start-worker.js b/bin/start-worker.js
index 4dbd5e273..a2f481689 100644
--- a/bin/start-worker.js
+++ b/bin/start-worker.js
@@ -16,6 +16,9 @@ var path = require('path'),
     config = {};
 
 var createDir = function(dir) {
+    if (path.dirname(dir) !== dir) {
+        createDir(path.dirname(dir));
+    }
     try {
         fs.statSync(dir);
     } catch (e) {
@@ -25,18 +28,20 @@ var createDir = function(dir) {
     }
     return false;
 };
-createDir(workerRootPath);
-createDir(workerPath);
+
+const symlink = function(origin, link) {
+    try {
+        fs.statSync(link);
+    } catch (e) {
+        childProcess.spawnSync('ln', ['-s', origin, link]);
+    }
+};
+
 createDir(workerTmp);
 
-// Create sym link to the node_modules
-var modules = path.join(workerRootPath, 'node_modules');
-try {
-    fs.statSync(modules);
-} catch (e) {
-    // Create dir
-    childProcess.spawnSync('ln', ['-s', `${__dirname}/../node_modules`, modules]);
-}
+// Create sym link to the node_modules and to deepforge
+const modules = path.join(workerRootPath, 'node_modules');
+symlink(`${__dirname}/../node_modules`, modules);
 
 var cleanUp = function() {
     console.log('removing worker directory ', workerPath);
@@ -47,12 +52,31 @@ var startExecutor = function() {
     process.on('SIGINT', cleanUp);
     process.on('uncaughtException', cleanUp);
 
+    // Configure the cache
+    const blobDir = process.env.DEEPFORGE_BLOB_DIR;
+    const isSharingBlob = process.env.DEEPFORGE_WORKER_USE_BLOB === 'true' &&
+        !!blobDir;
+
+    if (process.env.DEEPFORGE_WORKER_CACHE && isSharingBlob) {
+        // Create the cache directory and symlink the blob in cache/gme
+        createDir(process.env.DEEPFORGE_WORKER_CACHE);
+
+        const blobContentDir = path.join(blobDir, 'wg-content');
+        const gmeStorageCache = path.join(process.env.DEEPFORGE_WORKER_CACHE, 'gme');
+        rm_rf.sync(gmeStorageCache);
+        symlink(blobContentDir, gmeStorageCache);
+    }
+
     // Start the executor
+    const env = Object.assign({}, process.env);
+    env.DEEPFORGE_ROOT = path.join(__dirname, '..');
+
+    const options = {env: env};
     var execJob = spawn('node', [
         executorSrc,
         workerConfigPath,
         workerTmp
-    ]);
+    ], options);
     execJob.stdout.pipe(process.stdout);
     execJob.stderr.pipe(process.stderr);
 };
diff --git a/config/components.json b/config/components.json
index 939cf1317..5f91a66e7 100644
--- a/config/components.json
+++ b/config/components.json
@@ -132,5 +132,8 @@
     },
     "Compute": {
         "backends": ["gme", "local"]
+    },
+    "Storage": {
+        "backends": ["gme"]
     }
 }
diff --git a/src/common/globals.js b/src/common/globals.js
index 2082a4e69..3dec3ec21 100644
--- a/src/common/globals.js
+++ b/src/common/globals.js
@@ -1,6 +1,8 @@
 /* globals WebGMEGlobal, define*/
 // This file creates the DeepForge namespace and defines basic actions
 define([
+    'deepforge/storage/index',
+    'deepforge/viz/ConfigDialog',
     'panel/FloatingActionButton/styles/Materialize',
     'text!./NewOperationCode.ejs',
     'js/RegistryKeys',
@@ -9,6 +11,8 @@ define([
     'underscore',
     'q'
 ], function(
+    Storage,
+    ConfigDialog,
     Materialize,
     DefaultCodeTpl,
     REGISTRY_KEYS,
@@ -224,43 +228,6 @@ define([
         });
     };
 
-    // Creating Artifacts
-    var UPLOAD_PLUGIN = 'ImportArtifact';
-
-    var uploadArtifact = function() {
-        // Get the data types
-        var dataBase,
-            dataBaseId,
-            metanodes = client.getAllMetaNodes(),
-            dataTypes = [];
-
-        dataBase = metanodes.find(n => n.getAttribute('name') === 'Data');
-
-        if (!dataBase) {
-            this.logger.error('Could not find the base Data node!');
-            return;
-        }
-
-        dataBaseId = dataBase.getId();
-        dataTypes = metanodes.filter(n => n.isTypeOf(dataBaseId))
-            .filter(n => !n.getRegistry('isAbstract'))
-            .map(node => node.getAttribute('name'));
-
-        // Add the target type to the pluginMetadata...
-        var metadata = WebGMEGlobal.allPluginsMetadata[UPLOAD_PLUGIN];
-
-        WebGMEGlobal.InterpreterManager.configureAndRun(metadata, result => {
-            var msg = 'Artifact upload complete!';
-            if (!result) {
-                return;
-            }
-            if (!result.success) {
-                msg = `Artifact upload failed: ${result.error}`;
-            }
-            Materialize.toast(msg, 2000);
-        });
-    };
-
     DeepForge.last = {};
     DeepForge.create = {};
     DeepForge.register = {};
@@ -280,7 +247,39 @@ define([
         };
     });
 
-    DeepForge.create.Artifact = uploadArtifact;
+    // Creating Artifacts
+    const UPLOAD_PLUGIN = 'ImportArtifact';
+    const copy = data => JSON.parse(JSON.stringify(data));
+    DeepForge.create.Artifact = async function() {
+        const metadata = copy(WebGMEGlobal.allPluginsMetadata[UPLOAD_PLUGIN]);
+
+        metadata.configStructure.unshift({
+            name: 'artifactOptions',
+            displayName: 'New Artifact',
+            valueType: 'section'
+        });
+
+        if (Storage.getAvailableBackends().length > 1) {
+            metadata.configStructure.push({
+                name: 'storageOptions',
+                displayName: 'Storage',
+                valueType: 'section'
+            });
+            metadata.configStructure.push({
+                name: 'storage',
+                displayName: 'Storage Location',
+                valueType: 'string',
+                valueItems: Storage.getAvailableBackends(),
+                value: Storage.getAvailableBackends()[0],
+            });
+        }
+
+        const configDialog = new ConfigDialog(client);
+        const allConfigs = await configDialog.show(metadata);
+        const context = client.getCurrentPluginContext(UPLOAD_PLUGIN);
+        context.pluginConfig = allConfigs[UPLOAD_PLUGIN];
+        return await Q.ninvoke(client, 'runBrowserPlugin', UPLOAD_PLUGIN, context);
+    };
 
     //////////////////// DeepForge prev locations ////////////////////
     // Update DeepForge on project changed
diff --git a/src/common/plugin/GeneratedFiles.js b/src/common/plugin/GeneratedFiles.js
new file mode 100644
index 000000000..19d594a16
--- /dev/null
+++ b/src/common/plugin/GeneratedFiles.js
@@ -0,0 +1,76 @@
+/*globals define*/
+define([
+    'common/util/assert',
+    'deepforge/storage/index',
+], function(
+    assert,
+    Storage,
+) {
+
+    const GeneratedFiles = function(blobClient) {
+        this.blobClient = blobClient;
+        this._files = {};
+        this._data = {};
+    };
+
+    GeneratedFiles.prototype.addUserAsset = function (path, dataInfo) {
+        assert(!!dataInfo, `Adding undefined user asset: ${path}`);
+        dataInfo = typeof dataInfo === 'object' ? dataInfo : JSON.parse(dataInfo);
+        this._data[path] = dataInfo;
+    };
+
+    GeneratedFiles.prototype.getUserAssetPaths = function () {
+        return Object.keys(this._data);
+    };
+
+    GeneratedFiles.prototype.getUserAsset = function (path) {
+        return this._data[path];
+    };
+
+    GeneratedFiles.prototype.getUserAssets = function () {
+        return Object.entries(this._data);
+    };
+
+    GeneratedFiles.prototype.addFile = function (path, contents) {
+        this._files[path] = contents;
+    };
+
+    GeneratedFiles.prototype.appendToFile = function (path, contents) {
+        this._files[path] = (this._files[path] || '') + contents;
+    };
+
+    GeneratedFiles.prototype.getFile = function (path) {
+        return this._files[path];
+    };
+
+    GeneratedFiles.prototype.getFilePaths = function () {
+        return Object.keys(this._files);
+    };
+
+    GeneratedFiles.prototype.remove = function (path) {
+        delete this._files[path];
+        delete this._data[path];
+    };
+
+    GeneratedFiles.prototype.save = async function (artifactName) {
+        const artifact = this.blobClient.createArtifact(artifactName);
+
+        // Transfer the data files to the blob and create an artifact
+        const userAssets = this.getUserAssets();
+        if (userAssets.length) {
+            const objectHashes = {};
+            for (let i = userAssets.length; i--;) {
+                const [filepath, dataInfo] = userAssets[i];
+                const contents = await Storage.getFile(dataInfo);
+                const filename = filepath.split('/').pop();
+                const hash = await this.blobClient.putFile(filename, contents);
+                objectHashes[filepath] = hash;
+            }
+            await artifact.addObjectHashes(objectHashes);
+        }
+        await artifact.addFiles(this._files);
+        return await artifact.save();
+    };
+
+    return GeneratedFiles;
+});
diff --git a/src/common/storage/backends/StorageBackend.js b/src/common/storage/backends/StorageBackend.js
new file mode 100644
index 000000000..06644aafd
--- /dev/null
+++ b/src/common/storage/backends/StorageBackend.js
@@ -0,0 +1,33 @@
+/* globals define */
+define([
+    'module',
+    'q',
+], function(
+    module,
+    Q,
+) {
+
+    const StorageBackend = function(id, metadata) {
+        const {name, client} = metadata;
+        this.id = id;
+        this.name = name;
+        this.clientPath = client || './Client';
+    };
+
+    StorageBackend.prototype.getClient = async function(logger, config) {
+        //if (require.isBrowser) {
+            //throw new Error('Storage clients cannot be loaded in the browser.');
+        //}
+
+        const Client = await this.require(`deepforge/storage/backends/${this.id}/${this.clientPath}`);
+        return new Client(this.id, this.name, logger, config);
+    };
+
+    StorageBackend.prototype.require = function(path) {  // helper for loading async
+        const deferred = Q.defer();
+        require([path], deferred.resolve, deferred.reject);
+        return deferred.promise;
+    };
+
+    return StorageBackend;
+});
diff --git a/src/common/storage/backends/StorageClient.js b/src/common/storage/backends/StorageClient.js
new file mode 100644
index 000000000..1c224cb87
--- /dev/null
+++ b/src/common/storage/backends/StorageClient.js
@@ -0,0 +1,44 @@
+/* globals define, WebGMEGlobal */
+define([
+    'client/logger'
+], function(
+    Logger
+) {
+    const StorageClient = function(id, name, logger) {
+        this.id = id;
+        this.name = name;
+        if (!logger) {
+            let gmeConfig;
+            if (require.isBrowser) {
+                gmeConfig = WebGMEGlobal.gmeConfig;
+            } else {
+                gmeConfig = require.nodeRequire('../../../../config');
+            }
+            logger = Logger.create(`gme:storage:${id}`, gmeConfig.client.log);
+        }
+        this.logger = logger.fork(`storage:${id}`);
+    };
+
+    StorageClient.prototype.getFile = async function() {
+        throw new Error(`File download not implemented for ${this.name}`);
+    };
+
+    StorageClient.prototype.putFile = async function() {
+        throw new Error(`File upload not supported by ${this.name}`);
+    };
+
+    StorageClient.prototype.getDownloadURL = async function() {
+        // TODO: Remove this in favor of directly downloading w/ getFile, etc
+        throw new Error(`getDownloadURL not implemented for ${this.name}`);
+    };
+
+    StorageClient.prototype.getMetadata = async function() {
+        throw new Error(`getDownloadURL not implemented for ${this.name}`);
+    };
+
+    StorageClient.prototype.createDataInfo = function(data) {
+        return {backend: this.id, data};
+    };
+
+    return StorageClient;
+});
diff --git a/src/common/storage/backends/gme/Client.js b/src/common/storage/backends/gme/Client.js
new file mode 100644
index 000000000..4aadf22a0
--- /dev/null
+++ b/src/common/storage/backends/gme/Client.js
@@ -0,0 +1,59 @@
+/* globals define */
+define([
+    '../StorageClient',
+    'blob/BlobClient',
+    'module',
+], function(
+    StorageClient,
+    BlobClient,
+    module,
+) {
+    const GMEStorage = function(/*name, logger*/) {
+        StorageClient.apply(this, arguments);
+        const params = {
+            logger: this.logger.fork('BlobClient')
+        };
+        if (!require.isBrowser) {
+            const path = require.nodeRequire('path');
+            const PROJECT_ROOT = path.join(path.dirname(module.uri), '..', '..', '..', '..', '..');
+            const configPath = path.join(PROJECT_ROOT, 'config');
+            const gmeConfig = require.nodeRequire(configPath);
+            params.server = '127.0.0.1';
+            params.serverPort = gmeConfig.server.port;
+            params.httpsecure = false;
+        }
+        this.blobClient = new BlobClient(params);
+    };
+
+    GMEStorage.prototype = Object.create(StorageClient.prototype);
+
+    GMEStorage.prototype.getFile = async function(dataInfo) {
+        const {data} = dataInfo;
+        return await this.blobClient.getObject(data);
+    };
+
+    GMEStorage.prototype.putFile = async function(filename, content) {
+        const hash = await this.blobClient.putFile(filename, content);
+        return this.createDataInfo(hash);
+    };
+
+    GMEStorage.prototype.getMetadata = async function(dataInfo) {
+        const {data} = dataInfo;
+        return await this.blobClient.getMetadata(data);
+    };
+
+    GMEStorage.prototype.getDownloadURL = async function(dataInfo) {
+        const {data} = dataInfo;
+        return this.blobClient.getDownloadURL(data);
+    };
+
+    GMEStorage.prototype.getCachePath = async function(dataInfo) {
+        const metadata = await this.getMetadata(dataInfo);
+        const hash = metadata.content;
+        const dir = hash.substring(0, 2);
+        const filename = hash.substring(2);
+        return `${this.id}/${dir}/${filename}`;
+    };
+
+    return GMEStorage;
+});
diff --git a/src/common/storage/backends/gme/metadata.json b/src/common/storage/backends/gme/metadata.json
new file mode 100644
index 000000000..de5de9a79
--- /dev/null
+++ b/src/common/storage/backends/gme/metadata.json
@@ -0,0 +1,3 @@
+{
+    "name": "WebGME Blob Storage"
+}
diff --git a/src/common/storage/index.js b/src/common/storage/index.js
new file mode 100644
index 000000000..67c51eadc
--- /dev/null
+++ b/src/common/storage/index.js
@@ -0,0 +1,80 @@
+/*globals define, requirejs */
+(function() {
+    const STORAGE_BACKENDS = ['gme'];
+
+    define([
+        'module',
+        'deepforge/storage/backends/StorageBackend',
+    ].concat(STORAGE_BACKENDS.map(name => `text!deepforge/storage/backends/${name}/metadata.json`)),
+    function(
+        module,
+        StorageBackend,
+    ) {
+        const Storage = {};
+
+        Storage.getComponentId = function() {
+            return 'Storage';
+        };
+
+        Storage.getAvailableBackends = function() {
+            const settings = {backends: STORAGE_BACKENDS};  // all by default
+            if (require.isBrowser) {
+                const ComponentSettings = requirejs('js/Utils/ComponentSettings');
+                ComponentSettings.resolveWithWebGMEGlobal(
+                    settings,
+                    this.getComponentId()
+                );
+            } else {  // Running in NodeJS
+                const path = require('path');
+                const dirname = path.dirname(module.uri);
+                const deploymentSettings = JSON.parse(requirejs('text!' + dirname + '/../../../config/components.json'));
+                Object.assign(settings, deploymentSettings[this.getComponentId()]);
+            }
+
+            return settings.backends;
+        };
+
+        Storage.getBackend = function(id) {
+            const metadata = this.getStorageMetadata(id);
+            return new StorageBackend(id, metadata);
+        };
+
+        Storage.getStorageMetadata = function(id) {
+            id = id.toLowerCase();
+            if (!STORAGE_BACKENDS.includes(id)) {
+                throw new Error(`Storage backend not found: ${id}`);
+            }
+
+            const relativePath = `backends/${id}/metadata.json`;
+            const metadata = JSON.parse(requirejs(`text!deepforge/storage/${relativePath}`));
+            metadata.id = id;
+            return metadata;
+        };
+
+        Storage.getMetadata = async function(dataInfo, logger) {
+            const backend = this.getBackend(dataInfo.backend);
+            const client = await backend.getClient(logger);
+            return client.getMetadata(dataInfo);
+        };
+
+        Storage.getDownloadURL = async function(dataInfo, logger) {
+            const backend = this.getBackend(dataInfo.backend);
+            const client = await backend.getClient(logger);
+            return client.getDownloadURL(dataInfo);
+        };
+
+        Storage.getFile = async function(dataInfo, logger) {
+            const backend = this.getBackend(dataInfo.backend);
+            const client = await backend.getClient(logger);
+            return client.getFile(dataInfo);
+        };
+
+        Storage.getCachePath = async function(dataInfo, logger) {
+            const backend = this.getBackend(dataInfo.backend);
+            const client = await backend.getClient(logger);
+            return await client.getCachePath(dataInfo);
+        };
+
+        return Storage;
+    });
+})();
diff --git a/src/common/updates/Updates.js b/src/common/updates/Updates.js
index 39b11b882..c54341cac 100644
--- a/src/common/updates/Updates.js
+++ b/src/common/updates/Updates.js
@@ -1,7 +1,9 @@
 /* globals define */
 define([
+    'deepforge/storage/index',
     'q'
 ], function(
+    Storage,
     Q
 ) {
 
@@ -40,6 +42,34 @@ define([
                 // Set the default visualizer to TabbedTextEditor
                 core.setRegistry(utils, 'validVisualizers', 'TabbedTextEditor');
             }
+        },
+        {
+            name: 'UpdateDataNodesToUserAssets',
+            isNeeded: async function(core, rootNode) {
+                const pipelineRoot = core.getLibraryRoot(rootNode, 'pipeline');
+                const hasPipelineLibrary = !!pipelineRoot;
+                if (hasPipelineLibrary) {
+                    const version = core.getAttribute(pipelineRoot, 'version');
+                    const [major, minor, /*patch*/] = version.split('.').map(n => +n);
+                    return major === 0 && minor < 13;
+                }
+            },
+            apply: async function(core, rootNode, META) {
+                const isDataNode = node => core.getMetaType(node) === META['pipeline.Data'];
+                const dataNodes = (await core.loadSubTree(rootNode))
+                    .filter(isDataNode)
+                    .filter(node => !core.isLibraryElement(node));
+
+                const storageClient = await Storage.getBackend('gme').getClient();
+                for (let i = dataNodes.length; i--;) {
+                    const node = dataNodes[i];
+                    const hash = core.getAttribute(node, 'data');
+                    if (hash && !hash.includes('{')) {  // not already updated
+                        const dataInfo = storageClient.createDataInfo(hash);
+                        core.setAttribute(node, 'data', JSON.stringify(dataInfo));
+                    }
+                }
+            }
         }
     ];
 
diff --git a/src/plugins/ExecuteJob/ExecuteJob.js b/src/plugins/ExecuteJob/ExecuteJob.js
index f3a9629d2..300fbe779 100644
--- a/src/plugins/ExecuteJob/ExecuteJob.js
+++ b/src/plugins/ExecuteJob/ExecuteJob.js
@@ -629,58 +629,34 @@ define([
     };
 
     ExecuteJob.prototype.onDistOperationComplete = async function (node, fileHashes) {
-        const opName = this.getAttribute(node, 'name');
-        const resultTypes = await this.getResultTypes(fileHashes);
-        let nodeId = this.core.getPath(node),
-            outputMap = {},
-            outputs;
-
-
-        // Match the output names to the actual nodes
-        // Create an array of [name, node]
-        // For now, just match by type. Later we may use ports for input/outputs
-        // Store the results in the outgoing ports
-        return this.getOutputs(node)
-            .then(outputPorts => {
-                outputs = outputPorts.map(tuple => [tuple[0], tuple[2]]);
-                outputs.forEach(output => outputMap[output[0]] = output[1]);
-
-                // this should not be in directories -> flatten the data!
-                const hashes = outputs.map(tuple => {  // [ name, node ]
-                    let [name] = tuple;
-                    let artifactHash = fileHashes[name];
-                    return this.getContentHash(artifactHash, `outputs/${name}`);
-                });
-
-                return Q.all(hashes);
-            })
-            .then(hashes => {
-                // Create new metadata for each
-                hashes.forEach((hash, i) => {
-                    var name = outputs[i][0],
-                        dataType = resultTypes[name];
-
-                    if (dataType) {
-                        this.setAttribute(outputMap[name], 'type', dataType);
-                        this.logger.info(`Setting ${nodeId} data type to ${dataType}`);
-                    } else {
-                        this.logger.warn(`No data type found for ${nodeId}`);
-                    }
-
-                    if (hash) {
-                        this.setAttribute(outputMap[name], 'data', hash);
-                        this.logger.info(`Setting ${nodeId} data to ${hash}`);
-                    }
-                });
-
-                return this.onOperationComplete(node);
-            })
-            .catch(e => this.onOperationFail(node, `"${opName}" failed: ${e}`));
-    };
-
-    ExecuteJob.prototype.getResultTypes = async function (fileHashes) {
-        const mdHash = fileHashes['result-types'];
-        const hash = await this.getContentHashSafe(mdHash, 'result-types.json', ERROR.NO_TYPES_FILE);
+        const results = await this.getResults(fileHashes);
+        const nodeId = this.core.getPath(node);
+        const outputPorts = await this.getOutputs(node);
+        const outputs = outputPorts.map(tuple => [tuple[0], tuple[2]]);
+
+        for (let i = outputs.length; i--;) {
+            const [name, node] = outputs[i];
+            const {type, dataInfo} = results[name];
+
+            if (type) {
+                this.setAttribute(node, 'type', type);
+                this.logger.info(`Setting ${nodeId} data type to ${type}`);
+            } else {
+                this.logger.warn(`No data type found for ${nodeId}`);
+            }
+
+            if (dataInfo) {
+                this.setAttribute(node, 'data', dataInfo);
+                this.logger.info(`Setting ${nodeId} data to ${dataInfo}`);
+            }
+        }
+
+        return this.onOperationComplete(node);
+    };
+
+    ExecuteJob.prototype.getResults = async function (fileHashes) {
+        const mdHash = fileHashes['results'];
+        const hash = await this.getContentHashSafe(mdHash, 'results.json', ERROR.NO_TYPES_FILE);
         return await this.blobClient.getObjectAsJSON(hash);
     };
 
diff --git a/src/plugins/Export/Export.js b/src/plugins/Export/Export.js
index e081fdbba..6382f4ed4 100644
--- a/src/plugins/Export/Export.js
+++ b/src/plugins/Export/Export.js
@@ -5,6 +5,7 @@ define([
     'text!./metadata.json',
     './format',
     'plugin/GenerateJob/GenerateJob/GenerateJob',
+    'deepforge/plugin/GeneratedFiles',
     'deepforge/Constants',
     'blob/BlobConfig',
     'underscore',
@@ -13,6 +14,7 @@ define([
     pluginMetadata,
     FORMATS,
     PluginBase,
+    GeneratedFiles,
     CONSTANTS,
     BlobConfig,
     _,
@@ -80,13 +82,13 @@ define([
         }
         this.activeNodeDepth = this.core.getPath(this.activeNode).split('/').length + 1;
 
-        const files = {};
+        const files = new GeneratedFiles(this.blobClient);
         const name = this.core.getAttribute(this.activeNode, 'name');
         const staticInputs = this.getCurrentConfig().staticInputs;
         return this.createPipelineFiles(this.activeNode, files)
             .then(() => this.addStaticInputs(staticInputs, files))
             .then(() => this.createDefaultMainFile(this.activeNode, staticInputs, files))
-            .then(() => this.createArtifact(name, files))
+            .then(() => files.save(name))
             .then(hash => {
                 this.result.addArtifact(hash);
                 this.result.setSuccess(true);
@@ -126,20 +128,20 @@ define([
         return varName;
     };
 
-    Export.prototype.addStaticInputs = function (ids, files={}) {
+    Export.prototype.addStaticInputs = function (ids, files) {
         // Get the static inputs and add them in artifacts/
         return Q.all(ids.map(id => this.core.loadByPath(this.rootNode, id)))
             .then(nodes => {
                 nodes.forEach((node, i) => {
                     const name = this.getVariableNameFor(ids[i]);
-                    const hash = this.getAttribute(node, 'data');
-                    files._data[`artifacts/${name}`] = hash;
+                    const dataInfo = this.getAttribute(node, 'data');
+                    files.addUserAsset(`artifacts/${name}`, dataInfo);
                 });
                 return files;
             });
     };
 
-    Export.prototype.createDefaultMainFile = function (node, staticInputs, files={}) {
+    Export.prototype.createDefaultMainFile = function (node, staticInputs, files) {
         // Get the variable name for the pipeline
         const name = PluginBase.toUpperCamelCase(this.core.getAttribute(node, 'name'));
         const instanceName = this.getVariableName(name.toLowerCase());
@@ -210,7 +212,7 @@ define([
                     ].join('\n');
                 }
 
-                files['main.py'] = [
+                const mainPy = [
                     'import deepforge',
                     // Get the input operations from the cli
                     'import sys',
@@ -225,12 +227,13 @@ define([
                     `${instanceName} = ${name}()`,
                     runPipeline
                 ].join('\n');
+                files.addFile('main.py', mainPy);
                 // Add file for storing results
-                files['outputs/README.md'] = 'Results from the cli execution are stored here';
+                files.addFile('outputs/README.md', 'Results from the cli execution are stored here');
             });
     };
 
-    Export.prototype.createPipelineFiles = function (node, files={}) {
+    Export.prototype.createPipelineFiles = function (node, files) {
         const name = PluginBase.toUpperCamelCase(this.core.getAttribute(node, 'name'));
         // Generate the file for the pipeline in pipelines/
 
@@ -332,7 +335,7 @@ define([
                 code = code.concat(opInvocations);
 
                 const filename = PluginBase.toSnakeCase(name);
-                files[`pipelines/${filename}.py`] = [
+                const pipelinePy = [
                     importCode.join('\n'),
                     '',
                     `class ${name}():`,
@@ -340,9 +343,8 @@ define([
                     indent(indent(code.join('\n'))),
                     indent(indent(`return ${outputs}`))
                 ].join('\n');
-                files['pipelines/__init__.py'] = files['pipelines/__init__.py'] || '';
-                files['pipelines/__init__.py'] += `from pipelines.${filename} import ${name}\n`;
-
+                files.addFile(`pipelines/${filename}.py`, pipelinePy);
+                files.appendToFile('pipelines/__init__.py', `from pipelines.${filename} import ${name}\n`);
                 return Q.all(operations.map(node => this.createOperationFiles(node, files)));
             });
     };
diff --git a/src/plugins/ExportBranch/ExportBranch.js b/src/plugins/ExportBranch/ExportBranch.js
new file mode 100644
index 000000000..522a4054a
--- /dev/null
+++ b/src/plugins/ExportBranch/ExportBranch.js
@@ -0,0 +1,107 @@
+/*globals define*/
+/*eslint-env node, browser*/
+
+define([
+    'plugin/PluginConfig',
+    'text!./metadata.json',
+    'plugin/PluginBase',
+    'common/storage/util',
+    'blob/util',
+], function (
+    PluginConfig,
+    pluginMetadata,
+    PluginBase,
+    storageUtils,
+    blobUtil,
+) {
+    'use strict';
+
+    pluginMetadata = JSON.parse(pluginMetadata);
+
+    /**
+     * Initializes a new instance of ExportBranch.
+     * @class
+     * @augments {PluginBase}
+     * @classdesc This class represents the plugin ExportBranch.
+     * @constructor
+     */
+    function ExportBranch() {
+        // Call base class' constructor.
+        PluginBase.call(this);
+        this.pluginMetadata = pluginMetadata;
+    }
+
+    /**
+     * Metadata associated with the plugin. Contains id, name, version, description, icon, configStructure etc.
+     * This is also available at the instance at this.pluginMetadata.
+     * @type {object}
+     */
+    ExportBranch.metadata = pluginMetadata;
+
+    // Prototypical inheritance from PluginBase.
+    ExportBranch.prototype = Object.create(PluginBase.prototype);
+    ExportBranch.prototype.constructor = ExportBranch;
+
+    /**
+     * Main function for the plugin to execute. This will perform the execution.
+     * Notes:
+     * - Always log with the provided logger.[error,warning,info,debug].
+     * - Do NOT put any user interaction logic UI, etc. inside this method.
+     * - callback always has to be called even if error happened.
+     *
+     * @param {function(Error|null, plugin.PluginResult)} callback - the result callback
+     */
+    ExportBranch.prototype.main = async function (callback) {
+        const {projectId, branchName, commitHash} = this;
+        const projectJson = await storageUtils.getProjectJson(
+            this.project,
+            {branchName, commitHash}
+        );
+
+        const shortCommitHash = commitHash.substr(1, 6);
+        const filename = `${projectId}_${shortCommitHash}.webgmex`;
+        const withAssets = true;
+        projectJson.hashes.assets = await this.getUserAssetHashes();
+
+        const hash = await blobUtil.buildProjectPackage(
+            this.logger.fork('blobUtil'),
+            this.blobClient,
+            projectJson,
+            withAssets,
+            filename
+        );
+
+        this.result.addArtifact(hash);
+        this.result.setSuccess(true);
+        callback(null, this.result);
+    };
+
+    ExportBranch.prototype.getUserAssetHashes = async function () {
+        const nodes = await this.core.loadSubTree(this.rootNode);
+        const hashes = [];
+        for (let i = nodes.length; i--;) {
+            // If it has any userAssets which have a gme backend, record the hash
+            const node = nodes[i];
+            const attributes = this.core.getAttributeNames(node)
+                .filter(name => {
+                    const meta = this.core.getAttributeMeta(node, name) || {};
+                    return meta.type === 'userAsset';
+                });
+
+            const gmeDataInfos = attributes
+                .map(name => JSON.parse(this.core.getAttribute(node, name) || '{}'))
+                .filter(dataInfo => dataInfo.backend === 'gme');
+
+            for (let j = gmeDataInfos.length; j--;) {
+                // Check if the 
+                const hash = gmeDataInfos[j].data;
+                if (!hashes.includes(hash)) {
+                    hashes.push(hash);
+                }
+            }
+        }
+        return hashes;
+    };
+
+    return ExportBranch;
+});
diff --git a/src/plugins/ExportBranch/metadata.json b/src/plugins/ExportBranch/metadata.json
new file mode 100644
index 000000000..4a4ad666e
--- /dev/null
+++ b/src/plugins/ExportBranch/metadata.json
@@ -0,0 +1,15 @@
+{
+  "id": "ExportBranch",
+  "name": "ExportBranch",
+  "version": "0.1.0",
+  "description": "",
+  "icon": {
+    "class": "glyphicon glyphicon-cog",
+    "src": ""
+  },
+  "disableServerSideExecution": false,
+  "disableBrowserSideExecution": false,
+  "dependencies": [],
+  "writeAccessRequired": false,
+  "configStructure": []
+}
\ No newline at end of file
diff --git a/src/plugins/GenerateJob/GenerateJob.js b/src/plugins/GenerateJob/GenerateJob.js
index 57c483866..7e9d2ff42 100644
--- a/src/plugins/GenerateJob/GenerateJob.js
+++ b/src/plugins/GenerateJob/GenerateJob.js
@@ -9,6 +9,8 @@ define([
     'deepforge/plugin/Operation',
     'deepforge/OperationCode',
     'deepforge/plugin/PtrCodeGen',
+    'deepforge/plugin/GeneratedFiles',
+    'deepforge/storage/index',
     'text!./metadata.json',
     'plugin/PluginBase',
     'module'
@@ -20,6 +22,8 @@ define([
     OperationHelpers,
     OperationCode,
     PtrCodeGen,
+    GeneratedFiles,
+    Storage,
     pluginMetadata,
     PluginBase,
     module
@@ -83,52 +87,23 @@ define([
      *
      * @param {function(string, plugin.PluginResult)} callback - the result callback
      */
-    GenerateJob.prototype.main = function (callback) {
+    GenerateJob.prototype.main = async function (callback) {
         const name = this.getAttribute(this.activeNode, 'name');
         const opId = this.core.getPath(this.activeNode);
 
-        return this.createExecutableOperationFiles(this.activeNode)
-            .then(files => {
-                this.logger.info('Created operation files!');
-                const artifactName = `${name}_${opId.replace(/\//g, '_')}-execution-files`;
-                return this.createArtifact(artifactName, files);
-            })
-            .then(hash => {
-                this.result.setSuccess(true);
-                this.result.addArtifact(hash);
-                callback(null, this.result);
-            })
-            .catch(err => {
-                this.result.setSuccess(false);
-                callback(err, this.result);
-            });
-    }; 
-
-    GenerateJob.prototype.createArtifact = function (artifactName, files) {
-        let artifact = this.blobClient.createArtifact(artifactName);
-        const data = files._data;
-        delete files._data;
-
-        // Remove empty hashes
-        for (let file in data) {
-            if (!data[file]) {
-                this.logger.warn(`Empty data hash has been found for file "${file}". Removing it...`);
-                delete data[file];
-            }
-        }
+        const files = await this.createExecutableOperationFiles(this.activeNode);
+        this.logger.info('Created operation files!');
+        const artifactName = `${name}_${opId.replace(/\//g, '_')}-execution-files`;
 
-        return artifact.addObjectHashes(data)
-            .then(() => {
-                this.logger.info(`Added data hashes for "${artifactName}"`);
-                return artifact.addFiles(files);
-            })
-            .then(() => {
-                this.logger.info(`Added files for "${artifactName}"`);
-                return artifact.save();
-            });
-    };
+        // Remove user assets as they are downloaded by the worker
+        files.getUserAssetPaths().forEach(path => files.remove(path));
+        const hash = await files.save(artifactName);
+        this.result.setSuccess(true);
+        this.result.addArtifact(hash);
+        callback(null, this.result);
+    }; 
 
-    GenerateJob.prototype.createRunScript = function (inputs, files={}) {
+    GenerateJob.prototype.createRunScript = async function (files) {
         let runsh = [
             '# Bash script to download data files and run job',
             !this.settings.enableJobCaching ? `# Created at ${Date.now()}` : '',
@@ -143,44 +118,29 @@ define([
             `mkdir -p ${DATA_DIR}\n`
         ].join('\n');
 
-        inputs.forEach(input => {
-            let [dataPath, /* hash */, url] = input;
-            // Add to the run.sh file
+        const assetPaths = files.getUserAssetPaths();
+        for (let i = assetPaths.length; i--;) {
+            const dataPath = assetPaths[i];
+            const dataInfo = files.getUserAsset(dataPath);
+            const url = await Storage.getDownloadURL(dataInfo, this.logger);
             runsh += `wget $DEEPFORGE_URL${url} -O ${dataPath}\n`;
-        });
+        }
 
         runsh += 'python main.py';
-        files['run.sh'] = runsh;
+        files.addFile('run.sh', runsh);
         return runsh;
     };
 
-    GenerateJob.prototype.createDataMetadataFile = function (inputs, metadata, files={}) {
-        let inputData = {};
-
-        metadata.forEach((data, i) => {
-            // add the hashes for each input
-            let [dataPath, hash] = inputs[i];
-
-            inputData[dataPath] = {
-                req: hash,
-                cache: data.content
-            };
-        });
-
+    GenerateJob.prototype.createDataMetadataFile = async function (files) {
+        const inputData = _.object(files.getUserAssets());
         const content = JSON.stringify(inputData, null, 2);
-        files['input-data.json'] = content;
+        files.addFile('input-data.json', content);
         return content;
     };
 
-    GenerateJob.prototype.createExecConfig = function (node, outputNodes, files={}) {
-        var outputs,
-            fileList,
-            ptrFiles = Object.keys(files._data).filter(name => !name.startsWith(DATA_DIR));
-
-        fileList = Object.keys(files).concat(ptrFiles)
-            .filter(name => name !== '_data');
-
-        outputs = outputNodes.map(pair => pair[0])
+    GenerateJob.prototype.createExecConfig = function (node, outputNodes, files) {
+        const fileList = files.getFilePaths();
+        const outputs = outputNodes.map(pair => pair[0])
             .map(name => {
                 return {
                     name: name,
@@ -195,8 +155,8 @@ define([
                 resultPatterns: [STDOUT_FILE]
             },
             {
-                name: 'result-types',
-                resultPatterns: ['result-types.json']
+                name: 'results',
+                resultPatterns: ['results.json']
             },
             {
                 name: name + '-all-files',
@@ -211,94 +171,65 @@ define([
             resultArtifacts: outputs
         }, null, 2);
 
-        files['executor_config.json'] = config;
+        files.addFile('executor_config.json', config);
         return config;
     };
 
-    GenerateJob.prototype.createExecutableOperationFiles = function (node, files={}) {
-        let inputs = null;
-
-        return this.createOperationFiles(node, files)
-            .then(() => {  // add all operation definitions
-                const metaDict = this.core.getAllMetaNodes(node);
-                const metanodes = Object.keys(metaDict).map(id => metaDict[id]);
-                const operations = metanodes
-                    .filter(node => this.core.isTypeOf(node, this.META.Operation))
-                    .filter(node => this.core.getAttribute(node, 'code'))  // remove local ops
-                    .map(node => {
-                        const name = this.core.getAttribute(node, 'name');
-                        const filename = GenerateJob.toSnakeCase(name);
-                        const code = this.core.getAttribute(node, 'code');
-
-                        return [filename, name, code];
-                    });
-
-                operations.forEach(tuple => {
-                    const [filename, name, code] = tuple;
-
-                    // Add file to `operations/`
-                    files[`operations/${filename}.py`] = code;
-
-                    // Create the `operations/__init___.py`
-                    files['operations/__init__.py'] = files['operations/__init__.py'] || '';
-                    files['operations/__init__.py'] += `from operations.${filename} import ${name}\n`;
-                });
-            })
-            .then(() => {
-                inputs = Object.keys(files._data)
-                    .filter(filename => filename.startsWith(DATA_DIR))
-                    .map(name => [
-                        name,
-                        files._data[name],
-                        this.blobClient.getRelativeDownloadURL(files._data[name])
-                    ]);  // (path, hash, url) tuples
-
-                return this.createRunScript(inputs, files);
-            })
-            .then(() => {
-                const mdPromises = inputs.map(input => {  // Get the metadata for each input
-                    let [name, hash] = input;
-
-                    // data asset for "input"
-                    return this.blobClient.getMetadata(hash)
-                        .catch(() => {
-                            throw Error(`BLOB_FETCH_FAILED: ${name}`);
-                        });
-                });
+    GenerateJob.prototype.createExecutableOperationFiles = async function (node) {
+        const files = await this.createOperationFiles(node);
+        const metaDict = this.core.getAllMetaNodes(node);
+        const metanodes = Object.keys(metaDict).map(id => metaDict[id]);
+        const operations = metanodes
+            .filter(node => this.core.isTypeOf(node, this.META.Operation))
+            .filter(node => this.core.getAttribute(node, 'code'))  // remove local ops
+            .map(node => {
+                const name = this.core.getAttribute(node, 'name');
+                const filename = GenerateJob.toSnakeCase(name);
+                const code = this.core.getAttribute(node, 'code');
+
+                return [filename, name, code];
+            });
 
-                return Q.all(mdPromises);
-            })
-            .then(mds => this.createDataMetadataFile(inputs, mds, files))
-            .then(() => this.getOutputs(this.activeNode))
-            .then(outputs => this.createExecConfig(node, outputs, files))
-            .then(() => files);
+        operations.forEach(tuple => {
+            const [filename, name, code] = tuple;
+
+            // Add file to `operations/`
+            files.addFile(`operations/${filename}.py`, code);
+
+            files.appendToFile(
+                'operations/__init__.py',
+                `from operations.${filename} import ${name}\n`
+            );
+        });
+
+        await this.createRunScript(files);
+        await this.createDataMetadataFile(files);
+        const outputs = await this.getOutputs(this.activeNode);
+        await this.createExecConfig(node, outputs, files);
+
+        return files;
     };
 
     GenerateJob.prototype.isUtilsNode = function (node) {
         return this.core.getAttribute(node, 'name').includes('Utilities');
     };
 
-    GenerateJob.prototype.createCustomUtils = function (files={}) {
+    GenerateJob.prototype.createCustomUtils = async function (files) {
         // Load all custom utilities defined by the user
-        return this.core.loadChildren(this.rootNode)
-            .then(children => {
-                const utilsNode = children.find(child => this.isUtilsNode(child));
-                if (utilsNode) {  // backwards compatibility
-                    return this.core.loadChildren(utilsNode);
-                }
-                return [];
-            })
-            .then(modules => {
-                modules.forEach(node => {
-                    const name = this.core.getAttribute(node, 'name');
-                    const code = this.core.getAttribute(node, 'code');
-                    files[`utils/${name}`] = code;
-                });
-                return files;
-            });
+        const children = await this.core.loadChildren(this.rootNode);
+        const utilsNode = children.find(child => this.isUtilsNode(child));
+        const modules = utilsNode ? await this.core.loadChildren(utilsNode) : [];
+
+        modules.forEach(node => {
+            const name = this.core.getAttribute(node, 'name');
+            const code = this.core.getAttribute(node, 'code');
+            files.addFile(`utils/${name}`, code);
+        });
+
+        return files;
     };
 
-    GenerateJob.prototype.createOperationFiles = function (node, files={}) {
+    GenerateJob.prototype.createOperationFiles = async function (node, files) {
         // For each operation, generate the output files:
         //   artifacts/<arg-name>  (respective serialized input data)
         //   outputs/ (make dir for the outputs)
@@ -306,34 +237,27 @@ define([
         //   main.py (main file -> calls main and serializes outputs)
 
         // add the given files
+        files = files || new GeneratedFiles(this.blobClient);
         this.logger.info('About to generate operation execution files');
-        return this.createEntryFile(node, files)
-            .then(() => this.createCustomUtils(files))
-            .then(() => this.createInputs(node, files))
-            .then(() => this.createMainFile(node, files))
-            .then(() => files)
-            .fail(err => {
-                this.logger.error(err);
-                throw err;
-            });
+        await this.createEntryFile(node, files);
+        await this.createCustomUtils(files);
+        await this.createInputs(node, files);
+        await this.createMainFile(node, files);
+        return files;
     };
 
     GenerateJob.prototype.createEntryFile = function (node, files) {
         this.logger.info('Creating deepforge.py file...');
         const serializeTpl = _.template(Templates.DEEPFORGE_SERIALIZATION);
-        files['deepforge/serialization.py'] = serializeTpl(CONSTANTS);
-        files['deepforge/__init__.py'] = Templates.DEEPFORGE_INIT;
-        return this.getOutputs(node)
-            .then(outputs => {
-                var name = this.getAttribute(node, 'name'),
-                    content = {};
-
-                // inputs and outputs
-                content.name = name;
-                content.outputs = outputs.map(output => output[0]);
-
-                // Create the deepforge file
-            });
+        files.addFile('deepforge/serialization.py', serializeTpl(CONSTANTS));
+        files.addFile('deepforge/__init__.py', Templates.DEEPFORGE_INIT);
+        //const outputs = await this.getOutputs(node);
+        //const name = this.getAttribute(node, 'name');
+        //const content = {};
+
+        // inputs and outputs
+        //content.name = name;
+        //content.outputs = outputs.map(output => output[0]);
     };
 
     GenerateJob.prototype.getConnectionContainer = function () {
@@ -358,73 +282,67 @@ define([
             });
     };
 
-    GenerateJob.prototype.createInputs = function (node, files) {
-        files._data = files._data || {};  // data assets
-
+    GenerateJob.prototype.createInputs = async function (node, files) {
         this.logger.info('Retrieving inputs and deserialize fns...');
-        return this.getInputs(node)
-            .then(allInputs => {
-                // For each input, match the connection with the input name
-                //   [ name, type ] => [ name, type, node ]
-                //
-                // For each input,
-                //  - store the data in /inputs/<name>
-                let inputs = allInputs
-                    .filter(pair => !!this.getAttribute(pair[2], 'data'));  // remove empty inputs
-
-                files['start.js'] = _.template(Templates.START)({
-                    CONSTANTS,
-                    inputs: inputs.map(pair => pair[0])
-                });
-                inputs.forEach(pair => {
-                    var hash = this.getAttribute(pair[2], 'data');
-                    files._data[DATA_DIR + pair[0]] = hash;
-                });
-                // Add the deepforge matplotlib backend file...
-                files['backend_deepforge.py'] = Templates.MATPLOTLIB_BACKEND;
+        const allInputs = await this.getInputs(node);
+        // For each input, match the connection with the input name
+        //   [ name, type ] => [ name, type, node ]
+        //
+        // For each input,
+        //  - store the data in /inputs/<name>
+        const inputs = allInputs
+            .filter(pair => !!this.getAttribute(pair[2], 'data'));  // remove empty inputs
+
+        const startJS = _.template(Templates.START)({
+            CONSTANTS,
+            inputs: inputs.map(pair => pair[0])
+        });
+        files.addFile('start.js', startJS);
+        files.addFile('backend_deepforge.py', Templates.MATPLOTLIB_BACKEND);
 
-                return files;
-            });
+        inputs.forEach(pair => {
+            const dataInfo = this.getAttribute(pair[2], 'data');
+            const datapath = DATA_DIR + pair[0];
+            files.addUserAsset(datapath, dataInfo);
+        });
+
+        return files;
     };
 
-    GenerateJob.prototype.createMainFile = function (node, files) {
+    GenerateJob.prototype.createMainFile = async function (node, files) {
         this.logger.info('Creating main file...');
-        var content = {};
-        return this.getInputs(node)
-            .then(inputs => {
-                var name = this.getAttribute(node, 'name'),
-                    code = this.getAttribute(node, 'code');
-
-                content.name = name;
-
-                // Get input data arguments
-                content.inputs = inputs
-                    .map(pair => [  // [name, type, isNone?]
-                        pair[0],
-                        this.getAttribute(pair[2], 'type'),
-                        !this.getAttribute(pair[2], 'data')
-                    ]);  // remove empty inputs
-
-                // Add remaining code
-                content.code = code;
-                return this.getOutputs(node);
-            })
-            .then(outputs => {
-                content.outputs = outputs.map(output => output[0]);
-                content.arguments = this.getOperationArguments(node)
-                    .map(arg => arg.value).join(', ');
-                return this.getAllInitialCode();
-            })
-            .then(code => content.initCode = code)
-            .then(() => this.getReferencedContent(node))
-            .then(references => {
-                const filename = GenerateJob.toSnakeCase(content.name);
-                content.references = references;
-                files['main.py'] = _.template(Templates.MAIN)(content);
-                files[`operations/${filename}.py`] = content.code;
-                files['operations/__init__.py'] = files['operations/__init__.py'] || '';
-                files['operations/__init__.py'] += `from operations.${filename} import ${content.name}\n`;
-            });
+        const inputs = await this.getInputs(node);
+        const name = this.getAttribute(node, 'name');
+        const code = this.getAttribute(node, 'code');
+
+        // Add remaining code
+        const outputs = await this.getOutputs(node);
+        const initCode = await this.getAllInitialCode();
+        const references = await this.getReferencedContent(node);
+
+        const content = {};
+        content.name = name;
+        content.initCode = initCode;
+        content.code = code;
+        content.inputs = inputs
+            .map(pair => [  // [name, type, isNone?]
+                pair[0],
+                this.getAttribute(pair[2], 'type'),
+                !this.getAttribute(pair[2], 'data')
+            ]);  // remove empty inputs
+        content.outputs = outputs.map(output => output[0]);
+        content.arguments = this.getOperationArguments(node)
+            .map(arg => arg.value).join(', ');
+        content.references = references;
+
+        files.addFile('main.py', _.template(Templates.MAIN)(content));
+
+        const filename = GenerateJob.toSnakeCase(content.name);
+        files.addFile(`operations/${filename}.py`, content.code);
+        files.appendToFile(
+            'operations/__init__.py',
+            `from operations.${filename} import ${content.name}\n`
+        );
     };
 
     GenerateJob.validateVariableName = function (word) {
@@ -447,7 +365,6 @@ define([
     };
 
     GenerateJob.prototype.getAllInitialCode = function () {
-        // TODO: Get the InitCode's 'code' attribute and then all library code
         return this.core.loadChildren(this.rootNode)
             .then(children => {
                 const codeNodes = children.filter(child => this.isMetaTypeOf(child, this.META.Code));
diff --git a/src/plugins/GenerateJob/templates/start.ejs b/src/plugins/GenerateJob/templates/start.ejs
index 3a58d68fc..a45514c5a 100644
--- a/src/plugins/GenerateJob/templates/start.ejs
+++ b/src/plugins/GenerateJob/templates/start.ejs
@@ -1,31 +1,45 @@
-// A wrapper for the torch script which:
+// A wrapper for the worker script which:
 //   - merges stdout, stderr
 //   - receives some commands and uploads intermediate data
-var spawn = require('child_process').spawn,
-    fs = require('fs'),
-    path = require('path'),
-    log = console.error,
-    logger = {};
+const spawn = require('child_process').spawn;
+const fs = require('fs');
+const rm_rf = require('rimraf');
+const path = require('path');
+const COMMAND_PREFIX = '<%= CONSTANTS.START_CMD %>';
+const IMAGE = '<%= CONSTANTS.IMAGE.PREFIX %>';
+const log = console.error;
+const logger = {};
 
 // Create the stderr only logger
 ['error', 'warn', 'info', 'log', 'debug'].forEach(method => logger[method] = log);
+logger.fork = () => logger;
 
-// Get the BlobClient...
-var COMMAND_PREFIX = '<%= CONSTANTS.START_CMD %>',
-    IMAGE = '<%= CONSTANTS.IMAGE.PREFIX %>',
-    requirejs = require('webgme').requirejs,
-    remainingImageCount = 0,
-    exitCode;
+let remainingImageCount = 0;
+let exitCode;
+
+const assert = require('assert');
+const cwd = process.cwd();
+
+assert(process.env.DEEPFORGE_ROOT, 'Must have DEEPFORGE_ROOT environment variable set.');
+process.chdir(process.env.DEEPFORGE_ROOT);
+const webgme = require('webgme');
+const gmeConfig = require(process.env.DEEPFORGE_ROOT + '/config');
+webgme.addToRequireJsPaths(gmeConfig);
+process.chdir(cwd);
+
+const requirejs = webgme.requirejs;
 
 requirejs([
     'q',
-    'blob/BlobClient'
+    'blob/BlobClient',
+    'deepforge/storage/index',
 ], function(
     Q,
-    BlobClient
+    BlobClient,
+    Storage,
 ) {
     var url = process.env.ORIGIN_URL || 'http://127.0.0.1:8888',
-        CACHE_DIR = process.env.DEEPFORGE_WORKER_CACHE || './worker-cache',
+        workerCacheDir = process.env.DEEPFORGE_WORKER_CACHE,
         protocol = url.split('://').shift(),
         address,
         port = (url.split(':') || ['80']).pop();
@@ -34,14 +48,24 @@ requirejs([
     address = url.replace(protocol + '://', '')
         .replace(':' + port, '');
 
-    // Create CACHE_DIR if it doesn't exist
-    var prepareCache = function() {
-        var dirs = CACHE_DIR.replace(/\/$/, '').split('/'),
-            cacheParent;
+    // Create workerCacheDir if it doesn't exist
+    var prepareCache = async function() {
+        if (!workerCacheDir) {
+            workerCacheDir = './worker-cache';
+            const blobDir = process.env.DEEPFORGE_BLOB_DIR;
+            const isSharingBlob = process.env.DEEPFORGE_WORKER_USE_BLOB === 'true';
+            if (isSharingBlob && blobDir) {
+                await makeIfNeeded(workerCacheDir);
+
+                const blobContentDir = path.join(blobDir, 'wg-content');
+                const gmeStorageCache = path.join(workerCacheDir, 'gme');
+                rm_rf.sync(path.join(workerCacheDir, 'gme'));
+                await symlink(blobContentDir, gmeStorageCache);
+            }
+        } else {
+            await makeIfNeeded(workerCacheDir);
+        }
 
-        dirs.pop();
-        cacheParent = dirs.join('/');
-        return makeIfNeeded(cacheParent).then(() => makeIfNeeded(CACHE_DIR));
     };
 
     var prepareInputsOutputs = function() {
@@ -50,11 +74,15 @@ requirejs([
     };
 
 
-    var makeIfNeeded = function(dir) {
+    var makeIfNeeded = async function(dir) {
         var deferred = Q.defer(),
             job;
 
         log(`makeIfNeeded: ${JSON.stringify(dir)}`);
+        if (path.dirname(dir) !== dir) {
+            await makeIfNeeded(path.dirname(dir));
+        }
+
         fs.lstat(dir, (err, stat) => {
 
             if (err || !stat.isDirectory()) {
@@ -137,48 +165,20 @@ requirejs([
         process.stdout.write(result.join('\n'));
     };
 
-    var createCacheDir = function(hash) {
-        var dir = hash.substring(0, 2);
-        return makeIfNeeded(CACHE_DIR + '/' + dir);
+    var createCacheDir = function(cachePath) {
+        return makeIfNeeded(path.dirname(cachePath));
     };
 
-    var dataCachePath = function(hash) {
-        var dir = hash.substring(0, 2),
-            filename = hash.substring(2),
-            cachePath = `${CACHE_DIR}/${dir}/${filename}`;
-
-        // Get the path for data in the cache
-        return cachePath;
+    var dataCachePath = async function(dataInfo) {
+        const relPath = await Storage.getCachePath(dataInfo, logger);
+        return `${workerCacheDir}/${relPath}`;
     };
 
-    var makeSymLink = function(target, src) {
-        var deferred = Q.defer(),
-            job;
-
-        src = path.resolve(src);
-        target = path.resolve(target);
-        fs.stat(src, err => {
-            if (err && err.code === 'ENOENT') {
-                logger.debug(`creating symlink "ln -s ${target} ${src}"`);
-                job = spawn('ln', ['-s', target, src || '.']);
-                job.on('exit', code => {
-                    if (code) {
-                        deferred.reject(`Could not create symlink ${target} -> ${src||'.'}`);
-                        return;
-                    }
-                    deferred.resolve();
-                });
-            }
-            deferred.resolve();
-        });
-        return deferred.promise;
-    };
-
-    var getData = function(ipath, hashes) {
+    var getData = async function(ipath, dataInfo) {
         // Download the data and put it in the given path
         var deferred = Q.defer(),
             inputName = ipath.split('/')[1],
-            cachePath = dataCachePath(hashes.cache);
+            cachePath = await dataCachePath(dataInfo);
 
         
         logger.debug(`retrieving ${ipath}`);
@@ -186,11 +186,11 @@ requirejs([
             // Check if the data exists in the cache
             if (!err && cacheStats.isFile()) {
             logger.info(`${inputName} already cached. Skipping retrieval from blob`);
-                return makeSymLink(cachePath, ipath).then(deferred.resolve);
+                return symlink(cachePath, ipath).then(deferred.resolve);
             }
 
-            createCacheDir(hashes.cache)
-                .then(() => blobClient.getObject(hashes.req))
+            createCacheDir(cachePath)
+                .then(() => Storage.getFile(dataInfo, logger))
                 .then(buffer => fs.writeFile(cachePath, buffer, (err, result) => {
                     if (err) {
                         logger.error('Retrieving ' + ipath + ' failed!');
@@ -198,9 +198,9 @@ requirejs([
                     }
                     // Create the symlink
                     logger.info('Retrieved ' + ipath);
-                    return makeSymLink(cachePath, ipath).then(deferred.resolve);
+                    return symlink(cachePath, ipath).then(deferred.resolve);
                 }))
-                .fail(err => deferred.reject(`Could not retrieve "${inputName}" (${err})`));
+                .catch(err => deferred.reject(`Could not retrieve "${inputName}" (${err})`));
         });
 
         return deferred.promise;
@@ -239,16 +239,16 @@ requirejs([
     };
 
     process.on('exit', () => {
-        log('received "exit" event')
+        log('received "exit" event');
         cleanup();
     });
     process.on('SIGINT', function() {
-        log('received "SIGINT" event')
+        log('received "SIGINT" event');
         cleanup();
         process.exit(130);
     });
     process.on('uncaughtException', err => {
-        log('received "uncaughtException" event')
+        log('received "uncaughtException" event');
         log(err);
         cleanup();
     });
@@ -262,14 +262,52 @@ requirejs([
             job = spawn('python', ['main.py'], {detached: true});
             job.stdout.on('data', onStdout);
             job.stderr.on('data', onStderr);
-            job.on('close', code => {
+            job.on('close', async code => {
                 exitCode = code;
                 log('script finished w/ exit code:', code);
+                if (exitCode === 0) {
+                    const results = JSON.parse(fs.readFileSync('result-types.json'));
+                    const client = await Storage.getBackend('gme').getClient(logger);
+                    const outputNames = Object.keys(results);
+
+                    for (let i = outputNames.length; i--;) {
+                        const filename = outputNames[i];
+                        const contents = fs.readFileSync(`outputs/${filename}`);
+                        const dataInfo = await client.putFile(filename, contents);
+                        const type = results[filename];
+                        results[filename] = {type, dataInfo};
+                    }
+                    fs.writeFileSync('results.json', JSON.stringify(results));
+                }
                 checkFinished();
             });
         })
-        .fail(err => {
+        .catch(err => {
             console.log(`Data retrieval failed: ${err}`);
             process.exit(1);
         });
+
+    function symlink(target, src) {
+        var deferred = Q.defer(),
+            job;
+
+        src = path.resolve(src);
+        target = path.resolve(target);
+        fs.stat(src, err => {
+            if (err && err.code === 'ENOENT') {
+                logger.debug(`creating symlink "ln -s ${target} ${src}"`);
+                job = spawn('ln', ['-s', target, src || '.']);
+                job.on('exit', code => {
+                    if (code) {
+                        deferred.reject(`Could not create symlink ${target} -> ${src||'.'}`);
+                        return;
+                    }
+                    deferred.resolve();
+                });
+            }
+            deferred.resolve();
+        });
+        return deferred.promise;
+    };
+
 });
diff --git a/src/plugins/ImportArtifact/ImportArtifact.js b/src/plugins/ImportArtifact/ImportArtifact.js
index 1f7c0cd24..0200a1a47 100644
--- a/src/plugins/ImportArtifact/ImportArtifact.js
+++ b/src/plugins/ImportArtifact/ImportArtifact.js
@@ -2,13 +2,13 @@
 /*jshint node:true, browser:true*/
 
 define([
+    'deepforge/storage/index',
     'text!./metadata.json',
     'plugin/PluginBase',
-    'q'
 ], function (
+    Storage,
     pluginMetadata,
     PluginBase,
-    Q
 ) {
     'use strict';
 
@@ -47,77 +47,68 @@ define([
      *
      * @param {function(string, plugin.PluginResult)} callback - the result callback
      */
-    ImportArtifact.prototype.main = function (callback) {
-        var self = this,
-            config = this.getCurrentConfig(),
-            hash = config.dataHash,
-            baseName = config.dataTypeId,
-            name,
-            baseType,
-            dataNode,
-
-            metaDict,
-            metanodes;
-
-        // Create node of type "typeId" in the activeNode and set the hash, name
-        metaDict = this.core.getAllMetaNodes(this.activeNode);
-        metanodes = Object.keys(metaDict).map(id => metaDict[id]);
-        baseType = metanodes.find(node =>
+    ImportArtifact.prototype.main = async function (callback) {
+        const config = this.getCurrentConfig();
+        const hash = config.dataHash;
+        const baseName = config.dataTypeId;
+        const metaDict = this.core.getAllMetaNodes(this.activeNode);
+        const metanodes = Object.keys(metaDict).map(id => metaDict[id]);
+        const base = metanodes.find(node =>
             this.core.getAttribute(node, 'name') === 'Data'
         );
 
-        if (!baseType) {
+        if (!base) {
             callback(`Could not find data type "${baseName}"`, this.result);
             return;
         }
 
         // Get the base node
-        this.getArtifactsDir()
-            .then(targetDir => {
-                dataNode = this.core.createNode({
-                    base: baseType,
-                    parent: targetDir
-                });
-
-                this.core.setAttribute(dataNode, 'data', hash);
-                this.core.setAttribute(dataNode, 'type', baseName);
-                this.core.setAttribute(dataNode, 'createdAt', Date.now());
-                baseName = this.core.getAttribute(baseType, 'name');
-
-                var getName;
-                if (config.name) {
-                    getName = Q().then(() => config.name);
-                } else {
-                    getName = this.blobClient.getMetadata(hash)
-                        .then(md => {
-                            name = baseName[0].toLowerCase() + baseName.substring(1);
-                            if (md) {
-                                name = md.name.replace(/\.[^\.]*?$/, '');
-                            }
-                            return name;
-                        });
-                }
-                return getName;
-            })
-            .then(name => this.core.setAttribute(dataNode, 'name', name))
-            .then(() => this.save(`Uploaded "${name}" data`))
-            .then(function () {
-                self.result.setSuccess(true);
-                callback(null, self.result);
-            })
-            .fail(function (err) {
-                callback(err, self.result);
-            });
+        const parent = await this.getArtifactsDir();
+        const dataNode = this.core.createNode({base, parent});
 
+        const assetInfo = await this.transfer(hash, config.storage);
+        this.core.setAttribute(dataNode, 'data', JSON.stringify(assetInfo));
+        this.core.setAttribute(dataNode, 'type', baseName);
+        this.core.setAttribute(dataNode, 'createdAt', Date.now());
+
+        try {
+            const name = config.name || await this.getAssetName(hash) ||
+                baseName[0].toLowerCase() + baseName.substring(1);
+
+            this.core.setAttribute(dataNode, 'name', name);
+            await this.save(`Uploaded "${name}" data`);
+            this.result.setSuccess(true);
+            callback(null, this.result);
+        } catch (err) {
+            callback(err, this.result);
+        }
+
+    };
+
+    ImportArtifact.prototype.transfer = async function (hash, dstId) {
+        const metadata = await this.blobClient.getMetadata(hash);
+        const filename = metadata.name;
+
+        const gmeStorageClient = await Storage.getBackend('gme').getClient(this.logger);
+        const dataInfo = gmeStorageClient.createDataInfo(hash);
+        const content = await gmeStorageClient.getFile(dataInfo);
+        const dstStorage = await Storage.getBackend(dstId).getClient(this.logger);
+        return await dstStorage.putFile(filename, content);
+    };
+
+    ImportArtifact.prototype.getAssetName = async function (hash) {
+        const metadata = await this.blobClient.getMetadata(hash);
+        if (metadata) {
+            return metadata.name.replace(/\.[^.]*?$/, '');
+        }
     };
 
-    ImportArtifact.prototype.getArtifactsDir = function() {
+    ImportArtifact.prototype.getArtifactsDir = async function() {
         // Find the artifacts dir
-        return this.core.loadChildren(this.rootNode)
-            .then(children => children
-                .find(child => this.core.getAttribute(child, 'name') === 'MyArtifacts') ||
-                    this.activeNode
-            );
+        const children = await this.core.loadChildren(this.rootNode);
+        return children
+            .find(child => this.core.getAttribute(child, 'name') === 'MyArtifacts') ||
+                this.activeNode;
     };
 
     return ImportArtifact;
diff --git a/src/seeds/pipeline/pipeline.webgmex b/src/seeds/pipeline/pipeline.webgmex
index e0cd465c4..264f029c2 100644
Binary files a/src/seeds/pipeline/pipeline.webgmex and b/src/seeds/pipeline/pipeline.webgmex differ
diff --git a/src/seeds/pipeline/version.txt b/src/seeds/pipeline/version.txt
index d33c3a212..54d1a4f2a 100644
--- a/src/seeds/pipeline/version.txt
+++ b/src/seeds/pipeline/version.txt
@@ -1 +1 @@
-0.12.0
\ No newline at end of file
+0.13.0
diff --git a/src/visualizers/panels/ArtifactIndex/ArtifactIndexControl.js b/src/visualizers/panels/ArtifactIndex/ArtifactIndexControl.js
index 2653e13ee..00eeb68ae 100644
--- a/src/visualizers/panels/ArtifactIndex/ArtifactIndexControl.js
+++ b/src/visualizers/panels/ArtifactIndex/ArtifactIndexControl.js
@@ -2,9 +2,11 @@
 /*jshint browser: true*/
 
 define([
+    'deepforge/storage/index',
     'blob/BlobClient',
     'js/Constants'
 ], function (
+    Storage,
     BlobClient,
     CONSTANTS
 ) {
@@ -88,30 +90,27 @@ define([
     };
 
     // This next function retrieves the relevant node information for the widget
-    ArtifactIndexControl.prototype._getObjectDescriptor = function (nodeId) {
-        var node = this._client.getNode(nodeId),
-            type,
-            hash,
-            objDescriptor;
+    ArtifactIndexControl.prototype._getObjectDescriptor = async function (nodeId) {
+        const node = this._client.getNode(nodeId);
 
         if (node) {
-            type = node.getAttribute('type');
-            hash = node.getAttribute('data');
-            objDescriptor = {
+            const type = node.getAttribute('type');
+            const dataInfo = JSON.parse(node.getAttribute('data'));
+            const metadata = await Storage.getMetadata(dataInfo, this._logger);
+            const url = await Storage.getDownloadURL(dataInfo, this._logger);
+            const size = this._humanFileSize(metadata.size);
+
+            return {
                 id: node.getId(),
                 type: type,
                 name: node.getAttribute('name'),
                 createdAt: node.getAttribute('createdAt'),
-                dataURL: this.blobClient.getDownloadURL(hash),
-                parentId: node.getParentId()
+                dataURL: url,
+                parentId: node.getParentId(),
+                size: size
             };
         }
 
-        return this.blobClient.getMetadata(hash)
-            .then(metadata => {
-                objDescriptor.size = this._humanFileSize(metadata.size);
-                return objDescriptor;
-            });
     };
 
     ArtifactIndexControl.prototype._humanFileSize = function (bytes, si) {
diff --git a/src/visualizers/panels/WorkerHeader/ProjectNavigatorController.js b/src/visualizers/panels/WorkerHeader/ProjectNavigatorController.js
index 7720d9090..306236cba 100644
--- a/src/visualizers/panels/WorkerHeader/ProjectNavigatorController.js
+++ b/src/visualizers/panels/WorkerHeader/ProjectNavigatorController.js
@@ -1,12 +1,26 @@
 /* globals define, WebGMEGlobal */
 define([
     'js/Dialogs/Projects/ProjectsDialog',
+    'js/Loader/ProgressNotification',
     './ComputeDialog',
-    'js/Panels/Header/ProjectNavigatorController'
+    'js/Panels/Header/ProjectNavigatorController',
+    'js/Dialogs/Confirm/ConfirmDialog',
+    'js/Dialogs/Merge/MergeDialog',
+    'js/Dialogs/AddCommits/AddCommitsDialog',
+    'q',
+    'js/Utils/SaveToDisk',
+    'blob/BlobClient',
 ], function(
     ProjectsDialog,
+    ProgressNotification,
     ComputeDialog,
-    GMEProjectNavigatorController
+    GMEProjectNavigatorController,
+    ConfirmDialog,
+    MergeDialog,
+    AddCommitsDialog,
+    Q,
+    SaveToDisk,
+    BlobClient,
 ) {
     'use strict';
     var ProjectNavigatorController = function() {
@@ -101,5 +115,311 @@ define([
         };
     };
 
+    ProjectNavigatorController.prototype.addBranch = function (projectId, branchId, branchInfo, noUpdate) {
+        var self = this,
+            i,
+            deleteBranchItem,
+            mergeBranchItem,
+            addCommits,
+            undoLastCommitItem,
+            redoLastUndoItem;
+
+        if (self.projects.hasOwnProperty(projectId) === false) {
+            self.logger.warn('project is not in the list yet: ', projectId);
+            return;
+        }
+
+        if (self.projects[projectId].disabled) {
+            // do not show any branches if the project is disabled
+            return;
+        }
+
+        function showBranchHistory(data) {
+            self.showHistory(data);
+        }
+
+        function exportBranch(data) {
+            const progress = ProgressNotification.start('<strong>Exporting </strong> project ...');
+            const complete = () => {
+                clearInterval(progress.intervalId);
+                setTimeout(() => progress.note.close(), 5000);
+            };
+
+            self.exportBranch(data)
+                .then(result => {
+                    const {filename, url} = result;
+                    SaveToDisk.saveUrlToDisk(url);
+                    progress.note.update({
+                        message: '<strong>Exported </strong> project <a href="' +
+                        url + '" target="_blank">' + filename + '</a>',
+                        progress: 100,
+                        type: 'success'
+                    });
+                    complete();
+                })
+                .catch(err => {
+                    progress.note.update({
+                        message: '<strong>Failed to export: </strong>' + err.message,
+                        type: 'danger',
+                        progress: 100
+                    });
+                    complete();
+                });
+        }
+
+        function deleteBranch(data) {
+            var deleteBranchModal = new ConfirmDialog(),
+                deleteItem = WebGMEGlobal.getProjectDisplayedNameFromProjectId(data.projectId) +
+                    '  ::  ' + data.branchId;
+            deleteBranchModal.show({deleteItem: deleteItem}, function () {
+                self.gmeClient.deleteBranch(data.projectId,
+                    data.branchId,
+                    data.branchInfo.branchHash,
+                    function (err) {
+                        if (err) {
+                            self.logger.error('Failed deleting branch of project.',
+                                data.projectId, data.branchId, err);
+                        } else {
+                            self.removeBranch(data.projectId, data.branchId);
+                        }
+                    }
+                );
+            });
+
+        }
+
+        function mergeBranch(data) {
+            var progress = ProgressNotification.start('<strong>Merging </strong> branch ' +
+                data.branchId + ' into ' + self.$scope.navigator.items[self.navIdBranch].id + '...');
+
+            if (data.projectId !== self.gmeClient.getActiveProjectId()) {
+                self.logger.error(new Error('Cannot merge branch from a different project..'));
+                clearInterval(progress.intervalId);
+                progress.note.update({
+                    message: '<strong>Failed to merge: </strong> cannot merge branch from a different project.',
+                    type: 'danger',
+                    progress: 100
+                });
+            } else {
+                self.gmeClient.autoMerge(data.projectId,
+                    data.branchId, self.$scope.navigator.items[self.navIdBranch].id,
+                    function (err, result) {
+                        clearInterval(progress.intervalId);
+                        progress.note.update('progress', 100);
+                        progress.note.close();
+                        var mergeDialog = new MergeDialog(self.gmeClient);
+                        if (err) {
+                            self.logger.error('merge of branch failed', err);
+                            mergeDialog.show(err);
+                            return;
+                        }
+
+                        if (result && result.conflict && result.conflict.items.length > 0) {
+                            //TODO create some user-friendly way to show this type of result
+                            self.logger.error('merge ended in conflicts', result);
+                            mergeDialog.show('merge ended in conflicts', result);
+                        } else {
+                            self.logger.debug('successful merge');
+                            mergeDialog.show(null, result);
+                        }
+                    }
+                );
+            }
+        }
+
+        function selectBranch(data) {
+            self.selectBranch(data);
+        }
+
+        deleteBranchItem = {
+            id: 'deleteBranch',
+            label: 'Delete branch',
+            iconClass: 'glyphicon glyphicon-remove',
+            disabled: self.projects[projectId].projectIsReadOnly,
+            action: deleteBranch,
+            actionData: {
+                projectId: projectId,
+                branchId: branchId,
+                branchInfo: branchInfo
+            }
+        };
+
+        mergeBranchItem = {
+            id: 'mergeBranch',
+            label: 'Merge into current branch',
+            iconClass: 'fa fa-share-alt fa-rotate-90',
+            disabled: self.projects[projectId].projectIsReadOnly,
+            action: mergeBranch,
+            actionData: {
+                projectId: projectId,
+                branchId: branchId
+            }
+        };
+
+        addCommits = {
+            id: 'addCommits',
+            label: 'Add external commits ...',
+            iconClass: 'glyphicon glyphicon-fast-forward',
+            disabled: true,
+            action: function (data) {
+                self.gmeClient.getBranches(data.projectId, function (err, branches) {
+                    if (err) {
+                        self.logger.error(new Error('Failed getting branches before adding commits'));
+                        return;
+                    }
+
+                    var dialog = new AddCommitsDialog(self.gmeClient, WebGMEGlobal.gmeConfig, branches);
+                    dialog.show(data);
+                });
+            },
+            actionData: {
+                projectId: projectId,
+                branchName: branchId
+            }
+        };
+
+        undoLastCommitItem = {
+            id: 'undoLastCommit',
+            label: 'Undo last commit',
+            iconClass: 'fa fa-reply',
+            disabled: true, // TODO: set this from handler to enable/disable
+            action: function (actionData) {
+                self.gmeClient.undo(actionData.branchId, function (/*err*/) {
+                });
+            },
+            // Put whatever you need to get passed back above
+            actionData: {
+                projectId: projectId,
+                branchId: branchId,
+                branchInfo: branchInfo
+            }
+        };
+
+        redoLastUndoItem = {
+            id: 'redoLastUndo',
+            label: 'Redo last undo',
+            iconClass: 'fa fa-mail-forward',
+            disabled: true, // TODO: set this from handler to enable/disable
+            action: function (actionData) {
+                self.gmeClient.redo(actionData.branchId, function (/*err*/) {
+                });
+            },
+            // Put whatever you need to get passed back above
+            actionData: {
+                projectId: projectId,
+                branchId: branchId,
+                branchInfo: branchInfo
+            }
+        };
+
+        // create the new branch structure
+        self.projects[projectId].branches[branchId] = {
+            id: branchId,
+            label: branchId,
+            properties: {
+                commitHash: branchInfo.branchHash,
+                commitObject: branchInfo.commitObject || {time: Date.now()}
+            },
+            isSelected: false,
+            itemClass: self.config.branchMenuClass,
+            action: selectBranch,
+            actionData: {
+                projectId: projectId,
+                branchId: branchId
+            },
+            //itemTemplate: 'branch-selector-template',
+            menu: [
+                {
+                    items: [
+                        undoLastCommitItem,
+                        redoLastUndoItem,
+                        {
+                            id: 'branchHistory',
+                            label: 'Branch history ...',
+                            iconClass: 'glyphicon glyphicon-time',
+                            action: showBranchHistory,
+                            actionData: {
+                                projectId: projectId,
+                                branchId: branchId,
+                                branchInfo: branchInfo
+                            }
+                        },
+                        {
+                            id: 'createBranch',
+                            label: 'Create branch ...',
+                            iconClass: 'glyphicon glyphicon-plus',
+                            action: showBranchHistory,
+                            actionData: {
+                                projectId: projectId,
+                                branchId: branchId,
+                                branchInfo: branchInfo
+                            }
+                        },
+                        deleteBranchItem,
+                        mergeBranchItem,
+                        addCommits,
+                        {
+                            id: 'exportBranch',
+                            label: 'Export branch',
+                            iconClass: 'glyphicon glyphicon-export',
+                            action: exportBranch,
+                            actionData: {
+                                projectId: projectId,
+                                branchId: branchId,
+                                commitHash: branchInfo.branchHash
+                            }
+                        }
+                    ]
+
+                }
+            ]
+        };
+
+        self.projects[projectId].branches[branchId].deleteBranchItem = deleteBranchItem;
+        self.projects[projectId].branches[branchId].mergeBranchItem = mergeBranchItem;
+        self.projects[projectId].branches[branchId].undoLastCommitItem = undoLastCommitItem;
+        self.projects[projectId].branches[branchId].redoLastUndoItem = redoLastUndoItem;
+        self.projects[projectId].branches[branchId].applyCommitQueueItem = addCommits;
+
+        for (i = 0; i < self.projects[projectId].menu.length; i += 1) {
+
+            // find the branches id in the menu items
+            if (self.projects[projectId].menu[i].id === 'branches') {
+
+                // convert indexed branches to an array
+                self.projects[projectId].menu[i].items = self.mapToArray(self.projects[projectId].branches,
+                    [{key: 'properties.commitObject.time', reverse: true}, {key: 'id'}]);
+                break;
+            }
+        }
+
+        if (noUpdate === true) {
+
+        } else {
+            self.update();
+        }
+    };
+
+    ProjectNavigatorController.prototype.exportBranch = async function (data) {
+        const {projectId, branchId} = data;
+        const isActiveBranch = projectId === this.gmeClient.getActiveProjectId() &&
+            branchId === this.gmeClient.getActiveBranchName();
+        const commitHash = isActiveBranch ? this.gmeClient.getActiveCommitHash() :
+            data.commitHash;
+
+        const pluginId = 'ExportBranch';
+        const context = this.gmeClient.getCurrentPluginContext(pluginId);
+        context.managerConfig.branchName = branchId;
+        context.managerConfig.commitHash = commitHash;
+        context.pluginConfig = {};
+        const result = await Q.ninvoke(this.gmeClient, 'runBrowserPlugin', pluginId, context);
+
+        const [hash] = result.artifacts;
+        const bc = new BlobClient({logger: this.logger.fork('BlobClient')});
+        const filename = (await bc.getMetadata(hash)).name;
+        const url = bc.getDownloadURL(hash);
+        return {filename, url};
+    };
+
     return ProjectNavigatorController;
 });
diff --git a/test/unit/plugins/ExportBranch/ExportBranch.spec.js b/test/unit/plugins/ExportBranch/ExportBranch.spec.js
new file mode 100644
index 000000000..f75eb945a
--- /dev/null
+++ b/test/unit/plugins/ExportBranch/ExportBranch.spec.js
@@ -0,0 +1,97 @@
+/*eslint-env node, mocha*/
+
+describe('ExportBranch', function () {
+    const assert = require('assert');
+    const testFixture = require('../../../globals');
+    const gmeConfig = testFixture.getGmeConfig();
+    const {Q, expect} = testFixture;
+    const logger = testFixture.logger.fork('ExportBranch');
+    const PluginCliManager = testFixture.WebGME.PluginCliManager;
+    const manager = new PluginCliManager(null, logger, gmeConfig);
+    const projectName = 'testProject';
+    const pluginName = 'ExportBranch';
+    const context = {
+        project: null,
+        commitHash: null,
+        branchName: 'test',
+        activeNode: '/1',
+    };
+    let gmeAuth,
+        storage;
+
+    before(async function () {
+        gmeAuth = await testFixture.clearDBAndGetGMEAuth(gmeConfig, projectName);
+        storage = testFixture.getMemoryStorage(logger, gmeConfig, gmeAuth);
+        await storage.openDatabase();
+        const importParam = {
+            projectSeed: testFixture.path.join(testFixture.SEED_DIR, 'EmptyProject.webgmex'),
+            projectName: projectName,
+            branchName: 'master',
+            logger: logger,
+            gmeConfig: gmeConfig
+        };
+
+        const importResult = await testFixture.importProject(storage, importParam);
+        const {project, commitHash} = importResult;
+        context.project = project;
+        context.commitHash = commitHash;
+        await project.createBranch('test', commitHash);
+    });
+
+    after(async function () {
+        await storage.closeDatabase();
+        await gmeAuth.unload();
+    });
+
+    describe('basic tests', function() {
+        let pluginResult;
+
+        before(async () => {
+            const pluginConfig = {};
+            pluginResult = await Q.ninvoke(manager, 'executePlugin', pluginName, pluginConfig, context);
+        });
+
+        it('should exit successfully', async function () {
+            expect(typeof pluginResult).to.equal('object');
+            expect(pluginResult.success).to.equal(true);
+        });
+
+        it('should generate one artifact (webgmex file)', async function () {
+            expect(pluginResult.artifacts.length).to.equal(1);
+        });
+
+        it('should not update the branch', async function () {
+            const {project, commitHash} = context;
+            const branchHash = await project.getBranchHash('test');
+            expect(branchHash).to.equal(commitHash);
+        });
+    });
+
+    describe('hash detection', function() {
+        let hashes;
+        const gmeDataInfo = {backend: 'gme', data: '123'};
+        const otherDataInfo = {backend: 'none', data: 'abc'};
+
+        before(async () => {
+            const plugin = await manager.initializePlugin(pluginName);
+            await manager.configurePlugin(plugin, {}, context);
+            const {core} = plugin;
+            const [node] = await core.loadChildren(plugin.rootNode);
+
+            core.setAttributeMeta(node, 'data', {type: 'userAsset'});
+            core.setAttributeMeta(node, 'bad_data', {type: 'userAsset'});
+            core.setAttribute(node, 'data', JSON.stringify(gmeDataInfo));
+            core.setAttribute(node, 'bad_data', JSON.stringify(otherDataInfo));
+
+            hashes = await plugin.getUserAssetHashes();
+        });
+
+        it('should detect (gme) userAssets in the project', function () {
+            assert(hashes.includes(gmeDataInfo.data), 'Did not export gme userAssets.');
+        });
+
+        it('should ignore non-gme userAssets in the project', function () {
+            assert(!hashes.includes(otherDataInfo.data), 'Exported non-gme userAssets.');
+        });
+    });
+});
diff --git a/webgme-setup.json b/webgme-setup.json
index 48269c639..11bf1f279 100644
--- a/webgme-setup.json
+++ b/webgme-setup.json
@@ -52,6 +52,10 @@
       "ApplyUpdates": {
         "src": "src/plugins/ApplyUpdates",
         "test": "test/plugins/ApplyUpdates"
+      },
+      "ExportBranch": {
+        "src": "src/plugins/ExportBranch",
+        "test": "test/plugins/ExportBranch"
       }
     },
     "layouts": {
