Back to list
CBoser

construction-workflow-automation

by CBoser

0🍴 0📅 Dec 15, 2025

SKILL.md


name: construction-workflow-automation description: Specialized skill for implementing automated workflows and business logic for the construction platform. Use when building workflow engines for job lifecycle management (plan start → completion), PDSS document routing, option validation logic, schedule optimization, contract approvals, notification systems, or time-based triggers. Includes state machines, business rules, notification patterns, and integration orchestration for construction processes.

Construction Workflow Automation

Comprehensive guide for implementing automated workflows and business logic in the construction project management platform.

Workflow Architecture

Technology Stack

  • Workflow Engine: Custom state machine with event triggers
  • Job Scheduling: node-cron for time-based tasks
  • Message Queue: Bull (Redis-based) for async processing
  • Notifications: SendGrid (email), Twilio (SMS), Push notifications
  • Events: EventEmitter for internal events
  • Logging: Winston for workflow audit trails

Core Workflow Patterns

1. Job Lifecycle State Machine

// workflows/JobLifecycleWorkflow.js
const { EventEmitter } = require('events');

class JobLifecycleWorkflow extends EventEmitter {
  constructor() {
    super();
    this.states = {
      plan_start: {
        allowedTransitions: ['pdss_initiated', 'cancelled'],
        onEnter: this.handlePlanStart.bind(this),
        requiredData: ['planId', 'subdivisionId', 'lotNumber'],
      },
      pdss_initiated: {
        allowedTransitions: ['plan_review', 'plan_start', 'on_hold'],
        onEnter: this.handlePDSSInitiated.bind(this),
        requiredData: ['pdssDocuments'],
      },
      plan_review: {
        allowedTransitions: ['approved', 'plan_start', 'on_hold'],
        onEnter: this.handlePlanReview.bind(this),
        checks: [this.checkDocumentCompleteness],
      },
      approved: {
        allowedTransitions: ['in_production', 'plan_review', 'on_hold'],
        onEnter: this.handleApproved.bind(this),
        autoActions: ['createProductionSchedule', 'notifyTeam'],
      },
      in_production: {
        allowedTransitions: ['quality_check', 'on_hold'],
        onEnter: this.handleInProduction.bind(this),
        monitoringRules: ['checkScheduleDeviation', 'trackMilestones'],
      },
      quality_check: {
        allowedTransitions: ['completed', 'in_production', 'on_hold'],
        onEnter: this.handleQualityCheck.bind(this),
        requiredApprovals: ['qualityInspector'],
      },
      completed: {
        allowedTransitions: [],
        onEnter: this.handleCompleted.bind(this),
        finalActions: ['archiveDocuments', 'generateReport', 'updateMetrics'],
      },
      on_hold: {
        allowedTransitions: ['plan_start', 'pdss_initiated', 'plan_review', 'approved', 'in_production', 'cancelled'],
        onEnter: this.handleOnHold.bind(this),
        requiresReason: true,
      },
      cancelled: {
        allowedTransitions: [],
        onEnter: this.handleCancelled.bind(this),
        requiresReason: true,
      },
    };
  }

  /**
   * Transition job to new state
   */
  async transitionTo(job, newState, context = {}) {
    const currentState = this.states[job.status];
    const targetState = this.states[newState];

    if (!targetState) {
      throw new Error(`Invalid state: ${newState}`);
    }

    // Check if transition is allowed
    if (!currentState.allowedTransitions.includes(newState)) {
      throw new Error(
        `Cannot transition from ${job.status} to ${newState}. ` +
        `Allowed transitions: ${currentState.allowedTransitions.join(', ')}`
      );
    }

    // Validate required data
    if (targetState.requiredData) {
      const missingData = targetState.requiredData.filter(field => !job[field]);
      if (missingData.length > 0) {
        throw new Error(`Missing required data: ${missingData.join(', ')}`);
      }
    }

    // Run state checks
    if (targetState.checks) {
      for (const check of targetState.checks) {
        const result = await check(job, context);
        if (!result.passed) {
          throw new Error(`State check failed: ${result.reason}`);
        }
      }
    }

    // Execute transition
    const oldState = job.status;
    job.status = newState;
    job.statusUpdatedAt = new Date();
    
    // Save to database
    await job.save();

    // Log transition
    await this.logTransition(job, oldState, newState, context);

    // Execute onEnter actions
    if (targetState.onEnter) {
      await targetState.onEnter(job, context);
    }

    // Execute auto actions
    if (targetState.autoActions) {
      for (const action of targetState.autoActions) {
        await this[action](job, context);
      }
    }

    // Emit event for other systems
    this.emit('stateChanged', {
      jobId: job.id,
      jobNumber: job.jobNumber,
      oldState,
      newState,
      timestamp: new Date(),
    });

    return job;
  }

