Slack Creates an Automated WFH Employee Health Reporting System
Enhancing work efficiency with Slack Workflow combined with Google Sheets and App Script
ā¹ļøā¹ļøā¹ļø The following content is translated by OpenAI.
Click here to view the original Chinese version. | é»ę¤ę„ēę¬ęäøęē
Slack Creates an Automated WFH Employee Health Reporting System
Enhancing work efficiency with Slack Workflow combined with Google Sheets and App Script
Photo by Stephen Phillips ā Hostreviews.co.uk
Introduction
In response to the shift to remote work, the company is concerned about the health of all members, requiring daily reports on their health status, which are centrally recorded and managed by People Operations.
Our Flow Before Optimization
- [Automation] A Slack Channel sends a daily reminder message about the health form at 10 AM (the only automated part before optimization).
- Employees click the link to open the Google Form and fill out health questions.
- Data is stored back in Google Sheets for response records.
- [Manual] People Operations compares the list daily near the end of the workday to identify employees who forgot to fill out the form.
- [Manual] A reminder message is sent in the Slack Channel, tagging each employee who forgot to fill it out.
The above is our companyās health reporting tracking process. Each company will have different processes based on its size and operational methods. This article serves only as an optimization example to learn about Slack Workflow usage and basic App Script writing; actual implementation should be done on a case-by-case basis.
Issues
- Employees need to leave the Slack context to open the Google Form in a browser, which is especially inconvenient on mobile devices.
- Google Form can only automatically include email information, and cannot automatically add the respondentās name or department information.
- Daily manual comparisons and tagging consume a lot of manpower and time.
Solution
Having done quite a few automation projects, I found this process suitable for automation due to its fixed data source (employee list), simple conditions, and routine actions. Initially, I didnāt implement it because I couldnāt find a good way to fill it out (actually, I couldnāt find an interesting point to study). I left it aside until I saw this post by Hai Zongli, which made me realize that Slack Workflow can do more than just send scheduled messages; it also has form capabilities:
Image from: Hai Zongli
Now I was eager to get started!
If I could combine Slack Workflow Forms with automated messaging, wouldnāt that solve all the pain points mentioned above? The principle seemed feasible! So I began to implement it.
Flow After Optimization
First, hereās the optimized flow and results.
- [Automation] A Slack Channel sends a daily reminder message about the health form at 10 AM.
- Employees fill out health questions via Google Form or Slack Workflow Form.
- Data is stored back in Google Sheets for response records.
- People Operations clicks the āGenerate Unfilled Listā button daily near the end of the workday.
- [Automation] App Script compares the employee list with the filled-out list to identify those who havenāt filled it out.
- [Automation] Clicking āGenerate & Send Messageā automatically sends reminders to those who havenāt filled it out and tags them.
- Done!
Results
(Personal estimate)
- Each employee can reduce their filling time by about 30 seconds daily.
- People Operations can save about 20 to 30 minutes daily on this task.
Operational Principles
Using App Script to manage the Sheet:
- Store all externally input data in the Responses Sheet.
- Write an App Script function to distribute the Responses data into sheets by fill date; if a date sheet does not exist, create a new one, naming it directly by date for easy identification.
- Compare the current dateās sheet with the employee list to generate the unfilled list sheetās data.
- Read the unfilled list sheet to compose messages and send them to the designated Slack Channel.
- Integrating the Slack APP API allows automatic reading of the specified channel to import the employee list.
- Message content uses the Slack UID Tag
<@UID>
to tag unfilled members.
Identity Verification
The identity verification information connecting Google Form and Slack is the email, so please ensure that all company colleagues use their company email to fill out the Google Form, and that their Slack personal information also includes their company email.
Getting Started
After discussing the issues, optimization methods, and results, we now move on to the implementation phase; letās complete this automation case step by step together.
The article is a bit lengthy; feel free to skip sections you already understand, or directly create a copy from the completed result to learn while modifying.
Completed result form: https://forms.gle/aqGDCELpAiMFFoyDA
Completed result Google Sheet:
Create a Health Reporting Google Form and Link Responses to Google Sheets
Steps are omitted; if you have questions, please Google it. Here, I assume you have already created and linked the health reporting form.
Remember to check āCollect emailsā in the form:
This collects the email of the respondent for later comparison.
How to link responses to Google Sheets?
Switch to āResponsesā at the top of the form and click the āGoogle Sheets Icon.ā
Change the linked Sheet name:
It is recommended to change the linked Sheet name from Form Responses 1 to Responses for easier use.
Create a Slack Workflow Form Entry
After establishing the traditional Google Form entry, letās add a Slack filling method.
In any Slack conversation window, find the āblue lightning ā”ļøā below the input box and click it.
In the menu below āSearch shortcuts,ā type āworkflowā and select āOpen Workflow Builder.ā
This will list the workflows you have created or participated in. Click the āCreateā button in the upper right corner to create a new workflow.
In the first step, enter the workflow name (for display in the Workflow Builder interface).
Select āShortcutā as the workflow trigger.
Currently, there are five types of Slack workflow trigger points:
- Shortcut: Manually triggered by the āblue lightning ā”ļøā option, which will appear in the workflow menu, allowing you to start the workflow by clicking it.
- New channel member: When a new member joins the target channelā¦ (e.g., welcome message).
- Emoji reactions: When someone reacts to a message in the target channel with a specified emojiā¦ (perhaps to mark important messages as read).
- Scheduled date & time: Scheduled to trigger at a specified timeā¦ (e.g., to send a reminder message).
- Webhook: External webhook triggers, an advanced feature that can connect with third-party or self-hosted APIs to integrate internal workflows.
Here, we choose āShortcutā to create a manual trigger option.
Select which channel this workflow shortcut will be added under and enter the ādisplay name.ā
*A workflow shortcut can only be added to one channel.
The shortcut is created! Now, letās start adding steps to the workflow by clicking āAdd Step.ā
Select the āSend a formā step.
Title: Enter the form title.
Add a question: Enter the title of the first question (you can label the question number in the title, e.g., 1., 2., 3ā¦).
Choose a question type:
- Short answer: Single-line input box.
- Long answer: Multi-line input box.
- Select from a list: Single-choice list.
- Select a person: Choose a member from the same workspace.
- Select a channel or DM: Choose a member from the same workspace or a group DM or channel.
For example, with āSelect from a listā:
- Add list item: You can add an option.
- Default selection: Choose a default option.
- Make this required: Set this question as mandatory.
- Add Question: You can add more questions.
- The right-side āāā and āā¬ā can adjust the order, and āāā can expand for editing.
- You can choose whether to send the form responses back to the channel or to someone.
You can also choose to send responses toā¦:
- Person who clickedā¦: The person who clicked this form (the same as the respondent).
- Channel where workflow started: The channel to which this workflow was added.
After completing the form, click āSaveā to save the step.
*Here, we uncheck the option to send form responses back because we want to customize the message content in later steps.
Connect Slack Workflow Form with Google Sheets
If you havenāt added the Google Sheets app to Slack yet, you can click here to install the app.
Continuing from the previous step, click āAdd Stepā to add a new step, and select āAdd a spreadsheet rowā from Google Sheets for Workflow Builder.
- First, complete the Google account authorization by clicking āConnect account.ā
- Select a spreadsheet: Choose the target Google Sheet for responses, which should be the Google Sheet linked to the Google Form you created earlier.
- Sheet: Same as above.
- Column name: The first column where you want to fill in values; here, select question one.
Click the lower right āInsert Variableā to select āResponse to question oneā¦ā After inserting, you can use the left āAdd Columnā to add other fields, and so on to complete questions two, three, etc.
For the respondentās email, you can choose āPerson who submitted form.ā
Then click to insert the variable and select āEmailā to automatically include the respondentās email.
- Mention (default): Tag that user, raw data is
<@User ID>
. - Name: User name.
- Email: User email.
The Timestamp field is a bit tricky; I will provide additional setup methods later. For now, click āSaveā to save and then click āPublishā in the upper right corner to publish the shortcut.
After seeing the successful publication message, you can return to the Slack Channel to test it out.
At this point, clicking the lightning bolt will show the workflow form you just created, which you can click to fill out and test.
Left: Computer / Right: Mobile Version
We can fill in the information and click āSubmitā to test if it works correctly.
Success! However, we can see that the Timestamp field is empty; letās solve this problem next.
Obtaining the Fill Time from Slack Workflow Form
Currently, there is no global variable for the current timestamp available in Slack workflows, at least not yet. I only found a wish post on reddit.
Initially, I whimsically tried entering =NOW()
in the Column Value, but this would make all records show the current time, which is completely incorrect.
Thanks to the clever method provided by a user in that reddit post, you can create a clean Timestamp Sheet with a row of data and a column set to =NOW()
, forcing the column to update to the latest time when you use Update, allowing you to select and get the current timestamp.
As shown in the image, click here to view the example.
- Row: Used similarly to an ID, simply set it to ā1ā; this will be needed for Select & Update later to indicate the data row.
- Timestamp: Set the value to
=NOW()
to always display the current time. - Value: Used to trigger the Timestamp column to update the time; the content can be anything; here, I put the respondentās email to trigger the update.
You can right-click on the Sheet and select āHide Sheetā to hide this sheet since it is not meant for external use.
Now, return to the Slack Workflow Builder to edit the workflow form you just created.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
Click "Add Step" to add a new step:

