This article is the third in a series exploring fun, straightforward ways you can control workflows using Camunda Cloud — have a read of the others for some background if you’d like to see Camunda Cloud in action with Restzeebe, without a line of code:


In the first two articles of this series you learned how to execute workflows, how to control the flow and take different paths depending on the context, and how workers execute their individual code in a task. These examples might have felt a bit too theoretical, so now I would like to present you with a use case: controlling manual tasks with to-do lists.

Manual processes still exist, more often than you might think. For example, a person has to release a process, or complete a task that has not been automated so far.

How about the following case: a developer starts a new job in a company. There are tasks that have to be done before, at, and after the start. These include things like

  • Create an employee file
  • Create accounts for different services
  • Send a welcome package
  • Instruct employees

All these activities are mostly performed by humans. Wouldn’t it be nice if this process was modeled and all the responsible people got corresponding entries on their Trello board to get things done? And wouldn’t it be even better if the process is notified when the to-do entry is completed?

Well, I want that!

The good news is — we don’t need much to get there. Essentially, it’s the following points:

  • Trello API Key
  • Trello Webhook
  • Worker that creates new to-do entries
  • Worker that reacts to completed To-dos

Let’s start!

Setup

Trello is a great tool to organize and collaborate on shared tasks. To use our example with Trello we need three things:

The API Key and the Token are necessary to communicate with the Trello API. For our example we want to implement two actions:

  1. Create a new Trello Card.
  2. Get notified when something changes on a Trello board.

Create Trello Card

This task should of course be executed by a worker. The following controller takes care of the communication with the Trello API:

import axios, { AxiosRequestConfig, AxiosResponse } from 'axios'
import * as functions from 'firebase-functions'
import { v4 } from 'uuid'
import { Document } from '../types/Document.type'
import { StorageController } from './storage.controller'

const BASEURL = 'https://api.trello.com/1'

export enum TRELLO {
  KEY = 'key',
  TOKEN = 'token',
  ID_LIST = 'idList',
  NAME = 'name',
}

export enum ROUTE {
  CARDS = 'cards',
}

export class TrelloController {
  private trelloKey: string
  private trelloToken: string

  constructor(private store: StorageController) {
    this.trelloKey = functions.config().trello.key
    this.trelloToken = functions.config().trello.token
  }

  public async storeWebhookPayload(payload: any) {
    const uuid: string = v4()
    await this.store.set(Document.TRELLO_WEBHOOK_PAYLOAD, uuid, payload)
  }

  public async addCard(idList: string, name: string): Promise {
    const queryParams: URLSearchParams = new URLSearchParams()
    queryParams.append(TRELLO.ID_LIST, idList)
    queryParams.append(TRELLO.NAME, name)
    const result = await this.request('POST', ROUTE.CARDS, queryParams)
    return result ? result.id : undefined
  }

  private async request(
    method: 'GET' | 'POST' | 'PATCH' | 'DELETE',
    route: string,
    queryParams: URLSearchParams,
  ) {
    const params = queryParams
    params.append(TRELLO.KEY, this.trelloKey)
    params.append(TRELLO.TOKEN, this.trelloToken)

    const config: AxiosRequestConfig = {
      method,
      url: `${BASEURL}/${route}`,
      params,
    }

    try {
      const result: AxiosResponse = await axios(config)
      return result ? result.data : undefined
    } catch (error) {
      console.error(error)
    }
  }
}

What is still missing is the integration of the controller into the worker:

import { StorageController } from '../storage.controller'
import { TrelloController } from '../trello.controller'
import { ZeebeController } from '../zeebe.controller'

export class TrelloWorkerController {
  constructor(
    private zeebeController: ZeebeController,
    private store: StorageController,
  ) {}

  public createWorker(taskType: 'trelloAddCard') {
    this.zeebeController.getZeebeClient().createWorker({
      taskType,
      taskHandler: async (job: any, complete: any, worker: any) => {
        const idList = job.customHeaders.idlist
        const name = job.customHeaders.name

        const trelloController = new TrelloController(this.store)

        try {
          switch (taskType) {
            case 'trelloAddCard':
              const id: string = await trelloController.addCard(idList, name)
              complete.success({ id })
              break
            default:
              complete.failure(`Tasktype ${taskType} unknown`)
          }
        } catch (error) {
          complete.failure('Failed to send slack message')
        }
      },
    })
  }
}

