Skip to main content
Glama
workflow-operations.ts17.6 kB
/** * Workflow Operations Module * Handles all AEM workflow-related operations including start, status, complete, and cancel */ import { AxiosInstance } from 'axios'; import { IAEMConnector, ILogger, AEMConfig } from '../interfaces/index.js'; import { AEMOperationError, createAEMError, handleAEMHttpError, safeExecute, createSuccessResponse, AEM_ERROR_CODES } from '../error-handler.js'; export interface WorkflowRequest { model: string; payloadPath: string; title?: string; comment?: string; } export interface WorkflowResponse { success: boolean; operation: string; timestamp: string; data: { workflowId: string; model: string; payloadPath: string; title?: string; comment?: string; status: string; createdBy: string; createdAt: string; }; } export interface WorkflowStatusResponse { success: boolean; operation: string; timestamp: string; data: { workflowId: string; status: 'RUNNING' | 'COMPLETED' | 'SUSPENDED' | 'ABORTED' | 'FAILED'; currentStep: string; progress: number; startedBy: string; startedAt: string; completedAt?: string; steps: Array<{ name: string; status: 'PENDING' | 'ACTIVE' | 'COMPLETED' | 'FAILED'; startedAt?: string; completedAt?: string; assignee?: string; comment?: string; }>; }; } export interface WorkflowListResponse { success: boolean; operation: string; timestamp: string; data: { workflows: Array<{ workflowId: string; model: string; payloadPath: string; status: string; startedBy: string; startedAt: string; currentStep: string; progress: number; }>; totalCount: number; activeCount: number; completedCount: number; failedCount: number; }; } export interface WorkflowModelResponse { success: boolean; operation: string; timestamp: string; data: { models: Array<{ modelId: string; title: string; description: string; version: string; status: string; createdBy: string; createdAt: string; steps: Array<{ name: string; title: string; type: string; description?: string; }>; }>; totalCount: number; }; } export class WorkflowOperations { constructor( private httpClient: AxiosInstance, private logger: ILogger, private config: AEMConfig ) {} /** * Start a new workflow instance */ async startWorkflow(request: WorkflowRequest): Promise<WorkflowResponse> { return safeExecute<WorkflowResponse>(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: any) { 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') as WorkflowResponse; }, 'startWorkflow'); } /** * Get workflow status by ID */ async getWorkflowStatus(workflowId: string): Promise<WorkflowStatusResponse> { return safeExecute<WorkflowStatusResponse>(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') as WorkflowStatusResponse; } catch (error: any) { 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: string, stepName: string, comment?: string): Promise<{ success: boolean; operation: string; timestamp: string; data: { workflowId: string; stepName: string; comment?: string; status: string; completedAt: string; }; }> { 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: string, reason?: string): Promise<{ success: boolean; operation: string; timestamp: string; data: { workflowId: string; reason?: string; status: string; cancelledAt: string; }; }> { 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): Promise<WorkflowListResponse> { return safeExecute<WorkflowListResponse>(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: any) => ({ 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') as WorkflowListResponse; } catch (error: any) { throw handleAEMHttpError(error, 'listActiveWorkflows'); } }, 'listActiveWorkflows'); } /** * Get all available workflow models */ async getWorkflowModels(): Promise<WorkflowModelResponse> { return safeExecute<WorkflowModelResponse>(async () => { try { // Get workflow models from AEM const response = await this.httpClient.get('/etc/workflow/models.json', { params: { ':depth': '3' } }); const models: Array<{ modelId: string; title: string; description: string; version: string; status: string; createdBy: string; createdAt: string; steps: Array<{ name: string; title: string; type: string; description?: string; }>; }> = []; if (response.data && typeof response.data === 'object') { Object.entries(response.data).forEach(([key, value]: [string, any]) => { 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') as WorkflowModelResponse; } catch (error: any) { throw handleAEMHttpError(error, 'getWorkflowModels'); } }, 'getWorkflowModels'); } /** * Suspend a workflow instance */ async suspendWorkflow(workflowId: string, reason?: string): Promise<{ success: boolean; operation: string; timestamp: string; data: { workflowId: string; reason?: string; status: string; suspendedAt: string; }; }> { 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: string): Promise<{ success: boolean; operation: string; timestamp: string; data: { workflowId: string; status: string; resumedAt: string; }; }> { 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 private extractWorkflowId(responseData: any): string | null { // Extract workflow ID from AEM response if (responseData && responseData.path) { return responseData.path.split('/').pop(); } if (responseData && responseData.workflowId) { return responseData.workflowId; } return null; } private mapWorkflowStatus(aemStatus: string): 'RUNNING' | 'COMPLETED' | 'SUSPENDED' | 'ABORTED' | 'FAILED' { 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'; } } private parseWorkflowSteps(history: any[]): Array<{ name: string; status: 'PENDING' | 'ACTIVE' | 'COMPLETED' | 'FAILED'; startedAt?: string; completedAt?: string; assignee?: string; comment?: string; }> { return (history || []).map((step: any) => ({ name: step.name || step.stepName, status: this.mapStepStatus(step.status), startedAt: step.startedAt, completedAt: step.completedAt, assignee: step.assignee, comment: step.comment })); } private mapStepStatus(status: string): 'PENDING' | 'ACTIVE' | 'COMPLETED' | 'FAILED' { switch (status?.toUpperCase()) { case 'PENDING': return 'PENDING'; case 'ACTIVE': return 'ACTIVE'; case 'COMPLETED': return 'COMPLETED'; case 'FAILED': return 'FAILED'; default: return 'PENDING'; } } private getCurrentStep(steps: any[]): string { 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'; } private calculateProgress(steps: any[]): number { if (steps.length === 0) return 0; const completedSteps = steps.filter(step => step.status === 'COMPLETED').length; return Math.round((completedSteps / steps.length) * 100); } private calculateProgressFromHit(hit: any): number { // Calculate progress from query hit data const totalSteps = hit.totalSteps || 1; const completedSteps = hit.completedSteps || 0; return Math.round((completedSteps / totalSteps) * 100); } private parseModelSteps(modelData: any): Array<{ name: string; title: string; type: string; description?: string; }> { const steps: Array<{ name: string; title: string; type: string; description?: string; }> = []; if (modelData && modelData.nodes) { Object.entries(modelData.nodes).forEach(([key, value]: [string, any]) => { 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; } }

Implementation Reference

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