diff --git a/nextflow/resources/views/begin.html b/nextflow/resources/views/begin.html
deleted file mode 100644
index 9ba623a2..00000000
--- a/nextflow/resources/views/begin.html
+++ /dev/null
@@ -1,17 +0,0 @@
-<html>
-<button id="enable-button">Enable/Disable Nextflow</button>
-<button id="run-button">Nextflow Pipeline</button>
-</html>
-
-<script type = "text/javascript" nonce="<%=scriptNonce%>">
-
-    $(document).ready(function() {
-        $('#enable-button').click(function() {
-            window.location = LABKEY.ActionURL.buildURL('nextflow', 'nextFlowEnable.view');
-        });
-
-        $('#run-button').click(function() {
-            window.location = LABKEY.ActionURL.buildURL('nextflow', 'nextFlowRun.api');
-        });
-    })
-</script>
\ No newline at end of file
diff --git a/nextflow/resources/views/nextFlowConfiguration.html b/nextflow/resources/views/nextFlowConfiguration.html
deleted file mode 100644
index fd2a895c..00000000
--- a/nextflow/resources/views/nextFlowConfiguration.html
+++ /dev/null
@@ -1,88 +0,0 @@
-<table>
-    <tr>
-        <td class="labkey-form-label">NextFlow Config File Path</td>
-        <td><input type="text" name="nextFlowConfigFilePath" size=64></td>
-    </tr>
-    <tr>
-        <td class="labkey-form-label">AWS Account Name</td>
-        <td><input type="text" name="name" size=64></td>
-    </tr>
-    <tr>
-        <td class="labkey-form-label">AWS Identity</td>
-        <td><input type="text" name="identity" size=64></td>
-    </tr>
-    <tr>
-        <td class="labkey-form-label">AWS S3 Bucket Path</td>
-        <td><input type="text" name="s3BucketPath" size=64></td>
-    </tr>
-    <tr>
-        <td class="labkey-form-label">AWS Credential</td>
-        <td><input type="password" name="credential" size=64></td>
-    </tr>
-</table>
-<button id="submit-button">Submit</button>
-<button id="delete-button">Delete</button>
-<button id="cancel-button">Cancel</button>
-
-<script type="text/javascript" nonce="<%=scriptNonce%>">
-    $(document).ready(function() {
-        // load the current configuration on page load
-        LABKEY.Ajax.request({
-            url: LABKEY.ActionURL.buildURL('nextflow', 'GetNextFlowConfiguration.api'),
-            method: 'GET',
-            success: function(response) {
-                const parsed = JSON.parse(response.responseText);
-                if (parsed.config) {
-                    const response = parsed.config;
-                    $('input[name=nextFlowConfigFilePath]').val(response.nextFlowConfigFilePath);
-                    $('input[name=name]').val(response.accountName);
-                    $('input[name=identity]').val(response.identity);
-                    $('input[name=s3BucketPath]').val(response.s3BucketPath);
-                    $('input[name=credential]').val(response.credential);
-                }
-
-            },
-            failure: function(response) {
-                alert('Failed to load configuration');
-            }
-        });
-
-        $('#submit-button').click(function() {
-            let data = {
-                nextFlowConfigFilePath: $('input[name=nextFlowConfigFilePath]').val(),
-                accountName: $('input[name=name]').val(),
-                identity: $('input[name=identity]').val(),
-                s3BucketPath: $('input[name=s3BucketPath]').val(),
-                credential: $('input[name=credential]').val()
-            };
-            LABKEY.Ajax.request({
-                url: LABKEY.ActionURL.buildURL('nextflow', 'nextFlowConfiguration.api'),
-                method: 'POST',
-                params: data,
-                success: function(response) {
-                    window.location = LABKEY.ActionURL.buildURL('project', 'start');
-                },
-                failure: function(response) {
-                    alert('Failed to save configuration');
-                }
-            });
-        });
-
-        $('#delete-button').click(function() {
-            LABKEY.Ajax.request({
-                url: LABKEY.ActionURL.buildURL('nextflow', 'DeleteNextFlowConfiguration.api'),
-                method: 'POST',
-                success: function(response) {
-                    window.location = LABKEY.ActionURL.buildURL('project', 'start');
-                },
-                failure: function(response) {
-                    alert('Failed to delete configuration');
-                }
-            });
-        });
-
-        $('#cancel-button').click(function() {
-            window.location = LABKEY.ActionURL.buildURL('project', 'start');
-        });
-    });
-</script>
diff --git a/nextflow/resources/views/nextFlowConfiguration.view.xml b/nextflow/resources/views/nextFlowConfiguration.view.xml
deleted file mode 100644
index 8a589d3c..00000000
--- a/nextflow/resources/views/nextFlowConfiguration.view.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-<view xmlns="http://labkey.org/data/xml/view" title="NextFlow Configuration">
-    <dependencies>
-        <dependency path="internal/jQuery"/>
-    </dependencies>
-</view>
\ No newline at end of file
diff --git a/nextflow/src/org/labkey/nextflow/NextFlowConfiguration.java b/nextflow/src/org/labkey/nextflow/NextFlowConfiguration.java
new file mode 100644
index 00000000..e8480095
--- /dev/null
+++ b/nextflow/src/org/labkey/nextflow/NextFlowConfiguration.java
@@ -0,0 +1,71 @@
+package org.labkey.nextflow;
+
+public class NextFlowConfiguration
+{
+    private String nextFlowConfigFilePath;
+    private String accountName;
+    private String identity;
+    private String s3BucketPath;
+    private String credential;
+    private String apiKey;
+
+    public String getNextFlowConfigFilePath()
+    {
+        return nextFlowConfigFilePath;
+    }
+
+    public void setNextFlowConfigFilePath(String nextFlowConfigFilePath)
+    {
+        this.nextFlowConfigFilePath = nextFlowConfigFilePath;
+    }
+
+    public String getAccountName()
+    {
+        return accountName;
+    }
+
+    public void setAccountName(String accountName)
+    {
+        this.accountName = accountName;
+    }
+
+    public String getIdentity()
+    {
+        return identity;
+    }
+
+    public void setIdentity(String identity)
+    {
+        this.identity = identity;
+    }
+
+    public String getS3BucketPath()
+    {
+        return s3BucketPath;
+    }
+
+    public void setS3BucketPath(String s3BucketPath)
+    {
+        this.s3BucketPath = s3BucketPath;
+    }
+
+    public String getCredential()
+    {
+        return credential;
+    }
+
+    public void setCredential(String credential)
+    {
+        this.credential = credential;
+    }
+
+    public String getApiKey()
+    {
+        return apiKey;
+    }
+
+    public void setApiKey(String apiKey)
+    {
+        this.apiKey = apiKey;
+    }
+}
diff --git a/nextflow/src/org/labkey/nextflow/NextFlowController.java b/nextflow/src/org/labkey/nextflow/NextFlowController.java
index f22af232..b6b285d1 100644
--- a/nextflow/src/org/labkey/nextflow/NextFlowController.java
+++ b/nextflow/src/org/labkey/nextflow/NextFlowController.java
@@ -1,26 +1,25 @@
 package org.labkey.nextflow;
 
