L o a d i n g

Implement interactivity, scheduling, and monitoring in Slackbots for managing WordPress sites

Arshad Shah

May 8, 2025

Slackbots don’t have to wait for you to type out commands. With the right setup, your bot can help manage your WordPress sites by offering interactive buttons, dropdowns, scheduled tasks, and smart alerts — all right inside Slack.

In this article, we’ll show you how to add interactivity, automation, and monitoring to your Slack bot.

Prerequisites

Before you start, make sure you have:

  • A Slack App with bot permissions and a slash command.
  • A Kinsta account with API access and a site to test with.
  • Node.js and NPM installed locally.
  • Basic familiarity with JavaScript (or at least comfortable copying and tweaking code).
  • API keys for Slack and Kinsta.

Getting started

To build this Slackbot, Node.js and Slack’s Bolt framework are used to wire up slash commands that trigger actions via the Kinsta API.

We won’t rehash every step of creating a Slack app or getting Kinsta API access in this guide, as those are already covered in our earlier guide, How to Build a Slackbot With Node.js and Kinsta API for Site Management.

If you haven’t seen that one yet, read it first. It walks you through creating your Slack app, getting your bot token and signing secret, and getting your Kinsta API key.

Add interactivity to your Slackbot

Slackbots don’t have to rely on slash commands alone. With interactive components like buttons, menus, and modals, you can turn your bot into a much more intuitive and user-friendly tool.

Instead of typing /clear_cache environment_id, imagine clicking a button labeled Clear Cache right after checking a site’s status. To do this, you need Slack’s Web API client. Install it into your project with the command below:

npm install @slack/web-api

Then initialize it in your app.js:

const { WebClient } = require('@slack/web-api');
const web = new WebClient(process.env.SLACK_BOT_TOKEN);

Make sure SLACK_BOT_TOKEN is set in your .env file. Now, let’s enhance the /site_status command from the previous article. Instead of just sending text, we attach buttons for quick actions like Clear Cache, Create Backup, or Check Detailed Status.

Here’s what the updated handler looks like:

app.command('/site_status', async ({ command, ack, say }) => {
  await ack();
  
  const environmentId = command.text.trim();
  
  if (!environmentId) {
    await say('Please provide an environment ID. Usage: `/site_status [environment-id]`');
    return;
  }
  
  try {
    // Get environment status
    const response = await kinstaRequest(`/sites/environments/${environmentId}`);
    
    if (response && response.site && response.site.environments && response.site.environments.length > 0) {
      const env = response.site.environments[0];
      
      // Format the status message
      let statusMessage = formatSiteStatus(env);
      
      // Send message with interactive buttons
      await web.chat.postMessage({
        channel: command.channel_id,
        text: statusMessage,
        blocks: [
          {
            type: 'section',
            text: {
              type: 'mrkdwn',
              text: statusMessage
            }
          },
          {
            type: 'actions',
            elements: [
              {
                type: 'button',
                text: {
                  type: 'plain_text',
                  text: '🧹 Clear Cache',
                  emoji: true
                },
                value: environmentId,
                action_id: 'clear_cache_button'
              },
              {
                type: 'button',
                text: {
                  type: 'plain_text',
                  text: '📊 Detailed Status',
                  emoji: true
                },
                value: environmentId,
                action_id: 'detailed_status_button'
              },
              {
                type: 'button',
                text: {
                  type: 'plain_text',
                  text: '💾 Create Backup',
                  emoji: true
                },
                value: environmentId,
                action_id: 'create_backup_button'
              }
            ]
          }
        ]
      });
    } else {
      await say(`⚠ No environment found with ID: `${environmentId}``);
    }
  } catch (error) {
    console.error('Error checking site status:', error);
    await say(`❌ Error checking site status: ${error.message}`);
  }
});

Each button click triggers an action. Here’s how we handle the Clear Cache button:

// Add action handlers for the buttons
app.action('clear_cache_button', async ({ body, ack, respond }) => {
  await ack();
  
  const environmentId = body.actions[0].value;
  
  await respond(`🔄 Clearing cache for environment `${environmentId}`...`);
  
  try {
    // Call Kinsta API to clear cache
    const response = await kinstaRequest(
      `/sites/environments/${environmentId}/clear-cache`,
      'POST'
    );
    
    if (response && response.operation_id) {
      await respond(`✅ Cache clearing operation started! Operation ID: `${response.operation_id}``);
    } else {
      await respond('⚠ Cache clearing request was sent, but no operation ID was returned.');
    }
  } catch (error) {
    console.error('Cache clearing error:', error);
    await respond(`❌ Error clearing cache: ${error.message}`);
  }
});