Scroll down and select "Update a spreadsheet row"

In "Select a spreadsheet," choose the previously created Sheet, and in "Sheet," select the newly created "Timestamp" Sheet.
In "Choose a column to search," select "Row," and for "Define a cell value to find," enter "1."

In "Update these columns," select "Column name" as "Value," then click "Insert variable" -> "Person who submitted" -> "Select Email."
Click "Save" to finish! The timestamp update in the Sheet has been successfully triggered, and now it's time to read it out.

Return to the editing page and click "Add Step" again to add a new step. This time select "Select a spreadsheet row" to read the Timestamp.

The Search section is the same as "Update a spreadsheet row," then click "Save."

After saving, return to the step list page. You can drag and drop to change the order of the steps.
Change the order to "Update a spreadsheet row" -> "Select a spreadsheet" -> "Add a spreadsheet row."
This means: Update triggers the timestamp update -> Read the Timestamp -> Use it when adding a new Row.
In "Add a spreadsheet row," click "Edit":

Scroll to the bottom and click "Add Column" in the lower left corner, then click "Insert a variable" in the lower right corner, find the "Timestamp" variable in the "Select a spreadsheet" section, and inject it.

After clicking "Save" to save the step, return to the list page and click "Publish Change" in the upper right corner to publish the changes.
Now let's test the workflow shortcut again to see if the timestamp is written correctly.