  /**
   * Plan Start state handler
   */
  async handlePlanStart(job, context) {
    // Create initial PDSS entry
    await this.createInitialPDSS(job);

    // Assign project coordinator
    await this.assignCoordinator(job);

    // Send welcome notification to client
    await this.notifyClient(job, 'job_started');

    // Create initial timeline
    await this.createTimeline(job);
  }

  /**
   * PDSS Initiated state handler
   */
  async handlePDSSInitiated(job, context) {
    // Validate PDSS documents
    const validation = await this.validatePDSSDocuments(job);
    if (!validation.complete) {
      await this.notifyTeam(job, 'missing_pdss_documents', {
        missing: validation.missingDocuments,
      });
    }

    // Route documents for review
    await this.routeForReview(job);

    // Set review deadline
    await this.setReviewDeadline(job, 5); // 5 business days
  }

  /**
   * Plan Review state handler
   */
  async handlePlanReview(job, context) {
    // Assign reviewers based on plan complexity
    const reviewers = await this.assignReviewers(job);

    // Create review tasks
    for (const reviewer of reviewers) {
      await this.createReviewTask(job, reviewer);
    }

    // Set review checkpoints
    await this.scheduleReviewCheckpoint(job, 2); // Check in 2 days
  }

  /**
   * Approved state handler
   */
  async handleApproved(job, context) {
    // Generate production documents
    await this.generateProductionDocs(job);

    // Notify all stakeholders
    await this.notifyStakeholders(job, 'plan_approved');

    // Update subdivision metrics
    await this.updateSubdivisionMetrics(job.subdivisionId);
  }

  /**
   * Completion handler
   */
  async handleCompleted(job, context) {
    // Calculate actual vs estimated times
    const performance = await this.calculatePerformanceMetrics(job);

    // Update time estimation models
    await this.updateEstimationModels(job, performance);

    // Generate completion report
    await this.generateCompletionReport(job, performance);

    // Send satisfaction survey to client
    await this.sendSatisfactionSurvey(job);

    // Archive all documents
    await this.archiveJobDocuments(job);
  }

  /**
   * Check document completeness before allowing transition
   */
  async checkDocumentCompleteness(job, context) {
    const requiredDocs = [
      'site_plan',
      'floor_plan',
      'elevation_drawings',
      'foundation_plan',
    ];

    const missingDocs = [];
    for (const docType of requiredDocs) {
      const exists = await PDSSDocument.findOne({
        where: { jobId: job.id, documentType: docType, status: 'approved' },
      });
      if (!exists) {
        missingDocs.push(docType);
      }
    }

    return {
      passed: missingDocs.length === 0,
      reason: missingDocs.length > 0 
        ? `Missing required documents: ${missingDocs.join(', ')}`
        : null,
    };
  }

  /**
   * Log state transition for audit trail
   */
  async logTransition(job, oldState, newState, context) {
    await WorkflowLog.create({
      jobId: job.id,
      eventType: 'state_transition',
      oldState,
      newState,
      triggeredBy: context.userId || 'system',
      reason: context.reason,
      metadata: context.metadata,
      timestamp: new Date(),
    });
  }
}

module.exports = new JobLifecycleWorkflow();

2. Option Validation Workflow

// workflows/OptionValidationWorkflow.js

