Skip to main content

Automating Workflows with OnTask Integrations

developer sitting at computer

Offering product integrations can be one of the easiest ways to expand the feature set of a product without adding loads of time and cost on development resources. OnTask currently has external integrations with Salesforce, Sharepoint, and Brother scanners that we offer to our customers. Internally, we have an integration with Slack that allows our development team to be notified of different events that happen within OnTask. As a team of engineers, we are always looking for new ways that we could possibly use OnTask with other products, and luckily, we usually find some time to prototype out these integrations.

Being an Apple enthusiast myself, one of the first things I wanted to try to integrate OnTask with was Apple HomeKit so I could use Siri to launch workflows. I configured my Apple TV to work with the Home app and was all ready to go. Unfortunately, I quickly found out that HomeKit does not natively support customizations like this. Lucky for me, someone had already come up with a solution to this problem using Homebridge and a node module called homebridge-cmdswitch2.

According to their Github, Homebridge is “a lightweight NodeJS server you can run on your home network that emulates the iOS HomeKit API” and homebridge-cmdswitch2 “allows you to run Command Line Interface (CLI) commands via HomeKit”. After that, the setup was pretty simple to get everything working. I had to create a workflow in OnTask that I wanted to start, configure my Home app to work with Homebridge, and write this config file below. Before I knew it, Siri was launching workflows for me.

{
  "bridge": {
      "name": "Homebridge",
      "username": "CC:22:3D:E3:CE:30",
      "pin": "031-45-154"
  },
  
  "description": "Homebridge OnTask Config",

  "accessories": [],

  "platforms": [{
    "platform": "cmdSwitch2",
    "name": "CMD Switch",
    "switches": [{
      "name" : "OnTask Workflow",
      "on_cmd": "curl -d '{\"wfModelId\":\"ec6e7fc4-1c38-4ebf-8a32-a02ba6721ffe\", \"wfData\": {}}' -H \"Content-Type: application/json\" -X POST {LAUNCH_WORKFLOW_ENDPOINT}",
      "state_cmd": "exit 1",
      "off_cmd": "echo off"
    }]
 }]
}

One of the next integrations I wanted to try to play with was Slack. I mentioned that we already use it internally, but I wanted to see if there was a way that we could make a Slack integration that was useful for an end-user. After creating my own Slack app to play with, I wanted to be able to launch a workflow from Slack as well as send a message back to Slack notifying me that a workflow was launched. To do both of these things, I had to create a handler somewhere to take the callback IDs that Slack would send to our API and do something with them. The finished handler looked like this:

'use strict';
const qs = require('qs');
const request = require('request')

function handleInteractiveMessage(req, res) {
  const task = req.task;
  res.status(200);
  res.end();
  task.log.info({ source: 'slack-connector.handInteractiveMessage' }, qs.parse(req.body));
  const payload = JSON.parse(qs.parse(req.body).payload);
  const responseUrl = payload.response_url;
  const callbackId = payload.callback_id;
  task.log.info({ source: 'slack-connector.handleInteractiveMessage', responseUrl }, 'response url');
  task.log.info({ source: 'slack-connector.handleInteractiveMessage', callbackId }, 'callback id');
  if (payload.callback_id === 'ontask_workflow_start') {
    kickOffWorkflow(task);
  } else if (payload.callback_id === 'handle_workflow_actions' && payload.actions[0].value !== 'redirect') {
    confirmCancelWorkflow(task, responseUrl, payload.actions[0].value);
  } else if (payload.callback_id === 'confirm_cancel_workflow' && payload.actions[0].value !== 'No') {
    cancelWorkflow(task, payload.actions[0].value, responseUrl)
  }
}

function confirmCancelWorkflow(task, responseUrl, wfInstanceId) {
  return new Promise((resolve, reject) => {
    task.log.info({ source: 'slack-connector.confirmCancelWorkflow', wfInstanceId }, 'wf instance id');
    request({
      url: responseUrl,
      method: 'POST',
      json: true,
      body: {
        text: 'Are you sure you want to cancel this workflow?',
        attachments: [
          {
            fallback: 'See what\'s going on!',
            author_name: 'Owner: ntorretti',
            title: 'Confirm Cancel',
            text: 'Please confirm you want to cancel this workflow',
            callback_id: 'confirm_cancel_workflow',
            actions: [
              {
                name: 'action',
                type: 'button',
                text: 'Yes',
                style: '',
                value: wfInstanceId
              },
              {
                name: 'action',
                type: 'button',
                style: '',
                text: 'No',
                value: 'No'
              }
            ]
          }
        ]
      }
    }, (err, response) => {
      if (err) {
        task.log.error({ source: 'slack-connector.confirmCancelWorkflow', err }, 'whoops');
        reject(err);
      } else {
        task.log.info({ source: 'slack-connector.confirmCancelWorkflow', response }, 'response');
        resolve({ status: 'Request successfully sent to callback API endpoint.' });
      }
    });
  });
}

