みなさん、勤怠打刻してますか?
先日、このようなツイートをしたところ、思わぬ反響がありました。
https://twitter.com/paranishian/status/1575646345876340736
そこで、この仕組みの全体像や工夫した点などをまとめることにしました。
SlackやGASを使ったOps自動化に興味がある人に読んでもらえたら嬉しいです。
そもそもSlackにはfreeeが公式で提供している人事労務用のSlack appがあり、スラッシュコマンドを使って勤怠打刻できます。便利ですね。
https://slack.com/apps/AD98ZD3EV-freee
ただ、このアプリ、コマンドを打つのがとにかくめんどくさかったりします。
あるとき、同僚が「もっと気軽に勤怠打刻できたらええのになぁ」と言っているのを耳にしました。
そこで、スタンプで勤怠打刻できる仕組みを作り、運用を始めました。
それから数ヶ月後、会社にフレックスタイム制が導入されました。
「ワークライフバランス!!さいこう!」となりつつも「今月はあと何時間働けば良いんだっけ?」をいちいち計算しないといけないつらみが出てきました。
そこで、スタンプ勤怠打刻時に勤怠サマリレポートも通知するように改善しました。
スタンプを押すと、出勤・退勤打刻ができ、勤怠サマリも通知してくれます。
出勤時:Slack workflowが定時に挨拶してくるので、:shukkin: スタンプを押すと
人事労務freeeへの出勤打刻と勤怠サマリレポートを通知してくれます。
退勤時:Slack workflowが定時に挨拶してくるので、 :taikin: スタンプを押すと、
人事労務freeeへの退勤打刻の旨を通知してくれます。
ざっくりとした処理の流れは以下の通りです。
Slackbot、GAS、freee APIで構築しています。
Slackbotでreaction_addedイベントをsubscribeし、リクエストはGASが受け取ります。
そしてGAS側でユーザーの突合、freeeへの勤怠打刻、サマリ取得、Slack通知を実装しています。
一部になりますが、GASのコードも載せておきます。(記事用に少し改変しています)
constSHUKKIN='shukkin'constTAIKIN='taikin'constCOMPANY_ID=1234567functiondoPost(e){const params=JSON.parse(e.postData.getDataAsString());// SlackのEvent SubscriptionのRequest Verification用if(params.type==='url_verification'){returnContentService.createTextOutput(params.challenge);}const event= params.eventconst user= event.userconst reaction= event.reactionconst eventId= params.event_id// 特定のチャネル以外のスタンプは無視if(event.item.channel!=='C12345678')return;// shukkin, taikin 以外のスタンプは無視if(reaction!==SHUKKIN&& reaction!==TAIKIN)return; // Slackからの再送リクエストを無視するためにキャッシュを使う(10分)const cache=CacheService.getScriptCache()const cached= cache.get(eventId)if(cached){Logger.log(`すでに処理したイベントなのでスルーします。(eventId:${eventId})`)return} cache.put(eventId,true,60*10)if(reaction===SHUKKIN){const todayShukkinSheet=SpreadsheetApp.getActive().getSheetByName('today_shukkin_log')const users= todayShukkinSheet.getRange("B2:B").getValues().flat();// すでに出勤打刻済みなら無視if(users.includes(user))return;}elseif(reaction===TAIKIN){// 退勤は上書きしてよい}// Slack -> Freeeのユーザー突合const freeeUser=findFreeeUser(user)if(!freeeUser)return;// freeeに打刻const type= reaction===SHUKKIN?'clock_in':'clock_out'timeClocksToFreee(freeeUser.employeeId, type)const lines=[] lines.push(`<@${user}>`)if(reaction===SHUKKIN){ lines.push(`おはようございます!今日も頑張っていきましょう:muscle:`) lines.push(`✅ freee打刻済`)// 勤怠サマリを取得してテキスト整形する lines.push(getWorkTimeText(freeeUser.employeeId))}else{ lines.push(`お疲れ様でした!また明日:wave:`) lines.push(`✅ freee打刻済`)}const text= lines.join("\n")// Slackに通知notifyToKintaiChannel(text)// logに記録const logSheet=SpreadsheetApp.getActive().getSheetByName('kintai_log')const datetime=Utilities.formatDate(newDate(event.event_ts*1000),"Asia/Tokyo","yyyy-MM-dd HH:mm:ss") logSheet.appendRow([datetime, user, reaction])}functionfindSlackEmail(id){const sheet=SpreadsheetApp.getActive().getSheetByName('slack_users')// [[id, name, real_name, email], [id, name, real_name, email], ...]const user= sheet.getRange("A2:D").getValues().find((u)=> u[0]=== id);return!!user? user[3]:undefined}functionfindFreeeUser(slackUserId){const email=findSlackEmail(slackUserId)if(!email)returnundefinedconst sheet=SpreadsheetApp.getActive().getSheetByName('employee_freee_users')// [[メールアドレス, id], ...]const row= sheet.getRange("A2:B").getValues().find((u)=> u[0]=== email);return!!row?{email: row[0],employeeId: row[1]}:undefined}GASでfreeeのアクセストークンを取得する方法は、freeeさんが丁寧な記事を書いてくれています。ぜひ参考にしてください。
【freee API】GASを用いてGoogleスプレッドシートと連携する
また、今回使用しているAPIは以下の2つです。
POSTGEThttps://developer.freee.co.jp/reference/hr/reference
freeeアプリには以下の権限を付与しています。

注意点として、freeeアプリの認証は人事労務freeeの管理者が行ってください。
アプリを認可しただけでは、APIで権限が足りないと弾かれてしまいます。
このあたりめっちゃハマりました。
https://developer.freee.co.jp/reference/hr#ログインユーザーの情報を取得する
なお、Slackユーザーとfreeeユーザーの突合には、メールアドレスを利用しています。
こういったGASの自動化においては、シート設計(=DB設計)が肝だと考えています。
今回は以下の構成にしました。
QUERYしたシート
また、Slackユーザーのメールアドレスは、日次でユーザー一覧を取得するAPIを叩いてslack_usersシートに保存しています。
constSLACK_BOT_TOKEN='xoxb-12345'functionfetchSlackUsers(){// https://api.slack.com/methods/users.listconst apiResponse=callWebApi(SLACK_BOT_TOKEN,"users.list",{});const json=JSON.parse(apiResponse);const users=[] json.members.filter((m)=>!m.deleted&&!m.is_bot).forEach((m)=>{ users.push([m.id, m.name, m.real_name, m.profile.email])});const sheet=SpreadsheetApp.getActive().getSheetByName('slack_users') sheet.getRange("A2:D").clear() sheet.getRange(2,1, users.length,4).setValues(users)}functioncallWebApi(token, apiMethod, payload){const response=UrlFetchApp.fetch(`https://www.slack.com/api/${apiMethod}`,{method:"post",contentType:"application/json; charset=UTF-8",headers:{"Authorization":`Bearer${token}`},payload:JSON.stringify(payload),});return response;}Slack Events APIには、さまざまなリトライ条件があります。
https://dev.classmethod.jp/articles/slack-resend-matome/
GASでは頻繁にこれらの条件に引っかかってしまうので、キャッシュを利用して再送時の処理をスキップしています。
労働・・・したくないですよね。
人事労務freeeでは、「労働日数」や「所定内労働」など、「労働」が多用されていますが、Slackに通知する際は「勤怠」というやわらかめのワードに置き換えています。(とってもだいじ)
残業時間の計算なども要望として上がっているので、追加実装して試験運用中です。
働きすぎをそっと防止する仕組みを作っていきたいです。
ってなかんじで、Slackbot、GASを使った勤怠Ops自動化について書いてみました。
今回はfreeeでの実装でしたが、ほかの勤怠管理システムでもAPIがあれば実現可能だと思います。
めんどくさい作業はどんどん自動化して気持ちよく働きたいですね。
ちなみに、この仕組みはmicroCMS社に導入して運用しています。
エンジニア企業らしく、社内業務の自動化に積極的に取り組んでいるとても素敵な会社です。
https://microcms.co.jp/recruit
もしこの記事を読んで参考になりましたら、いいねやTwitterのフォローをしていただけるととても嬉しいです。
その他複数の企業で、Ops自動化・効率化をお手伝いしています。
興味がありましたらDM等でご相談ください。
それでは!👋
バッジを受け取った著者にはZennから現金やAmazonギフトカードが還元されます。