class OptionValidationWorkflow {
  /**
   * Real-time option validation as user selects
   */
  async validateSelection(planId, selectedOptions, elevation) {
    const validationSteps = [
      this.checkElevationCompatibility,
      this.checkOptionConflicts,
      this.checkRequiredDependencies,
      this.checkScheduleImpact,
      this.checkEngineeringRequirements,
      this.generateRecommendations,
    ];

    const results = {
      isValid: true,
      errors: [],
      warnings: [],
      recommendations: [],
      estimatedImpact: {},
    };

    // Run all validation steps
    for (const step of validationSteps) {
      const stepResult = await step.call(this, planId, selectedOptions, elevation);
      
      if (stepResult.errors) {
        results.errors.push(...stepResult.errors);
        results.isValid = false;
      }
      
      if (stepResult.warnings) {
        results.warnings.push(...stepResult.warnings);
      }
      
      if (stepResult.recommendations) {
        results.recommendations.push(...stepResult.recommendations);
      }
      
      if (stepResult.impact) {
        Object.assign(results.estimatedImpact, stepResult.impact);
      }
    }

    return results;
  }

  /**
   * Check elevation compatibility
   */
  async checkElevationCompatibility(planId, selectedOptions, elevation) {
    const options = await PlanOption.findAll({
      where: { optionCode: selectedOptions },
    });

    const incompatible = options.filter(
      opt => opt.availableElevations.length > 0 && 
             !opt.availableElevations.includes(elevation)
    );

    return {
      errors: incompatible.map(opt => ({
        type: 'ELEVATION_INCOMPATIBLE',
        optionCode: opt.optionCode,
        optionName: opt.optionName,
        message: `${opt.optionName} is not available for elevation ${elevation}`,
      })),
    };
  }

  /**
   * Check for option conflicts
   */
  async checkOptionConflicts(planId, selectedOptions, elevation) {
    const options = await PlanOption.findAll({
      where: { optionCode: selectedOptions },
    });

    const errors = [];

    for (const option of options) {
      const conflicts = option.conflictingOptions.filter(code =>
        selectedOptions.includes(code)
      );

      if (conflicts.length > 0) {
        const conflictNames = await PlanOption.findAll({
          where: { optionCode: conflicts },
          attributes: ['optionName'],
        });

        errors.push({
          type: 'OPTION_CONFLICT',
          optionCode: option.optionCode,
          optionName: option.optionName,
          conflictsWith: conflicts,
          message: `${option.optionName} conflicts with: ${
            conflictNames.map(c => c.optionName).join(', ')
          }`,
        });
      }
    }

    return { errors };
  }

  /**
   * Calculate schedule impact
   */
  async checkScheduleImpact(planId, selectedOptions, elevation) {
    const options = await PlanOption.findAll({
      where: { optionCode: selectedOptions },
    });

    const totalLeadTime = options.reduce(
      (sum, opt) => sum + (opt.leadTimeDays || 0),
      0
    );

    const warnings = [];
    const impact = {
      additionalDays: totalLeadTime,
      impactLevel: totalLeadTime > 14 ? 'high' : totalLeadTime > 7 ? 'medium' : 'low',
    };

    if (totalLeadTime > 14) {
      warnings.push({
        type: 'SCHEDULE_IMPACT',
        message: `Selected options will add ${totalLeadTime} days to schedule`,
        severity: 'high',
      });
    } else if (totalLeadTime > 7) {
      warnings.push({
        type: 'SCHEDULE_IMPACT',
        message: `Selected options will add ${totalLeadTime} days to schedule`,
        severity: 'medium',
      });
    }

    return { warnings, impact };
  }

  /**
   * Generate smart recommendations
   */
  async generateRecommendations(planId, selectedOptions, elevation) {
    const options = await PlanOption.findAll({
      where: { optionCode: selectedOptions },
    });

    // Get all compatible options
    const compatibleCodes = [...new Set(
      options.flatMap(opt => opt.compatibleOptions)
    )].filter(code => !selectedOptions.includes(code));

    if (compatibleCodes.length === 0) {
      return { recommendations: [] };
    }

    // Fetch compatible options
    const compatible = await PlanOption.findAll({
      where: { 
        optionCode: compatibleCodes,
        status: 'available',
      },
    });

    // Calculate recommendation scores
    const recommendations = compatible.map(opt => {
      const frequency = options.filter(
        selected => selected.compatibleOptions.includes(opt.optionCode)
      ).length;

      return {
        optionCode: opt.optionCode,
        optionName: opt.optionName,
        category: opt.category,
        reason: `Frequently selected with your current choices (${frequency}/${options.length})`,
        score: frequency / options.length,
      };
    });

    // Sort by score and return top 5
    recommendations.sort((a, b) => b.score - a.score);

    return {
      recommendations: recommendations.slice(0, 5),
    };
  }
}

