Build a Slackbot·续2
import { Hono } from "hono";
import { Bindings } from "../types";
const app = new Hono<{ Bindings: Bindings }>();
export default app;
Much like with the lookup route, you will need to parse the incoming payload inside of request, get the relevant issue data from it (refer to the GitHub API documentation on IssueEvent ↗︎ for the full payload schema), and send a formatted message to Slack to indicate what has changed. The final version will look something like this:

Compare this message format to the format returned when a user uses the /issue slash command. You will see that there is only one actual difference between the two: the addition of an action text on the first line, in the format An issue was $action:. This action, which is sent as part of the IssueEvent from GitHub, will be used as you construct a very familiar looking collection of blocks using Slack’s Block Kit.
Parsing event data
To start filling out the route, parse the request body formatted JSON into an object and construct some helper variables:
import { Hono } from "hono";
import { constructGhIssueSlackMessage } from "../utils/slack";
const app = new Hono();
app.post("/", async (c) => {
const { action, issue, repository } = await c.req.json();
const prefix_text = `An issue was ${action}:`;
const issue_string = `${repository.owner.login}/${repository.name}#${issue.number}`;
});
export default app;
An IssueEvent, the payload sent from GitHub as part of your webhook configuration, includes an action (what happened to the issue: for example, it was opened, closed, locked, etc.), the issue itself, and the repository, among other things.
Use c.req.json() to convert the payload body of the request from JSON into a plain JS object. Use ES6 destructuring to set action, issue and repository as variables you can use in your code. prefix_text is a string indicating what happened to the issue, and issue_string is the familiar string owner/repo#issue_number that you have seen before: while the lookup route directly used the text sent from Slack to fill in issue_string, you will construct it directly based on the data passed in the JSON payload.
Constructing and sending a Slack message
The messages your Slack bot sends back to your Slack channel from the lookup and webhook routes are incredibly similar. Because of this, you can re-use the existing constructGhIssueSlackMessage to continue populating src/handlers/webhook.ts. Import the function from src/utils/slack.ts, and pass the issue data into it:
import { Hono } from "hono";
import { constructGhIssueSlackMessage } from "../utils/slack";
const app = new Hono();
app.post("/", async (c) => {
const { action, issue, repository } = await c.req.json();
const prefix_text = `An issue was ${action}:`;
const issue_string = `${repository.owner.login}/${repository.name}#${issue.number}`;
const blocks = constructGhIssueSlackMessage(issue, issue_string, prefix_text);
});
export default app;
Importantly, the usage of constructGhIssueSlackMessage in this handler adds one additional argument to the function, prefix_text. Update the corresponding function inside of src/utils/slack.ts, adding prefix_text to the collection of text_lines in the message block, if it has been passed in to the function.
Add a utility function, compact, which takes an array, and filters out any null or undefined values from it. This function will be used to remove prefix_text from text_lines if it has not actually been passed in to the function, such as when called from src/handlers/lookup.ts. The full (and final) version of the src/utils/slack.ts looks like this:
import { Issue } from "../types";
const compact = (array: unknown[]) => array.filter((el) => el);
export const constructGhIssueSlackMessage = (
issue: Issue,
issue_string: string,
prefix_text?: string,
) => {
const issue_link = `<${issue.html_url}|${issue_string}>`;
const user_link = `<${issue.user.html_url}|${issue.user.login}>`;
const date = new Date(Date.parse(issue.created_at)).toLocaleDateString();
const text_lines = [
prefix_text,
`*${issue.title} - ${issue_link}*`,
issue.body,
`*${issue.state}* - Created by ${user_link} on ${date}`,
];
return [
{
type: "section",
text: {
type: "mrkdwn",
text: compact(text_lines).join("\n"),
},
accessory: {
type: "image",
image_url: issue.user.avatar_url,
alt_text: issue.user.login,
},
},
];
};
Back in src/handlers/webhook.ts, the blocks that are returned from constructGhIssueSlackMessage become the body in a new fetch request, an HTTP POST request to a Slack webhook URL. Once that request completes, return a response with status code 200, and the body text "OK":
import { Hono } from "hono";
import { constructGhIssueSlackMessage } from "../utils/slack";
import { Bindings } from "../types";
const app = new Hono<{ Bindings: Bindings }>();
app.post("/", async (c) => {
const { action, issue, repository } = await c.req.json();
const prefix_text = `An issue was ${action}:`;
const issue_string = `${repository.owner.login}/${repository.name}#${issue.number}`;
const blocks = constructGhIssueSlackMessage(issue, issue_string, prefix_text);
const fetchResponse = await fetch(c.env.SLACK_WEBHOOK_URL, {
body: JSON.stringify({ blocks }),
method: "POST",
headers: { "Content-Type": "application/json" },
});
return c.text("OK");
});
export default app;
The constant SLACK_WEBHOOK_URL represents the Slack Webhook URL that you created all the way back in the Incoming Webhook section of this tutorial.
Caution
Since this webhook allows developers to post directly to your Slack channel, keep it secret.
To use this constant inside of your codebase, use the wrangler secret command:
Set the SLACK_WEBHOOK_URL secretsh
npx wrangler secret put SLACK_WEBHOOK_URL
Enter a secret value: https://hooks.slack.com/services/abc123
Handling errors
Similarly to the lookup route, the webhook route should include some basic error handling. Unlike lookup, which sends responses directly back into Slack, if something goes wrong with your webhook, it may be useful to actually generate an erroneous response, and return it to GitHub.
To do this, write the custom error handler with app.onError() and return a new response with a status code of 500. The final version of src/routes/webhook.ts looks like this:
import { Hono } from "hono";
import { constructGhIssueSlackMessage } from "../utils/slack";
import { Bindings } from "../types";
const app = new Hono<{ Bindings: Bindings }>();
app.post("/", async (c) => {
const { action, issue, repository } = await c.req.json();
const prefix_text = `An issue was ${action}:`;
const issue_string = `${repository.owner.login}/${repository.name}#${issue.number}`;
const blocks = constructGhIssueSlackMessage(issue, issue_string, prefix_text);
const fetchResponse = await fetch(c.env.SLACK_WEBHOOK_URL, {
body: JSON.stringify({ blocks }),
method: "POST",
headers: { "Content-Type": "application/json" },
});
if (!fetchResponse.ok) throw new Error();
return c.text("OK");
});
app.onError((_e, c) => {
return c.json(
{
message: "Unable to handle webhook",
},
500,
);
});
export default app;
Deploy
By completing the preceding steps, you have finished writing the code for your Slack bot. You can now deploy your application.
Wrangler has built-in support for bundling, uploading, and releasing your Cloudflare Workers application. To do this, run the following command which will build and deploy your code.
npmyarnpnpmbun
npm i -- deploy
yarn add deploy
pnpm add deploy
bun add deploy
Deploying your Workers application should now cause issue updates to start appearing in your Slack channel, as the GitHub webhook can now successfully reach your Workers webhook route:

Related resources
In this tutorial, you built and deployed a Cloudflare Workers application that can respond to GitHub webhook events, and allow GitHub API lookups within Slack. If you would like to review the full source code for this application, you can find the repository on GitHub ↗︎.
If you want to get started building your own projects, review the existing list of Quickstart templates.
Was this helpful?
YesNo
On this page
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/workers/tutorials/build-a-slackbot/#page","headline":"Build a Slackbot","description":"Learn how to build a Slackbot with Hono and TypeScript in Cloudflare Workers","url":"https://developers.cloudflare.com/workers/tutorials/build-a-slackbot/","inLanguage":"en","image":"https://developers.cloudflare.com/workers/tutorials/build-a-slackbot/og.png?v=9160af7a2b8f5080","dateModified":"2026-04-08","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"},"keywords":["Hono","TypeScript"]}
description: This tutorial explains how to connect to a MongoDB database with Cloudflare Workers. The Workers application you create in this tutorial will query a movies database in MongoDB. title: Connect to a MongoDB database with Cloudflare Workers image: https://developers.cloudflare.com/workers/tutorials/connect-to-mongodb-atlas/og.png?v=cdb0c521bc47671d
Documentation Index
Fetch the complete documentation index at: https://developers.cloudflare.com/workers/llms.txt
Use this file to discover all available pages before exploring further.