Success!
#### Adding a Response to Slack Workflow Form
Just like with Google Forms, you can also add a response to the Slack workflow form.
On the editing step page, we can add another step by clicking "Add Step."

This time select "Send a message"

In "Send this message to," select "Person who submitted form"

For the message content, sequentially enter the question title, then click "Insert a variable" to select "Response to Question XXX," and you can also insert "Timestamp" at the end. Click "Save" to save the step, then click "Publish Changes"!
> _You can also use "Send a message" to send the response results to a specific Channel or DM._

Success!
The setup for the Slack workflow form is mostly complete; feel free to mix and match other functionalities.
### Google Sheet with App Script!
Next, we need to write an App Script to handle the submitted data.

First, in the Google Sheet toolbar, select "Tools" -> "Script editor"

You can click the upper left corner to give the project a name.
Now we can start writing the App Script! App Script is designed based on JavaScript, so you can directly use JavaScript code along with Google Sheet's library.
#### Distributing Responses Data to Each Date's Sheet According to Submission Date

```javascript
function formatData() {
var bufferSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Responses') // Name of the sheet storing responses
var rows = bufferSheet.getDataRange().getValues();
var fields = [];
var startDeleteIndex = -1;
var deleteLength = 0;
for(index in rows) {
if (index == 0) {
fields = rows[index];
continue;
}
var sheetName = rows[index][0].toLocaleDateString("en-US"); // Convert Date to String using US date format MM/DD/YYYY
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName); // Get MM/DD/YYYY Sheet
if (sheet == null) { // If not, create a new one
sheet = SpreadsheetApp.getActiveSpreadsheet().insertSheet(sheetName, bufferSheet.getIndex());
sheet.appendRow(fields);
}
sheet.appendRow(rows[index]); // Add data to the date Sheet
if (startDeleteIndex == -1) {
startDeleteIndex = +index + 1;
}
deleteLength += 1;
}
if (deleteLength > 0) {
bufferSheet.deleteRows(startDeleteIndex, deleteLength); // After moving to the specified Sheet, remove data from Responses
}
}
Paste the above code into the Code section and press ācontrolā + āsā to save.
Next, we need to add a trigger button in the Sheet ( it can only be triggered manually, it cannot be automatically triggered when data is written )
- First, create a new Sheet named āUnfilled Listā
- In the upper toolbar, select āInsertā -> āDrawingā
Using this interface, draw a button.
After clicking āSave and Close,ā you can adjust and move the button; click the upper right corner āā¦ā and select āAssign scriptā
Enter the function name āformatData.ā
You can click the added button to test its functionality
If āAuthorization Requiredā appears, click āContinueā to complete the verification
During the authentication process, āGoogle hasnāt verified this appā will appear, which is normal since the App Script we wrote has not been verified by Google. However, this is fine as it is for personal use.
You can click āAdvancedā in the lower left corner -> āGo to Health Report (Responses) (unsafe)ā
Click āAllowā
While the App Script is running, it will display āRunning Script,ā please do not click again to avoid duplicate execution.
You can only execute it again after it shows successful execution.
Success! The submitted data has been grouped by date.
Comparing the Current Dateās Sheet with the Employee List to Generate Unfilled List Sheet Data
Letās add another piece of code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// Compare with Employee List Sheet & Today's Submission Sheet to generate Unfilled List
function generateUnfilledList() {
var listSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Employee List') // Name of Employee List Sheet
var unfilledListSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Unfilled List') // Name of Unfilled List Sheet
var today = new Date();
var todayName = today.toLocaleDateString("en-US");
var todayListSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(todayName) // Get today's MM/DD/YYYY Sheet
if (todayListSheet == null) {
SpreadsheetApp.getUi().alert('Cannot find the Sheet for '+todayName+' or please execute "Organize Submission Data" first');
return;
}
var todayEmails = todayListSheet.getDataRange().getValues().map( x => x[1] ) // Get the list of Email Address data from today's Sheet (1 = Column B)
// index starts from 0, so 1 = Column B
// output: Email Address,zhgchgli@gmail.com,alan@gamil.com,b@gmail.com...
todayEmails.shift() // Remove the first data, the first one is the column name "Email Address," which is meaningless
// output: zhgchgli@gmail.com,alan@gamil.com,b@gmail.com...
unfilledListSheet.clear() // Clear the Unfilled List... preparing to re-enter data
unfilledListSheet.appendRow([todayName+" Unfilled List"]) // The first row displays the Sheet title
var rows = listSheet.getDataRange().getValues(); // Read Employee List Sheet
for(index in rows) {
if (index == 0) { // The first row is the header row, store it so that subsequent data generation can also include the header
unfilledListSheet.appendRow(rows[index]);
continue;
}
if (todayEmails.includes(rows[index][3])) { // If this employee's Email is in today's Sheet Email Address, it means they have filled it out, continue to skip...
continue;
}
unfilledListSheet.appendRow(rows[index]); // Write a row of data to the Unfilled List Sheet
}
}
After saving, follow the previous method to add the code, then add another button and assign the script ā āgenerateUnfilledList.ā
Once completed, you can click to test:
The Unfilled List has been successfully generated! If no content appears, please ensure:
- The Employee List has been filled out, or you can enter test data first
- You must complete the āOrganize Submission Dataā action first
Reading the Unfilled List Sheet to Compose a Message and Send it to a Designated Slack Channel
First, we need to add the Incoming WebHooks App to the Slack Channel, which we will use to send messages.
- In Slack, click āAppsā in the lower left corner -> āAdd appsā
- Search for āincomingā in the search box on the right
- Click āIncoming WebHooksā -> āAddā
Select the Channel where you want to send the unfilled messages.
Note the āWebhook URLā at the top
Scroll down to set the display name and profile picture for the Bot when sending messages; remember to click āSave Settingsā after making changes.
Return to our Google Sheet Script
Add another piece of code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
function postSlack() {
var ui = SpreadsheetApp.getUi();
var result = ui.alert(
'Are you sure you want to send the message?',
'Send unfilled reminder message to Slack Channel',
ui.ButtonSet.YES_NO);
// To avoid accidental clicks, first ask for confirmation
if (result == ui.Button.YES) {
var unfilledListSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Unfilled List') // Name of Unfilled List Sheet
var rows = unfilledListSheet.getDataRange().getValues();
var persons = [];
for(index in rows) {
if (index == 0 || index == 1) { // Skip the header and column title rows
continue;
}
var person = (rows[index][4] == "") ? (rows[index][2]) : ("<@"+rows[index][4]+">"); // Mark the target, if there is a slack uid, use it first; if not, just display the nickname; 2 = Column B / 4 = Column E
if (person == "") { // If neither is present, consider it as abnormal data, ignore it
continue;
}
persons.push("ā¢ "+person+'\n') // Store the target in the array
}
if (persons.length <= 0) { // If there are no targets to be marked, it means everyone has filled it out, cancel the message sending
return;
}
var preText = "*[Health Report Announcement:loudspeaker:]*\nThe company cares about everyone's health, please remind the following teammates to fill out their health status report daily, thank you:wink:\n\nToday's unfilled health status report list\n\n" // Message opening content...
var postText = "\n\nFilling out the health status report allows the company to understand the physical condition of teammates, please ensure everyone fills it out daily >< Thank you all:woman-bowing::skin-tone-2:" // Message closing content...
var payload = {
"text": preText+persons.join('')+postText,
"attachments": [{
"fallback": "Here you can place the Google Form fill link",
"actions": [
{
"name": "form_link",
"text": "Go to Health Status Report",
"type": "button",
"style": "primary",
"url": "Here you can place the Google Form fill link"
}
],
"footer": ":rocket:Tip: Click the input box below the " :zap:ļøLightning" -> "Shortcut Name" to fill it out directly."
}
]
};
var res = UrlFetchApp.fetch('Enter your Slack incoming app Webhook URL here',{
method : 'post',
contentType : 'application/json',
payload : JSON.stringify(payload)
})
}
}
After saving, follow the previous method to add the code, then add another button and assign the script ā āpostSlack.ā
Once completed, you can click to test:
Success!!! (The display of @U123456 not successfully tagging the person is because the ID was randomly entered)
At this point, the main functionalities are complete!
Note
Please note that the official recommendation is to use the new Slack APP APIās chat.postMessage to send messages. The convenient method of Incoming Webhook will be deprecated in the future. This was done for convenience and will need to be adjusted to the new method along with the next chapter āImport Employee List.ā
Importing Employee List
Here we need to create a Slack APP.
Click āCreate New Appā in the upper right corner
- Choose ā From scratch ā
- Enter ā App Name ā and the Workspace you want to add it to
- After successful creation, select āOAuth & Permissionsā from the left menu for settings
- Scroll down to the Scopes section
Add the following items by clicking āAdd an OAuth Scopeā:
- channels:read
- users:read
- users:read.email
- If you want to send messages using the APP, you can add chat.postMessage
- Go back to the top and click āInstall to workspaceā or āReinstall to workspaceā
*If Scopes are added, you need to reinstall it again.
After installation, copy the
Bot User OAuth Token
Use the web version of Slack to open the Channel you want to import the list into ```
1
Get the URL from the browser:
https://app.slack.com/client/TXXXX/CXXXX
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
Where `CXXXX` is the Channel ID for this channel, make a note of this message.
10\.
Return to our Google Sheet Script
Add another piece of code:
```javascript
function loadEmployeeList() {
var formData = {
'token': 'Bot User OAuth Token',
'channel': 'Channel ID',
'limit': 500
};
var options = {
'method' : 'post',
'payload' : formData
};
var response = UrlFetchApp.fetch('https://slack.com/api/conversations.members', options);
var data = JSON.parse(response.getContentText());
for (index in data["members"]) {
var uid = data["members"][index];
var formData = {
'token': 'Bot User OAuth Token',
'user': uid
};
var options = {
'method' : 'post',
'payload' : formData
};
var response = UrlFetchApp.fetch('https://slack.com/api/users.info', options);
var user = JSON.parse(response.getContentText());
var email = user["user"]["profile"]["email"];
var real_name = user["user"]["profile"]["real_name_normalized"];
var title = user["user"]["profile"]["title"];
var row = [title, real_name, real_name, email, uid]; // Fill in according to Column
var listSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Employee List'); // Name of the Employee List Sheet
listSheet.appendRow(row);
}
}
But this time we donāt need to add a button, as the import only needs to be done the first time; just save the file and run it directly.
First, press ācontrolā + āsā to save, change the dropdown menu above to select āloadEmployeeListā, and click āRunā to start importing the list into the Employee List Sheet.
Manually Add New Employee Data
If new employees join later, you can directly add a row in the Employee List Sheet and fill in the information. The Slack UID can be found directly on Slack:
Click on the person whose UID you want to view, then click āView full profileā
Click āMoreā and select āCopy member IDā to get the UID. UXXXXX
DONE!
All the above steps have been completed, and you can start automating the tracking of employee health status.
The completed file is available below; you can create a copy from the following Google Sheet to modify and use:
Additional Notes
- If you want to use Scheduled date & time to send form messages regularly, note that in this case, the form can only be filled out once, so it is not suitable for use hereā¦ (at least in the current version). Therefore, scheduled reminder messages still can only use plain text + Google Form links.
- Currently, there is no way to link to a Shortcut to open the Form
- Google Sheet App Script to prevent duplicate execution:
If you want to prevent accidentally pressing again during execution, which would cause duplicate execution, you can add the following at the beginning of the function:
1
2
3
4
5
if (PropertiesService.getScriptProperties().getProperty('FUNCTIONNAME') == 'true') {
SpreadsheetApp.getUi().alert('Busy... please try again later');
return;
}
PropertiesService.getScriptProperties().setProperty('FUNCTIONNAME', 'true');
At the end of the function, add:
1
PropertiesService.getScriptProperties().setProperty('FUNCTIONNAME', 'true');
Replace FUNCTIONNAME with the target function name.
Use a global variable to control execution.
Applications Related to iOS Development
This can be used to integrate CI/CD, wrapping the originally clunky command operations in a GUI, for example, in conjunction with the Slack Bitrise APP, using Slack Workflow forms to trigger Build commands:
After submission, it will send commands to a private channel with the Bitrise APP, e.g.:
1
bitrise workflow:app_store|branch:develop|ENV[version]:4.32.0
This will trigger Bitrise to execute the CI/CD Flow.
Further Reading
- Using Python + Google Cloud Platform + Line Bot to Automate Routine Tasks
- Using Google Apps Script to Forward Gmail Messages to Slack
- Crashlytics + Big Query to Create a More Immediate and Convenient Crash Tracking Tool
- Crashlytics + Google Analytics to Automatically Query App Crash-Free Users Rate
If you have any questions or suggestions, feel free to contact me.
If you have any automation-related optimization needs, feel free to hire me, thank you.
```
This article was first published on Medium ā”ļø Click Here
Automatically converted and synchronized using ZMediumToMarkdown and Medium-to-jekyll-starter.