module.exports = new OptionValidationWorkflow();

3. Notification System

// workflows/NotificationWorkflow.js
const nodemailer = require('nodemailer');
const twilio = require('twilio');

class NotificationWorkflow {
  constructor() {
    this.emailTransporter = nodemailer.createTransport({
      service: 'SendGrid',
      auth: {
        user: process.env.SENDGRID_USER,
        pass: process.env.SENDGRID_API_KEY,
      },
    });

    this.twilioClient = twilio(
      process.env.TWILIO_ACCOUNT_SID,
      process.env.TWILIO_AUTH_TOKEN
    );

    // Notification templates
    this.templates = {
      job_started: {
        subject: 'Your Project Has Started - Job {jobNumber}',
        channels: ['email'],
      },
      plan_approved: {
        subject: 'Plans Approved - Job {jobNumber}',
        channels: ['email', 'sms'],
      },
      schedule_delay: {
        subject: 'Schedule Update - Job {jobNumber}',
        channels: ['email', 'push'],
        priority: 'high',
      },
      missing_documents: {
        subject: 'Action Required - Missing Documents',
        channels: ['email'],
        priority: 'high',
      },
      quality_issue: {
        subject: 'URGENT - Quality Issue Detected',
        channels: ['email', 'sms', 'push'],
        priority: 'urgent',
      },
      job_completed: {
        subject: 'Project Complete - Job {jobNumber}',
        channels: ['email'],
      },
    };
  }

  /**
   * Send notification through appropriate channels
   */
  async send(notificationType, recipients, data) {
    const template = this.templates[notificationType];
    if (!template) {
      throw new Error(`Unknown notification type: ${notificationType}`);
    }

    const promises = [];

    for (const channel of template.channels) {
      switch (channel) {
        case 'email':
          promises.push(this.sendEmail(template, recipients, data));
          break;
        case 'sms':
          promises.push(this.sendSMS(template, recipients, data));
          break;
        case 'push':
          promises.push(this.sendPushNotification(template, recipients, data));
          break;
      }
    }

    const results = await Promise.allSettled(promises);

    // Log notification sending
    await NotificationLog.create({
      notificationType,
      recipients: recipients.map(r => r.email || r.phone),
      channels: template.channels,
      priority: template.priority || 'normal',
      status: results.every(r => r.status === 'fulfilled') ? 'sent' : 'partial',
      timestamp: new Date(),
    });

    return results;
  }

  /**
   * Send email notification
   */
  async sendEmail(template, recipients, data) {
    const subject = this.interpolate(template.subject, data);
    const html = await this.renderEmailTemplate(template, data);

    return this.emailTransporter.sendMail({
      from: process.env.FROM_EMAIL,
      to: recipients.map(r => r.email).join(','),
      subject,
      html,
    });
  }

  /**
   * Schedule notification for future delivery
   */
  async schedule(notificationType, recipients, data, sendAt) {
    await ScheduledNotification.create({
      notificationType,
      recipients,
      data,
      sendAt,
      status: 'pending',
    });
  }

  /**
   * Digest notifications (e.g., daily summary)
   */
  async sendDailyDigest(userId) {
    const today = new Date();
    today.setHours(0, 0, 0, 0);

    // Get all jobs updated today
    const updatedJobs = await Job.findAll({
      where: {
        updatedAt: { [Op.gte]: today },
      },
      include: ['plan', 'subdivision'],
    });

    // Get pending approvals
    const pendingApprovals = await Job.findAll({
      where: {
        status: 'plan_review',
        assignedTo: userId,
      },
    });

    const digestData = {
      updatedJobs: updatedJobs.length,
      pendingApprovals: pendingApprovals.length,
      jobs: updatedJobs.map(job => ({
        jobNumber: job.jobNumber,
        status: job.status,
        subdivision: job.subdivision.subdivisionName,
      })),
    };

    await this.send('daily_digest', [{ email: user.email }], digestData);
  }

  /**
   * Interpolate template variables
   */
  interpolate(template, data) {
    return template.replace(/\{(\w+)\}/g, (match, key) => data[key] || match);
  }
}

module.exports = new NotificationWorkflow();

4. Automated Scheduling