The basic implementation should already look familiar to you from the last article. It should be emphasized that the list and the name are not static. The parameter idList is the ID of the Trello list on which the new entry should be created. The name is the title of the new card. At the end, the id of the created card is written back to the process context.

Set up a Webhook

Our goal is to be notified when something changes on a board. If Trello cards are moved to Done, our process should be notified. For this purpose Trello offers Webhooks. All we have to do is to provide an HTTP endpoint which is called by Trello when something changes.

For this we provide the following endpoint:

const express = require('express')
import { NextFunction, Request, Response } from 'express'
import { StorageController } from '../../controller/storage.controller'
import { ZeebeController } from '../../controller/zeebe.controller'
import { TrelloBoardType } from '../../types/TrelloBoard.type'
import { Error, ErrorType } from '../../utils/Error'

export class TrelloWebhookRouter {
  public router = express.Router({ mergeParams: true })

  constructor(store: StorageController) {
    this.router.post(
      '/',
      async (req: Request, res: Response, next: NextFunction) => {
        const payload: TrelloBoardType = req.body as TrelloBoardType

        try {
          if (
            payload &&
            payload.action &&
            payload.action.type === 'updateCard' &&
            payload.action.data.listAfter.name === 'Done'
          ) {
            const id = payload.action.data.card.id
            const zeebeController = new ZeebeController()
            await zeebeController.publishMessage(id, 'Card done')
          }
          res.send()
        } catch (error) {
          throw new Error(ErrorType.Internal)
        }
      },
    )

    this.router.get(
      '/',
      async (req: Request, res: Response, next: NextFunction) => {
        res.send()
      },
    )
  }
}

Two special features to which we would like to respond:

We check whether a card has been changed:

payload.action.type === 'updateCard' &&

And we check if the cards are on the Done list after the change:

payload.action.data.listAfter.name === "Done";

The Id of the card that has changed is shown above:

const id = payload.action.data.card.id;

We use this Id as CorrelationKey to the Message Event in the process, so that the correct instance reacts accordingly.

Finally, the only thing missing is to create the webhook for Trello, and we can set this up using the Trello API. For this we send a POST request to the API with the following query parameters:

  • key: API Key
  • token: API Token
  • idModel: The id of the Trello board for which we want to get changes.
  • description: A description of the webhook.
  • callbackUrl: This is our HTTP Entpoint

Finally the POST request looks like this:

POST https://api.trello.com/1/webhooks?key=xxx&token=xxx&idModel=xxx&description=restzeebe&callbackURL=xxx

Let’s model the process

It’s time to put all the pieces together! Model a process with a service task to create a new Trello card and a Message Event waiting for a Trello card to be completed. Or, if you’re not feeling as confident about modeling you can download the process.

You can see the whole thing in action here:

In the video two browser windows are arranged one below the other. In the upper window there is a tab with Restzeebe and Operate, in the lower window you can see the Trello Board that is used. The following happens:

  1. Restzeebe: Starting a new process instance with the BPMN Process Id trello.
  2. Trello Board: A new Trello Card is created with the title Nice!. So the worker has received a new task and created a new Trello Card via the Trello API accordingly.
  3. Operate: A running process instance is visible, which waits in the Message Event.
  4. Trello Board: We complete the Trello Card by moving it to the Done list.
  5. Operate: The process instance is no longer in the Message Event, but is completed. The Trello Webhook signaled the change and our backend sent a message to the Workflow Engine.

Now comes the wow moment (hopefully!)

Of course, the process is very simple, but it should only be the proof of concept. Since the Worker was implemented generically, we can configure lists freely. From the upper simple process we can model a process that sets up todos when a new employee signs his contract:

The worker shown above is only a very first iteration. It can, of course, become even more generic, so ideally someone who has nothing to do with the technical implementation can design and modify the process.

And of course I don’t have to mention that Trello is just an example. Trello can be replaced by any other task management tool that offers an API:

  • Github Issues
  • Jira
  • Todoist
  • Plus so many others

I hope it helped to show how you can re-use the use case in your context! I’m a big fan of automation so you have plenty of time for other things to put on your to-do list 😉


Let me know if the article was helpful! And if you like the content follow me on TwitterLinkedIn or GitHub 🙂

This article was originally published on Dev.to — you can read the original here.