Delayed Responses to Slack Slash Commands

Slack limits the time to the first slash command response to 3 seconds, which is a problem for longer tasks. If your command needs to talk to external resources that might get blocked, or genuinely need more time to complete, implementing everything in a single response won’t work.

Claudia Bot Builder, since version 1.4.0, offers a simple solution for delayed responses. This tutorial demonstrates how to work around the timing issues by using two important Claudia.js features:

Prerequisites

What we’ll implement

Slack Slash commands support delayed and multiple responses, allowing a bot to respond to a command up to 5 times in 30 minutes. We’ll create a simple timer, which you can tell how many seconds to wait. It will respond immediately with a confirmation about the timer, and then again after the specified period.

How we’ll implement it

Claudia Bot Builder simplifies messaging workflows, but it enables only one response per request. In this case, we’ll need to send two responses. This would typically need the primary request to call another Lambda process asynchronously.

Before Claudia Bot Builder 1.4.0, you had to create a separate Lambda function for this, and complicate deployment. From version 1.4.0 onwards, you can use the same Lambda function for both the primary and the delayed response. The key trick is to intercept the second request and not allow it to go through the normal Web API request pipeline.

Responding to the primary request

To provide Slack users an immediate response, the bot will reply to the primary request with a confirmation message. It will also trigger an asynchronous call to the same Lambda function, without waiting for the response, and pass the original message. For that, we use the AWS SDK. The Bot Builder works with Promises for asynchronous requests, so we’ll just wrap the AWS SDK Lambda call into a Promise.

const aws = require('aws-sdk');
const lambda = new aws.Lambda();
const botBuilder = require('claudia-bot-builder');

// ...
const api = botBuilder((message, apiRequest) => {
  return new Promise((resolve, reject) => {
    lambda.invoke({
      FunctionName: apiRequest.lambdaContext.functionName,
      Qualifier: apiRequest.lambdaContext.functionVersion,
      InvocationType: 'Event',
      Payload: JSON.stringify({
        slackEvent: message // this will enable us to detect the event later and filter it
      })
    }, (err, done) => {
      if (err) return reject(err);
      resolve(done);
    });
  }).then(() => {
    return { // the initial response
      text: `Ok, I'll ping you in ${seconds}s.`,
      response_type: 'in_channel'
    }
  }).catch(() => {
    return `Could not setup the timer`
  });
});

The bot builder request processor gets two arguments. The first is the message coming from the bot engine, and the second is the Claudia API Builder Request. We can use the .lambdaContext object to get the name and the version of the currently executing function. This makes it easy to recursively call the correct version, in case you use different aliases for development, testing and production.

When invoking a Lambda function, you can specify the invocation type. Using the Event invocation type is effectively fire-and-forget, which is perfect for the secondary request, because we want to complete the primary process without waiting on the timer to end.

The Payload field of the Lambda call will turn into the event body for the recursive call. Because we want to prevent normal API routing and request processing, we need to mark it somehow so it’s easy to detect. In this case, we’re passing the original message in the slackEvent field, and we’ll check for that later.

Finally, when the invocation succeeds, we just respond to the user that the timer is active. In this case, we’re using the in_channel response type so that all the Slack users in that channel see the message.

Responding to the secondary request

When the recursive request comes in, we don’t want to let Claudia API Builder route it as if it came from the API Gateway. Instead, we’ll block the normal request, and take over. For that, we need to define an intercept function. If it gets a normal web request, it just needs to return it back, which will continue with the normal process. If it gets the one we marked with slackEvent, it needs to return false (or a Promise resolving to false), to stop the request processing pipeline.

The Claudia Bot Builder has a helper function .slackDelayedReply that handles the workflow of sending delayed messages to Slack. Just pass in the original message and the response.

const promiseDelay = require('promise-delay');
const slackDelayedReply = botBuilder.slackDelayedReply;

api.intercept((event) => {
  if (!event.slackEvent) // if this is a normal web request, let it run
    return event;

  const message = event.slackEvent;
  const seconds = parseInt(message.text, 10);

  return promiseDelay(seconds * 1000)
    .then(() => {
      return slackDelayedReply(message, {
        text: `${seconds} seconds passed.`,
        response_type: 'in_channel'
      })
    })
    .then(() => false); // prevent normal execution
});

Configuring the Lambda function

To make things run, we need to configure the Lambda function a bit. First of all, the default time AWS will let Lambda functions run is 3 seconds, so we need to extend that. We can use --timeout when creating the function using claudia create for that. The second thing we need to do is set up IAM privileges so the Lambda function can call itself. By default, AWS will not allow that. So use --allow-recursion when deploying with Claudia, and it will create all the required IAM settings.

So the final create command will looks similar to this:

claudia create --api-module bot --region us-east-1 --timeout 120 --allow-recursion --configure-slack-slash-command

Full example

Check out the Slack Delayed Response example project for the complete source code, which you can run immediately.

Did you like this tutorial? Get notified when we publish the next one.

Once a month, high value mailing list, no ads or spam. (Check out the past issues)