You can follow the same pattern for the backup and status buttons, just linking each one to the appropriate API endpoint or command logic.

// Handlers for other buttons
app.action('detailed_status_button', async ({ body, ack, respond }) => {
  await ack();
  const environmentId = body.actions[0].value;
  // Implement detailed status check similar to the /detailed_status command
  // ...
});

app.action('create_backup_button', async ({ body, ack, respond }) => {
  await ack();
  const environmentId = body.actions[0].value;
  // Implement backup creation similar to the /create_backup command
  // ...
});

Use a dropdown to select a site

Typing environment IDs isn’t fun. And expecting every team member to remember which ID belongs to which environment? That’s not realistic.

Let’s make this more intuitive. Instead of asking users to type /site_status [environment-id], we’ll give them a Slack dropdown where they can pick a site from a list. Once they select one, the bot will show the status and attach the same quick-action buttons we implemented earlier.

To do this, we:

  • Fetch all sites from the Kinsta API
  • Fetch the environments for each site
  • Build a dropdown menu with these options
  • Handle the user’s selection and display the site’s status

Here’s the command that shows the dropdown:

app.command('/select_site', async ({ command, ack, say }) => {
  await ack();
  
  try {
    // Get all sites
    const response = await kinstaRequest('/sites');
    
    if (response && response.company && response.company.sites) {
      const sites = response.company.sites;
      
      // Create options for each site
      const options = [];
      
      for (const site of sites) {
        // Get environments for this site
        const envResponse = await kinstaRequest(`/sites/${site.id}/environments`);
        
        if (envResponse && envResponse.site && envResponse.site.environments) {
          for (const env of envResponse.site.environments) {
            options.push({
              text: {
                type: 'plain_text',
                text: `${site.name} (${env.name})`
              },
              value: env.id
            });
          }
        }
      }
      
      // Send message with dropdown
      await web.chat.postMessage({
        channel: command.channel_id,
        text: 'Select a site to manage:',
        blocks: [
          {
            type: 'section',
            text: {
              type: 'mrkdwn',
              text: '*Select a site to manage:*'
            },
            accessory: {
              type: 'static_select',
              placeholder: {
                type: 'plain_text',
                text: 'Select a site'
              },
              options: options.slice(0, 100), // Slack has a limit of 100 options
              action_id: 'site_selected'
            }
          }
        ]
      });
    } else {
      await say('❌ Error retrieving sites. Please check your API credentials.');
    }
  } catch (error) {
    console.error('Error:', error);
    await say(`❌ Error retrieving sites: ${error.message}`);
  }
});

When a user picks a site, we handle that with this action handler:

// Handle the site selection
app.action('site_selected', async ({ body, ack, respond }) => {
  await ack();
  
  const environmentId = body.actions[0].selected_option.value;
  const siteName = body.actions[0].selected_option.text.text;
  
  // Get environment status
  try {
    const response = await kinstaRequest(`/sites/environments/${environmentId}`);
    
    if (response && response.site && response.site.environments && response.site.environments.length > 0) {
      const env = response.site.environments[0];
      
      // Format the status message
      let statusMessage = `*${siteName}* (ID: `${environmentId}`)nn${formatSiteStatus(env)}`;
      
      // Send message with interactive buttons (similar to the site_status command)
      // ...
    } else {
      await respond(`⚠ No environment found with ID: `${environmentId}``);
    }
  } catch (error) {
    console.error('Error:', error);
    await respond(`❌ Error retrieving environment: ${error.message}`);
  }
});

Now that our bot can trigger actions with a button and select sites from a list, let’s make sure we don’t accidentally run risky operations.

Confirmation dialogs

Some operations should never run accidentally. Clearing a cache might sound harmless, but if you’re working on a production site, you probably don’t want to do it with a single click — especially if you were just checking the site status. That’s where Slack modals (dialogs) come in.

Instead of immediately clearing the cache when the clear_cache_button is clicked, we show a confirmation modal. Here’s how:

app.action('clear_cache_button', async ({ body, ack, context }) => {
  await ack();
  
  const environmentId = body.actions[0].value;
  
  // Open a confirmation dialog
  try {
    await web.views.open({
      trigger_id: body.trigger_id,
      view: {
        type: 'modal',
        callback_id: 'clear_cache_confirmation',
        private_metadata: environmentId,
        title: {
          type: 'plain_text',
          text: 'Confirm Cache Clearing'
        },
        blocks: [
          {
            type: 'section',
            text: {
              type: 'mrkdwn',
              text: `Are you sure you want to clear the cache for environment `${environmentId}`?`
            }
          }
        ],
        submit: {
          type: 'plain_text',
          text: 'Clear Cache'
        },
        close: {
          type: 'plain_text',
          text: 'Cancel'
        }
      }
    });
  } catch (error) {
    console.error('Error opening confirmation dialog:', error);
  }
});