// workflows/AutomatedScheduler.js
const cron = require('node-cron');
const { Op } = require('sequelize');

class AutomatedScheduler {
  constructor() {
    this.jobs = [];
    this.initialize();
  }

  initialize() {
    // Daily at 6 AM: Send digest notifications
    this.schedule('0 6 * * *', 'dailyDigest', this.sendDailyDigests);

    // Every hour: Check for overdue reviews
    this.schedule('0 * * * *', 'overdueReviews', this.checkOverdueReviews);

    // Every 15 minutes: Update schedule metrics
    this.schedule('*/15 * * * *', 'scheduleMetrics', this.updateScheduleMetrics);

    // Daily at 8 AM: Generate daily reports
    this.schedule('0 8 * * *', 'dailyReports', this.generateDailyReports);

    // Weekly on Monday at 7 AM: Weekly planning
    this.schedule('0 7 * * 1', 'weeklyPlanning', this.weeklyPlanning);

    // Daily at 10 PM: Backup workflow logs
    this.schedule('0 22 * * *', 'backupLogs', this.backupWorkflowLogs);
  }

  /**
   * Schedule a cron job
   */
  schedule(cronExpression, name, handler) {
    const job = cron.schedule(cronExpression, async () => {
      console.log(`Running scheduled task: ${name}`);
      try {
        await handler.call(this);
      } catch (error) {
        console.error(`Error in scheduled task ${name}:`, error);
        await this.logError(name, error);
      }
    });

    this.jobs.push({ name, cronExpression, job });
    console.log(`Scheduled: ${name} (${cronExpression})`);
  }

  /**
   * Send daily digest to all users
   */
  async sendDailyDigests() {
    const users = await User.findAll({ where: { active: true } });
    
    for (const user of users) {
      await NotificationWorkflow.sendDailyDigest(user.id);
    }
  }

  /**
   * Check for overdue plan reviews
   */
  async checkOverdueReviews() {
    const overdueJobs = await Job.findAll({
      where: {
        status: 'plan_review',
        reviewDeadline: { [Op.lt]: new Date() },
      },
      include: ['plan', 'subdivision'],
    });

    for (const job of overdueJobs) {
      // Escalate to manager
      await NotificationWorkflow.send(
        'overdue_review',
        [{ email: job.manager.email }],
        {
          jobNumber: job.jobNumber,
          daysOverdue: Math.floor(
            (new Date() - job.reviewDeadline) / (1000 * 60 * 60 * 24)
          ),
        }
      );

      // Add flag to job
      await job.update({
        flags: [...job.flags, 'overdue_review'],
      });
    }
  }

  /**
   * Weekly planning workflow
   */
  async weeklyPlanning() {
    // Generate upcoming week schedule
    const nextWeekStart = new Date();
    nextWeekStart.setDate(nextWeekStart.getDate() + 7);
    const nextWeekEnd = new Date(nextWeekStart);
    nextWeekEnd.setDate(nextWeekEnd.getDate() + 7);

    const upcomingJobs = await Job.findAll({
      where: {
        estimatedStartDate: {
          [Op.between]: [nextWeekStart, nextWeekEnd],
        },
        status: { [Op.in]: ['approved', 'in_production'] },
      },
      include: ['plan', 'subdivision'],
    });

    // Generate resource allocation report
    const report = await this.generateResourceAllocation(upcomingJobs);

    // Send to project managers
    const managers = await User.findAll({
      where: { role: 'project_manager' },
    });

    for (const manager of managers) {
      await NotificationWorkflow.send(
        'weekly_planning',
        [{ email: manager.email }],
        { report }
      );
    }
  }

  /**
   * Stop all scheduled jobs
   */
  stopAll() {
    this.jobs.forEach(({ name, job }) => {
      job.stop();
      console.log(`Stopped: ${name}`);
    });
  }
}

module.exports = new AutomatedScheduler();

5. Integration Orchestration

// workflows/IntegrationOrchestrator.js