+import org.apache.commons.lang3.StringUtils;
 import org.apache.logging.log4j.Logger;
 import org.labkey.api.action.ApiResponse;
 import org.labkey.api.action.ApiSimpleResponse;
 import org.labkey.api.action.FormViewAction;
 import org.labkey.api.action.MutatingApiAction;
-import org.labkey.api.action.ReadOnlyApiAction;
+import org.labkey.api.action.SimpleViewAction;
 import org.labkey.api.action.SpringActionController;
+import org.labkey.api.admin.AdminUrls;
 import org.labkey.api.data.PropertyManager;
 import org.labkey.api.data.PropertyStore;
-import org.labkey.api.module.Module;
-import org.labkey.api.module.ModuleHtmlView;
-import org.labkey.api.module.ModuleLoader;
 import org.labkey.api.pipeline.PipeRoot;
 import org.labkey.api.pipeline.PipelineJob;
 import org.labkey.api.pipeline.PipelineService;
 import org.labkey.api.pipeline.PipelineStatusUrls;
 import org.labkey.api.security.AdminConsoleAction;
 import org.labkey.api.security.RequiresPermission;
-import org.labkey.api.security.SecurityManager;
 import org.labkey.api.security.permissions.AdminOperationsPermission;
-import org.labkey.api.security.permissions.AdminPermission;
+import org.labkey.api.security.permissions.InsertPermission;
+import org.labkey.api.security.permissions.ReadPermission;
 import org.labkey.api.security.permissions.SiteAdminPermission;
 import org.labkey.api.util.Button;
 import org.labkey.api.util.PageFlowUtil;
@@ -28,20 +27,23 @@
 import org.labkey.api.util.logging.LogHelper;
 import org.labkey.api.view.ActionURL;
 import org.labkey.api.view.HtmlView;
+import org.labkey.api.view.JspView;
 import org.labkey.api.view.NavTree;
+import org.labkey.api.view.UnauthorizedException;
 import org.labkey.api.view.ViewBackgroundInfo;
 import org.labkey.nextflow.pipeline.NextFlowPipelineJob;
 import org.springframework.validation.BindException;
 import org.springframework.validation.Errors;
 import org.springframework.web.servlet.ModelAndView;
 
-import java.util.HashSet;
-import java.util.Set;
-
+import static org.labkey.api.util.DOM.Attribute.checked;
 import static org.labkey.api.util.DOM.Attribute.method;
+import static org.labkey.api.util.DOM.Attribute.name;
+import static org.labkey.api.util.DOM.Attribute.type;
+import static org.labkey.api.util.DOM.Attribute.value;
 import static org.labkey.api.util.DOM.DIV;
+import static org.labkey.api.util.DOM.INPUT;
 import static org.labkey.api.util.DOM.LK.FORM;
-import static org.labkey.api.util.DOM.P;
 import static org.labkey.api.util.DOM.at;
 import static org.labkey.nextflow.NextFlowManager.NEXTFLOW_CONFIG;
 
@@ -49,7 +51,6 @@ public class NextFlowController extends SpringActionController
 {
     private static final DefaultActionResolver _actionResolver = new DefaultActionResolver(NextFlowController.class);
     public static final String NAME = "nextflow";
-    private static final String IS_NEXTFLOW_ENABLED = "enabled";
 
     private static final Logger LOG = LogHelper.getLogger(NextFlowController.class, NAME);
 
@@ -58,22 +59,42 @@ public NextFlowController()
         setActionResolver(_actionResolver);
     }
 
-    @RequiresPermission(AdminPermission.class)
-    public class GetNextFlowConfigurationAction extends ReadOnlyApiAction<Object>
+    @RequiresPermission(ReadPermission.class)
+    public static class BeginAction extends SimpleViewAction<Object>
     {
         @Override
-        public ApiResponse execute(Object form, BindException errors) throws Exception
+        public ModelAndView getView(Object o, BindException errors)
+        {
+            boolean enabled = NextFlowManager.get().isEnabled(getContainer());
+            return new HtmlView("NextFlow",
+                    DIV(
+                        DIV("NextFlow integration is " + (enabled ? "enabled" : "disabled") + " in this " + (getContainer().isProject() ? "project" : "folder") + "."),
+                        DIV(
+                                getContainer().hasPermission(getUser(), SiteAdminPermission.class) ?
+                                new Button.ButtonBuilder("Enable/Disable").href(new ActionURL(NextFlowEnableAction.class, getContainer())).build() : null,
+                            " ",
+                            enabled && getContainer().hasPermission(getUser(), InsertPermission.class) ?
+                                    new Button.ButtonBuilder("Run NextFlow Analysis").href(new ActionURL(NextFlowRunAction.class, getContainer())).build() : null)));
+        }
+
+        @Override
+        public void addNavTrail(NavTree root)
         {
-            return new ApiSimpleResponse("config", PropertyManager.getEncryptedStore().getProperties(NEXTFLOW_CONFIG));
+            root.addChild("NextFlow");
         }
     }
 