In the code above, we use web.views.open() to launch a modal with a clear title, a warning message, and two buttons — Clear Cache and Cancel — and store the environmentId in private_metadata so we have it when the user clicks Clear Cache.

Once the user clicks the Clear Cache button in the modal, Slack sends a view_submission event. Here’s how to handle it and proceed with the actual operation:

// Handle the confirmation dialog submission
app.view('clear_cache_confirmation', async ({ ack, body, view }) => {
  await ack();
  
  const environmentId = view.private_metadata;
  const userId = body.user.id;
  
  // Find a DM channel with the user to respond to
  const result = await web.conversations.open({
    users: userId
  });
  
  const channel = result.channel.id;
  
  await web.chat.postMessage({
    channel,
    text: `🔄 Clearing cache for environment `${environmentId}`...`
  });
  
  try {
    // Call Kinsta API to clear cache
    const response = await kinstaRequest(
      `/sites/environments/${environmentId}/clear-cache`,
      'POST'
    );
    
    if (response && response.operation_id) {
      await web.chat.postMessage({
        channel,
        text: `✅ Cache clearing operation started! Operation ID: `${response.operation_id}``
      });
    } else {
      await web.chat.postMessage({
        channel,
        text: '⚠ Cache clearing request was sent, but no operation ID was returned.'
      });
    }
  } catch (error) {
    console.error('Cache clearing error:', error);
    await web.chat.postMessage({
      channel,
      text: `❌ Error clearing cache: ${error.message}`
    });
  }
});

In this code, after the user confirms, we grab the environmentId from private_metadata, open a private DM using web.conversations.open() to avoid cluttering public channels, run the API request to clear the cache, and follow up with a success or error message depending on the result.

Progress indicators

Some Slack commands are instant, such as clearing a cache or checking a status. But others? Not so much.

Creating a backup or deploying files can take several seconds or even minutes. And if your bot just sits there silent during that time, users might assume something broke.

Slack doesn’t give you a native progress bar, but we can fake one with a little creativity. Here’s a helper function that updates a message with a visual progress bar using block kit:

async function updateProgress(channel, messageTs, text, percentage) {
  // Create a progress bar
  const barLength = 20;
  const filledLength = Math.round(barLength * (percentage / 100));
  const bar = '█'.repeat(filledLength) + '░'.repeat(barLength - filledLength);
  
  await web.chat.update({
    channel,
    ts: messageTs,
    text: `${text} [${percentage}%]`,
    blocks: [
      {
        type: 'section',
        text: {
          type: 'mrkdwn',
          text: `${text} [${percentage}%]n`${bar}``
        }
      }
    ]
  });
}

Let’s integrate this into a /create_backup command. Instead of waiting for the whole operation to complete before replying, we’ll check in with the user at each step.

app.command('/create_backup', async ({ command, ack, say }) => {
  await ack();
  
  const args = command.text.split(' ');
  const environmentId = args[0];
  const tag = args.length > 1 ? args.slice(1).join(' ') : `Manual backup ${new Date().toISOString()}`;
  
  if (!environmentId) {
    await say('Please provide an environment ID. Usage: `/create_backup [environment-id] [optional-tag]`');
    return;
  }
  
  // Post initial message and get its timestamp for updates
  const initial = await say('🔄 Initiating backup...');
  const messageTs = initial.ts;
  
  try {
    // Update progress to 10%
    await updateProgress(command.channel_id, messageTs, '🔄 Creating backup...', 10);
    
    // Call Kinsta API to create a backup
    const response = await kinstaRequest(
      `/sites/environments/${environmentId}/manual-backups`,
      'POST',
      { tag }
    );
    
    if (response && response.operation_id) {
      await updateProgress(command.channel_id, messageTs, '🔄 Backup in progress...', 30);
      
      // Poll the operation status
      let completed = false;
      let percentage = 30;
      
      while (!completed && percentage  setTimeout(resolve, 3000));
        
        // Check operation status
        const statusResponse = await kinstaRequest(`/operations/${response.operation_id}`);
        
        if (statusResponse && statusResponse.operation) {
          const operation = statusResponse.operation;
          
          if (operation.status === 'completed') {
            completed = true;
            percentage = 100;
          } else if (operation.status === 'failed') {
            await web.chat.update({
              channel: command.channel_id,
              ts: messageTs,
              text: `❌ Backup failed! Error: ${operation.error || 'Unknown error'}`
            });
            return;
          } else {
            // Increment progress
            percentage += 10;
            if (percentage > 95) percentage = 95;
            
            await updateProgress(
              command.channel_id, 
              messageTs, 
              '🔄 Backup in progress...', 
              percentage
            );
          }
        }
      }
      
      // Final update
      await web.chat.update({
        channel: command.channel_id,
        ts: messageTs,
        text: `✅ Backup completed successfully!`,
        blocks: [
          {
            type: 'section',
            text: {
              type: 'mrkdwn',
              text: `✅ Backup completed successfully!n*Tag:* ${tag}n*Operation ID:* `${response.operation_id}``
            }
          }
        ]
      });
    } else {
      await web.chat.update({
        channel: command.channel_id,
        ts: messageTs,
        text: '⚠ Backup request was sent, but no operation ID was returned.'
      });
    }
  } catch (error) {
    console.error('Backup creation error:', error);
    
    await web.chat.update({
      channel: command.channel_id,
      ts: messageTs,
      text: `❌ Error creating backup: ${error.message}`
    });
  }
});

