diff --git a/packages/server/src/controllers/executions/index.ts b/packages/server/src/controllers/executions/index.ts
index 074b0efa9a9..6cd610d7a83 100644
--- a/packages/server/src/controllers/executions/index.ts
+++ b/packages/server/src/controllers/executions/index.ts
@@ -112,10 +112,22 @@ const deleteExecutions = async (req: Request, res: Response, next: NextFunction)
}
}
+const abortExecution = async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ const executionId = req.params.id
+ const workspaceId = req.user?.activeWorkspaceId
+ const result = await executionsService.abortExecution(executionId, workspaceId)
+ return res.json(result)
+ } catch (error) {
+ next(error)
+ }
+}
+
export default {
getAllExecutions,
deleteExecutions,
getExecutionById,
getPublicExecutionById,
- updateExecution
+ updateExecution,
+ abortExecution
}
diff --git a/packages/server/src/routes/executions/index.ts b/packages/server/src/routes/executions/index.ts
index 1b9458f625c..21e809f059b 100644
--- a/packages/server/src/routes/executions/index.ts
+++ b/packages/server/src/routes/executions/index.ts
@@ -7,6 +7,9 @@ const router = express.Router()
router.get('/', checkAnyPermission('executions:view'), executionController.getAllExecutions)
router.get(['/', '/:id'], checkAnyPermission('executions:view'), executionController.getExecutionById)
+// ABORT
+router.post('/:id/abort', checkAnyPermission('executions:update'), executionController.abortExecution)
+
// PUT
router.put(['/', '/:id'], checkAnyPermission('executions:update'), executionController.updateExecution)
diff --git a/packages/server/src/services/executions/index.ts b/packages/server/src/services/executions/index.ts
index 062337aad02..b2539133a1e 100644
--- a/packages/server/src/services/executions/index.ts
+++ b/packages/server/src/services/executions/index.ts
@@ -4,7 +4,7 @@ import { ChatMessage } from '../../database/entities/ChatMessage'
import { Execution } from '../../database/entities/Execution'
import { InternalFlowiseError } from '../../errors/internalFlowiseError'
import { getErrorMessage } from '../../errors/utils'
-import { ExecutionState, IAgentflowExecutedData } from '../../Interface'
+import { ExecutionState, IAgentflowExecutedData, MODE } from '../../Interface'
import { _removeCredentialId } from '../../utils'
import { getRunningExpressApp } from '../../utils/getRunningExpressApp'
@@ -163,10 +163,68 @@ const deleteExecutions = async (executionIds: string[], workspaceId?: string): P
}
}
+/**
+ * Abort a running execution by triggering its AbortController and updating the state to STOPPED
+ * @param executionId The execution ID to abort
+ * @param workspaceId Optional workspace ID for access control
+ * @returns Object with success status
+ */
+const abortExecution = async (executionId: string, workspaceId?: string): Promise<{ success: boolean }> => {
+ try {
+ const appServer = getRunningExpressApp()
+ const executionRepository = appServer.AppDataSource.getRepository(Execution)
+
+ const query: any = { id: executionId }
+ if (workspaceId) query.workspaceId = workspaceId
+
+ const execution = await executionRepository.findOneBy(query)
+ if (!execution) {
+ throw new InternalFlowiseError(StatusCodes.NOT_FOUND, `Execution ${executionId} not found`)
+ }
+
+ if (execution.state !== 'INPROGRESS') {
+ throw new InternalFlowiseError(
+ StatusCodes.BAD_REQUEST,
+ `Execution ${executionId} is not in progress (current state: ${execution.state})`
+ )
+ }
+
+ // Abort the running process using the same key format as buildChatflow/PredictionQueue: chatflowId_chatId
+ const abortControllerId = `${execution.agentflowId}_${execution.sessionId}`
+
+ if (process.env.MODE === MODE.QUEUE) {
+ // In queue mode, publish an abort event for the worker to process
+ await appServer.queueManager.getPredictionQueueEventsProducer().publishEvent({
+ eventName: 'abort',
+ id: abortControllerId
+ })
+ } else {
+ // In main mode, abort directly from the pool
+ appServer.abortControllerPool.abort(abortControllerId)
+ }
+
+ // Update execution state to STOPPED
+ execution.state = 'STOPPED' as ExecutionState
+ execution.stoppedDate = new Date()
+ await executionRepository.save(execution)
+
+ return { success: true }
+ } catch (error) {
+ if (error instanceof InternalFlowiseError) {
+ throw error
+ }
+ throw new InternalFlowiseError(
+ StatusCodes.INTERNAL_SERVER_ERROR,
+ `Error: executionsService.abortExecution - ${getErrorMessage(error)}`
+ )
+ }
+}
+
export default {
getExecutionById,
getAllExecutions,
deleteExecutions,
getPublicExecutionById,
- updateExecution
+ updateExecution,
+ abortExecution
}
diff --git a/packages/ui/src/api/executions.js b/packages/ui/src/api/executions.js
index 9135fa7cd8b..3c8fac04936 100644
--- a/packages/ui/src/api/executions.js
+++ b/packages/ui/src/api/executions.js
@@ -5,11 +5,13 @@ const deleteExecutions = (executionIds) => client.delete('/executions', { data:
const getExecutionById = (executionId) => client.get(`/executions/${executionId}`)
const getExecutionByIdPublic = (executionId) => client.get(`/public-executions/${executionId}`)
const updateExecution = (executionId, body) => client.put(`/executions/${executionId}`, body)
+const abortExecution = (executionId) => client.post(`/executions/${executionId}/abort`)
export default {
getAllExecutions,
deleteExecutions,
getExecutionById,
getExecutionByIdPublic,
- updateExecution
+ updateExecution,
+ abortExecution
}
diff --git a/packages/ui/src/ui-component/table/ExecutionsListTable.jsx b/packages/ui/src/ui-component/table/ExecutionsListTable.jsx
index 1b7dd68f996..0deee35ec02 100644
--- a/packages/ui/src/ui-component/table/ExecutionsListTable.jsx
+++ b/packages/ui/src/ui-component/table/ExecutionsListTable.jsx
@@ -5,6 +5,8 @@ import moment from 'moment'
import { styled } from '@mui/material/styles'
import {
Box,
+ Chip,
+ IconButton,
Paper,
Skeleton,
Table,
@@ -14,6 +16,7 @@ import {
TableHead,
TableRow,
TableSortLabel,
+ Tooltip,
useTheme,
Checkbox
} from '@mui/material'
@@ -86,7 +89,7 @@ const getIconColor = (state) => {
}
}
-export const ExecutionsListTable = ({ data, isLoading, onExecutionRowClick, onSelectionChange }) => {
+export const ExecutionsListTable = ({ data, isLoading, onExecutionRowClick, onSelectionChange, onAbortExecution }) => {
const theme = useTheme()
const customization = useSelector((state) => state.customization)
@@ -198,6 +201,7 @@ export const ExecutionsListTable = ({ data, isLoading, onExecutionRowClick, onSe
Created
+ Actions
@@ -222,6 +226,9 @@ export const ExecutionsListTable = ({ data, isLoading, onExecutionRowClick, onSe
+
+
+
@@ -283,6 +290,58 @@ export const ExecutionsListTable = ({ data, isLoading, onExecutionRowClick, onSe
onExecutionRowClick(row)}>
{moment(row.createdDate).format('MMM D, YYYY h:mm A')}
+
+ {row.state === 'INPROGRESS' && (
+
+ {
+ event.stopPropagation()
+ onAbortExecution && onAbortExecution(row.id)
+ }}
+ >
+
+
+
+ )}
+ {row.state === 'FINISHED' && (
+ }
+ label='Finished'
+ size='small'
+ color='success'
+ variant='outlined'
+ />
+ )}
+ {(row.state === 'ERROR' || row.state === 'TIMEOUT') && (
+ }
+ label={row.state === 'ERROR' ? 'Error' : 'Timeout'}
+ size='small'
+ color='error'
+ variant='outlined'
+ />
+ )}
+ {row.state === 'TERMINATED' && (
+ }
+ label='Terminated'
+ size='small'
+ color='error'
+ variant='outlined'
+ />
+ )}
+ {row.state === 'STOPPED' && (
+ }
+ label='Stopped'
+ size='small'
+ color='warning'
+ variant='outlined'
+ />
+ )}
+
)
})}
@@ -300,6 +359,7 @@ ExecutionsListTable.propTypes = {
isLoading: PropTypes.bool,
onExecutionRowClick: PropTypes.func,
onSelectionChange: PropTypes.func,
+ onAbortExecution: PropTypes.func,
className: PropTypes.string
}
diff --git a/packages/ui/src/views/agentexecutions/ExecutionDetails.jsx b/packages/ui/src/views/agentexecutions/ExecutionDetails.jsx
index e89b4c81067..cdc448e1fda 100644
--- a/packages/ui/src/views/agentexecutions/ExecutionDetails.jsx
+++ b/packages/ui/src/views/agentexecutions/ExecutionDetails.jsx
@@ -33,7 +33,8 @@ import {
IconRelationOneToManyFilled,
IconShare,
IconWorld,
- IconX
+ IconX,
+ IconPlayerStop
} from '@tabler/icons-react'
// Project imports
@@ -293,7 +294,17 @@ const MIN_DRAWER_WIDTH = 400
const DEFAULT_DRAWER_WIDTH = window.innerWidth - 400
const MAX_DRAWER_WIDTH = window.innerWidth
-export const ExecutionDetails = ({ open, isPublic, execution, metadata, onClose, onProceedSuccess, onUpdateSharing, onRefresh }) => {
+export const ExecutionDetails = ({
+ open,
+ isPublic,
+ execution,
+ metadata,
+ onClose,
+ onProceedSuccess,
+ onUpdateSharing,
+ onRefresh,
+ onAbortExecution
+}) => {
const [drawerWidth, setDrawerWidth] = useState(Math.min(DEFAULT_DRAWER_WIDTH, MAX_DRAWER_WIDTH))
const [executionTree, setExecution] = useState([])
const [expandedItems, setExpandedItems] = useState([])
@@ -825,6 +836,19 @@ export const ExecutionDetails = ({ open, isPublic, execution, metadata, onClose,
>
+ {!isPublic && localMetadata?.state === 'INPROGRESS' && (
+
+ }
+ variant='outlined'
+ color='error'
+ label='Abort'
+ className={'button'}
+ onClick={() => onAbortExecution && onAbortExecution(localMetadata?.id)}
+ />
+
+ )}
@@ -983,7 +1007,8 @@ ExecutionDetails.propTypes = {
onClose: PropTypes.func,
onProceedSuccess: PropTypes.func,
onUpdateSharing: PropTypes.func,
- onRefresh: PropTypes.func
+ onRefresh: PropTypes.func,
+ onAbortExecution: PropTypes.func
}
ExecutionDetails.displayName = 'ExecutionDetails'
diff --git a/packages/ui/src/views/agentexecutions/index.jsx b/packages/ui/src/views/agentexecutions/index.jsx
index 2a6e366509b..4002fe3a80e 100644
--- a/packages/ui/src/views/agentexecutions/index.jsx
+++ b/packages/ui/src/views/agentexecutions/index.jsx
@@ -32,17 +32,18 @@ import { Available } from '@/ui-component/rbac/available'
// API
import executionsApi from '@/api/executions'
import useApi from '@/hooks/useApi'
-import { useSelector } from 'react-redux'
+import { useSelector, useDispatch } from 'react-redux'
// icons
import execution_empty from '@/assets/images/executions_empty.svg'
-import { IconTrash } from '@tabler/icons-react'
+import { IconTrash, IconX } from '@tabler/icons-react'
// const
import TablePagination, { DEFAULT_ITEMS_PER_PAGE } from '@/ui-component/pagination/TablePagination'
import { ExecutionsListTable } from '@/ui-component/table/ExecutionsListTable'
import { omit } from 'lodash'
import { ExecutionDetails } from './ExecutionDetails'
+import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from '@/store/actions'
// ==============================|| AGENT EXECUTIONS ||============================== //
@@ -54,6 +55,9 @@ const AgentExecutions = () => {
const getAllExecutions = useApi(executionsApi.getAllExecutions)
const deleteExecutionsApi = useApi(executionsApi.deleteExecutions)
const getExecutionByIdApi = useApi(executionsApi.getExecutionById)
+ const abortExecutionApi = useApi(executionsApi.abortExecution)
+
+ const dispatch = useDispatch()
const [error, setError] = useState(null)
const [isLoading, setLoading] = useState(true)
@@ -172,6 +176,10 @@ const AgentExecutions = () => {
setOpenDeleteDialog(false)
}
+ const handleAbortExecution = (executionId) => {
+ abortExecutionApi.request(executionId)
+ }
+
useEffect(() => {
getAllExecutions.request({ page: 1, limit: DEFAULT_ITEMS_PER_PAGE })
@@ -212,6 +220,33 @@ const AgentExecutions = () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [deleteExecutionsApi.data])
+ useEffect(() => {
+ if (abortExecutionApi.data) {
+ // Show success toast
+ dispatch(
+ enqueueSnackbarAction({
+ message: 'Execution aborted successfully',
+ options: {
+ key: new Date().getTime() + Math.random(),
+ variant: 'success',
+ action: (key) => (
+
+ )
+ }
+ })
+ )
+ // Refresh the executions list
+ getAllExecutions.request({
+ page: currentPage,
+ limit: pageLimit
+ })
+ }
+
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [abortExecutionApi.data])
+
useEffect(() => {
if (getExecutionByIdApi.data) {
const execution = getExecutionByIdApi.data
@@ -384,6 +419,7 @@ const AgentExecutions = () => {
data={executions}
isLoading={isLoading}
onSelectionChange={handleExecutionSelectionChange}
+ onAbortExecution={handleAbortExecution}
onExecutionRowClick={(execution) => {
setOpenDrawer(true)
const executionDetails =
@@ -416,6 +452,7 @@ const AgentExecutions = () => {
getAllExecutions.request()
getExecutionByIdApi.request(executionId)
}}
+ onAbortExecution={handleAbortExecution}
/>
>
)}