-    @RequiresPermission(AdminPermission.class)
-    public class DeleteNextFlowConfigurationAction extends MutatingApiAction<Object>
+
+    @RequiresPermission(SiteAdminPermission.class)
+    public static class DeleteNextFlowConfigurationAction extends MutatingApiAction<Object>
     {
         @Override
-        public ApiResponse execute(Object form, BindException errors) throws Exception
+        public ApiResponse execute(Object form, BindException errors)
         {
+            if (!getContainer().isRoot())
+            {
+                throw new UnauthorizedException();
+            }
             PropertyStore store = PropertyManager.getEncryptedStore();
             store.deletePropertySet(NEXTFLOW_CONFIG);
             return new ApiSimpleResponse("success", true);
@@ -93,175 +114,151 @@ public void validateCommand(NextFlowConfiguration target, Errors errors)
         }
 
         @Override
-        public ModelAndView getView(NextFlowConfiguration nextFlowConfiguration, boolean reshow, BindException errors) throws Exception
+        public ModelAndView getView(NextFlowConfiguration newConfig, boolean reshow, BindException errors)
         {
-            return ModuleHtmlView.get(ModuleLoader.getInstance().getModule(NextFlowModule.class), "nextFlowConfiguration");
+            NextFlowConfiguration existingConfig = NextFlowManager.get().getConfiguration();
+            if (existingConfig != null)
+            {
+                if (StringUtils.isEmpty(newConfig.getNextFlowConfigFilePath()))
+                {
+                    newConfig.setNextFlowConfigFilePath(existingConfig.getNextFlowConfigFilePath());
+                }
+                if (StringUtils.isEmpty(newConfig.getAccountName()))
+                {
+                    newConfig.setAccountName(existingConfig.getAccountName());
+                }
+                if (StringUtils.isEmpty(newConfig.getIdentity()))
+                {
+                    newConfig.setIdentity(existingConfig.getIdentity());
+                }
+                if (StringUtils.isEmpty(newConfig.getCredential()))
+                {
+                    newConfig.setCredential(existingConfig.getCredential());
+                }
+                if (StringUtils.isEmpty(newConfig.getS3BucketPath()))
+                {
+                    newConfig.setS3BucketPath(existingConfig.getS3BucketPath());
+                }
+                if (StringUtils.isEmpty(newConfig.getApiKey()))
+                {
+                    newConfig.setApiKey(existingConfig.getApiKey());
+                }
+            }
+
+            return new JspView<>("/org/labkey/nextflow/nextFlowConfiguration.jsp", newConfig, errors);
         }
 
         @Override
-        public boolean handlePost(NextFlowConfiguration nextFlowConfiguration, BindException errors) throws Exception
+        public boolean handlePost(NextFlowConfiguration newConfig, BindException errors)
         {
-            NextFlowManager.get().addConfiguration(nextFlowConfiguration, errors);
+            NextFlowConfiguration existingConfig = NextFlowManager.get().getConfiguration();
+            if (existingConfig != null)
+            {
+                if (StringUtils.isEmpty(newConfig.getApiKey()))
+                {
+                    newConfig.setApiKey(existingConfig.getApiKey());
+                }
+                if (StringUtils.isEmpty(newConfig.getCredential()))
+                {
+                    newConfig.setCredential(existingConfig.getCredential());
+                }
+            }
+            NextFlowManager.get().saveConfig(newConfig, errors);
             return !errors.hasErrors();
         }
 
         @Override
         public URLHelper getSuccessURL(NextFlowConfiguration nextFlowConfiguration)
         {
-            return getContainer().getStartURL(getUser());
+            return PageFlowUtil.urlProvider(AdminUrls.class).getAdminConsoleURL();
         }
 
         @Override
         public void addNavTrail(NavTree root)
         {
-
+            root.addChild("Admin Console", PageFlowUtil.urlProvider(AdminUrls.class).getAdminConsoleURL());
+            root.addChild("Configure NextFlow");
         }
     }
 
-    public static class NextFlowConfiguration
+    public static class EnabledForm
     {
-        private String nextFlowConfigFilePath;
-        private String accountName;
-        private String identity;
-        private String s3BucketPath;
-        private String credential;
-
-        public String getNextFlowConfigFilePath()
-        {
-            return nextFlowConfigFilePath;
-        }
-
-        public void setNextFlowConfigFilePath(String nextFlowConfigFilePath)
-        {
-            this.nextFlowConfigFilePath = nextFlowConfigFilePath;
-        }
+        Boolean _enabled;
 
-        public String getAccountName()
+        public Boolean getEnabled()
         {
-            return accountName;
+            return _enabled;
         }
 
-        public void setAccountName(String accountName)
+        public void setEnabled(Boolean enabled)
         {
-            this.accountName = accountName;
-        }
-
-        public String getIdentity()
-        {
-            return identity;
-        }
-
-        public void setIdentity(String identity)
-        {
-            this.identity = identity;
-        }
-
-        public String getS3BucketPath()
-        {
-            return s3BucketPath;
-        }
-
-        public void setS3BucketPath(String s3BucketPath)
-        {
-            this.s3BucketPath = s3BucketPath;
-        }
-
-        public String getCredential()
-        {
-            return credential;
-        }
-
-        public void setCredential(String credential)
-        {
-            this.credential = credential;
+            _enabled = enabled;
         }
     }
 
     @RequiresPermission(SiteAdminPermission.class)