Success/failure notifications

Right now, your bot probably sends back plain text like ✅ Success or ❌ Failed. It works, but it’s bland, and it doesn’t help users understand why something succeeded or what they should do if it fails.

Let’s fix that with proper formatting for success and error messages alongside useful context, suggestions, and clean formatting.

Add these utilities to your utils.js so you can reuse them across all commands:

function formatSuccessMessage(title, details = []) {
  let message = `✅ *${title}*nn`;
  
  if (details.length > 0) {
    details.forEach(detail => {
      message += `• ${detail.label}: ${detail.value}n`;
    });
  }
  
  return message;
}

function formatErrorMessage(title, error, suggestions = []) {
  let message = `❌ *${title}*nn`;
  message += `*Error:* ${error}nn`;
  
  if (suggestions.length > 0) {
    message += '*Suggestions:*n';
    suggestions.forEach(suggestion => {
      message += `• ${suggestion}n`;
    });
  }
  
  return message;
}

module.exports = {
  connectToSite,
  logCommand,
  formatSuccessMessage,
  formatErrorMessage
};

These functions take structured input and turn it into Slack-friendly markdown with emoji, labels, and line breaks. Much easier to scan in the middle of a busy Slack thread. Here’s what that looks like inside a real command handler. Let’s use /clear_cache as the example:

app.command('/clear_cache', async ({ command, ack, say }) => {
  await ack();
  
  const environmentId = command.text.trim();
  
  if (!environmentId) {
    await say('Please provide an environment ID. Usage: `/clear_cache [environment-id]`');
    return;
  }
  
  try {
    await say('🔄 Processing...');
    
    // Call Kinsta API to clear cache
    const response = await kinstaRequest(
      `/sites/environments/${environmentId}/clear-cache`,
      'POST'
    );
    
    if (response && response.operation_id) {
      const { formatSuccessMessage } = require('./utils');
      
      await say(formatSuccessMessage('Cache Clearing Started', [
        { label: 'Environment ID', value: ``${environmentId}`` },
        { label: 'Operation ID', value: ``${response.operation_id}`` },
        { label: 'Status', value: 'In Progress' }
      ]));
    } else {
      const { formatErrorMessage } = require('./utils');
      
      await say(formatErrorMessage(
        'Cache Clearing Error',
        'No operation ID returned',
        [
          'Check your environment ID',
          'Verify your API credentials',
          'Try again later'
        ]
      ));
    }
  } catch (error) {
    console.error('Cache clearing error:', error);
    
    const { formatErrorMessage } = require('./utils');
    
    await say(formatErrorMessage(
      'Cache Clearing Error',
      error.message,
      [
        'Check your environment ID',
        'Verify your API credentials',
        'Try again later'
      ]
    ));
  }
});

Automate WordPress tasks with scheduled jobs

So far, everything your Slackbot does happens when someone explicitly triggers a command. But not everything should depend on someone remembering to run it.

What if your bot could automatically back up your sites every night? Or check if any site is down every morning before the team wakes up.

We’ll use the node-schedule library to run tasks based on cron expressions. First, install it:

npm install node-schedule

Now, set it up at the top of your app.js:

const schedule = require('node-schedule');

We’ll also need a way to track active scheduled jobs so users can list or cancel them later:

const scheduledJobs = {};

Creating the schedule task command

We’ll start with a basic /schedule_task command that accepts a task type (backup, clear_cache, or status_check), the environment ID, and a cron expression.

/schedule_task backup 12345 0 0 * * *

This would schedule a daily backup at midnight. Here’s the full command handler:

app.command('/schedule_task', async ({ command, ack, say }) => {
  await ack();

  const args = command.text.split(' ');
  if (args.length  {
      console.log(`Running scheduled ${taskType} for environment ${environmentId}`);

      try {
        switch (taskType) {
          case 'backup':
            await kinstaRequest(`/sites/environments/${environmentId}/manual-backups`, 'POST', {
              tag: `Scheduled backup ${new Date().toISOString()}`
            });
            break;
          case 'clear_cache':
            await kinstaRequest(`/sites/environments/${environmentId}/clear-cache`, 'POST');
            break;
          case 'status_check':
            const response = await kinstaRequest(`/sites/environments/${environmentId}`);
            const env = response?.site?.environments?.[0];
            if (env) {
              console.log(`Status: ${env.display_name} is ${env.is_blocked ? 'blocked' : 'running'}`);
            }
            break;
        }
      } catch (err) {
        console.error(`Scheduled ${taskType} failed for ${environmentId}:`, err.message);
      }
    });

    scheduledJobs[jobId] = {
      job,
      taskType,
      environmentId,
      cronSchedule,
      userId: command.user_id,
      createdAt: new Date().toISOString()
    };

    await say(`✅ Scheduled task created!
*Task:* ${taskType}
*Environment:* `${environmentId}`
*Cron:* `${cronSchedule}`
*Job ID:* `${jobId}`

To cancel this task, run `/cancel_task ${jobId}``);
  } catch (err) {
    console.error('Error creating scheduled job:', err);
    await say(`❌ Failed to create scheduled task: ${err.message}`);
  }
});

Cancelling scheduled tasks

If something changes or the task isn’t needed anymore, users can cancel it with:

/cancel_task

Here’s the implementation:

app.command('/cancel_task', async ({ command, ack, say }) => {
  await ack();

  const jobId = command.text.trim();

  if (!scheduledJobs[jobId]) {
    await say(`⚠ No task found with ID: `${jobId}``);
    return;
  }

  scheduledJobs[jobId].job.cancel();
  delete scheduledJobs[jobId];

  await say(`✅ Task `${jobId}` has been cancelled.`);
});

Listing all scheduled tasks

Let’s also let users view all the jobs that have been scheduled:

app.command('/list_tasks', async ({ command, ack, say }) => {
  await ack();

  const tasks = Object.entries(scheduledJobs);
  if (tasks.length === 0) {
    await say('No scheduled tasks found.');
    return;
  }

  let message = '*Scheduled Tasks:*nn';

  for (const [jobId, job] of tasks) {
    message += `• *Job ID:* `${jobId}`n`;
    message += `  - Task: ${job.taskType}n`;
    message += `  - Environment: `${job.environmentId}`n`;
    message += `  - Cron: `${job.cronSchedule}`n`;
    message += `  - Created by: nn`;
  }

  message += '_Use `/cancel_task [job_id]` to cancel a task._';
  await say(message);
});

This gives your Slackbot a whole new level of autonomy. Backups, cache clears, and status checks no longer have to be someone’s job. They just happen quietly, reliably, and on schedule.

Recurring maintenance

Sometimes, you want to run a group of maintenance tasks at regular intervals, like weekly backups and cache clears on Sunday nights. That’s where maintenance windows come in.

A maintenance window is a scheduled block of time when the bot automatically runs predefined tasks like:

  • Creating a backup
  • Clearing the cache
  • Sending start and completion notifications

The format is simple:

/maintenance_window [environment_id] [day_of_week] [hour] [duration_hours]

For example:

/maintenance_window 12345 Sunday 2 3

This means that every Sunday at 2 AM, maintenance tasks are run for 3 hours. Here’s the full implementation:

// Add a command to create a maintenance window
app.command('/maintenance_window', async ({ command, ack, say }) => {
  await ack();
  
  // Expected format: environment_id day_of_week hour duration
  // Example: /maintenance_window 12345 Sunday 2 3
  const args = command.text.split(' ');
  
  if (args.length < 4) {
    await say('Please provide all required parameters. Usage: `/maintenance_window [environment_id] [day_of_week] [hour] [duration_hours]`');
    return;
  }
  
  const [environmentId, dayOfWeek, hour, duration] = args;
  
  // Validate inputs
  const validDays = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
  if (!validDays.includes(dayOfWeek)) {
    await say(`Invalid day of week. Please choose from: ${validDays.join(', ')}`);
    return;
  }
  
  const hourInt = parseInt(hour, 10);
  if (isNaN(hourInt) || hourInt  23) {
    await say('Hour must be a number between 0 and 23.');
    return;
  }
  
  const durationInt = parseInt(duration, 10);
  if (isNaN(durationInt) || durationInt  12) {
    await say('Duration must be a number between 1 and 12 hours.');
    return;
  }
  
  // Convert day of week to cron format
  const dayMap = {
    'Sunday': 0,
    'Monday': 1,
    'Tuesday': 2,
    'Wednesday': 3,
    'Thursday': 4,
    'Friday': 5,
    'Saturday': 6
  };
  
  const cronDay = dayMap[dayOfWeek];
  
  // Create cron schedule for the start of the maintenance window
  const cronSchedule = `0 ${hourInt} * * ${cronDay}`;
  
  // Generate a unique job ID
  const jobId = `maintenance_${environmentId}_${Date.now()}`;
  
  // Schedule the job
  try {
    const job = schedule.scheduleJob(cronSchedule, async function() {
      // Start of maintenance window
      await web.chat.postMessage({
        channel: command.channel_id,
        text: `🔧 *Maintenance Window Started*n*Environment:* `${environmentId}`n*Duration:* ${durationInt} hoursnnAutomatic maintenance tasks are now running.`
      });
      
      // Perform maintenance tasks
      try {
        // 1. Create a backup
        const backupResponse = await kinstaRequest(
          `/sites/environments/${environmentId}/manual-backups`,
          'POST',
          { tag: `Maintenance backup ${new Date().toISOString()}` }
        );
        
        if (backupResponse && backupResponse.operation_id) {
          await web.chat.postMessage({
            channel: command.channel_id,
            text: `✅ Maintenance backup created. Operation ID: `${backupResponse.operation_id}``
          });
        }
        
        // 2. Clear cache
        const cacheResponse = await kinstaRequest(
          `/sites/environments/${environmentId}/clear-cache`,
          'POST'
        );
        
        if (cacheResponse && cacheResponse.operation_id) {
          await web.chat.postMessage({
            channel: command.channel_id,
            text: `✅ Cache cleared. Operation ID: `${cacheResponse.operation_id}``
          });
        }
        
        // 3. Schedule end of maintenance window notification
        setTimeout(async () => {
          await web.chat.postMessage({
            channel: command.channel_id,
            text: `✅ *Maintenance Window Completed*n*Environment:* `${environmentId}`nnAll maintenance tasks have been completed.`
          });
        }, durationInt * 60 * 60 * 1000); // Convert hours to milliseconds
      } catch (error) {
        console.error('Maintenance tasks error:', error);
        await web.chat.postMessage({
          channel: command.channel_id,
          text: `❌ Error during maintenance: ${error.message}`
        });
      }
    });
    
    // Store the job for later cancellation
    scheduledJobs[jobId] = {
      job,
      taskType: 'maintenance',
      environmentId,
      cronSchedule,
      dayOfWeek,
      hour: hourInt,
      duration: durationInt,
      userId: command.user_id,
      createdAt: new Date().toISOString()
    };
    
    await say(`✅ Maintenance window scheduled!
*Environment:* `${environmentId}`
*Schedule:* Every ${dayOfWeek} at ${hourInt}:00 for ${durationInt} hours
*Job ID:* `${jobId}`

To cancel this maintenance window, use `/cancel_task ${jobId}``);
  } catch (error) {
    console.error('Error scheduling maintenance window:', error);
    await say(`❌ Error scheduling maintenance window: ${error.message}`);
  }
});

Automated reporting

You don’t want to wake up every Monday wondering if your WordPress site was backed up or if it’s been down for hours. With automated reporting, your Slack bot can give you and your team a quick performance summary on a schedule.

This kind of report is great for keeping tabs on things like:

  • The current site status
  • Backup activity over the past 7 days
  • PHP version and primary domain
  • Any red flags, like blocked environments or missing backups

Let’s build a /schedule_report command that automates this.

