Skip to main content
Glama
workflow-operations.js15.1 kB
/** * Workflow Operations Module * Handles all AEM workflow-related operations including start, status, complete, and cancel */ import { createAEMError, handleAEMHttpError, safeExecute, createSuccessResponse, AEM_ERROR_CODES } from '../error-handler.js'; export class WorkflowOperations { httpClient; logger; config; constructor(httpClient, logger, config) { this.httpClient = httpClient; this.logger = logger; this.config = config; } /** * Start a new workflow instance */ async startWorkflow(request) { return safeExecute(async () => { const { model, payloadPath, title, comment } = request; if (!model || !payloadPath) { throw createAEMError(AEM_ERROR_CODES.INVALID_PARAMETERS, 'Model and payload path are required', { model, payloadPath }); } // Validate payload path exists try { await this.httpClient.get(`${payloadPath}.json`); } catch (error) { if (error.response?.status === 404) { throw createAEMError(AEM_ERROR_CODES.INVALID_PARAMETERS, `Payload path not found: ${payloadPath}`, { payloadPath }); } throw handleAEMHttpError(error, 'startWorkflow'); } // Start workflow using AEM's workflow API const workflowData = { model: model, payload: payloadPath, title: title || `Workflow for ${payloadPath}`, comment: comment || 'Started via AEM MCP Server' }; const response = await this.httpClient.post('/etc/workflow/instances', workflowData, { headers: { 'Content-Type': 'application/json' } }); // Extract workflow ID from response const workflowId = this.extractWorkflowId(response.data); if (!workflowId) { throw createAEMError(AEM_ERROR_CODES.SYSTEM_ERROR, 'Failed to extract workflow ID from response', { response: response.data }); } return createSuccessResponse({ workflowId, model, payloadPath, title: workflowData.title, comment: workflowData.comment, status: 'RUNNING', createdBy: 'admin', // In real implementation, get from auth context createdAt: new Date().toISOString() }, 'startWorkflow'); }, 'startWorkflow'); } /** * Get workflow status by ID */ async getWorkflowStatus(workflowId) { return safeExecute(async () => { if (!workflowId) { throw createAEMError(AEM_ERROR_CODES.INVALID_PARAMETERS, 'Workflow ID is required', { workflowId }); } try { // Get workflow instance details const response = await this.httpClient.get(`/etc/workflow/instances/${workflowId}.json`); const workflowData = response.data; // Parse workflow status and steps const status = this.mapWorkflowStatus(workflowData.state); const steps = this.parseWorkflowSteps(workflowData.history || []); const currentStep = this.getCurrentStep(steps); const progress = this.calculateProgress(steps); return createSuccessResponse({ workflowId, status, currentStep, progress, startedBy: workflowData.startedBy || 'admin', startedAt: workflowData.startedAt || new Date().toISOString(), completedAt: status === 'COMPLETED' ? workflowData.completedAt : undefined, steps }, 'getWorkflowStatus'); } catch (error) { if (error.response?.status === 404) { throw createAEMError(AEM_ERROR_CODES.INVALID_PARAMETERS, `Workflow not found: ${workflowId}`, { workflowId }); } throw handleAEMHttpError(error, 'getWorkflowStatus'); } }, 'getWorkflowStatus'); } /** * Complete a workflow step */ async completeWorkflowStep(workflowId, stepName, comment) { return safeExecute(async () => { if (!workflowId || !stepName) { throw createAEMError(AEM_ERROR_CODES.INVALID_PARAMETERS, 'Workflow ID and step name are required', { workflowId, stepName }); } // Complete the workflow step const stepData = { action: 'complete', comment: comment || `Step ${stepName} completed via AEM MCP Server` }; const response = await this.httpClient.post(`/etc/workflow/instances/${workflowId}/steps/${stepName}`, stepData, { headers: { 'Content-Type': 'application/json' } }); return createSuccessResponse({ workflowId, stepName, comment: stepData.comment, status: 'COMPLETED', completedAt: new Date().toISOString() }, 'completeWorkflowStep'); }, 'completeWorkflowStep'); } /** * Cancel a workflow instance */ async cancelWorkflow(workflowId, reason) { return safeExecute(async () => { if (!workflowId) { throw createAEMError(AEM_ERROR_CODES.INVALID_PARAMETERS, 'Workflow ID is required', { workflowId }); } // Cancel the workflow const cancelData = { action: 'abort', reason: reason || 'Cancelled via AEM MCP Server' }; const response = await this.httpClient.post(`/etc/workflow/instances/${workflowId}`, cancelData, { headers: { 'Content-Type': 'application/json' } }); return createSuccessResponse({ workflowId, reason: cancelData.reason, status: 'ABORTED', cancelledAt: new Date().toISOString() }, 'cancelWorkflow'); }, 'cancelWorkflow'); } /** * List active workflow instances */ async listActiveWorkflows(limit = 20) { return safeExecute(async () => { try { // Query active workflows const response = await this.httpClient.get('/bin/querybuilder.json', { params: { type: 'cq:WorkflowInstance', 'property': 'state', 'property.value': 'RUNNING', 'p.limit': limit.toString(), 'p.hits': 'full' } }); const workflows = (response.data.hits || []).map((hit) => ({ workflowId: hit.path?.split('/').pop() || hit.name, model: hit['jcr:content/model'] || hit.model, payloadPath: hit['jcr:content/payload'] || hit.payload, status: hit['jcr:content/state'] || hit.state, startedBy: hit['jcr:content/startedBy'] || hit.startedBy, startedAt: hit['jcr:content/startedAt'] || hit.startedAt, currentStep: hit['jcr:content/currentStep'] || hit.currentStep, progress: this.calculateProgressFromHit(hit) })); return createSuccessResponse({ workflows, totalCount: workflows.length, activeCount: workflows.length, completedCount: 0, failedCount: 0 }, 'listActiveWorkflows'); } catch (error) { throw handleAEMHttpError(error, 'listActiveWorkflows'); } }, 'listActiveWorkflows'); } /** * Get all available workflow models */ async getWorkflowModels() { return safeExecute(async () => { try { // Get workflow models from AEM const response = await this.httpClient.get('/etc/workflow/models.json', { params: { ':depth': '3' } }); const models = []; if (response.data && typeof response.data === 'object') { Object.entries(response.data).forEach(([key, value]) => { if (key.startsWith('jcr:') || key.startsWith('sling:')) return; if (value && typeof value === 'object') { const modelPath = `/etc/workflow/models/${key}`; const modelData = value['jcr:content'] || value; models.push({ modelId: key, title: modelData['jcr:title'] || key, description: modelData['jcr:description'] || '', version: modelData['version'] || '1.0', status: modelData['status'] || 'enabled', createdBy: modelData['jcr:createdBy'] || 'admin', createdAt: modelData['jcr:created'] || new Date().toISOString(), steps: this.parseModelSteps(modelData) }); } }); } return createSuccessResponse({ models, totalCount: models.length }, 'getWorkflowModels'); } catch (error) { throw handleAEMHttpError(error, 'getWorkflowModels'); } }, 'getWorkflowModels'); } /** * Suspend a workflow instance */ async suspendWorkflow(workflowId, reason) { return safeExecute(async () => { if (!workflowId) { throw createAEMError(AEM_ERROR_CODES.INVALID_PARAMETERS, 'Workflow ID is required', { workflowId }); } // Suspend the workflow const suspendData = { action: 'suspend', reason: reason || 'Suspended via AEM MCP Server' }; const response = await this.httpClient.post(`/etc/workflow/instances/${workflowId}`, suspendData, { headers: { 'Content-Type': 'application/json' } }); return createSuccessResponse({ workflowId, reason: suspendData.reason, status: 'SUSPENDED', suspendedAt: new Date().toISOString() }, 'suspendWorkflow'); }, 'suspendWorkflow'); } /** * Resume a suspended workflow instance */ async resumeWorkflow(workflowId) { return safeExecute(async () => { if (!workflowId) { throw createAEMError(AEM_ERROR_CODES.INVALID_PARAMETERS, 'Workflow ID is required', { workflowId }); } // Resume the workflow const resumeData = { action: 'resume' }; const response = await this.httpClient.post(`/etc/workflow/instances/${workflowId}`, resumeData, { headers: { 'Content-Type': 'application/json' } }); return createSuccessResponse({ workflowId, status: 'RUNNING', resumedAt: new Date().toISOString() }, 'resumeWorkflow'); }, 'resumeWorkflow'); } // Helper methods extractWorkflowId(responseData) { // Extract workflow ID from AEM response if (responseData && responseData.path) { return responseData.path.split('/').pop(); } if (responseData && responseData.workflowId) { return responseData.workflowId; } return null; } mapWorkflowStatus(aemStatus) { switch (aemStatus?.toUpperCase()) { case 'RUNNING': return 'RUNNING'; case 'COMPLETED': return 'COMPLETED'; case 'SUSPENDED': return 'SUSPENDED'; case 'ABORTED': return 'ABORTED'; case 'FAILED': return 'FAILED'; default: return 'RUNNING'; } } parseWorkflowSteps(history) { return (history || []).map((step) => ({ name: step.name || step.stepName, status: this.mapStepStatus(step.status), startedAt: step.startedAt, completedAt: step.completedAt, assignee: step.assignee, comment: step.comment })); } mapStepStatus(status) { switch (status?.toUpperCase()) { case 'PENDING': return 'PENDING'; case 'ACTIVE': return 'ACTIVE'; case 'COMPLETED': return 'COMPLETED'; case 'FAILED': return 'FAILED'; default: return 'PENDING'; } } getCurrentStep(steps) { const activeStep = steps.find(step => step.status === 'ACTIVE'); if (activeStep) { return activeStep.name; } const lastCompletedStep = steps.filter(step => step.status === 'COMPLETED').pop(); return lastCompletedStep ? lastCompletedStep.name : 'Unknown'; } calculateProgress(steps) { if (steps.length === 0) return 0; const completedSteps = steps.filter(step => step.status === 'COMPLETED').length; return Math.round((completedSteps / steps.length) * 100); } calculateProgressFromHit(hit) { // Calculate progress from query hit data const totalSteps = hit.totalSteps || 1; const completedSteps = hit.completedSteps || 0; return Math.round((completedSteps / totalSteps) * 100); } parseModelSteps(modelData) { const steps = []; if (modelData && modelData.nodes) { Object.entries(modelData.nodes).forEach(([key, value]) => { if (key.startsWith('jcr:') || key.startsWith('sling:')) return; if (value && typeof value === 'object') { steps.push({ name: key, title: value['jcr:title'] || key, type: value['jcr:primaryType'] || 'unknown', description: value['jcr:description'] }); } }); } return steps; } } //# sourceMappingURL=workflow-operations.js.map

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/indrasishbanerjee/aem-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server