-    public static class NextFlowEnableAction extends FormViewAction
+    public static class NextFlowEnableAction extends FormViewAction<EnabledForm>
     {
-
         @Override
-        public void validateCommand(Object target, Errors errors)
+        public void validateCommand(EnabledForm target, Errors errors)
         {
 
         }
 
         @Override
-        public ModelAndView getView(Object form, boolean reshow, BindException errors) throws Exception
+        public ModelAndView getView(EnabledForm form, boolean reshow, BindException errors)
         {
-            PropertyStore store = PropertyManager.getNormalStore();
-            PropertyManager.PropertyMap map = store.getProperties(NextFlowManager.NEXTFLOW_ENABLE);
-            String btnTxt = "Enable NextFlow";
-            // check if nextflow is enabled
-            if (Boolean.parseBoolean(map.get(IS_NEXTFLOW_ENABLED)))
-            {
-                btnTxt = "Disable NextFlow";
-            }
-            else
-            {
-                btnTxt = "Enable NextFlow";
-            }
+            Boolean status = NextFlowManager.get().getEnabledState(getContainer());
+            boolean inheritedStatus = NextFlowManager.get().isEnabled(getContainer().getParent());
 
-            return new HtmlView("Enable/Disable Nextflow", DIV( P("NextFlow is currently " + (Boolean.parseBoolean(map.get(IS_NEXTFLOW_ENABLED)) ? "enabled" : "disabled")),
+            return new HtmlView("Enable/Disable NextFlow",
                     FORM(at(method, "POST"),
-                            new Button.ButtonBuilder(btnTxt).submit(true).build())));
+                        DIV(INPUT(at(type, "radio", name, "enabled", value, Boolean.TRUE.toString(), (status == Boolean.TRUE ? checked : null), null)),
+                            "Enabled"),
+                        DIV(INPUT(at(type, "radio", name, "enabled", value, Boolean.FALSE.toString(), (status == Boolean.FALSE ? checked : null), null)),
+                            "Disabled"),
+                            DIV(INPUT(at(type, "radio", name, "enabled", value, "", (status == null ? checked : null), null)),
+                                    getContainer().isRoot() ?
+                                            "Unset" :
+                                            "Inherited from " + getContainer().getParent().getPath() + " (currently " + (inheritedStatus ? "enabled" : "disabled") + ")"),
+                        new Button.ButtonBuilder("Save").submit(true).build(), " ",
+                        new Button.ButtonBuilder("Cancel").href(getContainer().getStartURL(getUser())).build()));
         }
 
         @Override
-        public boolean handlePost(Object form, BindException errors) throws Exception
+        public boolean handlePost(EnabledForm form, BindException errors)
         {
-            PropertyStore store = PropertyManager.getNormalStore();
-            PropertyManager.WritablePropertyMap map = store.getWritableProperties(NextFlowManager.NEXTFLOW_ENABLE, true);
-            if (map.isEmpty())
-            {
-                map.put(IS_NEXTFLOW_ENABLED, Boolean.TRUE.toString());
-            }
-            else
-            {
-                if (Boolean.parseBoolean(map.get(IS_NEXTFLOW_ENABLED)))
-                {
-                    map.put(IS_NEXTFLOW_ENABLED, Boolean.FALSE.toString());
-                }
-                else
-                {
-                    map.put(IS_NEXTFLOW_ENABLED, Boolean.TRUE.toString());
-                }
-            }
-            map.save();
+            NextFlowManager.get().saveEnabledState(getContainer(), form.getEnabled());
             return true;
         }
 
         @Override
         public void addNavTrail(NavTree root)
         {
-
+            root.addChild("Enable/Disable NextFlow");
         }
 
         @Override
-        public URLHelper getSuccessURL(Object o)
+        public URLHelper getSuccessURL(EnabledForm o)
         {
             return getContainer().getStartURL(getUser());
         }
     }
 
     @RequiresPermission(AdminOperationsPermission.class)
