View the workflow code on Github here.. View all of the files, fork the project and deploy to heroku in a few clicks.
The workflow Where To Lunch is a friendly slack bot to recommend restaurants and organize a daily vote for nearby lunch options.
The bot uses two Zenaton workflows:
configureWorkflow.js
to set the current location and fetch placesproposePlaceAndCollectVotes.js
to manage the vote processThe slackbot is configured by using slack commands and when the /wheretolunch configure [your address]
is entered, the configureWorkflow.js is launched.
It extract restaurants around the address with a maximum radius of 1500 meters and a price not higher than €€€ using the Google Places API. The results are saved in Airtable.
// The Zenaton engine orchestrates all tasks of this workflow and related logic via the Zenaton agent.
// Every step is executed at the right moment on your servers and monitored on Zenaton dashboard.
"use strict";
const { workflow } = require("zenaton");
module.exports = workflow("configureWorkflow", {
*handle(
teamId,
channelId,
address,
radius = 1500,
maxPrice = 3,
numberOfPlaces = 5,
reset = true
) {
// Init
this.teamId = teamId;
this.channelId = channelId;
this.address = address;
this.radius = radius;
this.maxprice = maxPrice;
this.numberOfPlaces = numberOfPlaces;
this.places = [];
// List team places
this.places = yield this.run.task(
"listTeamPlaces",
process.env.AIRTABLE_PLACES_TABLE,
this.teamId,
this.channelId
);
// Delete places if reset = true
if (reset && this.places.length > 0) {
const recordIds = this.places.map(place => place.airtable_id);
yield this.run.task(
"deleteRecords",
process.env.AIRTABLE_PLACES_TABLE,
recordIds
);
this.places = [];
}
const placesToFetch = this.numberOfPlaces - this.places.length;
if (placesToFetch > 0) {
// Search missing places from Google Place API
const newPlaces = yield this.run.task(
"getPlacesFromGoogleApi",
this.address,
this.radius,
this.maxprice,
this.numberOfPlaces
);
// Save new places to Airtable
yield this.run.task(
"createAirtableRecords",
process.env.AIRTABLE_PLACES_TABLE,
this.teamId,
this.channelId,
newPlaces
);
this.places.push(newPlaces);
}
},
id() {
return this.teamId;
}
});
This workflow is triggered when the users would have to vote. The slack command is /wheretolunch vote_now
. It gets the result from Airtable and display the list of restaurant suggested. It counts users votes and get the one that have the highest number of votes to send a message in the channel announcing the winner.
// The Zenaton engine orchestrates all tasks of this workflow and related logic via the Zenaton agent.
// Every step is executed at the right moment on your servers and monitored on Zenaton dashboard.
"use strict";
const { workflow, duration } = require("zenaton");
const WEEK_END = [0, 6];
const NUMBER_EMOJIS = [":one:", ":two:", ":three:", ":four:", ":five:"];
module.exports = workflow("proposePlaceAndCollectVotes", {
*handle(teamId, channelId) {
const { dayOfWeek } = this.run.task("getCurrentDate");
this.teamId = teamId;
this.channelId = channelId;
this.places = [];
this.users = [];
this.winner = null;
this.voteDuration = 15;
this.enabledDuringWeekend = false;
if (this.enabledDuringWeekend || !WEEK_END.indexOf(dayOfWeek) > -1) {
// List team places
const places = yield this.run
.task(
"listTeamPlaces",
process.env.AIRTABLE_PLACES_TABLE,
this.teamId,
this.channelId
);
// Set vote to 0 for each places
this.places = places.map(place => {
return { ...place, vote: 0 };
});
// List all channel members
const membersRes = yield this.run.task("slack", "conversations.members", {
channel: this.channelId
});
this.users = membersRes.members;
// Post the poll on the Slack channel
this.pollMessage = yield this.run.task("slack", "chat.postMessage", {
channel: this.channelId,
text: buildPollText(this.places)
});
// Send an ephemeral vote message to all channel members
const sendTasks = this.users.map(userId => [
"slack",
"chat.postEphemeral",
{
channel: this.channelId,
user: userId,
attachments: buildVoteAttachments(places.length)
}
]);
yield this.run.task(...sendTasks);
// Awaits the end of the vote
yield this.wait.for(duration.minutes(this.voteDuration));
// Get the winner place
this.winner = getWinner(this.places);
// Post the winner place in the Slack channel
yield this.run.task("slack", "chat.postMessage", {
channel: this.channelId,
text: `Today you will lunch at ${placeLink(this.winner)}!`
});
}
},
id() {
return "votes:" + this.teamId;
},
*onEvent(name, data) {
if (name === "VoteEvent" && this.winner == null) {
// Take the vote into account
const placeNumber = parseInt(data.payload.actions[0].value);
this.places[placeNumber - 1].vote += 1;
// Update the poll message
yield this.run.task("slack", "chat.update", {
channel: this.channelId,
text: buildPollText(this.places),
ts: this.pollMessage.ts
});
}
}
});
function placeLink(place) {
const gMapInfo = JSON.parse(place.payload);
return `//www.google.com/maps/search/?api=1&query=${gMapInfo.geometry.location.lat},${gMapInfo.geometry.location.lng}&query_place_id=${place.place_id}|${place.name}>`;
}
function getWinner(places) {
return places.sort((place1, place2) => place2.vote - place1.vote)[0];
}
function buildPollText(places) {
return places.reduce((acc, place, index) => {
return (
acc + `${NUMBER_EMOJIS[index]} ${placeLink(place)} \`${place.vote}\`\n\n`
);
}, "");
}
function buildVoteAttachments(optionCount) {
const actions = [...Array(optionCount).keys()].map(optionIndex => {
return {
name: "place",
type: "button",
text: NUMBER_EMOJIS[optionIndex],
value: optionIndex + 1
};
});
return [
{
text: "Choose where you want to lunch.",
callback_id: "vote_button",
actions: actions
}
];
}
Zenaton provides a scheduler. Therefore, it's easy to schedule the vote for the restaurants every day at special time. You have access to your tasks or workflows schedules in your dashboard. You can pause, resume or kill them. More information are in the documentation here.
It's also possible to schedule the vote directly from Slack using the command /wheretolunch schedule_vote 15 11 * * MON-FRI
using cron expressions.
To deploy your own instance, you will need a:
Then, you can follow the steps described on the readme in Github. The code can be quickly deployed on Heroku or you can use another cloud provider (see going to production).
View the workflow code on Github here.. View all of the files, fork the project and deploy to heroku in a few clicks.
Sample Projects
Start building workflows
Sign-up and run a sample project Learn more