function sendCancelConfirmation(task, responseUrl) {
  return new Promise((resolve, reject) => {
    request({
      url: responseUrl,
      method: 'POST',
      json: true,
      body: {
        text: 'We have successfully cancelled your workflow.',
      }
    }, (err, response) => {
      if (err) {
        task.log.error({ source: 'slack-connector.sendCancelConfirmation', err }, 'whoops');
        reject(err);
      } else {
        task.log.info({ source: 'slack-connector.sendCancelConfirmation', response }, 'response');
        resolve({ status: 'Request successfully sent to callback API endpoint.' });
      }
    });
  });
}

function cancelWorkflow(task, wfInstanceId, responseUrl) {
  return new Promise((resolve, reject) => {
    request({
      headers: { 'content-type': 'application/json' },
      url: {CANCEL_WORKFLOW_ENDPOINT},
      method: 'POST'
    }, (err, response) => {
      if (err) {
        task.log.error({ source: 'slack-connector.cancelWorkflow', err }, 'whoops');
        reject(err);
      } else {
        task.log.info({ source: 'slack-connector.cancelWorkflow', response }, 'yay');
        sendCancelConfirmation(task, responseUrl);
        resolve(response);
      }
    });
  });
}

function kickOffWorkflow(task) {
  return new Promise((resolve, reject) => {
    request({
      headers: { 'content-type': 'application/json' },
      url: {LAUNCH_WORKFLOW_ENDPOINT},
      method: 'POST',
      body: JSON.stringify({
        wfModelId: 'd32aa8ba-f153-46b5-8c62-81d14327c924',
        wfInstanceName: 'Untitled',
        wfData: {
          Originator: 'Natalie Torretti',
          Originator_Email: 'ntorretti@accusoft.com',
          email: 'ntorretti@accusoft.com'
        }
      }),
    }, (err, response) => {
      if (err) {
        task.log.error({ source: 'slack-connector.handleInteractiveMessage', err }, 'whoops');
        reject(err);
      } else {
        task.log.info({ source: 'slack-connector.handleInteractiveMessage', response }, 'yay');
        resolve(response);
      }
    });
  });
}

module.exports.initialize = (params, imports, ready) => {
  const framework = imports['prv-common-service-base'];
  const task = framework.taskLogging.createTask();
  const server = imports.server;

  task.begin('Initializing Slack connector component');

  server.post('/interactiveMessage', handleInteractiveMessage);

  task.log.info({ source: 'slack-connector.initialize' }, 'Slack connector component initialized');
  task.end();
  ready();
};

Slack has some great documentation and really is built to handle these kind of integrations. It was not that much work to get everything configured after I had the connector in place. I still needed one more piece of code on the OnTask end to be able to actually send the message to Slack. That send message function ended up looking like this:

const sendSlackMessage = imports => (task, redirectUrl, workflowInstanceId, message) => {
  return new Promise((resolve, reject) => {
    task.log.info({ source: 'utils.sendSlackMessage' }, 'sending slack message');
    request({
      url: 'https://hooks.slack.com/services/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',
      method: 'POST',
      json: true,
      body: {
        text: JSON.stringify(message),
        attachments: [
          {
            fallback: 'See what\'s going on!',
            author_name: 'Owner: ntorretti',
            title: 'Workflow Actions',
            text: 'Here are some actions you can take!',
            callback_id: 'handle_workflow_actions',
            actions: [
              {
                name: 'action',
                type: 'button',
                text: 'View dashboard',
                style: '',
                value: 'redirect',
                url: {LINK_URL}
              },
              {
                name: 'action',
                type: 'button',
                text: 'Go to workflow',
                style: '',
                value: 'redirect',
                url: redirectUrl
              },
              {
                name: 'action',
                type: 'button',
                style: '',
                text: 'Cancel Workflow',
                value: workflowInstanceId
              }
            ]
          }
        ]
      }
    }, (err, response) => {
      if (err) {
        task.log.error({ source: 'utils.sendSlackMessage', err }, 'whoops');
        reject(err);
      } else {
        task.log.info({ source: 'utils.sendSlackMessage', response }, 'response');
        resolve({ status: 'Request successfully sent to callback API endpoint.' });
      }
    });
  });
}

We called this the send SlackMessage function after the “Start Demo Workflow” was started. This sends a message back to Slack that gives the user a couple different actions that they could take on that message. If a user clicked one of the buttons in the message, a request with a specific callback ID was sent to the slack handler and the appropriate action was taken. The first image below shows how I integrated starting a workflow in Slack and the second image shows the message we sent back to Slack after a workflow was started giving the user different actions they can take.

These are just a few ways that Accusoft’s OnTask can be integrated into your daily routines. Whether it’s starting a workflow with Siri or enabling automated Slack processes, OnTask is built to help you and your organization to find the best way to automate processes.

Natalie Torretti

Natalie Torretti, Software Engineer III

Natalie joined Accusoft as a software engineer in 2016. She started on the eDocr team but has spent the last two and a half years on OnTask development team. Natalie has been a large contributor to the React front-end UI of OnTask and made several enhancements to the back-end microservices as well. She obtained her B.S. in Biobehavioral Health with a Psychology minor from Penn State as well as her A.S.T. in Information Technology from South Hills School of Business and Technology. When Natalie is not writing code she enjoys working out, spending time at the pool, and playing with her dogs.