-    public class NextFlowRunAction extends FormViewAction
+    public class NextFlowRunAction extends FormViewAction<Object>
     {
-        private ActionURL _successURL;
         @Override
         public void validateCommand(Object o, Errors errors)
         {
-            PropertyStore store = PropertyManager.getNormalStore();
-            PropertyManager.PropertyMap map = store.getProperties(NextFlowManager.NEXTFLOW_ENABLE);
-            if (!Boolean.parseBoolean(map.get(IS_NEXTFLOW_ENABLED)))
+            if (!NextFlowManager.get().isEnabled(getContainer()))
             {
                 errors.reject(ERROR_MSG, "NextFlow is not enabled");
             }
         }
 
         @Override
-        public ModelAndView getView(Object o, boolean b, BindException errors) throws Exception
+        public ModelAndView getView(Object o, boolean b, BindException errors)
         {
             return new HtmlView("NextFlow Runner", DIV("Run NextFlow Pipeline",
                     FORM(at(method, "POST"),
@@ -271,24 +268,10 @@ public ModelAndView getView(Object o, boolean b, BindException errors) throws Ex
         @Override
         public boolean handlePost(Object o, BindException errors) throws Exception
         {
-            // check if nextflow is enabled
-            PropertyStore store = PropertyManager.getNormalStore();
-            PropertyManager.PropertyMap map = store.getProperties(NextFlowManager.NEXTFLOW_ENABLE);
-            if (map == null || !Boolean.parseBoolean(map.get(IS_NEXTFLOW_ENABLED)))
-            {
-                errors.reject(ERROR_MSG, "NextFlow is not enabled");
-                return false;
-            }
-
-            try (SecurityManager.TransformSession session = SecurityManager.createTransformSession(getViewContext()))
-            {
-                // TODO: pass the apiKey to Nextflow job
-                String apiKey = session.getApiKey();
-                ViewBackgroundInfo info = getViewBackgroundInfo();
-                PipeRoot root = PipelineService.get().findPipelineRoot(info.getContainer());
-                PipelineJob job = new NextFlowPipelineJob(info, root, apiKey);
-                PipelineService.get().queueJob(job);
-            }
+            ViewBackgroundInfo info = getViewBackgroundInfo();
+            PipeRoot root = PipelineService.get().findPipelineRoot(info.getContainer());
+            PipelineJob job = new NextFlowPipelineJob(info, root);
+            PipelineService.get().queueJob(job);
 
             return !errors.hasErrors();
         }
diff --git a/nextflow/src/org/labkey/nextflow/NextFlowManager.java b/nextflow/src/org/labkey/nextflow/NextFlowManager.java
index 23684b32..485380d7 100644
--- a/nextflow/src/org/labkey/nextflow/NextFlowManager.java
+++ b/nextflow/src/org/labkey/nextflow/NextFlowManager.java
@@ -1,10 +1,10 @@
 package org.labkey.nextflow;
 
 import org.apache.commons.lang3.StringUtils;
+import org.labkey.api.data.Container;
 import org.labkey.api.data.CoreSchema;
 import org.labkey.api.data.DbScope;
 import org.labkey.api.data.PropertyManager;
-import org.labkey.api.data.PropertyStore;
 import org.springframework.validation.BindException;
 
 import java.util.HashMap;
@@ -15,21 +15,18 @@
 public class NextFlowManager
 {
     public static final String NEXTFLOW_CONFIG = "nextflow-config";
-    public static final String NEXTFLOW_ENABLE = "nextflow-enable";
+    private static final String NEXTFLOW_ENABLE_PROP_CATEGORY = "nextflow-enable";
 
     private static final String NEXTFLOW_ACCOUNT_NAME = "accountName";
     private static final String NEXTFLOW_CONFIG_FILE_PATH = "nextFlowConfigFilePath";
     private static final String NEXTFLOW_IDENTITY = "identity";
     private static final String NEXTFLOW_CREDENTIAL = "credential";
     private static final String NEXTFLOW_S3_BUCKET_PATH = "s3BucketPath";
+    private static final String NEXTFLOW_API_KEY = "apiKey";
 
-    private static final NextFlowManager _instance = new NextFlowManager();
-
-    // Normal store is used for enabled/disabled module
-    private static final PropertyStore _normalStore = PropertyManager.getNormalStore();
+    private static final String IS_NEXTFLOW_ENABLED = "enabled";
 
-    // Encrypted store is used for aws settings & nextflow file configuration
-    private static final PropertyStore _encryptedStore = PropertyManager.getEncryptedStore();
+    private static final NextFlowManager _instance = new NextFlowManager();
 
     private NextFlowManager()
     {
@@ -42,48 +39,83 @@ public static NextFlowManager get()
     }
 
 
-    private void checkArgs(String nextFlowConfigFilePath, String name,  String identity, String credential,String s3BucketPath, BindException errors)
+    private void checkArgs(NextFlowConfiguration config, BindException errors)
     {
-        if (StringUtils.isEmpty(nextFlowConfigFilePath))
+        if (StringUtils.isEmpty(config.getNextFlowConfigFilePath()))
             errors.rejectValue("nextFlowConfigFilePath", ERROR_MSG, "NextFlow config file path is required");
 
-        if (StringUtils.isEmpty(name))
-            errors.rejectValue("name", ERROR_MSG, "AWS account name is required");
-
-        if (StringUtils.isEmpty(identity))
-            errors.rejectValue("identity", ERROR_MSG, "AWS identity is required");
-
-        if (StringUtils.isEmpty(credential))
-            errors.rejectValue("credential", ERROR_MSG, "AWS credential is required");
+        // Not yet used
+//        if (StringUtils.isEmpty(config.getAccountName()))
+//            errors.rejectValue("accountName", ERROR_MSG, "AWS account name is required");
+//        if (StringUtils.isEmpty(config.getIdentity()))
+//            errors.rejectValue("identity", ERROR_MSG, "AWS identity is required");
+//        if (StringUtils.isEmpty(config.getCredential()))
+//            errors.rejectValue("credential", ERROR_MSG, "AWS credential is required");
 
+        if (StringUtils.isEmpty(config.getS3BucketPath()))
+            errors.rejectValue("credential", ERROR_MSG, "S3 bucket path is required");
     }
 
-    public NextFlowController.NextFlowConfiguration getConfiguration()
+    public NextFlowConfiguration getConfiguration()
     {
-        PropertyManager.PropertyMap props = _encryptedStore.getWritableProperties(NEXTFLOW_CONFIG, false);
+        PropertyManager.PropertyMap props = PropertyManager.getEncryptedStore().getWritableProperties(NEXTFLOW_CONFIG, false);
         if (props != null)
         {
-            NextFlowController.NextFlowConfiguration configuration = new NextFlowController.NextFlowConfiguration();
+            NextFlowConfiguration configuration = new NextFlowConfiguration();
             configuration.setAccountName(props.get(NEXTFLOW_ACCOUNT_NAME));
             configuration.setNextFlowConfigFilePath(props.get(NEXTFLOW_CONFIG_FILE_PATH));
             configuration.setIdentity(props.get(NEXTFLOW_IDENTITY));
             configuration.setCredential(props.get(NEXTFLOW_CREDENTIAL));
             configuration.setS3BucketPath(props.get(NEXTFLOW_S3_BUCKET_PATH));
+            configuration.setApiKey(props.get(NEXTFLOW_API_KEY));
             return configuration;
         }
 
         return null;
     }
 
-    public void addConfiguration(NextFlowController.NextFlowConfiguration configuration, BindException errors)
+    /**
+     * Checks in the specified container and traverses up the container tree to determine if NextFlow integration
+     * is enabled directly or in a parent container.
+     */
+    public boolean isEnabled(Container c)
     {
-        checkArgs(configuration.getNextFlowConfigFilePath(), configuration.getAccountName(), configuration.getIdentity(), configuration.getCredential(), configuration.getS3BucketPath(), errors);
+        do
+        {
+            PropertyManager.PropertyMap map = PropertyManager.getProperties(c, NEXTFLOW_ENABLE_PROP_CATEGORY);
+            if (map.containsKey(IS_NEXTFLOW_ENABLED))
+            {
+                return Boolean.parseBoolean(map.get(IS_NEXTFLOW_ENABLED));
+            }
+            c = c.getParent();
+        }
+        while (c != null);
+
+        return false;
+    }
+
+    /**
+     * @return configured state for the container (or null if not configured there), for whether NextFlow is enabled
+     */
+    public Boolean getEnabledState(Container c)
+    {
+        PropertyManager.PropertyMap map = PropertyManager.getProperties(c, NEXTFLOW_ENABLE_PROP_CATEGORY);
+        if (map.containsKey(IS_NEXTFLOW_ENABLED))
+        {
+            return Boolean.parseBoolean(map.get(IS_NEXTFLOW_ENABLED));
+        }
+        return null;
+    }
+
+    public void saveConfig(NextFlowConfiguration configuration, BindException errors)
+    {
+        checkArgs(configuration, errors);
 
         if (!errors.hasErrors())
             saveConfiguration(configuration);
     }
 
-    private void saveConfiguration( NextFlowController.NextFlowConfiguration configuration)
+    private void saveConfiguration( NextFlowConfiguration configuration)
     {
         try (DbScope.Transaction tx = CoreSchema.getInstance().getSchema().getScope().ensureTransaction())
         {
@@ -93,8 +125,9 @@ private void saveConfiguration( NextFlowController.NextFlowConfiguration configu
             properties.put(NEXTFLOW_CREDENTIAL, configuration.getCredential());
             properties.put(NEXTFLOW_S3_BUCKET_PATH, configuration.getS3BucketPath());
             properties.put(NEXTFLOW_ACCOUNT_NAME, configuration.getAccountName());
+            properties.put(NEXTFLOW_API_KEY, configuration.getApiKey());
 
-            PropertyManager.WritablePropertyMap props = _encryptedStore.getWritableProperties(NEXTFLOW_CONFIG, true);
+            PropertyManager.WritablePropertyMap props = PropertyManager.getEncryptedStore().getWritableProperties(NEXTFLOW_CONFIG, true);
             props.clear();
             props.putAll(properties);
             props.save();
@@ -103,4 +136,17 @@ private void saveConfiguration( NextFlowController.NextFlowConfiguration configu
         }
     }
 
+    public void saveEnabledState(Container container, Boolean enabled)
+    {
+        PropertyManager.WritablePropertyMap map = PropertyManager.getWritableProperties(container, NEXTFLOW_ENABLE_PROP_CATEGORY, true);
+        if (enabled == null)
+        {
+            map.delete();
+        }
+        else
+        {
+            map.put(IS_NEXTFLOW_ENABLED, enabled.toString());
+            map.save();
+        }
+    }
 }
diff --git a/nextflow/src/org/labkey/nextflow/NextFlowModule.java b/nextflow/src/org/labkey/nextflow/NextFlowModule.java
index a962764d..68b35cd0 100644
--- a/nextflow/src/org/labkey/nextflow/NextFlowModule.java
+++ b/nextflow/src/org/labkey/nextflow/NextFlowModule.java
@@ -1,14 +1,15 @@
 package org.labkey.nextflow;
 
 import org.jetbrains.annotations.NotNull;
-import org.labkey.api.data.Container;
 import org.labkey.api.data.ContainerManager;
 import org.labkey.api.module.ModuleContext;
 import org.labkey.api.module.SpringModule;
+import org.labkey.api.pipeline.PipelineService;
 import org.labkey.api.security.permissions.AdminPermission;
 import org.labkey.api.settings.AdminConsole;
 import org.labkey.api.view.ActionURL;
 import org.labkey.api.view.WebPartFactory;
+import org.labkey.nextflow.pipeline.NextFlowPipelineProvider;
 
 import java.util.Collection;
 import java.util.List;
@@ -26,6 +27,8 @@ protected void startupAfterSpringConfig(ModuleContext moduleContext)
     protected void init()
     {
         addController(NextFlowController.NAME, NextFlowController.class);
+
+        PipelineService.get().registerPipelineProvider(new NextFlowPipelineProvider(this));
     }
 
     @Override
diff --git a/nextflow/src/org/labkey/nextflow/nextFlowConfiguration.jsp b/nextflow/src/org/labkey/nextflow/nextFlowConfiguration.jsp
new file mode 100644
index 00000000..f2f738b6
--- /dev/null
+++ b/nextflow/src/org/labkey/nextflow/nextFlowConfiguration.jsp
@@ -0,0 +1,59 @@
+<%@ taglib prefix="labkey" uri="http://www.labkey.org/taglib" %>
+<%@ page import="org.labkey.api.view.HttpView" %>
+<%@ page import="org.labkey.nextflow.NextFlowConfiguration" %>
+<%@ page import="org.labkey.api.util.Button" %>
+<%@ page import="org.labkey.api.util.PageFlowUtil" %>
+<%@ page import="org.labkey.api.admin.AdminUrls" %>
+<%@ page import="org.labkey.api.security.permissions.AdminOperationsPermission" %>
+<%@ page extends="org.labkey.api.jsp.JspBase" %>
+<%
+    NextFlowConfiguration form = (NextFlowConfiguration) HttpView.currentModel();
+    boolean hasAdminOpsPerms = getContainer().hasPermission(getUser(), AdminOperationsPermission.class);
+%>
+<labkey:form method="POST">
+<labkey:errors />
+<table class="lk-fields-table">
+    <tr>
+        <td class="labkey-form-label"><label for="nextFlowConfigFilePath">NextFlow Config File Path</label></td>
+        <td><labkey:input type="text" name="nextFlowConfigFilePath" id="nextFlowConfigFilePath" size="64" value="<%=h(form.getNextFlowConfigFilePath())%>" /></td>
+    </tr>
+    <tr>
+        <td class="labkey-form-label"><label for="accountNameInput">AWS Account Name</label></td>
+        <td><labkey:input type="text" name="accountName" id="accountNameInput" size="64" value="<%=h(form.getAccountName())%>" /></td>
+    </tr>
+    <tr>
+        <td class="labkey-form-label"><label for="identityInput">AWS Identity</label></td>
+        <td><labkey:input type="text" name="identity" id="identityInput" size="64" value="<%= h(form.getIdentity()) %>" /></td>
+    </tr>
+    <tr>
+        <td class="labkey-form-label"><label for="credentialInput">AWS Credential</label></td>
+        <td><labkey:input type="password" name="credential" id="credentialInput" size="64" placeholder='<%= form.getCredential() != null ? "value already set, overwrite to replace" : "" %>' /></td>
+    </tr>
+    <tr>
+        <td class="labkey-form-label"><label for="s3BucketPathInput">AWS S3 Bucket Path</label></td>
+        <td><labkey:input type="text" name="s3BucketPath" id="s3BucketPathInput" size="64" value="<%=h(form.getS3BucketPath())%>" /></td>
+    </tr>
+    <tr>
+        <td class="labkey-form-label"><label for="apiKeyInput">API Key (optional)</label></td>
+        <td><labkey:input type="password" name="apiKey" id="apiKeyInput" size="64" placeholder='<%= form.getApiKey() != null ? "value already set, overwrite to replace" : "" %>' /></td>
+    </tr>
+</table>
+    <%= new Button.ButtonBuilder("Save").submit(true).primary(true).enabled(hasAdminOpsPerms) %>
+    <%= new Button.ButtonBuilder("Delete").onClick("deleteConfig()").enabled(hasAdminOpsPerms) %>
+    <%= new Button.ButtonBuilder("Cancel").href(PageFlowUtil.urlProvider(AdminUrls.class).getAdminConsoleURL()) %>
+</labkey:form>
+
+<script type="text/javascript" nonce="<%=getScriptNonce()%>">
+    function deleteConfig() {
+        LABKEY.Ajax.request({
+            url: LABKEY.ActionURL.buildURL('nextflow', 'DeleteNextFlowConfiguration.api'),
+            method: 'POST',
+            success: function (response) {
+                window.location = <%= q(PageFlowUtil.urlProvider(AdminUrls.class).getAdminConsoleURL()) %>
+            },
+            failure: function (response) {
+                alert('Failed to delete configuration');
+            }
+        });
+    }
+</script>
diff --git a/nextflow/src/org/labkey/nextflow/pipeline/NextFlowPipelineJob.java b/nextflow/src/org/labkey/nextflow/pipeline/NextFlowPipelineJob.java
index 42c2882f..92d952b5 100644
--- a/nextflow/src/org/labkey/nextflow/pipeline/NextFlowPipelineJob.java
+++ b/nextflow/src/org/labkey/nextflow/pipeline/NextFlowPipelineJob.java
@@ -1,7 +1,6 @@
 package org.labkey.nextflow.pipeline;
 
 import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
 import org.labkey.api.pipeline.PipeRoot;
 import org.labkey.api.pipeline.PipelineJob;
 import org.labkey.api.pipeline.PipelineJobService;
@@ -9,32 +8,23 @@
 import org.labkey.api.pipeline.TaskPipeline;
 import org.labkey.api.util.FileUtil;
 import org.labkey.api.util.URLHelper;
-import org.labkey.api.util.UnexpectedException;
 import org.labkey.api.view.ViewBackgroundInfo;
 
 import java.io.File;
-import java.io.IOException;
 
 public class NextFlowPipelineJob extends PipelineJob
 {
-    private String _apiKey;
-    // For serialization
+    @SuppressWarnings("unused") // For serialization
     protected NextFlowPipelineJob()
     {}
 
-    public NextFlowPipelineJob(ViewBackgroundInfo info, @NotNull PipeRoot root, String apiKey)
+    public NextFlowPipelineJob(ViewBackgroundInfo info, @NotNull PipeRoot root)
     {
         super(null, info, root);
-        this._apiKey = apiKey;
         setLogFile(new File(String.valueOf(root.getLogDirectory()), FileUtil.makeFileNameWithTimestamp("NextFlowPipelineJob", "log")).toPath());
     }
 
-    public String getApiKey()
-    {
-        return _apiKey;
-    }
-
-    @Override
+   @Override
     public URLHelper getStatusHref()
     {
         return null;
@@ -47,7 +37,7 @@ public String getDescription()
     }
 
     @Override
-    public TaskPipeline getTaskPipeline()
+    public TaskPipeline<?> getTaskPipeline()
     {
         return PipelineJobService.get().getTaskPipeline(new TaskId(NextFlowPipelineJob.class));
     }
diff --git a/nextflow/src/org/labkey/nextflow/pipeline/NextFlowPipelineProvider.java b/nextflow/src/org/labkey/nextflow/pipeline/NextFlowPipelineProvider.java
new file mode 100644
index 00000000..449a3154
--- /dev/null
+++ b/nextflow/src/org/labkey/nextflow/pipeline/NextFlowPipelineProvider.java
@@ -0,0 +1,29 @@
+package org.labkey.nextflow.pipeline;
+
+import org.labkey.api.module.Module;
+import org.labkey.api.pipeline.PipeRoot;
+import org.labkey.api.pipeline.PipelineDirectory;
+import org.labkey.api.pipeline.PipelineProvider;
+import org.labkey.api.security.permissions.InsertPermission;
+import org.labkey.api.view.ViewContext;
+import org.labkey.nextflow.NextFlowManager;
+import org.labkey.nextflow.NextFlowModule;
+
+public class NextFlowPipelineProvider extends PipelineProvider
+{
+    public NextFlowPipelineProvider(NextFlowModule owningModule)
+    {
+        super("NextFlow", owningModule);
+    }
+
+    @Override
+    public void updateFileProperties(ViewContext context, PipeRoot pr, PipelineDirectory directory, boolean includeAll)
+    {
+        if (!context.getContainer().hasPermission(context.getUser(), InsertPermission.class))
+            return;
+        if (!NextFlowManager.get().isEnabled(context.getContainer()))
+            return;
+    }
+
+
+}
diff --git a/nextflow/src/org/labkey/nextflow/pipeline/NextFlowRunTask.java b/nextflow/src/org/labkey/nextflow/pipeline/NextFlowRunTask.java
index a75312a3..2934ea2e 100644
--- a/nextflow/src/org/labkey/nextflow/pipeline/NextFlowRunTask.java
+++ b/nextflow/src/org/labkey/nextflow/pipeline/NextFlowRunTask.java
@@ -2,19 +2,24 @@
 
 import org.apache.logging.log4j.Logger;
 import org.jetbrains.annotations.NotNull;
-import org.labkey.api.files.FileContentService;
 import org.labkey.api.pipeline.AbstractTaskFactory;
 import org.labkey.api.pipeline.AbstractTaskFactorySettings;
 import org.labkey.api.pipeline.PipelineJob;
 import org.labkey.api.pipeline.PipelineJobException;
-import org.labkey.api.pipeline.PipelineJobService;
 import org.labkey.api.pipeline.RecordedActionSet;
+import org.labkey.api.security.SecurityManager;
 import org.labkey.api.util.FileType;
-import org.labkey.api.util.FileUtil;
-import org.labkey.nextflow.NextFlowController;
+import org.labkey.nextflow.NextFlowConfiguration;
 import org.labkey.nextflow.NextFlowManager;
 
+import java.io.BufferedReader;
 import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 
@@ -29,19 +34,109 @@ public NextFlowRunTask(Factory factory, PipelineJob job)
     public @NotNull RecordedActionSet run() throws PipelineJobException
     {
         Logger log = getJob().getLogger();
-        NextFlowController.NextFlowConfiguration config = NextFlowManager.get().getConfiguration();
+
+        SecurityManager.TransformSession session = null;
+
+        try
+        {
+            NextFlowConfiguration config = NextFlowManager.get().getConfiguration();
+            if (config == null)
+            {
+                throw new PipelineJobException("No NextFlow configuration found");
+            }
+
+            // Use the configured API key if set
+            String apiKey = config.getApiKey();
+            if (apiKey == null)
+            {
+                session = SecurityManager.createTransformSession(getJob().getUser());
+                apiKey = session.getApiKey();
+            }
+
+            // Need to pass to the main process directly in the future to allow concurrent execution for different users
+            ProcessBuilder secretsPB = new ProcessBuilder("nextflow", "secrets", "set", "PANORAMA_API_KEY", apiKey);
+            log.info("Job Started");
+            File dir = getJob().getLogFile().getParentFile();
+            getJob().runSubProcess(secretsPB, dir);
+
+            ProcessBuilder executionPB = new ProcessBuilder(getArgs());
+            getJob().runSubProcess(executionPB, dir);
+            log.info("Job Finished");
+            return new RecordedActionSet();
+        }
+        finally
+        {
+            if (session != null)
+            {
+                session.close();
+            }
+        }
+    }
+
+    private boolean hasAwsSection(File configFile) throws PipelineJobException
+    {
+        try (FileInputStream fIn = new FileInputStream(configFile);
+             InputStreamReader isReader = new InputStreamReader(fIn, StandardCharsets.UTF_8);
+             BufferedReader reader = new BufferedReader(isReader))
+        {
+            String line;
+            while ((line = reader.readLine()) != null)
+            {
+                line = line.trim();
+                // Ignore comments
+                if (!line.startsWith("//"))
+                {
+                    if (line.startsWith("aws"))
+                    {
+                        return true;
+                    }
+                }
+            }
+            return false;
+        }
+        catch (IOException e)
+        {
+            throw new PipelineJobException(e);
+        }
+    }
+
+
+    private @NotNull List<String> getArgs() throws PipelineJobException
+    {
+        NextFlowConfiguration config = NextFlowManager.get().getConfiguration();
         String nextFlowConfigFilePath = config.getNextFlowConfigFilePath();
-        String s3BucketPath = config.getS3BucketPath();
-        String s3Path = "s3://" + s3BucketPath;
-        String apiKey = getJob().getApiKey();
-        ProcessBuilder pb = new ProcessBuilder( "nextflow" , "secrets", "set", "PANORAMA_API_KEY", apiKey);
-        log.info("Job Started");
-        File dir = FileContentService.get().getDefaultRootInfo(getJob().getContainer()).getPath().toFile();
-        getJob().runSubProcess(pb, dir);
-        pb.command("nextflow" , "run", "-resume", "-r", "main", "-profile", "aws", "mriffle/nf-skyline-dia-ms", "-bucket-dir", s3Path, "-c", nextFlowConfigFilePath);
-        getJob().runSubProcess(pb, dir);
-        log.info("Job Finished");
-        return new RecordedActionSet();
+
+        if (nextFlowConfigFilePath == null)
+        {
+            throw new PipelineJobException("No NextFlow config file specified");
+        }
+
+        File configFile = new File(nextFlowConfigFilePath);
+        if (!configFile.isFile())
+        {
+            throw new PipelineJobException("NextFlow config file not found");
+        }
+
+        boolean aws = hasAwsSection(configFile);
+
+        List<String> args = new ArrayList<>(Arrays.asList("nextflow", "run", "-resume", "-r", "main"));
+        if (aws)
+        {
+            args.add("-profile");
+            args.add("aws");
+        }
+        args.add("mriffle/nf-skyline-dia-ms");
+        if (aws)
+        {
+            String s3BucketPath = config.getS3BucketPath();
+            String s3Path = "s3://" + s3BucketPath;
+
+            args.add("-bucket-dir");
+            args.add(s3Path);
+        }
+        args.add("-c");
+        args.add(nextFlowConfigFilePath);
+        return args;
     }
 
     @Override