diff --git a/gui/src/components/north/NorthTool.js b/gui/src/components/north/NorthTool.js
index 8e2c8ff5a697a49888232d3fc8609ad618f414e4..8ea9ceb491c0366060067fd3293c68f77efc8dea 100644
--- a/gui/src/components/north/NorthTool.js
+++ b/gui/src/components/north/NorthTool.js
@@ -51,6 +51,53 @@ const LaunchButton = React.memo(function LaunchButton(props) {
   return <Button color="primary" variant="contained" size="small" {...props} />
 })
 
+export function useNorthToolHook(tool, uploadId, path) {
+  const { name, path_prefix, with_path } = tool
+  const { api } = useApi()
+  const { raiseError } = useErrors()
+
+  const getToolStatus = useCallback(() => {
+    return api
+      .get(`north/${name}?upload_id=${uploadId}`)
+      .then(response => ({state: response.data.state, uploadid_is_mounted: response.upload_id_is_mounted}))
+      .catch(error => {
+        raiseError(error)
+        return { state: "error", uploadid_is_mounted: null }
+      })
+  }, [api, raiseError, name, uploadId])
+
+  const launch = useCallback(() => {
+    return api.post(`north/${name}?upload_id=${uploadId}`)
+      .then(response => {
+        let toolUrl = `${northBase}/user/${response.username}/${response.tool}`
+
+        if (with_path && response.upload_mount_dir && path) {
+          if (path_prefix) {
+            toolUrl = `${toolUrl}/${path_prefix}`
+          }
+          toolUrl = `${toolUrl}/${response.upload_mount_dir}/${path}`
+        }
+
+        return { toolUrl, state: response.data.state, uploadid_is_mounted: response.upload_id_is_mounted }
+      })
+      .catch(error => {
+        raiseError(error)
+        return { toolUrl: null, state: "stopped", uploadid_is_mounted: null }
+      })
+  }, [api, raiseError, name, uploadId, path, path_prefix, with_path])
+
+  const stop = useCallback(() => {
+    return api.delete(`north/${name}`)
+      .then(() => "stopped")
+      .catch(error => {
+        raiseError(error)
+        return "error"
+      })
+  }, [api, raiseError, name])
+
+  return { launch, stop, getToolStatus }
+}
+
 export const NorthToolButtons = React.memo(function NorthToolButton() {
   const {name, launch, stop, state} = useNorthTool()
   return (
@@ -90,68 +137,37 @@ const useStyles = makeStyles(theme => ({
 }))
 
 const NorthTool = React.memo(function NorthTool({tool, uploadId, path, children}) {
-  const {name, title, version, description, short_description, icon, path_prefix, with_path} = tool
   const styles = useStyles()
-  const {api} = useApi()
-  const {raiseError} = useErrors()
-
-  const [state, setState] = useState('stopped')
-
-  const getToolStatus = useCallback(() => {
-    return api.get(`north/${name}`)
-      .then(response => {
-        return response.data.state
-      }).catch(raiseError)
-  }, [api, raiseError, name])
+  const [toolState, setToolState] = useState("stopped")
+  const { launch, stop, getToolStatus } = useNorthToolHook(tool, uploadId, path)
 
   useEffect(() => {
-    const toolStatus = getToolStatus()
-    if (toolStatus) {
-      toolStatus.then((toolStatus) => {
-        setState(toolStatus)
-      })
+    async function fetchStatus() {
+      const {state: status} = await getToolStatus()
+      setToolState(status)
     }
-  }, [setState, getToolStatus])
-
-  const launch = useCallback(() => {
-    // We get the current actual tools status and do not use the one used to display the status!
-    setState('starting')
-    api.post(`north/${name}?upload_id=${uploadId}`)
-      .then((response) => {
-        console.log(response)
-        let toolUrl = `${northBase}/user/${response.username}/${response.tool}`
-        if (with_path && response.upload_mount_dir && path) {
-          if (path_prefix) {
-            toolUrl = `${toolUrl}/${path_prefix}`
-          }
-          toolUrl = `${toolUrl}/${response.upload_mount_dir}/${path}`
-        }
-        console.log(toolUrl)
-        window.open(toolUrl, name)
-        setState(response.data.state)
-      })
-      .catch(errors => {
-        raiseError(errors)
-        setState('stopped')
-      })
-  }, [setState, api, raiseError, name, with_path, uploadId, path, path_prefix])
-
-  const stop = useCallback(() => {
-    setState('stopping')
-    api.delete(`north/${name}`)
-      .then((response) => {
-        console.log(response)
-        setState('stopped')
-      })
-      .catch(raiseError)
-  }, [api, raiseError, setState, name])
+    fetchStatus()
+  }, [getToolStatus])
+
+  const handleLaunch = useCallback(async () => {
+    setToolState("starting")
+    const { toolUrl, state } = await launch()
+    setToolState(state)
+    if (toolUrl) window.open(toolUrl, tool.name)
+  }, [launch, tool.name])
+
+  const handleStop = useCallback(async () => {
+    setToolState("stopping")
+    const updatedState = await stop()
+    setToolState(updatedState)
+  }, [stop])
 
   const value = useMemo(() => ({
-    state: state,
-    launch: launch,
-    stop: stop,
+    state: toolState,
+    launch: handleLaunch,
+    stop: handleStop,
     ...tool
-  }), [tool, state, launch, stop])
+  }), [tool, toolState, handleLaunch, handleStop])
 
   if (children) {
     return (
@@ -165,15 +181,17 @@ const NorthTool = React.memo(function NorthTool({tool, uploadId, path, children}
     <northToolContext.Provider value={value}>
       <Box marginY={1}>
         <Box display="flex" flexDirection="row" marginBottom={1}>
-          {icon ? (
+          {tool.icon ? (
             <Icon classes={{root: styles.iconRoot}}>
-              <img className={styles.imageIcon} src={`${process.env.PUBLIC_URL}/${icon}`} alt="icon"/>
+              <img className={styles.imageIcon} src={`${process.env.PUBLIC_URL}/${tool.icon}`} alt="icon"/>
             </Icon>
           ) : (
             <AssessmentIcon classes={{root: styles.iconRoot}}/>
           )}
           <Box flexGrow={1}>
-            <Typography><b>{title || name}{version && <span> ({version})</span>}</b>: {short_description || description}</Typography>
+            <Typography>
+              <b>{tool.title || tool.name}{tool.version && <span> ({tool.version})</span>}</b>: {tool.short_description || tool.description}
+            </Typography>
           </Box>
         </Box>
         <NorthToolButtons />
diff --git a/gui/src/components/search/SearchResults.js b/gui/src/components/search/SearchResults.js
index 2c953355d8a6e2eb14b34867b79ea1ef9c7b2528..4bae7ed710d59369f77a9281b25428838fd79f1f 100644
--- a/gui/src/components/search/SearchResults.js
+++ b/gui/src/components/search/SearchResults.js
@@ -18,7 +18,7 @@
 import React, { useState, useMemo, useEffect, useCallback } from 'react'
 import PropTypes from 'prop-types'
 import jmespath from 'jmespath'
-import { Paper, Typography, Tooltip, IconButton } from '@material-ui/core'
+import {Paper, Typography, Tooltip, IconButton, Button, Dialog, DialogTitle, DialogContent} from '@material-ui/core'
 import { Alert } from '@material-ui/lab'
 import Icon from '@material-ui/core/Icon'
 import GitHubIcon from '@material-ui/icons/GitHub'
@@ -35,15 +35,36 @@ import { MaterialRowActions } from '../material/MaterialDetails'
 import { pluralize, formatInteger, parseJMESPath } from '../../utils'
 import { isEmpty, isArray } from 'lodash'
 import { useSearchContext } from './SearchContext'
+import {useTools} from "../north/NorthPage"
+import DialogActions from "@material-ui/core/DialogActions"
+import {useNorthToolHook} from "../north/NorthTool"
+import {useKeycloak} from "@react-keycloak/web"
+import {useErrors} from "../errors"
+import {northBase} from "../../config"
+
+/**
+ * Helper function to resolve a jmespath-like path to a quantity in an entry
+ */
+export const extractPathContent = ({action, data, key}) => {
+  const {path} = parseJMESPath(action[key])
+  return jmespath.search(data, path)
+}
 
 /**
  * Used to retrieve an URL link from the row metadata and display a link icon to
  * that resource.
  */
 export const ActionURL = React.memo(({action, data}) => {
-  const {path} = parseJMESPath(action.path)
-  let href = jmespath.search(data, path)
-  href = isArray(href) ? href[0] : href
+  const {raiseError} = useErrors()
+  const href = extractPathContent({action, data, key: 'path'})
+
+  if (isArray(href)) {
+    raiseError(`
+    Encountered and array in ${action.path}. Expected a string.
+    Update the path in you app to target only one item in the array.`
+    )
+    return
+  }
   const disabled = !href
   const size = 'medium'
   const svgIcon = {
@@ -62,6 +83,150 @@ ActionURL.propTypes = {
   data: PropTypes.object.isRequired // ES index data
 }
 
+/**
+ * Used to build a north container and open it in a new tab.
+ */
+export const NorthURL = React.memo(({ action, data }) => {
+  const {raiseError} = useErrors()
+  const { keycloak } = useKeycloak()
+  const isAuthenticated = keycloak.authenticated
+
+  const northTools = useTools()
+  const [openDialog, setOpenDialog] = useState(false)
+  const [toolUrl, setToolUrl] = useState(undefined)
+  const [dialogMessage, setDialogMessage] = useState("")
+  const [showStopOnly, setShowStopOnly] = useState(false)
+
+  const filepath = extractPathContent({ action, data, key: "filepath" })
+
+  const toolsData = action?.tool_name
+    ? Object.keys(northTools)
+        .filter(tool => tool === action.tool_name)
+        .map(key => {
+          const { with_path, mount_path, icon, path_prefix } = northTools[key]
+          return {
+            name: key,
+            title: key,
+            with_path,
+            mount_path,
+            icon,
+            path_prefix
+          }
+        })
+    : []
+
+  const tool = toolsData.length > 0 ? toolsData[0] : null
+  const { launch, stop, getToolStatus } = useNorthToolHook(tool ?? {}, data?.upload_id ?? "", filepath)
+
+  const handleLaunch = async () => {
+    if (!tool) return
+    const {state: currentState, uploadid_is_mounted} = await getToolStatus()
+    if (currentState === "stopped") {
+      const { toolUrl } = await launch()
+      setToolUrl(toolUrl)
+      if (toolUrl) window.open(toolUrl, tool.name)
+    } else if (uploadid_is_mounted === false) {
+      setDialogMessage(
+        `The upload ${data?.upload_id} is not mounted. You need to stop the tool before relaunching it.
+        Stopping the tool may cause loss of unsaved data.`)
+      setShowStopOnly(true)
+      setOpenDialog(true)
+    } else {
+      setToolUrl(toolUrl)
+      setDialogMessage("The tool is already running. Stopping it will delete any unsaved data.")
+      setShowStopOnly(false)
+      setOpenDialog(true)
+    }
+  }
+
+  const handleStopAndRetry = async () => {
+    await handleStop()
+    setOpenDialog(false)
+  }
+
+  const handleStop = async () => {
+    if (!tool) return
+    await stop()
+    setOpenDialog(false)
+  }
+
+  const handleOpenTool = () => {
+    setOpenDialog(false)
+    window.open(
+      toolUrl || `${northBase}/user/${keycloak.tokenParsed?.preferred_username}/${tool.name}`,
+      "_blank"
+    )
+  }
+
+  const disabled = !filepath || !isAuthenticated
+  const tooltipMessage = !isAuthenticated
+    ? "You must be logged in to use this tool."
+    : !filepath
+    ? "No file path found. Tool cannot be launched."
+    : action.description || ""
+
+  if (!tool) {
+    return null
+  }
+
+  if (isArray(filepath)) {
+    raiseError(`
+    Encountered and array in ${action.filepath}. Expected a string.
+    Update the path in you app to target only one item in the array.`
+    )
+    return
+  }
+
+  return (
+    <>
+      <Tooltip title={tooltipMessage}>
+        <div>
+          <IconButton
+            onClick={handleLaunch}
+            size="medium"
+            disabled={disabled}
+            style={{ filter: disabled ? "grayscale(100%)" : "none" }}
+          >
+            <Icon fontSize="medium">
+              {tool.icon ? (
+                <img
+                  style={{ width: "30px", height: "30px", objectFit: "contain" }}
+                  src={`${process.env.PUBLIC_URL}/${tool.icon}`}
+                  alt="icon"
+                />
+              ) : (
+                <Icon fontSize="medium">{action?.icon || "launch"}</Icon>
+              )}
+            </Icon>
+          </IconButton>
+        </div>
+      </Tooltip>
+
+      <Dialog open={openDialog} onClose={() => setOpenDialog(false)}>
+        <DialogTitle>Tool Status</DialogTitle>
+        <DialogContent>
+          <p>{dialogMessage}</p>
+        </DialogContent>
+        <DialogActions>
+          <Button onClick={handleStopAndRetry} color="error">
+            Stop {action.tool_name}
+          </Button>
+          {!showStopOnly && (
+            <Button onClick={handleOpenTool} color="primary">
+              Open {action.tool_name}
+            </Button>
+          )}
+          <Button onClick={() => setOpenDialog(false)}>Cancel</Button>
+        </DialogActions>
+      </Dialog>
+    </>
+  )
+})
+NorthURL.propTypes = {
+  action: PropTypes.object.isRequired,
+  data: PropTypes.object.isRequired
+}
+
 /**
  * Displays the list of search results.
  */
@@ -101,7 +266,8 @@ export const SearchResults = React.memo(({
     const actionComponents = [];
     (rows?.actions?.items || []).forEach((action, i) => {
       const component = {
-        url: <ActionURL key={i} action={action} data={data}/>
+        url: <ActionURL key={i} action={action} data={data}/>,
+        north: <NorthURL key={i} action={action} data={data}/>
       }[action.type]
       if (component) {
         actionComponents.push(component)
diff --git a/nomad/app/v1/routers/north.py b/nomad/app/v1/routers/north.py
index 73c795bcf98baf1a2e78a37a02c94c2ccfe14312..49d108b02ebe81e96c7a7a8a52481a5aa2f520a3 100644
--- a/nomad/app/v1/routers/north.py
+++ b/nomad/app/v1/routers/north.py
@@ -65,6 +65,7 @@ class ToolResponseModel(BaseModel):
     username: str
     upload_mount_dir: str | None = None
     data: ToolModel
+    upload_id_is_mounted: bool | None = None
 
 
 class ToolsResponseModel(BaseModel):
@@ -148,10 +149,42 @@ async def tool(name: str) -> ToolModel:
 async def get_tool(
     tool: ToolModel = Depends(tool),
     user: User = Depends(create_user_dependency(required=True)),
+    upload_id: str | None = None,
 ):
-    return ToolResponseModel(
-        tool=tool.name, username=user.username, data=_get_status(tool, user)
-    )
+    if upload_id:
+        url = f'{config.hub_url()}/api/users/{user.username}'
+        response = requests.get(url, headers=hub_api_headers)
+        return ToolResponseModel(
+            tool=tool.name,
+            username=user.username,
+            data=_get_status(tool, user),
+            upload_id_is_mounted=_check_uploadid_is_mounted(
+                tool, response.json(), upload_id
+            ),
+        )
+    else:
+        return ToolResponseModel(
+            tool=tool.name, username=user.username, data=_get_status(tool, user)
+        )
+
+
+def _check_uploadid_is_mounted(
+    tool: ToolModel, response: dict, upload_id: str
+) -> bool | None:
+    try:
+        servers = response.get('servers', {})
+        tool_data = servers.get(tool.name, {})
+        if not tool_data:
+            return None
+        user_options = tool_data.get('user_options', {})
+        uploads = user_options.get('uploads', [])
+        for upload in uploads:
+            host_path = upload.get('host_path', '')
+            if upload_id in host_path:
+                return True
+        return False
+    except Exception:
+        return None
 
 
 @router.post(
@@ -240,12 +273,23 @@ async def start_tool(
     # Check if the tool/named server already exists
     _get_status(tool, user)
     if tool.state != ToolStateEnum.stopped:
-        return ToolResponseModel(
-            tool=tool.name,
-            username=user.username,
-            data=_get_status(tool, user),
-            upload_mount_dir=upload_mount_dir,
-        )
+        if upload_id and response.json().get('servers', None):
+            return ToolResponseModel(
+                tool=tool.name,
+                username=user.username,
+                data=_get_status(tool, user),
+                upload_mount_dir=upload_mount_dir,
+                upload_id_is_mounted=_check_uploadid_is_mounted(
+                    tool, response.json(), upload_id
+                ),
+            )
+        else:
+            return ToolResponseModel(
+                tool=tool.name,
+                username=user.username,
+                data=_get_status(tool, user),
+                upload_mount_dir=upload_mount_dir,
+            )
 
     url = f'{config.hub_url()}/api/users/{user.username}/servers/{tool.name}'
     access_token = generate_simple_token(
diff --git a/nomad/config/models/ui.py b/nomad/config/models/ui.py
index ed171ebd81925128e2e02dcf960a5d2483f900f3..10f8395a9d9ae49820da0570a537dd4361407d29 100644
--- a/nomad/config/models/ui.py
+++ b/nomad/config/models/ui.py
@@ -374,14 +374,43 @@ class RowActionURL(RowAction):
         return values
 
 
+class RowActionNorth(RowAction):
+    """Action that will open a NORTH tool given its name in the archive."""
+
+    filepath: str = Field(
+        description="""JMESPath pointing to a path in the archive that contains the filepath."""
+    )
+    tool_name: str = Field(description="""Name of the NORTH tool to open.""")
+    type: Literal['north'] = Field(
+        'north', description='Set as `north` to get this widget type.'
+    )
+
+    @model_validator(mode='before')
+    @classmethod
+    def _validate(cls, values):
+        if isinstance(values, BaseModel):
+            values = values.model_dump(exclude_none=True)
+        values['type'] = 'north'
+        return values
+
+
+RowActionsItemType = Annotated[
+    Union[
+        RowActionNorth,
+        RowActionURL,
+    ],
+    Field(discriminator='type'),
+]
+
+
 class RowActions(Options):
     """Controls the visualization of row actions that are shown at the end of each row."""
 
     enabled: bool = Field(True, description='Whether to enable row actions.')
-    options: dict[str, RowActionURL] | None = Field(
+    options: dict[str, RowActionsItemType] | None = Field(
         None, deprecated="""Deprecated, use 'items' instead."""
     )
-    items: list[RowActionURL] | None = Field(
+    items: list[RowActionsItemType] | None = Field(
         None, description='List of actions to show for each row.'
     )