class IntegrationOrchestrator {
  /**
   * Orchestrate job creation across all systems
   */
  async createJobWorkflow(jobData) {
    const steps = [
      {
        name: 'validateInput',
        handler: this.validateJobInput,
        rollback: null,
      },
      {
        name: 'createJobRecord',
        handler: this.createJobRecord,
        rollback: this.deleteJobRecord,
      },
      {
        name: 'initializePDSS',
        handler: this.initializePDSS,
        rollback: this.deletePDSS,
      },
      {
        name: 'createTimeline',
        handler: this.createTimeline,
        rollback: this.deleteTimeline,
      },
      {
        name: 'assignTeam',
        handler: this.assignTeam,
        rollback: this.unassignTeam,
      },
      {
        name: 'sendNotifications',
        handler: this.sendCreationNotifications,
        rollback: null,
      },
    ];

    const context = { jobData, createdResources: [] };
    const completedSteps = [];

    try {
      // Execute steps sequentially
      for (const step of steps) {
        console.log(`Executing step: ${step.name}`);
        const result = await step.handler.call(this, context);
        context[step.name] = result;
        completedSteps.push(step);
      }

      return context.createJobRecord;
    } catch (error) {
      console.error('Job creation workflow failed:', error);

      // Rollback completed steps in reverse order
      for (const step of completedSteps.reverse()) {
        if (step.rollback) {
          try {
            await step.rollback.call(this, context);
          } catch (rollbackError) {
            console.error(`Rollback failed for ${step.name}:`, rollbackError);
          }
        }
      }

      throw error;
    }
  }

  /**
   * Validate job input data
   */
  async validateJobInput(context) {
    const { jobData } = context;
    
    // Verify plan exists
    const plan = await Plan.findByPk(jobData.planId);
    if (!plan) {
      throw new Error(`Plan not found: ${jobData.planId}`);
    }

    // Verify subdivision exists
    const subdivision = await Subdivision.findByPk(jobData.subdivisionId);
    if (!subdivision) {
      throw new Error(`Subdivision not found: ${jobData.subdivisionId}`);
    }

    // Validate options if provided
    if (jobData.selectedOptions && jobData.selectedOptions.length > 0) {
      const validation = await OptionValidationWorkflow.validateSelection(
        jobData.planId,
        jobData.selectedOptions,
        jobData.elevation
      );

      if (!validation.isValid) {
        throw new Error(`Invalid option selection: ${
          validation.errors.map(e => e.message).join(', ')
        }`);
      }
    }

    return { validated: true };
  }
}

module.exports = new IntegrationOrchestrator();

Queue-Based Processing

// queues/jobQueue.js
const Queue = require('bull');
const jobQueue = new Queue('job-processing', process.env.REDIS_URL);

// Process jobs asynchronously
jobQueue.process('generateReport', async (job) => {
  const { jobId, reportType } = job.data;
  
  // Generate report (long-running task)
  const report = await generateReport(jobId, reportType);
  
  // Store in S3
  await uploadToS3(report);
  
  // Notify user
  await NotificationWorkflow.send('report_ready', [...], { report });
  
  return { reportUrl: report.url };
});

// Add job to queue
const addReportJob = async (jobId, reportType) => {
  await jobQueue.add('generateReport', { jobId, reportType }, {
    attempts: 3,
    backoff: {
      type: 'exponential',
      delay: 2000,
    },
  });
};

Monitoring & Alerting

// Monitor workflow health
const monitorWorkflows = () => {
  setInterval(async () => {
    // Check for stuck jobs
    const stuckJobs = await Job.findAll({
      where: {
        status: { [Op.notIn]: ['completed', 'cancelled'] },
        updatedAt: { [Op.lt]: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) },
      },
    });

    if (stuckJobs.length > 0) {
      await alertTeam('stuck_jobs', { count: stuckJobs.length });
    }

    // Check queue length
    const queueLength = await jobQueue.count();
    if (queueLength > 1000) {
      await alertTeam('high_queue_length', { length: queueLength });
    }
  }, 300000); // Every 5 minutes
};

Score

Total Score

60/100

Based on repository quality metrics

SKILL.md

SKILL.mdファイルが含まれている

+20
LICENSE

ライセンスが設定されている

+10
説明文

100文字以上の説明がある

0/10
人気

GitHub Stars 100以上

0/15
最近の活動

3ヶ月以内に更新がある

0/10
フォーク

10回以上フォークされている

0/5
Issue管理

オープンIssueが50未満

+5
言語

プログラミング言語が設定されている

+5
タグ

1つ以上のタグが設定されている

0/5

Reviews

💬

Reviews coming soon