// Add a command to schedule weekly reporting
app.command('/schedule_report', async ({ command, ack, say }) => {
  await ack();
  
  // Expected format: environment_id day_of_week hour
  // Example: /schedule_report 12345 Monday 9
  const args = command.text.split(' ');
  
  if (args.length < 3) {
    await say('Please provide all required parameters. Usage: `/schedule_report [environment_id] [day_of_week] [hour]`');
    return;
  }
  
  const [environmentId, dayOfWeek, hour] = args;
  
  // Validate inputs
  const validDays = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
  if (!validDays.includes(dayOfWeek)) {
    await say(`Invalid day of week. Please choose from: ${validDays.join(', ')}`);
    return;
  }
  
  const hourInt = parseInt(hour, 10);
  if (isNaN(hourInt) || hourInt  23) {
    await say('Hour must be a number between 0 and 23.');
    return;
  }
  
  // Convert day of week to cron format
  const dayMap = {
    'Sunday': 0,
    'Monday': 1,
    'Tuesday': 2,
    'Wednesday': 3,
    'Thursday': 4,
    'Friday': 5,
    'Saturday': 6
  };
  
  const cronDay = dayMap[dayOfWeek];
  
  // Create cron schedule for the report
  const cronSchedule = `0 ${hourInt} * * ${cronDay}`;
  
  // Generate a unique job ID
  const jobId = `report_${environmentId}_${Date.now()}`;
  
  // Schedule the job
  try {
    const job = schedule.scheduleJob(cronSchedule, async function() {
      // Generate and send the report
      await generateWeeklyReport(environmentId, command.channel_id);
    });
    
    // Store the job for later cancellation
    scheduledJobs[jobId] = {
      job,
      taskType: 'report',
      environmentId,
      cronSchedule,
      dayOfWeek,
      hour: hourInt,
      userId: command.user_id,
      createdAt: new Date().toISOString()
    };
    
    await say(`✅ Weekly report scheduled!
*Environment:* `${environmentId}`
*Schedule:* Every ${dayOfWeek} at ${hourInt}:00
*Job ID:* `${jobId}`

To cancel this report, use `/cancel_task ${jobId}``);
  } catch (error) {
    console.error('Error scheduling report:', error);
    await say(`❌ Error scheduling report: ${error.message}`);
  }
});

// Function to generate weekly report
async function generateWeeklyReport(environmentId, channelId) {
  try {
    // Get environment details
    const response = await kinstaRequest(`/sites/environments/${environmentId}`);
    
    if (!response || !response.site || !response.site.environments || !response.site.environments.length) {
      await web.chat.postMessage({
        channel: channelId,
        text: `⚠ Weekly Report Error: No environment found with ID: `${environmentId}``
      });
      return;
    }
    
    const env = response.site.environments[0];
    
    // Get backups for the past week
    const backupsResponse = await kinstaRequest(`/sites/environments/${environmentId}/backups`);
    
    let backupsCount = 0;
    let latestBackup = null;
    
    if (backupsResponse && backupsResponse.environment && backupsResponse.environment.backups) {
      const oneWeekAgo = new Date();
      oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);
      
      const recentBackups = backupsResponse.environment.backups.filter(backup => {
        const backupDate = new Date(backup.created_at);
        return backupDate >= oneWeekAgo;
      });
      
      backupsCount = recentBackups.length;
      
      if (recentBackups.length > 0) {
        latestBackup = recentBackups.sort((a, b) => b.created_at - a.created_at)[0];
      }
    }
    
    // Get environment status
    const statusEmoji = env.is_blocked ? '🔴' : '🟢';
    const statusText = env.is_blocked ? 'Blocked' : 'Running';
    
    // Create report message
    const reportDate = new Date().toLocaleDateString('en-US', {
      weekday: 'long',
      year: 'numeric',
      month: 'long',
      day: 'numeric'
    });
    
    const reportMessage = `📊 *Weekly Report - ${reportDate}*
*Site:* ${env.display_name}
*Environment ID:* `${environmentId}`

*Status Summary:*
• Current Status: ${statusEmoji} ${statusText}
• PHP Version: ${env.container_info?.php_engine_version || 'Unknown'}
• Primary Domain: ${env.primaryDomain?.name || env.domains?.[0]?.name || 'N/A'}

*Backup Summary:*
• Total Backups (Last 7 Days): ${backupsCount}
• Latest Backup: ${latestBackup ? new Date(latestBackup.created_at).toLocaleString() : 'N/A'}
• Latest Backup Type: ${latestBackup ? latestBackup.type : 'N/A'}

*Recommendations:*
• ${backupsCount === 0 ? '⚠ No recent backups found. Consider creating a manual backup.' : '✅ Regular backups are being created.'}
• ${env.is_blocked ? '⚠ Site is currently blocked. Check for issues.' : '✅ Site is running normally.'}

_This is an automated report. For detailed information, use the `/site_status ${environmentId}` command._`;
    
    await web.chat.postMessage({
      channel: channelId,
      text: reportMessage
    });
  } catch (error) {
    console.error('Report generation error:', error);
    await web.chat.postMessage({
      channel: channelId,
      text: `❌ Error generating weekly report: ${error.message}`
    });
  }
}

Error handling and monitoring

Once your bot starts performing real operations like modifying environments or triggering scheduled tasks, you need more than console.log() to keep track of what’s happening behind the scenes.

Let’s break this down into clean, maintainable layers:

Structured logging with Winston

Instead of printing logs to the console, use winston to send structured logs to files, and optionally to services like Loggly or Datadog. Install it with the command below:

npm install winston

Next, set up logger.js:

const winston = require('winston');
const fs = require('fs');
const path = require('path');

const logsDir = path.join(__dirname, '../logs');
if (!fs.existsSync(logsDir)) fs.mkdirSync(logsDir);

const logger = winston.createLogger({
  level: 'info',
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.json()
  ),
  defaultMeta: { service: 'wordpress-slack-bot' },
  transports: [
    new winston.transports.Console({ format: winston.format.simple() }),
    new winston.transports.File({ filename: path.join(logsDir, 'error.log'), level: 'error' }),
    new winston.transports.File({ filename: path.join(logsDir, 'combined.log') })
  ]
});

module.exports = logger;

Then, in your app.js, swap out any console.log or console.error calls with:

const logger = require('./logger');

logger.info('Cache clear initiated', { userId: command.user_id });
logger.error('API failure', { error: err.message });

Send alerts to admins via Slack

You already have the ADMIN_USERS env variable, use it to notify your team directly in Slack when something critical fails:

async function alertAdmins(message, metadata = {}) {
  for (const userId of ADMIN_USERS) {
    const dm = await web.conversations.open({ users: userId });
    const channel = dm.channel.id;

    let alert = `🚨 *${message}*n`;
    for (const [key, value] of Object.entries(metadata)) {
      alert += `• *${key}:* ${value}n`;
    }

    await web.chat.postMessage({ channel, text: alert });
  }
}

Use it like this:

await alertAdmins('Backup Failed', {
  environmentId,
  error: error.message,
  user: ``
});

Track performance with basic metrics

Don’t go full Prometheus if you’re just trying to see how healthy your bot is. Keep a lightweight performance object:

const metrics = {
  apiCalls: 0,
  errors: 0,
  commands: 0,
  totalTime: 0,
  get avgResponseTime() {
    return this.apiCalls === 0 ? 0 : this.totalTime / this.apiCalls;
  }
};

Update this inside your kinstaRequest() helper:

const start = Date.now();
try {
  metrics.apiCalls++;
  const res = await fetch(...);
  return await res.json();
} catch (err) {
  metrics.errors++;
  throw err;
} finally {
  metrics.totalTime += Date.now() - start;
}

Expose it via a command like /bot_performance:

app.command('/bot_performance', async ({ command, ack, say }) => {
  await ack();

  if (!ADMIN_USERS.includes(command.user_id)) {
    return await say('⛔ Not authorized.');
  }

  const msg = `📊 *Bot Metrics*
• API Calls: ${metrics.apiCalls}
• Errors: ${metrics.errors}
• Avg Response Time: ${metrics.avgResponseTime.toFixed(2)}ms
• Commands Run: ${metrics.commands}`;

  await say(msg);
});

Optional: Define recovery steps

If you want to implement recovery logic (like retrying cache clears via SSH), just create a helper like:

async function attemptRecovery(environmentId, issue) {
  logger.warn('Attempting recovery', { environmentId, issue });

  if (issue === 'cache_clear_failure') {
    // fallback logic here
  }

  // Return a recovery status object
  return { success: true, message: 'Fallback ran.' };
}

Keep it out of your main command logic unless it’s a critical path. In many cases, it’s better to log the error, alert admins, and let humans decide what to do.

Deploy and manage your Slackbot

Once your bot is feature-complete, you should deploy it to a production environment where it can run 24/7.

Kinsta’s Sevalla is an excellent place to host bots like this. It supports Node.js apps, environment variables, logging, and scalable deployments out of the box.

Alternatively, you can containerize your bot using Docker or deploy it to any cloud platform that supports Node.js and background services.

Here are a few things to keep in mind before going live:

  • Use environment variables for all secrets (Slack tokens, Kinsta API keys, SSH keys).
  • Set up logging and uptime monitoring so you know when something breaks.
  • Run your bot with a process manager like PM2 or Docker’s restart: always policy to keep it alive after crashes or restarts.
  • Keep your SSH keys secure, especially if you’re using them for automation.

Summary

You’ve now taken your Slackbot from a simple command handler to a powerful tool with real interactivity, scheduled automation, and solid monitoring. These features make your bot more useful, more reliable, and way more pleasant to use, especially for teams managing multiple WordPress sites.

And when you pair that with the power of the Kinsta API and stress-free hosting from Kinsta, you’ve got a setup that’s both scalable and dependable.

The post Implement interactivity, scheduling, and monitoring in Slackbots for managing WordPress sites appeared first on Kinsta®.