# Slack Platform Documentation - Deno (Deno Slack SDK) > Documentation for the Deno (Deno Slack SDK) stack only. Pair with llms-full-platform.txt for platform concepts, auth, and API reference. > Generated from the built site. Each page is also available individually at its URL + `.md`. > Page index: https://docs.slack.dev/llms-sitemap.md Source: https://docs.slack.dev/tools/deno-slack-sdk # Deno Slack SDK Workflow apps require a paid plan Join the [Developer Program](https://api.slack.com/developer-program) and provision a sandbox with access to all Slack features for free. You can create Slack-hosted workflows written in TypeScript using the [Deno Slack SDK](https://github.com/slackapi/deno-slack-sdk). Workflows are a combination of functions, executed in order. There are a three types of functions: * **Slack functions** enable Slack-native actions, like creating a channel or sending a message. * **Connector functions** enable actions native to services _outside_ of Slack. Google Sheets, Dropbox and Microsoft Excel are a few of the services with available connector functions. * **Custom functions** enable developer-specific actions. Pass in any desired inputs, perform any actions you can code up, and pass on outputs to other parts of your workflows. Workflows are invoked via triggers. You can invoke workflows: * via a link within Slack, * on a schedule, * when specified events occurs, * or via webhooks. Workflows make use of specifically-designed features of the Slack platform such as [datastores](/tools/deno-slack-sdk/guides/using-datastores), a Slack-hosted way to store data. While in development, you can keep your project mostly to yourself, or share it with a close collaborator. If your Slack admin requires approval of app installations, they’ll need to approve what you’re creating first. The app management UI on `api.slack.com/apps` doesn’t support configuring workflow apps. Workflow apps are also currently not eligible for listing in the Slack Marketplace. ## Getting help {#getting-help} These docs have lots of information on the Deno Slack SDK. Please explore! If you otherwise get stuck, we're here to help. The following are the best ways to get assistance working through your issue: * [Issue Tracker](http://github.com/slackapi/deno-slack-sdk/issues) for questions, bug reports, feature requests, and general discussion related to Bolt for JavaScript. Try searching for an existing issue before creating a new one. * [Email](mailto:support@slack.com) our developer support team: `support@slack.com`. --- Source: https://docs.slack.dev/tools/deno-slack-sdk/guides/adding-interactivity # Adding interactivity Workflow apps require a paid plan Join the [Developer Program](https://api.slack.com/developer-program) and provision a sandbox with access to all Slack features for free. Adding interactivity to your app adds a dynamic experience that makes it more substantive than just a bot sending messages. There are a few different ways to achieve interactivity in a workflow app. 1. Collect user input **[through a form](/tools/deno-slack-sdk/guides/creating-a-form)** and use it later in the workflow. 2. Create an **[interactive message](/tools/deno-slack-sdk/guides/creating-an-interactive-message)** with varying options of back-and-forth with the user. 3. Use **[interactive modals](/tools/deno-slack-sdk/guides/creating-an-interactive-modal)** to ask your users questions, allow actions, and update based on the information they give you. ## Basic elements of interactivity {#basic-elements} All options share some basic elements in common. * Interactivity parameter * Blocks with interactive parts * Interactivity handlers ### Interactivity parameter {#interactivity-parameter} In order to prevent inundating users with pop-ups they didn't ask for, all app interactivity requires an interactivity parameter. This is the user's consent to interact with the app; only a user's interaction can open a form or modal. Whether the user is opening a [form](/tools/deno-slack-sdk/guides/creating-a-form#add-interactivity), [modal](/tools/deno-slack-sdk/guides/creating-an-interactive-modal#add-interactivity), or sending an [interactive message](/tools/deno-slack-sdk/guides/creating-an-interactive-message), this looks the same—including an `input_parameter` of the type `interactivity`. Not supported in Workflow Builder [Custom functions](/tools/deno-slack-sdk/guides/creating-custom-functions) that require interactivity inputs are not currently supported in Workflow Builder. ### Interactive blocks {#interactive-blocks} Both modals and interactive messages allow for [interactive blocks](https://docs.slack.dev/reference/block-kit/block-elements), a flexible and dynamic way to create visually appealing app interaction. Check out an example in step 2 of [Creating an interactive message](/tools/deno-slack-sdk/guides/creating-an-interactive-message#add-block-kit), explore the [Block Kit reference](https://docs.slack.dev/reference/block-kit/block-elements) for a menu of interactive options, then try them out in [Block Kit Builder](https://app.slack.com/block-kit-builder/). ### Interactivity handlers {#interactivity-handlers} An interactive form is a static mechanism that merely gathers information from the user, but modals and messages allow for more of a back-and-forth interaction. This means being able to respond to your user's actions and inputs dynamically. This is achieved by utilizing interactivity handlers. Handler name Description Where it can be used `BlockActionsHandler` Used to respond to interactivity that happens within an [interactive block](https://docs.slack.dev/reference/block-kit/block-elements). [`Messages`](https://docs.slack.dev/messaging) [`Modals`](https://docs.slack.dev/surfaces/modals) `BlockSuggestionHandler` Used alongside a [select menu of external data source](https://docs.slack.dev/reference/block-kit/block-elements/multi-select-menu-element#external_multi_select) element. [`Messages`](https://docs.slack.dev/messaging) [`Modals`](https://docs.slack.dev/surfaces/modals) `ViewSubmissionHandler` Used to update a modal view after it has been submitted. [`Modals`](https://docs.slack.dev/surfaces/modals) `ViewClosedHandler` Used to update an app after a view has been closed. [`Modals`](https://docs.slack.dev/surfaces/modals) `UnhandledEventHandler` Used as a catch-all for unhandled events. [`Messages`](https://docs.slack.dev/messaging) [`Modals`](https://docs.slack.dev/surfaces/modals) It's best practice to properly handle a function's success or error when a modal is submitted or closed. Refer to [creating an interactive modal](/tools/deno-slack-sdk/guides/creating-an-interactive-modal) for more details. View sample payloads for these handlers in the [Interaction payloads documentation](https://docs.slack.dev/reference/interaction-payloads). ## Invoking interactivity handlers {#invoking} Each handler contains two arguments, a `constraint` and a `handler`, such that its invocation will look like this: * `addBlockActionsHandler(constraint, handler)` * `addBlockSuggestionHandler(constraint, handler)` * `addViewSubmissionHandler(constraint, handler)` * `addViewClosedHandler(constraint, handler)` * `addUnhandledEventHandler(constraint, handler)` If any incoming event matches the `constraint`, the specified handler will be invoked with the event payload. The `handler` arugment is the handler function that you define—what you want to happen in this event. Every type of handler function has the same context properties available to it, which are the same as the [context properties](/tools/deno-slack-sdk/guides/creating-custom-functions#context) available to custom functions. This allows for authoring focused, single-purpose handlers and provides a concise, yet flexible API for registering handlers to specific interactions. What the `constraint` field allows depends on the type of handler. ### Block actions and block suggestions {#block-handlers} For the `BlockActionsHandler` and the `BlockSuggestionHandler`, the `constraint` can be either a `BlockActionConstraintField` or a `BlockActionConstraintObject`. `BlockActionConstraintField` can be one of three options. ``` type BlockActionConstraintField = string | string[] | RegExp; ``` * When provided as a `string`, it must match the field exactly. * When provided as an array of `string`, it must match one of the array values exactly. * When provided as a `RegExp`, the regular expression must match. The `BlockActionConstraintObject` contains two properties. ``` type BlockActionConstraintObject = { block_id?: BlockActionConstraintField; action_id?: BlockActionConstraintField;}; ``` When the `constraint` is provided as an object in the form of a `BlockActionConstraintObject`, it can contain either or both a `block_id` and an `action_id`. * Each of these properties is a `BlockActionConstraintField` (see above). * If both the `action_id` and `block_id` properties exist on the constraint, then both `action_id` and `block_id` properties must match any incoming action. * If only one of these properties is provided, then only the provided property must match. See an example of the `BlockActionsHandler` and the `BlockSuggestionHandler` in action in the [Creating an interactive message](/tools/deno-slack-sdk/guides/creating-an-interactive-message) guide. ### View handlers {#view-handlers} The `constraint` field for the view handlers is a bit different than in the `BlockActionsHandler` and `BlockSuggestionHandler`. ``` SlackFunction({ ... }).addViewSubmissionHandler("my_view_callback_id", async (ctx) => { ... }); ``` For view handlers, the `constraint` argument can be either a `string`, `string[]`, or a `RegExp`. * A simple `string` constraint must match a view's `callback_id` exactly. * A `string []` constraint must match a view's `callback_id` to any of the strings in the array. * A regular expression constraint must match a view's `callback_id`. ### Unhandled handlers {#unhandled-handlers} The `UnhandledEventHandler` handles everything unaccounted for. It then makes sense that this handler does not accept a `constraint` argument. It does, however, accept the same `handler` argument as the other handlers, which is the handler function you define. Remember, all custom function [context properties](/tools/deno-slack-sdk/guides/creating-custom-functions#context) are available for use here. ``` .addUnhandledEventHandler(({ body: _body }) => { console.log("unhandled event happened"); //add some other actions }) ``` ## Next steps {#next-steps} Ready to get started? ✨ Get started with collecting user input by **[creating a form](/tools/deno-slack-sdk/guides/creating-a-form)**. ✨ Dazzle your users by sending them an **[interactive message](/tools/deno-slack-sdk/guides/creating-an-interactive-message)**. ✨ Create some back-and-forth banter with **[interactive modals](/tools/deno-slack-sdk/guides/creating-an-interactive-modal)**. --- Source: https://docs.slack.dev/tools/deno-slack-sdk/guides/adding-items-to-a-datastore # Adding items to a datastore Workflow apps require a paid plan Join the [Developer Program](https://api.slack.com/developer-program) and provision a sandbox with access to all Slack features for free. There are a few ways you can add information to a datastore. You can: * [Create or replace items with `put` and `bulkPut`](#create-replace) * [Create or update items with `update`](#update) There's an important distinction between these methods! The `put` and `bulkPut` methods _replace_ entire existing items, while the `update` method will only _update_ the provided attributes for items. Be careful to not accidentally lose information when using the `put` and `bulkPut` methods. Slack CLI commands You can also add items to a datastore with the [`datastore put`](/tools/slack-cli/reference/commands/slack_datastore_put), [`datastore bulk-put`](/tools/slack-cli/reference/commands/slack_datastore_bulk-put), and [`datastore update`](/tools/slack-cli/reference/commands/slack_datastore_update) Slack CLI commands. The `datastore bulk-put` command even supports importing data from a [JSON Lines](https://jsonlines.org/) file. ## Create or replace items with put and bulkPut {#create-replace} There are two methods for creating and replacing items in datastores: * The [`apps.datastore.put`](https://docs.slack.dev/reference/methods/apps.datastore.put) method is best for single items. * The [`apps.datastore.bulkPut`](https://docs.slack.dev/reference/methods/apps.datastore.bulkPut) method is best for multiple items. They work quite similarly. Example: Using the `put` method ``` const putResp = await client.apps.datastore.put< typeof DraftDatastore.definition >({ datastore: DraftDatastore.name, item: { id: draftId, created_by: inputs.created_by, message: inputs.message, channels: inputs.channels, channel: inputs.channel, icon: inputs.icon, username: inputs.username, status: DraftStatus.Draft, }, }); ``` Example: Using the `bulkPut` method ``` const putResp = await client.apps.datastore.bulkPut< typeof DraftDatastore.definition >({ datastore: DraftDatastore.name, items: [ { id: draftId, created_by: inputs.created_by, message: inputs.message, channels: inputs.channels, channel: inputs.channel, icon: inputs.icon, username: inputs.username, status: DraftStatus.Draft, }, { id: draftId2, created_by: inputs.created_by, message: inputs.message, channels: inputs.channels, channel: inputs.channel, icon: inputs.icon, username: inputs.username, status: DraftStatus.Draft, }, ] }); ``` That's the general format for each method - but let's look a full example to help connect the dots. In this example, we create a custom function that creates and sends an announcement draft to a channel. Values for each of the datastore's attributes are passed in to create that announcement draft. First is the custom function definition: ``` // /functions/create_draft/definition.tsimport { DefineFunction, Schema } from "deno-slack-sdk/mod.ts";export const CREATE_DRAFT_FUNCTION_CALLBACK_ID = "create_draft";export const CreateDraftFunctionDefinition = DefineFunction({ callback_id: CREATE_DRAFT_FUNCTION_CALLBACK_ID, title: "Create a draft announcement", description: "Creates and sends an announcement draft to channel for review before sending", source_file: "functions/create_draft/handler.ts", input_parameters: { properties: { created_by: { type: Schema.slack.types.user_id, description: "The user that created the announcement draft", }, message: { type: Schema.types.string, description: "The text content of the announcement", }, channel: { type: Schema.slack.types.channel_id, description: "The channel where the announcement will be drafted", }, channels: { type: Schema.types.array, items: { type: Schema.slack.types.channel_id, }, description: "The channels where the announcement will be posted", }, icon: { type: Schema.types.string, description: "Optional custom bot icon to use display in announcements", }, username: { type: Schema.types.string, description: "Optional custom bot emoji avatar to use in announcements", }, }, required: [ "created_by", "message", "channel", "channels", ], }, output_parameters: { properties: { draft_id: { type: Schema.types.string, description: "Datastore identifier for the draft", }, message: { type: Schema.types.string, description: "The content of the announcement", }, message_ts: { type: Schema.types.string, description: "The timestamp of the draft message in the Slack channel", }, }, required: ["draft_id", "message", "message_ts"], },}); ``` Next we have the handler function for `CreateDraftFunction`. With it, we create a new datastore record using the `put` method: ``` // /functions/create_draft/handler.tsimport { SlackFunction } from "deno-slack-sdk/mod.ts";import { CreateDraftFunctionDefinition } from "./definition.ts";import { buildDraftBlocks } from "./blocks.ts";import { confirmAnnouncementForSend, openDraftEditView, prepareSendAnnouncement, saveDraftEditSubmission,} from "./interactivity_handler.ts";import { ChatPostMessageParams, DraftStatus } from "./types.ts";import DraftDatastore from "../../datastores/drafts.ts";/** * This is the handling code for the CreateDraftFunction. It will: * 1. Create a new datastore record with the draft * 2. Build a Block Kit message with the draft and send it to input channel * 3. Update the draft record with the successful sent drafts timestamp * 4. Pause function completion until user interaction */export default SlackFunction( CreateDraftFunctionDefinition, async ({ inputs, client }) => { const draftId = crypto.randomUUID(); // 1. Create a new datastore record with the draft const putResp = await client.apps.datastore.put< typeof DraftDatastore.definition >({ datastore: DraftDatastore.name, item: { id: draftId, created_by: inputs.created_by, message: inputs.message, channels: inputs.channels, channel: inputs.channel, icon: inputs.icon, username: inputs.username, status: DraftStatus.Draft, }, }); if (!putResp.ok) { const draftSaveErrorMsg = `Error saving draft announcement. Contact the app maintainers with the following information - (Error detail: ${putResp.error})`; console.log(draftSaveErrorMsg); return { error: draftSaveErrorMsg }; } ... ``` If the call was successful, the payload's `ok` property will be `true`, and the `item` or `items` property will return a copy of the data you just inserted: ``` { "ok": true, "datastore": "drafts", "item": { "id": "906dba92-44f5-4680-ada9-065149e4e930", "created_by": "U045A5X302V", "message": "This is a test message", "channels": ["C039ARY976C"], "channel": "C038M39A2TV", "icon": "", "username": "Slackbot", "status": "draft", }} ``` If the call was not successful, the payload's `ok` property will be `false`, and you will have a error `code` and `message` property available: ``` { "ok": false, "error": "datastore_error", "errors": [ { "code": "some_error_code", "message": "A description of the error", "pointer": "/datastore/drafts" } ]} ``` Datastore bulk API methods may _partially_ fail The `partial_failure` error message indicates that some items were successfully processed while others need to be retried. This is likely due to rate limits. Call the method again with only those failed items. You'll find a `failed_items` array within the API response. The array contains all the items that failed, in the same format they were passed in. Copy the `failed_items` array and use it in your request. If you're adding new data via the `put` or `bulkPut` method, provide each item with a new primary key value in the `id` property. If you're updating an existing items, provide the `id` of each item you wish to replace. Note that a `put` or `bulkPut` request replaces each entire specified object, if it exists. "This datastore size is _just right_" The total allowable size of an item (all fields in a record) must be less than 400 KB. ## Create or update an item with update {#update} Updating only some of an item's attributes is done with the [`apps.datastore.update`](https://docs.slack.dev/reference/methods/apps.datastore.update) API method. Let's see how that works by passing in values for only some of the datastore's attributes: ``` // /functions/create_draft_interactivity_handler.ts...export const saveDraftEditSubmission: ViewSubmissionHandler< typeof CreateDraftFunction.definition> = async ( { inputs, view, client },) => { // Get the datastore draft ID from the modal's private metadata const { id, thread_ts } = JSON.parse(view.private_metadata || ""); const message = view.state.values.message_block.message_input.value; // Update the saved message const updateResp = await client.apps.datastore.update({ datastore: DraftDatastore.name, item: { id: id, message: message, // This call will update only the message of the draft announcement }, }); if (!updateResp.ok) { const updateDraftMessageErrorMsg = `Error updating draft ${id} message. Contact the app maintainers with the following - (Error detail: ${putResp.error})`; console.log(updateDraftMessageErrorMsg); return; } ... ``` If the call was successful, the payload's `ok` property will be `true`, and the `item` property will return a copy of the updated data: ``` { "ok": true, "datastore": "drafts", "item": { "id": "906dba92-44f5-4680-ada9-065149e4e930", "created_by": "U045A5X302V", "message": "This is a message that will be sent", "channels": ["C039ARY976C"], "channel": "C038M39A2TV", "icon": "", "username": "Slackbot", "status": "draft", }} ``` If the call was not successful, the payload's `ok` property will be `false`, and you will have a error `code` and `message` property available: ``` { "ok": false, "error": "datastore_error", "errors": [ { "code": "some_error_code", "message": "A description of the error", "pointer": "/datastore/drafts" } ]} ``` If an item with the provided `id` doesn't exist in the datastore, `update` will insert the item using the provided attributes. --- Source: https://docs.slack.dev/tools/deno-slack-sdk/guides/calling-slack-api-methods # Calling Slack API methods Workflow apps require a paid plan Join the [Developer Program](https://api.slack.com/developer-program) and provision a sandbox with access to all Slack features for free. With workflow apps, you can interface with the Slack API we've come to know and love. While building, you can access and make calls to the Slack API via the `client` context property of a `SlackFunction`. Think of `SlackFunction` as a utility employed for implementing [custom functions](/tools/deno-slack-sdk/guides/creating-custom-functions). It ensures input validity, provides auto-complete functionality, and manages the app's access token so you don't have to (you can thank us later). To use the `client` context property, import `SlackFunction` from `deno-slack-sdk` to your custom function file and add the `client` context property to your `SlackFunction`, as shown below. Only the `inputs` and `client` context properties are shown in this example, but other context properties you might want to include are outlined on the [custom functions](/tools/deno-slack-sdk/guides/creating-custom-functions#context) page. ``` // functions/example_function.tsimport { DefineFunction, Schema, SlackFunction } from "deno-slack-sdk/mod.ts";export const ExampleFunctionDefinition = DefineFunction({ callback_id: "example_function_def", title: "Example function", source_file: "functions/example_function.ts",});export default SlackFunction( ExampleFunctionDefinition, ({ inputs, client }) => { // Add `client` here // ... ``` The `client` property allows you to access the Slack API in one of two ways: * `client..` (e.g., `client.chat.postMessage`) * `client.apiCall('')` (e.g., `client.apiCall('chat.postMessage', {/* ... */});`) These API calls return a promise, so be sure to `await` their responses. A promise in TypeScript allows for asynchronous programming - handling multiple tasks at the same time. For example, let's look at our sample app, [Deno Request Time Off](https://github.com/slack-samples/deno-request-time-off), and send a message to a user's manager to request time off: ``` // functions/send_time_off_request_to_manager/definition.tsimport { DefineFunction, Schema } from "deno-slack-sdk/mod.ts";export const SendTimeOffRequestToManagerFunction = DefineFunction({ callback_id: "send_time_off_request_to_manager", title: "Request Time Off", description: "Sends your manager a time off request to approve or deny", source_file: "functions/send_time_off_request_to_manager/mod.ts", input_parameters: { properties: { interactivity: { type: Schema.slack.types.interactivity, }, employee: { type: Schema.slack.types.user_id, description: "The user requesting the time off", }, manager: { type: Schema.slack.types.user_id, description: "The manager approving the time off request", }, start_date: { type: "slack#/types/date", description: "Time off start date", }, end_date: { type: "slack#/types/date", description: "Time off end date", }, reason: { type: Schema.types.string, description: "The reason for the time off request", }, }, required: [ "employee", "manager", "start_date", "end_date", "interactivity", ], }, output_parameters: { properties: {}, required: [], },});// functions/send_time_off_request_to_manager/mod.tsimport { SendTimeOffRequestToManagerFunction } from "./definition.ts";import { SlackFunction } from "deno-slack-sdk/mod.ts";import BlockActionHandler from "./block_actions.ts";import { APPROVE_ID, DENY_ID } from "./constants.ts";import timeOffRequestHeaderBlocks from "./blocks.ts";export default SlackFunction( SendTimeOffRequestToManagerFunction, async ({ inputs, client }) => { console.log("Forwarding the following time off request:", inputs); // ... // Send the message to the manager const msgResponse = await client.chat.postMessage({ channel: inputs.manager, blocks, // Fallback text to use when rich media can't be displayed (i.e. notifications) as well as for screen readers text: "A new time off request has been submitted", }); if (!msgResponse.ok) { console.log("Error during request chat.postMessage!", msgResponse.error); } // IMPORTANT! Set `completed` to false in order to keep the interactivity // points (the approve/deny buttons) "alive" // We will set the function's complete state in the button handlers below. return { completed: false, }; } // ... ``` Most API endpoints require specific [permission scopes](https://docs.slack.dev/reference/scopes). Add scopes to your app by listing them in the `botScopes` property of your [manifest](/tools/deno-slack-sdk/guides/using-the-app-manifest). ## Slack API methods {#slack-api-methods} You can call any Slack API method that is accessible via a [bot token](https://docs.slack.dev/authentication/tokens#bot) listed in the [method documentation](https://docs.slack.dev/reference/methods). On those pages, you will discover more ways to access Slack API methods, but for use within workflow apps, we recommend using `SlackFunction` outlined here. ## Next up {#next-up} ➡️ **To learn more about the custom functions** you can implement using `SlackFunction`, check out [custom functions](/tools/deno-slack-sdk/guides/creating-custom-functions). --- Source: https://docs.slack.dev/tools/deno-slack-sdk/guides/collaborating-with-teammates # Collaborating with teammates Workflow apps require a paid plan Join the [Developer Program](https://api.slack.com/developer-program) and provision a sandbox with access to all Slack features for free. Have multiple developers working on an app? Never fear! Teams can collaborate when building and deploying workflow apps. ## Deploy the app to make it available to collaborators {#deploy-to-collaborators} Let's say you're working along on an app and you realize it's going to need several types of triggers. Your teammate Luke is the resident trigger expert, so you ask if he'll jump in and help you out, to which he enthusiastically agrees—hooray! The first thing you'll do is to deploy your app using the `slack deploy` command. ✨ **For directions on how to deploy your app**, refer to [deploy to Slack](/tools/deno-slack-sdk/guides/deploying-to-slack). This will create an `apps.json` configuration file in your `.slack` folder (this folder may be hidden). This file contains information about your deployed apps, such as the installed workspace, the app name, the app ID, and the team ID. You'll want to check this file into version control in case you want to collaborate on the same deployed apps with others; if two or more people want to deploy or update the same app on the same workspace, the `apps.json` contents must be the same for everyone. ## Add collaborators {#collaborators} Now, as long as Luke is in the same workspace as you, you can add Luke as a collaborator on your app right from the Slack CLI by entering the `slack collaborator add` command along with their email address or user ID. Choose your deployed environment, and voilà! Luke is now a collaborator on your app. To double-check, you can run the `slack collaborator list` command and choose your deployed environment—you should see yourself and Luke in the list. In the meantime, Luke can clone the GitHub repository containing the files that comprise your app, including the `apps.json` file, to their local machine. If Luke also wants to deploy the app to the same workspace as you, they will have to run the `slack login` command within that workspace. Once logged in, Luke will have the same access to run the `slack deploy` command (and other Slack CLI commands) as you. ### Remove collaborators and security considerations {#remove} If a collaborator is removed from an app by using the `slack collaborator remove` command or otherwise has their access to the app revoked, they may still retain their OAuth (user and bot) or app-level tokens. It is therefore recommended that the app owner rotate secrets and tokens after an app collaborator is removed in order to avoid a potential security vulnerability. ## Develop locally {#develop} Both you and Luke can now develop locally on your unique instances of the app. Use the `slack run` command while working to see your changes in real-time within your local environment. ✨ **For information about developing locally**, refer to [local development](/tools/deno-slack-sdk/guides/developing-locally). ### App instances {#app-instances} Worthy of note is that there are now three instances of your app in existence: * The deployed version of the app that exists within your shared workspace * Your local version of the app * Luke's local version of the app Both your local projects will include an `apps.dev.json` file in your respective `.slack` folders, which are unique to your app and your local development environments. These files are only for local development and **should not** be checked into version control. ## Deploy updates to production {#deploy-to-production} Once you and Luke have completed development, either you or Luke can deploy the app to production by running the `slack deploy` command. However, be aware that this could lead to a situation in which Luke makes a change and runs the `slack deploy` command--and at the same time, you make a different, unrelated change and run the `slack deploy` command. Since you're both deploying the same app, the second deploy will overwrite the first. It is therefore important to either automate or coordinate your deployments with care. Teamwork makes the dream work! --- Source: https://docs.slack.dev/tools/deno-slack-sdk/guides/controlling-access-to-custom-functions # Controlling access to custom functions Workflow apps require a paid plan Join the [Developer Program](https://api.slack.com/developer-program) and provision a sandbox with access to all Slack features for free. To make a function available so that another user (or many users) can access workflows that reference that function, you'll use the [`function access`](/tools/slack-cli/reference/commands/slack_function) Slack CLI command. At this time, functions can be made available to: * _everyone_ in workspaces where the app has access, * your app's _collaborators_, * or _specific users_. In order to enable the [`function access`](/tools/slack-cli/reference/commands/slack_function) command, your app must have been deployed _at least once before_ attempting to make your function available to others. You must also re-deploy your app after using `function access`. Anytime you make permission changes to your function using the `function access` command, your app must be redeployed, _each time after_, in order for the updates to be available in your app's workspace. ## Grant access to one person {#single-access} Given: * a function with a [callback ID](/tools/deno-slack-sdk/guides/creating-custom-functions#fields) of `get_next_song` * a user with ID `U1234567` You can make your `get_next_song` function available to the user `U1234567` like this: ``` $ slack function access --name get_next_song --users U1234567 --grant ``` _To revoke access, replace `--grant` with `--revoke`._ ## Grant access to multiple people {#multi-access} Given: * a function with a [callback ID](/tools/deno-slack-sdk/guides/creating-custom-functions#fields) of `calculate_royalties` * users with the following IDs: `U1111111`, `U2222222`, and `U3333333` You can make your function `calculate_royalties` available to the above users like this: ``` $ slack function access --name calculate_royalties --users U1111111,U2222222,U3333333 --grant ``` _To revoke access, replace `--grant` with `--revoke`._ ## Grant access to all collaborators {#collaborator-access} Given: * a function with a [callback ID](/tools/deno-slack-sdk/guides/creating-custom-functions#fields) of `notify_escal_team` You can make your `notify_escal_team` function available to all of your app's [collaborators](/tools/slack-cli/reference/commands/slack_collaborator) like this: ``` $ slack function access --name notify_escal_team --app_collaborators --grant ``` ## Grant access to all workspace members {#all-access} Given: * a function with a [callback ID](/tools/deno-slack-sdk/guides/creating-custom-functions#fields) of `get_customer_profile` You can make your `get_customer_profile` function available to everyone in your workspace like this: ``` $ slack function access --name get_customer_profile --everyone --grant ``` ## Grant access using the prompt-based approach {#distribute-prompt} The prompt-based approach allows you to distribute your function to one user, to multiple people, to collaborators, or to everyone in an interactive prompt. To activate the flow, use the following command in your terminal: ``` $ slack function access ``` Given: * a function with a [callback ID](/tools/deno-slack-sdk/guides/creating-custom-functions#fields) of `reverse` You will answer the first prompt in the following manner: #### Choose the name of the function you'd like to distribute {#choose-the-name-of-the-function-youd-like-to-distribute} ``` > reverse (Reverse) ``` #### Choose who you'd like to to have access to your function {#choose-who-youd-like-to-to-have-access-to-your-function} If going from `everyone` or `app_collaborators` **to** specific users, you should be offered the option of adding collaborators to specific users. ``` > specific users (current) app collaborators only everyone ``` #### Choose an action {#choose-an-action} ``` > granting a user access revoking a user's access ``` #### Provide ID(s) of one or more user in your workspace {#provide-ids-of-one-or-more-user-in-your-workspace} Given: * a user's ID in your workspace: `U0123456789` You will answer the following prompt below: ``` : U0123456789 ``` You can add multiple users at the same time. To do this, separate the user IDs with a comma (e.g. `U0123456789`, `UA987654321`). After you've finished this flow, you'll receive a message indicating the type of distribution you chose. ## Guests and external users {#guests-external} Guests and external users are limited in the workflows they may run based on the scopes defined for the functions in the workflows. There is a predefined set of scopes that are considered "risky", which guest users cannot run. These scopes include the following: * [`channels:manage`](https://docs.slack.dev/reference/scopes/channels.manage) - create and manage public channels * [`channels:write.invites`](https://docs.slack.dev/reference/scopes/channels.write.invites) - invite members to public channels * [`groups:write`](https://docs.slack.dev/reference/scopes/groups.write) - create and manage private channels * [`groups:write.invites`](https://docs.slack.dev/reference/scopes/groups.write.invites) - invite members to private channels * [`usergroups:write`](https://docs.slack.dev/reference/scopes/usergroups.write) - create and manage usergroups If a guest or external user attempts to run a workflow containing a function with one of these scopes, they will receive an error. ## More distribution options {#distribution-options} For more distributions options, including how to **revoke** access, head to the [distribute command reference](/tools/slack-cli/reference/commands/slack_function). --- Source: https://docs.slack.dev/tools/deno-slack-sdk/guides/controlling-permissions-for-admins # Controlling permissions for Admins Workflow apps require a paid plan Join the [Developer Program](https://api.slack.com/developer-program) and provision a sandbox with access to all Slack features for free. As part of the [broader access controls available to administrators](https://slack.com/help/categories/200122103-Workspace-administration#workspace-settings-permissions), administrators can ensure only approved apps are installed and available to users. ## Approval process for admins {#approval-admins} If a workspace has the [Admin-Approved Apps](https://slack.com/help/articles/222386767-Manage-app-installation-settings-for-your-workspace) feature enabled, apps must be approved by a Workspace Admin (as set in your workspace settings) before they can be deployed. However, even if a workspace has Admin-Approved Apps enabled, workspace owners can still run `slack deploy` to deploy apps or `slack run` to run apps locally without requesting Admin-Approved Apps permission. The Admin-Approved Apps approval process does not apply to standalone workspaces. When a developer deploys an app, administrators will receive a notification, either from Slackbot or using the [Admin-Approved Apps API workflow](https://docs.slack.dev/admins/managing-app-approvals) as determined by the organization. The approval notification will include which [OAuth scopes](https://docs.slack.dev/authentication/installing-with-oauth#asking) the app is requesting, as well as any outgoing domains the app may want to access. Outgoing domains are a new concept, and apply only to apps deployed to Slack's managed infrastructure. These are domains the app may require access to — for example, if a developer writes a [function](/tools/deno-slack-sdk/guides/creating-slack-functions) that makes a request to an external API, they will need to include that API in their outgoing domains. Outgoing domains do not constrain which ports on those domains a function can communicate with. Administrators can now approve or deny apps based on these defined outgoing domains, in the same way they would OAuth scopes. ### Admin-Approved Apps and connector functions for admins {#admin-connectors} Developers can create apps that call connector functions. These connector functions are contained by another app; for example, if a developer wishes to add a row to a Google spreadsheet or to update that same row, they could call the respective [Google Sheets connector functions](/tools/deno-slack-sdk/reference/connector-functions#google_sheets). In addition to the approval process for developer apps described above, you can also explicitly approve or deny apps that use connector functions for use in Enterprise organization workspaces based on the specified connector function. For example, if a developer's app calls a connector function that has not yet been approved for your workspace, you will be notified for approval when the developer attempts to install their app. In this example, you would approve or deny the specific Google Sheets connector function for use in your workspace. If you deny the connector function, running `manifest validate` will inform the developer that the connector function is denied for use in the workspace. If you approve it, running `manifest validate` will install the specified connector function to the workspace. For more information and a list of connector functions and their containing apps, refer to [connector functions](/tools/deno-slack-sdk/reference/connector-functions). ### Changes to the APIs {#api-changes} If you are using the [Admin-Approved Apps APIs](https://docs.slack.dev/admins/managing-app-approvals) to manage your app approval process, there will be some changes to the API responses you receive as well as some new parameters that you can send to account for the new concept of outgoing domains that applies to apps deployed to Slack's managed infrastructure. The following endpoints will now have a `domains` field next to the existing `scopes` field, as a string array: * [`admin.apps.approved.list`](https://docs.slack.dev/reference/methods/admin.apps.approved.list) * [`admin.apps.restricted.list`](https://docs.slack.dev/reference/methods/admin.apps.restricted.list) * [`admin.apps.requests.list`](https://docs.slack.dev/reference/methods/admin.apps.requests.list) A response would look like this: ``` "scopes": [ { "name": "app_mentions:read", "description": "View messages that directly mention @your_slack_app in conversations that the app is in", "token_type": "bot" }],"domains": ['slack.com'], ``` Additionally, the following endpoints will now have an optional `domains` string array field for including outgoing domains that should be included in the approve or deny request: * [`admin.apps.approve`](https://docs.slack.dev/reference/methods/admin.apps.approve) * [`admin.apps.restrict`](https://docs.slack.dev/reference/methods/admin.apps.restrict) If the `domains` array is left empty, the method will look up the domains specified by the app. ## Approval process for developers {#approval-developers} For developers, the most important thing to know is that you may run into extra steps when deploying your apps. If the administrators of your workspace have enabled [Admin-Approved Apps](https://slack.com/help/articles/222386767-Manage-app-installation-settings-for-your-workspace), it means your app requires approval before it can be deployed. In this case, after you run `slack deploy`, a prompt will notify you via the CLI that admin approval is required on this workspace. You'll also be prompted to enter `y` or `n` to send a request to the workspace administrator for approval to install your app. Administrators will see which OAuth scopes your app is requesting, as well as which outgoing domains your app is requesting access to. Outgoing domains are specified in the `outgoingDomains` array of your apps `manifest.ts` file as comma-separated strings. Administrators may also ask for an additional description for your app. If this is enabled, you will be asked to provide that information when you deploy your app using the CLI. Once you have approval, you'll receive a notification from Slackbot, and you can then deploy your app. If you receive a Slackbot notification that your app was denied, reach out to your workspace administrator. Finally, if your app needs to request a new OAuth scope or outgoing domain, it will again trigger the approval process above. The existing app installation will continue to function, but the new scope or outgoing domain will not be functional until the app is reapproved and redeployed. ### Admin-Approved Apps and connector functions for developers {#dev-connectors} To request approval for an app that uses a connector function requiring admin approval, perform the following steps (Enterprise organization workspaces only): 1. Install your app via the CLI. If the app uses a connector function that requires admin approval, you will be prompted with that information about the connector function and asked if you would like to submit a request for approval. 2. Select `Y`. The CLI wil prompt you for a reason; after entering one, an approval request will be created for the admin of the workspace. Running `manifest validate` will show that the connector app is now pending approval. 3. If you enter `N`, the CLI will do nothing, and your app will not be submitted for approval. --- Source: https://docs.slack.dev/tools/deno-slack-sdk/guides/creating-a-custom-type # Creating a custom type Workflow apps require a paid plan Join the [Developer Program](https://api.slack.com/developer-program) and provision a sandbox with access to all Slack features for free. Custom types provide a way to introduce reusable, sharable types to your workflow apps. Once registered in your manifest, you can use custom types as input or output parameters in any of your app's [functions](/tools/deno-slack-sdk/guides/creating-slack-functions), [workflows](/tools/deno-slack-sdk/guides/creating-workflows), or [datastores](/tools/deno-slack-sdk/guides/using-datastores). The possibilities are endless! ## Defining a type {#define-type} Types can be defined with a top level `DefineType` export. In the example below, a custom type object is defined for use in the [Deno Announcement Bot](https://github.com/slack-samples/deno-announcement-bot) sample app: ``` // types/incident.tsimport { DefineType, Schema } from "deno-slack-sdk/mod.ts";export const AnnouncementCustomType = DefineType({ name: "Announcement", type: Schema.types.object, properties: { channel_id: { type: Schema.slack.types.channel_id, }, success: { type: Schema.types.boolean, }, permalink: { type: Schema.types.string, }, error: { type: Schema.types.string, }, }, required: ["channel_id", "success"],}); ``` Another way custom types can be defined is within a function, where they can be immediately used: ``` // Define the custom typeimport { DefineFunction, DefineType, Schema } from "deno-slack-sdk/mod.ts";export const AnnouncementCustomType = DefineType({ name: "Announcement", type: Schema.types.object, properties: { channel_id: { type: Schema.slack.types.channel_id, }, success: { type: Schema.types.boolean, }, permalink: { type: Schema.types.string, }, error: { type: Schema.types.string, }, }, required: ["channel_id", "success"],});// Define your function, which uses the custom type we just definedexport const PrepareSendAnnouncementFunctionDefinition = DefineFunction({ callback_id: "send_announcement", title: "Send an announcement", description: "Sends a message to one or more channels", source_file: "functions/send_announcement/handler.ts", input_parameters: { properties: { message: { type: Schema.types.string, description: "The content of the announcement", }, channels: { type: Schema.types.array, items: { type: Schema.slack.types.channel_id, }, description: "The destination channels of the announcement", }, icon: { type: Schema.types.string, description: "Optional custom bot icon to use display in announcements", }, username: { type: Schema.types.string, description: "Optional custom bot emoji avatar to use in announcements", }, draft_id: { type: Schema.types.string, description: "The datastore ID of the draft message if one was created", }, }, required: [ "message", "channels", ], }, output_parameters: { properties: { announcements: { type: Schema.types.array, items: { type: AnnouncementCustomType, }, description: "Array of objects that includes a channel ID and permalink for each announcement successfully sent", }, }, required: ["announcements"], },});// Finish implementing your functionexport default SlackFunction(PrepareSendAnnouncementFunctionDefinition, async ({ inputs, client }) => {// ... ``` If your custom type will be used in an array, create the array as a custom type too. For example, if we wanted an array of `AnnouncementCustomType`, it would look like this: ``` // Define the custom typeimport { DefineFunction, DefineType, Schema } from "deno-slack-sdk/mod.ts";export const AnnouncementCustomType = DefineType({ name: "Announcement", type: Schema.types.object, properties: { channel_id: { type: Schema.slack.types.channel_id, }, success: { type: Schema.types.boolean, }, permalink: { type: Schema.types.string, }, error: { type: Schema.types.string, }, }, required: ["channel_id", "success"],});// Define the array with the items as the custom typeexport const AnnouncementArray = DefineType({ name: "AnnouncementArray", type: Schema.types.array, items: { type: AnnouncementCustomType },}) ``` Fully defined arrays If a property on your custom type is an array, be sure to define its properties in the `items` field (refer to example [here](/tools/deno-slack-sdk/reference/slack-types#array)). Untyped objects are not currently supported. ## Registering a type {#register-type} To register newly-defined types for use with your app, add them to the `types` array when defining your [manifest](/tools/deno-slack-sdk/guides/using-the-app-manifest). Here's an example, again from the [Deno Announcement Bot](https://github.com/slack-samples/deno-announcement-bot) sample app. All custom types must be registered in the [manifest](/tools/deno-slack-sdk/guides/using-the-app-manifest) for them to be available for use. ``` import { Manifest } from "deno-slack-sdk/mod.ts";import AnnouncementDatastore from "./datastores/announcements.ts";import DraftDatastore from "./datastores/drafts.ts";import { AnnouncementCustomType } from "./functions/post_summary/types.ts";import CreateAnnouncementWorkflow from "./workflows/create_announcement.ts";export default Manifest({ name: "Announcement Bot", description: "Send an announcement to one or more channels", icon: "assets/icon.png", outgoingDomains: ["cdn.skypack.dev"], datastores: [DraftDatastore, AnnouncementDatastore], types: [AnnouncementCustomType], workflows: [ CreateAnnouncementWorkflow, ], botScopes: [ "commands", "chat:write", "chat:write.public", "chat:write.customize", "datastore:read", "datastore:write", ],}); ``` ## Referencing a type {#reference-type} To use a custom type as a [function](/tools/deno-slack-sdk/guides/creating-slack-functions) parameter, import the type: ``` import { DefineFunction, DefineType, Schema } from "deno-slack-sdk/mod.ts";// ... ``` Then, set the parameter's `type` property to the custom type it should reference: ``` // ...input_parameters: { incident: { title: "A String Cheese Incident", type: IncidentType, },},// ... ``` In the example above, the `title` property from the custom type `IncidentType` is being overridden with the string "A String Cheese Incident". Let's look at using custom types in a bit more depth with another one of our sample apps, the [Virtual Running Buddies](https://github.com/slack-samples/deno-virtual-running-buddies) app. Taking a look at [`/types/runner_stats.ts`](https://github.com/slack-samples/deno-virtual-running-buddies/blob/main/types/runner_stats.ts) you'll find the definition for `RunnerStatsType`: ``` import { DefineType, Schema } from "deno-slack-sdk/mod.ts";export const RunnerStatsType = DefineType({ title: "Runner Stats", description: "Information about the recent runs for a runner", name: "runner_stats", type: Schema.types.object, properties: { runner: { type: Schema.slack.types.user_id }, weekly_distance: { type: Schema.types.number }, total_distance: { type: Schema.types.number }, }, required: ["runner", "weekly_distance", "total_distance"],}); ``` Information about each runner is collected as a RunnerStatsType, which includes who the runner is (`user_id`), how far they ran each week (`weekly_distance`), and the total distance they've run so far (`total_distance`). This type is then used to describe the array output of the `CollectRunnerStatsFunction` which is called when the `DisplayLeaderBoardWorkflow` is started. Take a look at the `CollectRunnerStatsFunction` definition here to see how the custom type is returned as an output. ``` import { DefineFunction, Schema, SlackFunction } from "deno-slack-sdk/mod.ts";import RunningDatastore, { RUN_DATASTORE } from "../datastores/run_data.ts";import { RunnerStatsType } from "../types/runner_stats.ts";export const CollectRunnerStatsFunction = DefineFunction({ callback_id: "collect_runner_stats", title: "Collect runner stats", description: "Gather statistics of past runs for all runners", source_file: "functions/collect_runner_stats.ts", input_parameters: { properties: {}, required: [], }, output_parameters: { properties: { runner_stats: { type: Schema.types.array, items: { type: RunnerStatsType }, description: "Weekly and all-time total distances for runners", }, }, required: ["runner_stats"], },});export default SlackFunction(CollectRunnerStatsFunction, async ({ client }) => { // Query the datastore for all the data we collected const runs = await client.apps.datastore.query< typeof RunningDatastore.definition >({ datastore: RUN_DATASTORE }); if (!runs.ok) { return { error: `Failed to retrieve past runs: ${runs.error}` }; } const runners = new Map(); // ... runners object is constructed // Return an array with runner stats return { outputs: { runner_stats: [...runners.entries()].map((r) => r[1]) }, };}); ``` The `map` function you see here in the `outputs` is converting the entries in the `runners` map to an array, then mapping these entries to an array of `RunnerStatsType`. Another way of writing the map part of the function would be: ``` function (r) { return r[1];} ``` `r[1]` is the value in the `runners` array, whereas `r[0]` would be the key, so the map function is essentially mapping an array of small arrays. `RunnerStatsType` here is returned as the output of the function, and the function sets the properties of the custom type before returning it. Pretty cool, huh? To see a full tutorial on this sample app, head over to [Create a social app to log runs with running buddies](/tools/deno-slack-sdk/tutorials/virtual-running-buddies-app). ## TypeScript-friendly type definitions {#define-property} Object types are not supported within Workflow Builder at this time If your function will be used within Workflow Builder, we suggest not using the Object types at this time. Use the `DefineProperty` helper function to get TypeScript-friendly type definitions for your input and output parameters. This is an optional helper utility that is highly recommended for TypeScript source code. While your code will still work without it, we recommend using `DefineProperty` when you have an object parameter with optional sub-properties so that your IDE autocomplete pop-ups will accurately respect the optional nature of properties. If all sub-properties are all required, you don’t need to use `DefineProperty`. Let's illustrate this with an example: ``` const messageAlertFunction = DefineFunction({ ... input_parameters: { properties: { msg_context: DefineProperty({ type: Schema.types.object, properties: { message_ts: { type: Schema.types.string }, channel_id: { type: Schema.types.string }, user_id: { type: Schema.types.string }, }, required: ["message_ts"] }) } }, }); ``` --- Source: https://docs.slack.dev/tools/deno-slack-sdk/guides/creating-a-form # Creating a form Workflow apps require a paid plan Join the [Developer Program](https://api.slack.com/developer-program) and provision a sandbox with access to all Slack features for free. Forms are a straight-forward way to collect user input and pass it onto to other parts of your workflow. Their interactivity is one way - users interact with a static form. You cannot update the form itself based on user input. For example, say you need to collect some information from a user, send it to your system, then update a Slack channel with a link to a summary. Each task can be configured as a step in your workflow, allowing for user interactivity data to be passed to each step sequentially until the process is complete. Forms are created with the [`OpenForm`](/tools/deno-slack-sdk/reference/slack-functions/open_form) Slack function. ✨ **If you only need to update an already-created form**, refer to the [`OpenForm`](/tools/deno-slack-sdk/reference/slack-functions/open_form) Slack function reference page. ## 1. Add interactivity to your workflow {#add-interactivity} First let's take a look at the "Send a Greeting" workflow from the [Hello World sample app](https://github.com/slack-samples/deno-hello-world). Making your app interactive is the key to collecting user data. To accomplish this, an [`interactivity`](/tools/deno-slack-sdk/reference/slack-types#interactivity) input parameter must be included as a property in your workflow definition. The `interactivity` parameter is required to ensure users don't experience any unexpected or unwanted forms appearing - only their interaction can open a form. as in the following code snippet: ``` // workflows/greeting_workflow.tsimport { DefineWorkflow, Schema } from "deno-slack-sdk/mod.ts";import { GreetingFunctionDefinition } from "../functions/greeting_function.ts";const GreetingWorkflow = DefineWorkflow({ callback_id: "greeting_workflow", title: "Send a greeting", description: "Send a greeting to channel", input_parameters: { properties: { interactivity: { type: Schema.slack.types.interactivity, }, channel: { type: Schema.slack.types.channel_id, }, }, required: ["interactivity"], },}); ``` ## 2. Add a form to your workflow {#add-form} Now that you've added the `interactivity` property into your workflow, it's time to add the [`OpenForm`](/tools/deno-slack-sdk/reference/slack-functions/open_form) Slack function to a step in your workflow. While some of the functions you add to your workflow will be [custom functions](/tools/deno-slack-sdk/guides/creating-custom-functions), a variety of [Slack functions](/tools/deno-slack-sdk/guides/creating-slack-functions) that cover some of the most common tasks executed on our platform are also available. The [`OpenForm`](/tools/deno-slack-sdk/reference/slack-functions/open_form) Slack function allows for the collection of user input. ### Form element schema {#element-schema} The fields of a form are made up of different types of form elements. Form elements have several properties you can customize depending on the element type. Links using Markdown are supported in the top-level description, but not in individual element descriptions. Property Type Description Required? `name` String The internal name of the element Required `title` String Title of the form shown to the user. Maximum length is 25 characters Required `type` `Schema.slack.types.*` The [type of form element](/tools/deno-slack-sdk/guides/creating-a-form#type-parameters) to display Required `description` String Description of the form shown to the user Optional `default` Same type as `type` Default value for this field Optional The following parameters are available for each type when defining your form. For each parameter listed above, `type` is required. An important distinction: some element types are prefixed with `Schema.types`, while some are prefixed with `Schema.slack.types`. #### Form types and parameters {#type-parameters} Type Parameters Notes [`Schema.types.string`](/tools/deno-slack-sdk/reference/slack-types#string) `title`, `description`, `default`, `minLength`, `maxLength`, `format`, `enum`, `choices`, `long`, `type` If the `long` parameter is provided and set to `true`, it will render as a multi-line text box. Otherwise, it renders as a single-line text input field. In addition, basic input validation can be done by setting `format` to either `email` or `url` [`Schema.types.boolean`](/tools/deno-slack-sdk/reference/slack-types#boolean) `title`, `description`, `default`, `type` A boolean rendered as a radio button in the form [`Schema.types.integer`](/tools/deno-slack-sdk/reference/slack-types#integer) `title`, `description`, `default`, `enum`, `choices`, `type`, `minimum`, `maximum` A whole number, such as `-1`, `0`, or `31415926535` [`Schema.types.number`](/tools/deno-slack-sdk/reference/slack-types#number) `title`, `description`, `default`, `enum`, `choices`, `type`, `minimum`, `maximum` A number that allows decimal points, such as `13557523.0005` [`Schema.types.array`](/tools/deno-slack-sdk/reference/slack-types#array) `title`, `description`, `default`, `type`, `items`, `maxItems`, `display_type` The required `items` parameter is an object itself, which must have a `type` sub-property defined. It can accept multiple different kinds of sub-properties based on the type chosen. Can be [`Schema.types.string`](/tools/deno-slack-sdk/reference/slack-types#string), [`Schema.slack.types.channel_id`](/tools/deno-slack-sdk/reference/slack-types#channelid), [`Schema.slack.types.user_id`](/tools/deno-slack-sdk/reference/slack-types#userid). The `display_type` parameter can be used if the `items` object has the `type` parameter set to `Schema.types.string` and contains an `enum` parameter. The `display_type` parameter can be then set to `multi_static_select` (default) or `checkboxes`. [`Schema.slack.types.date`](/tools/deno-slack-sdk/reference/slack-types#date) `title`, `description`, `default`, `enum`, `choices`, `type` A string containing a date, displayed in `YYYY-MM-DD` format [`Schema.slack.types.timestamp`](/tools/deno-slack-sdk/reference/slack-types#timestamp) `title`, `description`, `default`, `enum`, `choices`, `type` A Unix timestamp in seconds, rendered as a [date picker](https://docs.slack.dev/reference/block-kit/block-elements/date-picker-element) [`Schema.slack.types.user_id`](/tools/deno-slack-sdk/reference/slack-types#userid) `title`, `description`, `default`, `enum`, `choices`, `type` A user picker [`Schema.slack.types.channel_id`](/tools/deno-slack-sdk/reference/slack-types#channelid) `title`, `description`, `default`, `enum`, `choices`, `type` A channel picker [`Schema.slack.types.rich_text`](/tools/deno-slack-sdk/reference/slack-types#rich-text) `title`, `description`, `default`, `type` A way to nicely format messages in your app. Note that this type cannot be converted to other message types, such as a string [`Schema.slack.types.file_id`](/tools/deno-slack-sdk/reference/slack-types#fileid) `title`, `description`, `type`, `allowed_filetypes_group`, `allowed_filetypes` Needs the [`files:read`](https://docs.slack.dev/reference/scopes/files.read) scope. An additional example: a form element from the [Give Kudos](https://github.com/slack-samples/deno-give-kudos) sample app. ``` // workflows/give_kudos.tsconst kudo = GiveKudosWorkflow.addStep( Schema.slack.functions.OpenForm, { title: "Give someone kudos", interactivity: GiveKudosWorkflow.inputs.interactivity, submit_label: "Share", description: "Continue the positive energy through your written word", fields: { elements: [{ name: "doer_of_good_deeds", title: "Whose deeds are deemed worthy of a kudo?", description: "Recognizing such deeds is dazzlingly desirable of you!", type: Schema.slack.types.user_id, }, { name: "kudo_channel", title: "Where should this message be shared?", type: Schema.slack.types.channel_id, }, { name: "kudo_message", title: "What would you like to say?", type: Schema.types.string, long: true, }, { name: "kudo_vibe", title: 'What is this kudo\'s "vibe"?', description: "What sorts of energy is given off?", type: Schema.types.string, enum: [ "Appreciation for someone 🫂", "Celebrating a victory 🏆", "Thankful for great teamwork ⚽️", "Amazed at awesome work ☄️", "Excited for the future 🎉", "No vibes, just plants 🪴", ], }], required: ["doer_of_good_deeds", "kudo_channel", "kudo_message"], }, },); ``` Add the [`OpenForm`](/tools/deno-slack-sdk/reference/slack-functions/open_form) Slack function as a step in your workflow: ``` // workflows/greeting_workflow.tsconst inputForm = GreetingWorkflow.addStep( Schema.slack.functions.OpenForm, { title: "Send a greeting", interactivity: GreetingWorkflow.inputs.interactivity, submit_label: "Send greeting", fields: { elements: [{ name: "recipient", title: "Recipient", type: Schema.slack.types.user_id, }, { name: "channel", title: "Channel to send message to", type: Schema.slack.types.channel_id, default: GreetingWorkflow.inputs.channel, }, { name: "message", title: "Message to recipient", type: Schema.types.string, long: true, }], required: ["recipient", "channel", "message"], }, },); ``` Forms have two output parameters: * `fields`: The same field names in the inputs, which are returned as outputs with the values entered by the user * `interactivity`: The context about the form submit action interactive event Use these output parameters to pass the information you collected from the user to subsequent steps in a workflow. When using the [`OpenForm`](/tools/deno-slack-sdk/reference/slack-functions/open_form) Slack function, either add it as the first step in your workflow or ensure the preceding step is interactive, as an interactive step will generate a fresh pointer to use for opening the form. For example, use the interactive button in a later step in your workflow, which can be added with the [`Send a message`](/tools/deno-slack-sdk/reference/slack-functions/send_message) Slack function immediately before opening the form. It is important to validate the inputs you receive from the user: first, that the user is authorized to pass the input, and second, that the user is passing a value you expect to receive and nothing more. The example below passes the user's input data into the second step of the workflow, a [custom function](/tools/deno-slack-sdk/guides/creating-custom-functions), by using the output parameter `fields` and selecting the desired output element by name (i.e. `recipient` and `message`) ``` // workflows/greeting_workflow.tsconst greetingFunctionStep = GreetingWorkflow.addStep( GreetingFunctionDefinition, { recipient: inputForm.outputs.fields.recipient, message: inputForm.outputs.fields.message, },); ``` User input data can also be passed to [Slack functions](/tools/deno-slack-sdk/guides/creating-slack-functions). This example sends the user's message to a specific channel specified by the user. ``` // workflows/greeting_workflow.tsGreetingWorkflow.addStep(Schema.slack.functions.SendMessage, { channel_id: inputForm.outputs.fields.channel, message: greetingFunctionStep.outputs.greeting,});export default GreetingWorkflow; ``` Take note of the `title`, `description`, and `submit_label` fields. It is important be descriptive with these fields, as these are the first things the user will see once the workflow is started and your form is displayed to them: ![form-metadata](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAmAAAAB4CAMAAABir749AAAAaVBMVEX////R0dHW1tYgHyD4+Pjb29rx8fEAelr6+/z19vYiet82NTbl5+mVlJQ1ieJQUFAbZ6ViYmNxcHHI3+Ojo6TBwcB9rNeDg4Olx9jiqT2xsbF+vKw9frJflsHMy8tLoovdt28eim7DkzwJsHnpAAANMklEQVR42uydi2KqOBCGo4E0aYIh3A8I6nn/h9yZBLzreu223fltBQ2gxa//TIaALCKR3ihGu4D0VYAJY+J4Tje6PXUzRpwDTInYKMlIpCclhYnFKWDGEF2kVzFmzBFgKha0W0ivk4jVAWCxon1CeqVUvAeYMuRfpFd7mNkBJgztD9KrFTqTzCdglN+TXp/p+zSMkYGR3mlhDDMwyvBJ78rCGEVI0jtjpAeM9gXpHZoAm9OuIL1DcwKMRICRCDASiQAj3SCFQwJj/H3o7qjgRYD9SOWrj5dplR/UFcSTJdGjg9pfAJh4aJiGomPvV/j6eKnyAz6efncHhL4GsCix2hYXarUJb+/bmoQNRZrTwauLWr0WsNXevn/BIZ2Dw453AKaaKr/QlHHtNC/PN9b3AqYTAuy6Pl6svU2/ouIePwCYaKrFYlFdaORasugSRvc6mOHJ8b8B6asAky8BTN4JmPF0LapqcT4xirmOwtyQ2QzeYp0MzibwMgIeZyNgZWbdbFwhszYpSuHa1rbM1Nah+4Xp4DisA2LClYW1uDJMsqIgsH4jYFI2I1xwt8iZjORJriU11wk6Tst1jbFN+5CZYKBzjgfASniOh7gntK4t163hmvMC8MwsLDNOS2hxsJYGK/OrDOCBFjbrCKyfEiJvB0xUW7q8KrZGiSPG5pZznkkATQEWNQAWAy+WFTA/5WBywGhZBNYSpgAYIAicywFCzOrttPUhUnNo1spvwnIJq1Il5bc5WB7oWuwrWv8BnRYeZnXwImstgKWBCAYelCFA2xzMlDUCh1mWMyXMGlgUUYKVYI1pOgKmQ/PAM9yMcZzqFr/IwaSS+aF3BQNbNMjXn3l0utWWZ2BaBWjYAub4bOtgMYQ+GwADW+LcqS1guFK7ne452AiYgEiqS+Lq9zhYVMZRngJdE1+f4VYt+j9gYev4EDBVKDSqWmJv0hcaRsA8WyNgFmJcGQCT2vlySwDM8vBHTdNTB2vBwYiqK4B13eqNDnb9dOx5Mb8fMFXOhUg9UXD3iXDhD/z2aGDro5dMID33CXzCXZvoYgvYwHVR8wmwsrQTYOBI0MsMgEH2XxQ2207n3CbzAwdLYHE99UBJJ4BhobR7m4NJl1whTGYukw842GDksq+CdW0NDIBLzgGmEiDGDljRhxlntoAhGrb1gMkSupaFDjmYdXUGETEAxlpcu9xNM0jnDhwMVquxW0k6D1ief3RPlfevOljkrhJWu/p+B5Oxj5Gfwb8+R7pGwP7+KU/zbREdz4xb2j2UWyxrTNhLvitsCXEwPboAi/C+Zwmwi4ChfXVHR8A3jRG3HhO/6mCRK64Rpgb1QJJvIEbKNEC1pQvv1gjY8OTxqtKHVHtzt9By5ytlpCuA4aiIfaAMy/NIbj6a0yPjorvDwczczeZXPeyRXqQqZxAjR8K2P3D3BwE714m8S7PaZcXtb1kVmavptJR/Acwj1u3yMpjdAHDmBLAN6251sFmdOefm7D7CbilTzMtYNukYH7f3iyUCtqZz3L4nYPtxsmONh6ljooHmLseFVs2my1cNy5vVLQ6mEpcMsR/fdZmwsi4fKbSOMXLUYpqp0cDWdBmx7woYmNg4v5EMZzc5UwBYLhsDvrViOZNdzkzT3eJgdbZ7NLjzKYoEh3sEMDGEGLljy88mATD6iL8rYNijHFED44k6iIf4xGbz8aEUACY2Y/S8wcFKt/ugVZ1d+NTB5R4BDGKkiar0gK7PEbCSDtp83xC5V62AWCg3m3H462pl2IjW6rYcTGZ1myQ13MQVvpg07CHAMEaaPcCC/r6iE0l6E2Ddcb21YV0ArFMqV7cAtudgsXNZkhRJ4mZX+HowyYcYOfYjD4U5/t8yoo/4O5Ypun1wsAP5MbCVB2yFGX9+p4MNrhzrYMN9fN0GWIQxsjkCbIkGVleU439DwDq2n4qBeeWQzQNcQnUrAGzVyQkwYK7b/LuDFWOFPnLZNb7arH0IMB8j8yPAfCcyW5wA9rprn+fhTzkd1Ghy4uoKYCfRcdNETDYbjI5gX7Dzmm4C7KOR8oZeZDzfAnaFr0d7kWOMTA8J8zm++zx5Pf6q4cxy2fhptTxuaZbkm5cAA7rOHezevGg0RXE1PhaueAwwNR+MPIyR4UDRspFvA6xJ5QXAZNoQWBcAg/TrPxwPFj2Wg2GMjPf7kTioInQiT6sULwOsH89bOgXs3FME2I8ek78fI5Guz8/+0nBWXrRZFrOyxi23vvCWN3lVCVH1DTMVemw+nfOmqh5PsGzyHBuhVTZ9pUabyvE8gL7yNIV2fzKmqkyeUox8P2BfOSY/mkGM9LXWyg86rMrSH+o+c6BI6yzR3Bgc8SA12hkE175Z9suqgtC2RLb6fkqz+qZPBethio152lfNMthTniom0iU8D4+rFJ7v/XqyWTKVUpr/uxwMY6SSy8o7WFPOYrM+N9rQOxj0aCO4cxaHDUZTNmVSIKtfwnwE3OQsUgqcSoWEqkem4C5HcEygJ0+RK+njocCkS6XgXOB/iCgB9ssczMdIBjGy93SJSHrAzh0o8jmYdWzgc2bDqOgGWGHICLASwRSD3jJNUx8kcwDGOxos5QFiIYVvcKE+JFzgbKDgfzk4HltSlv+7HGyMkcVIF664Xq/P5fhbwJjN4vGaEvuAwa/vBCrvYBAP+3QErNoCVk0O5uNphbaXoyR6oV+WHOy3ORiLy/l8hl8hOa0ghIlNdB4wiXGy5Vkoux04GMbKKUWXU8jz0PgQaZAsb08G5vvRwcwEFPhfiKI0iOOXORhT8SwWB0V1eHCuL8etMTWedSY1np594mCsT6v9UleDDpbmQCEm+ZXM+7EGAY8b//wS4TPQkYzQ5pbTBkle77x80/NDZYS5FTCpVHRTbYBDHzKcGVtbdsbBgI7t+xbLFPqWANgSUrIKzatK0+XoTlXPoOOKHVDAu0/9AqO99RWBNel9F6CT4vnRpAffO/qyKxyOhSx9ftzjVKMIi8rxqSjkXWZXDPbxcueZe+/UUAq2T9j7LqFpYvEMYlLEB4i++BKarT47lOccHRNz+UFu1V/qKjY9YfUlAkLmTyg+4vPFgBXnDSw/E96aESVR7Tvq5X8equN/DV8QQKJndJSm//dXmSZwviNjj+toY3QZc9JbRYCRCDASAUYiEWAkAoxEgJFIBBiJACMRYCQSAUYiwEgEGO0KEgFGIsBIJAKMRICRCDASiQAjEWAkAoxEIsBIBBiJACORCDASAUYiPQaYfNt3Yd28ZUXXG/jhgBXWZsOFVZJbvp0dUTE6uQCSfGLLKF3QJ/ejAUt4UmRcPIGBNowJe4GDC1+eygoC7H8CmLce8YSDCX7tgqq2JQf7XwMm+RTbyiwrIJ7VpnD++5tN7ZJaT20uKUtWFqaumUhcjQuEaQwWiIqhNa79tyVBQ9aWfpGCZ8mA3ybukgliWKg2AJgJC+NDaCoGeP0SXxU3K3bvhwD74Q6W8XoeLKVubcYY11lhtWJCa+ArAFbypLVgdYm2WSK0bRMds3E6gw2AeIutSY28WmjgDrcqau7qkhXAoNaBsIFnbZ1BZA4Lz3VdaAuhVNvC8Rmbc1sn2u3eDwH2wwFTtea2ZP6rYTDaISExL1nNI7CzABher7zk8p/2zm3VdRCKom28oGiQiCL4kP//zLM0Xno7nMJOOXQzx0OlxZrQDJYzFiJZEctnpc750e51iqyCkULe0VBUvFwrjMewuT36vMyZvk2RR+cyO0caYXOlWiUSjA4RmB7nA8G+fpmCR8dEZJ6gq1s3WqCXstdCT0orXf1s2ltnqKNzo70R7PiGolF4f0Jw8STWHps7puQ8M1h9UTay/bIV7+hlKXZemR3nA8F+wTqYZIEu8pXQU7A0BdPOOLN3wXzpKEb7JNglM5JPTcFy7eHdcag47yKpM0807T4LJsb5QLAvF6wkI82yoPxzGNEE224q2G5sfV51fdv29xjts2AuzEf8F8FErVpmm3ets4IlR3HvhWDjfCDYdwsWWbDLRoFoc+KypFnBMos8jpC/+ZTVYcXCAtchj9ayKPi9YMante1TYpIsASvate0LshpK8uvoTB6r9YVg43wg2JdXsGAYo2t5kZ4xs6ohmEqM7vcOwbILqze+FbRYvrHM1jNy5kYwua1rcuwwLDAaq47dQhmncUma3nk3xgTzQrB+PhDs6zNYf5q+elhunf/ylPxFSX9+g9+1D9tFhDoXtqxFB3wc+/446q/7mSiJa/ZLQv6/cC7HYOKbvXe2xlyXHQAEewsd/LF4/x7XtNVFeADBAIBgAIIBAMEABAMQDAAIBv6nYFiiAh9AdcEE/oIBH0CLJpi1+DHA+VjbBJMCPwb4QASTQzDMkeB0ZJkhi2CcW5QwcHrEF7YLprVACgNnJzChRwXjEoaBs/0qCawLpmEY+IhfXTAybEHSB2fl+6X71QQrNcyKxUqs6YOfhntpF2HJL30nWClilhy7AvAjyC7SS3evhmBkmJYAnICeft0IRiVNA3AGUyr+B7VMa3t176bVAAAAAElFTkSuQmCC "Sample form metadata") ## 3. Add your workflow to your manifest {#manifest-workflow} With a workflow defined and steps outlined, it's time to make this an official part of your app! Add the workflow definition to your manifest as in the following example: ``` // manifest.tsimport { Manifest } from "deno-slack-sdk/mod.ts";import GreetingWorkflow from "./workflows/greeting_workflow.ts";export default Manifest({ name: "deno-hello-world", description: "A sample that demonstrates using a function, workflow and trigger to send a greeting", icon: "assets/default_new_app_icon.png", workflows: [GreetingWorkflow], outgoingDomains: [], botScopes: ["commands", "chat:write", "chat:write.public"],}); ``` ✨ **To learn more about workflows**, check out the [workflows](/tools/deno-slack-sdk/guides/creating-workflows) page. ## 4. Add a trigger to kick off your workflow {#add-trigger} Let's add the needed momentum to your workflow and create a [link trigger](/tools/deno-slack-sdk/guides/creating-link-triggers#create-cli__create-a-link-trigger-with-a-trigger-file). In the trigger definition, add `interactivity` as an input value. This value holds context about the user interactivity that invoked this trigger, and passes it along to your workflow. In a separate file, define your trigger in the following way: ``` // triggers/greeting_trigger.tsimport { Trigger } from "deno-slack-sdk/types.ts";import { TriggerContextData, TriggerTypes } from "deno-slack-api/mod.ts";import GreetingWorkflow from "../workflows/greeting_workflow.ts";const greetingTrigger: Trigger = { type: TriggerTypes.Shortcut, name: "Send a greeting", description: "Send greeting to channel", workflow: `#/workflows/${GreetingWorkflow.definition.callback_id}`, inputs: { interactivity: { value: TriggerContextData.Shortcut.interactivity, }, channel: { value: TriggerContextData.Shortcut.channel_id, }, },};export default greetingTrigger; ``` Run the following CLI command to create the link trigger: ``` $ slack trigger create --trigger-def triggers/greeting_trigger.ts...⚡ Trigger created Trigger ID: Ft0123ABC456 Trigger Type: shortcut Trigger Name: Send a greeting URL: https://slack.com/shortcuts/Ft0123ABC456/c001a02b13c42de35f47b55a89aad33c ``` You now have a shortcut `URL` to share in a channel or save as a bookmark, which allows you to kick off your workflow and open your form. ✨ **To learn more about starting workflows with triggers**, head to the [triggers overview](/tools/deno-slack-sdk/guides/using-triggers) page. --- Source: https://docs.slack.dev/tools/deno-slack-sdk/guides/creating-an-app # Creating an app Workflow apps require a paid plan Join the [Developer Program](https://api.slack.com/developer-program) and provision a sandbox with access to all Slack features for free. An app goes through stages of development, from creation to experimentation and development to production. Sometimes a [removal](/tools/slack-cli/guides/removing-an-app) happens too. The [Slack CLI](/tools/slack-cli) provides a set of commands to make managing these stages a bit easier with the following offerings: * `slack create` to [create a brand new app](#create-app) * `slack app link` to [link an existing app to a project](#link-app) ## Verify workspace authentication {#verify} Before you can create (or remove) an app, ensure your CLI is authenticated into the workspace you want to develop in. You can do so with the `slack auth list` command: ``` $ slack auth listmyworkspace (Team ID: T123456789)User ID: U123456789Last update: 2022-03-24 18:20:47 -07:00Authorization Level: WorkspaceTo change your active workspace authorization run slack login ``` ## Create an app {#create-app} With your CLI authenticated into the workspace you want to develop in, the next step is to scaffold an app with the `slack create` command: ``` $ slack create my-app ``` The above command will scaffold a new app called `my-app` in a directory with the same name. If you don't pass an app name, `slack` will scaffold an app with a random alphanumeric name. You will be presented with three options to build from: * The introductory [Issue Submission](https://github.com/slack-samples/deno-issue-submission) app * The scaffolded [Deno Starter Template](https://github.com/slack-samples/deno-starter-template) * The completely blank [Deno Blank Template](https://github.com/slack-samples/deno-blank-template) If you'd like to build from a specific sample app, see [Create an app from a template](#templates) below. ``` ? Select a template to build from: [Use arrows to move]> Issue submission (default sample) Basic app that demonstrates an issue submission workflow Scaffolded project Solid foundation that includes a Slack datastore Blank project A, well.. blank project View more samples Guided tutorials can be found at docs.slack.dev/samples ``` Once you select an option the Slack CLI will get you set up for success. ``` $ slack create my-app⚙️ Creating a new Slack app in ~/programming/my-app📦 Installed project dependencies✨ my-app successfully created🧭 Explore the documentation to learn more Read the README.md or peruse the docs over at docs.slack.dev/deno-slack-sdk Find available commands and usage info with `slack help`📋 Follow the steps below to begin development Change into your project directory with `cd my-app` Develop locally and see changes in real-time with `slack run` When you're ready to deploy for production use `slack deploy` Create a trigger to invoke your workflows `slack trigger create` ``` After creating an app, don't forget to `cd` into your app project's directory. ➡️ **To keep building your own app**, learn about your app's manifest in the [manifest](/tools/deno-slack-sdk/guides/using-the-app-manifest) section. ⤵️ **To use a sample app as a template instead**, read on! ### Create an app from a template {#templates} Evaluate third-party apps Exercise caution before trusting third-party and open source applications and automations (those outside of [`slack-samples`](https://github.com/slack-samples)). Review all source code created by third-parties before running `slack create` or `slack deploy`. We have a [collection of sample apps](https://github.com/slack-samples) containing a bevy of use cases. Find one particularly suited for your needs? Great! You can use it as a template to build from. Create an app from a template by using the `create` command with the `--template` (or `-t`) flag and passing the link to the template's GitHub repo. For example, the following command creates an app using our [Welcome Bot](https://github.com/slack-samples/deno-welcome-bot) app as a template: ``` slack create my-welcome-bot-app -t https://github.com/slack-samples/deno-welcome-bot ``` #### Create an app from a specific branch {#branch} Use a specific branch of a template repo by using the `--branch` (or `-b`) flag and passing the name of a branch: ``` slack create my-welcome-bot-app -t https://github.com/slack-samples/deno-welcome-bot -b main ``` * * * ## Link an app {#link-app} Apps that were created without the CLI can still be used with the CLI for ease in app management. This requires the `app link` command to save information about the app with the project it exists on: ``` slack app link --app A0123456789 --team T0123456789 --environment "deployed" ``` ``` 🏠 An existing app was added to the project my-workspace: App ID: A0123456789 Team ID: T0123456789 Status: Installed ``` This command can be used without flags, but the above example would use the app ID "A01234567890" from team "T01234567890" - the team the app was created on - as a "deployed" app. Following selections in commands will reveal this app as an option and it's all set for CLI use! Another `--environment` option is "local" and this saves apps to `.slack/apps.dev.json`. Local apps are intended for personal development and experimentation while "deployed" apps - saved to `.slack/apps.json` - serve the needs of production. Just one app ID can exist for each combination of team ID and environment to avoid accidental selections with duplicated possibilities. The provided app ID must also exist for this `link` command to complete with success. * * * ## Onward {#onward} Your wish is our command! For information about other actions you can perform from the CLI, refer to the [Slack CLI commands guide](/tools/slack-cli/guides/running-slack-cli-commands). Check out more about how to configure your app via the [manifest](/tools/deno-slack-sdk/guides/using-the-app-manifest). You can also start building [functions](/tools/deno-slack-sdk/guides/creating-slack-functions) to perform some logic, chain them together in [workflows](/tools/deno-slack-sdk/guides/creating-workflows), and create [triggers](/tools/deno-slack-sdk/guides/using-triggers) to invoke those workflows. --- Source: https://docs.slack.dev/tools/deno-slack-sdk/guides/creating-an-interactive-message # Creating an interactive message Workflow apps require a paid plan Join the [Developer Program](https://api.slack.com/developer-program) and provision a sandbox with access to all Slack features for free. Interactive messages are messages containing interactive Block Kit elements. Send interactive messages to users to collect dynamic input from users, and use that input to kick off other parts of your workflows. Interactive messages are created with Block Kit, and have their interactions reflected by Block Kit action events. This page will guide you through adding Block Kit interactivity to your app's message. ✨ **To learn more about Block Kit**, refer to [Building with Block Kit](https://docs.slack.dev/block-kit/) and [Interactivity in Block Kit](https://docs.slack.dev/block-kit/#making-things-interactive). ## 1. Create the function {#create-function} Let's look at the example in the [Deno Request Time Off app](https://github.com/slack-samples/deno-request-time-off). It contains a workflow where one step is sending a message with two button options: **"Approve"** and **"Deny"**. When someone clicks either button, our app will handle these button interactions (which are composed in [Block Kit Actions](https://docs.slack.dev/reference/block-kit/blocks/actions-block/)) and update the employee with notice that their request was either approved or denied. First, we'll look at the function definition for [SendTimeOffRequestToManagerFunction](https://github.com/slack-samples/deno-request-time-off/blob/main/functions/send_time_off_request_to_manager/definition.ts) that defines the inputs that will appear in the message, and the outputs from the approver's interaction with the message: ``` import { DefineFunction, Schema } from "deno-slack-sdk/mod.ts";/** * Custom function that sends a message to the user's manager asking for approval * for the time off request. The message includes some Block Kit with two interactive * buttons: one to approve, and one to deny. */export const SendTimeOffRequestToManagerFunction = DefineFunction({ callback_id: "send_time_off_request_to_manager", title: "Request Time Off", description: "Sends your manager a time off request to approve or deny", source_file: "functions/send_time_off_request_to_manager/mod.ts", input_parameters: { properties: { interactivity: { type: Schema.slack.types.interactivity, }, employee: { type: Schema.slack.types.user_id, description: "The user requesting the time off", }, manager: { type: Schema.slack.types.user_id, description: "The manager approving the time off request", }, start_date: { type: Schema.slack.types.date, description: "Time off start date", }, end_date: { type: Schema.slack.types.date, description: "Time off end date", }, reason: { type: Schema.types.string, description: "The reason for the time off request", }, }, required: [ "employee", "manager", "start_date", "end_date", "interactivity", ], }, output_parameters: { properties: {}, required: [], },}); ``` ## 2. Add interactive Block Kit elements {#add-block-kit} Using Block Kit, you can build a message layout that contains two button: **"Approve"** and **"Deny"**. To keep our app tidy, we have the implementation in a [separate file](https://github.com/slack-samples/deno-request-time-off/blob/main/functions/send_time_off_request_to_manager/mod.ts). Here is the first part that creates the blocks: ``` import { SendTimeOffRequestToManagerFunction } from "./definition.ts";import { SlackFunction } from "deno-slack-sdk/mod.ts";import { APPROVE_ID, DENY_ID } from "./constants.ts";import timeOffRequestHeaderBlocks from "./blocks.ts";// Custom function that sends a message to the user's manager asking// for approval for the time off request. The message includes some Block Kit with two// interactive buttons: one to approve, and one to deny.export default SlackFunction( SendTimeOffRequestToManagerFunction, async ({ inputs, client }) => { console.log("Forwarding the following time off request:", inputs); // Create a block of Block Kit elements composed of several header blocks // plus the interactive approve/deny buttons at the end const blocks = timeOffRequestHeaderBlocks(inputs).concat([{ "type": "actions", // This is the type of layout block; learn more about other layout blocks types at https://docs.slack.dev/reference/block-kit/blocks "block_id": "approve-deny-buttons", "elements": [ { type: "button", text: { type: "plain_text", text: "Approve", }, action_id: APPROVE_ID, // <-- important! we will differentiate between buttons using these IDs style: "primary", }, { type: "button", text: { type: "plain_text", text: "Deny", }, action_id: DENY_ID, // <-- important! we will differentiate between buttons using these IDs style: "danger", }, ], }]); // To be continued in the next step... ``` ## 3. Add the message functionality {#post} There are two Block Kit parameters that your Block Kit element will use for interactivity with other aspects of your workflow: * The `action_id` property. This uniquely identifies a particular interactive component. This will be used to route the interactive callback to the correct handler when an interaction happens on that element. * The `block_id` property. This uniquely identifies the entire Block Kit element. Then, we can use the provided Slack client in the function handler to call the [`chat.postMessage`](https://docs.slack.dev/reference/methods/chat.postMessage) method directly to post our message. The message will contain two buttons the user can interact with: one for **"Approve"** and one for **"Deny"**. ``` // Send the message to the manager with the Slack client const msgResponse = await client.chat.postMessage({ channel: inputs.manager, blocks, // Fallback text to use when rich media can't be displayed (i.e. notifications) as well as for screen readers text: "A new time off request has been submitted", }); if (!msgResponse.ok) { console.log("Error during request chat.postMessage!", msgResponse.error); } // IMPORTANT! Set `completed` to false in order to keep the interactivity // points (the approve/deny buttons) "alive" // We will set the function's complete state in the button handlers below. return { completed: false, }; }, // Create an 'actions handler', which is a function that will be invoked // when specific interactive Block Kit elements (like buttons!) are interacted // with.)// To be completed in the next step... ``` We return `completed: false` here to ensure the function execution does not complete until the interactivity is complete. The function execution will be completed in the action handler in the next section. ## 4. Add a Block Kit handler to respond to Block Kit element interactions {#blockkit} Now that we have some interactive components to listen for, let's define a handler to react to interactions with these components. There are two Block Kit handlers: * the action handler * the suggestions handler ### Using the Block actions handler {#block-actions-handler} When the interactive components are used in a function, we use `addBlockActionsHandler` chained onto the function to handle what happens after the interaction. In the same function source file (and "chaining" off our function implementation), we'll define a handler that will listen for actions performed on one of the two interactive components (`APPROVE_ID` and `DENY_ID`) that we'll attach to the message using the [`addBlockActionsHandler`](https://docs.slack.dev/reference/interaction-payloads/block_actions-payload) helper method. ``` // ... continued from the step above.addBlockActionsHandler( // listen for interactions with components with the following action_ids [APPROVE_ID, DENY_ID], // interactions with the above two action_ids get handled by the function below async function ({ action, body, client }) { console.log("Incoming action handler invocation", action); const approved = action.action_id === APPROVE_ID; // Send manager's response as a message to employee const msgResponse = await client.chat.postMessage({ channel: body.function_data.inputs.employee, blocks: [{ type: "context", elements: [ { type: "mrkdwn", text: `Your time off request from ${body.function_data.inputs.start_date} to ${body.function_data.inputs.end_date}` + `${ body.function_data.inputs.reason ? ` for ${body.function_data.inputs.reason}` : "" } was ${ approved ? " :white_check_mark: Approved" : ":x: Denied" } by <@${body.user.id}>`, }, ], }], text: `Your time off request was ${approved ? "approved" : "denied"}!`, }); if (!msgResponse.ok) { console.log( "Error during requester update chat.postMessage!", msgResponse.error, ); } ``` The final piece is to update the manager's message to remove the buttons and reflect the approval state: ``` // Update the manager's message to remove the buttons and reflect the approval // state. Nice little touch to prevent further interactions with the buttons // after one of them were clicked. const msgUpdate = await client.chat.update({ channel: body.container.channel_id, ts: body.container.message_ts, blocks: timeOffRequestHeaderBlocks(body.function_data.inputs).concat([ { type: "context", elements: [ { type: "mrkdwn", text: `${ approved ? " :white_check_mark: Approved" : ":x: Denied" }`, }, ], }, ]), }); if (!msgUpdate.ok) { console.log("Error during manager chat.update!", msgUpdate.error); } // And now we can mark the function as 'completed' - which is required as // we explicitly marked it as incomplete in the main function handler. await client.functions.completeSuccess({ function_execution_id: body.function_data.execution_id, outputs: {}, }); },); ``` Remember to mark the function as completed. This is required since we explicitly marked it as incomplete in the main function handler previously. ### Using the Block suggestion handler {#block-suggestion-handler} Use `addBlockSuggestionHandler` to respond to events that are uniquely created by the [select menu of external data source](https://docs.slack.dev/reference/block-kit/block-elements/multi-select-menu-element#external_multi_select) interactive Block element. Similarly implemented as the Block actions handler above, a user would create a block with the [select menu of external data source](https://docs.slack.dev/reference/block-kit/block-elements/multi-select-menu-element#external_multi_select) element, then chain the handler onto their function. Let's take a look at an example; this one posts an inspirational quote. Once invoked, this function will post a message with a drop-down select menu and a button. The options rendered in the select menu will be dynamically loaded from an external API. Here is the function definition: ``` import { DefineFunction, Schema } from "deno-slack-sdk/mod.ts";export const QuoteFunction = DefineFunction({ callback_id: "quote", title: "Inspire Me", description: "Get an inspirational quote", source_file: "functions/quote/mod.ts", // <-- important! Make sure this is where the logic for your function - which we will write in the next section - exists. input_parameters: { properties: { requester_id: { type: Schema.slack.types.user_id, description: "Requester", }, channel_id: { type: Schema.slack.types.channel_id, description: "Channel", }, }, required: [ "requester_id", "channel_id", ], }, output_parameters: { properties: { quote: { type: Schema.types.string, description: "Quote", }, }, required: ["quote"], },}); ``` With `QuoteFunction` defined, we can add the interactive elements: ``` import { SlackFunction } from "deno-slack-sdk/mod.ts";// QuoteFunction is the function we defined in the previous sectionimport { QuoteFunction } from "./definition.ts";export default SlackFunction(QuoteFunction, async ({ inputs, client }) => { console.log("Incoming quote request!"); await client.chat.postMessage({ channel: inputs.channel_id, blocks: [{ "type": "actions", "block_id": "so-inspired", "elements": [{ type: "external_select", placeholder: { type: "plain_text", text: "Inspire", }, action_id: "ext_select_input", }, { type: "button", text: { type: "plain_text", text: "Post", }, action_id: "post_quote", }], }], }); // Important to set completed: false! We should set the function's complete // status later - in the action handler responding to the button click return { completed: false, };}); ``` If this feels familiar to the Block actions handler example above, it's because it is! In the same way, we can then chain `addBlockSuggestionHandler` onto the function just as we did with `addBlockActionsHandler`: ``` export default SlackFunction(QuoteFunction, async ({ inputs, client }) => { // ... the rest of your QuoteFunction logic here ...}).addBlockSuggestionHandler( "ext_select_input", // The first argument to addBlockActionsHandler can accept an action_id string, among many other formats! // Check the API reference at the end of this document for the full list of supported options async ({ body, client }) => { // The second argument is the handler function itself console.log("Incoming suggestion handler invocation", body); // Fetch some inspirational quotes const apiResp = await fetch( "https://motivational-quote-api.herokuapp.com/quotes", ); const quotes = await apiResp.json(); console.log("Returning", quotes.length, "quotes"); const opts = { "options": quotes.map((q) => ({ value: `${q.id}`, text: { type: "plain_text", text: q.quote.slice(0, 70) }, })), }; return opts; },); ``` Using the example above, you could next code what happens after the button click, such as posting the selection to the channel. ## Handling errors {#errors} It's important to validate the input data you receive from the user. 1. First, validate that the user is authorized to pass the input. 2. Second, validate that the user is passing a value you expect to receive, and nothing more. ## Onward {#onward} Now you have some interactivity weaved within your app, hooray! 💻 **For an expanded version of the sample code provided above**, check out our [Request Time Off sample app](https://github.com/slack-samples/deno-request-time-off). ✨ **To learn more about leveraging built-in powers or defining your own**, check out [Slack functions](/tools/deno-slack-sdk/guides/creating-slack-functions) and [custom functions](/tools/deno-slack-sdk/guides/creating-custom-functions). ✨ **For more details about handling events**, check out [creating an interactive modal](/tools/deno-slack-sdk/guides/creating-an-interactive-modal). --- Source: https://docs.slack.dev/tools/deno-slack-sdk/guides/creating-an-interactive-modal # Creating an interactive modal Workflow apps require a paid plan Join the [Developer Program](https://api.slack.com/developer-program) and provision a sandbox with access to all Slack features for free. A [modal](https://docs.slack.dev/surfaces/modals) is similar to an alert box, pop-up, or dialog box within Slack. Modals capture and maintain focus within Slack until the user submits or closes the modal. This makes them a powerful piece of app functionality for engaging with users. Interactive modals are modals containing interactive [Block Kit elements](https://docs.slack.dev/block-kit/). Modals have a larger catalog of available interactive Block Kit elements than messages. Modals can be opened via a Block Kit interaction or a [link trigger](/tools/deno-slack-sdk/guides/creating-link-triggers). A modal is updated by View events (close and submit) to reflect the user's inputs as they interact with the modal. This guide will use the an example file from our [deno-code-snippets](https://github.com/slack-samples/deno-code-snippets) repository. ✨ **If you'd like a full sample app that uses modal interactivity**, check out the [Simple Survey sample app](https://github.com/slack-samples/deno-simple-survey). ## Add interactivity to your function definition {#add-interactivity} A function needs to have an `interactivity` parameter added to have interactive functionality. The `interactivity` parameter is required to ensure users don't experience any unexpected or unwanted modals appearing—only their interaction can open a modal. The `interactivity` parameter is short-lived for this same reason, meaning as a developer you will need to keep grabbing a new one from the user as continued consent to modal views and updates. For modals, `interactivity` takes the form of the unique identifier `interactivity_pointer`. There are two different ways to retrieve and consume an `interactivity_pointer` when working with modals. 1. When opening a modal view via [link trigger](/tools/deno-slack-sdk/guides/creating-link-triggers), add a property with the type `Schema.slack.types.interactivity` to the `properties` object within a function's `input_parameters`. Your function can then access that interactivity event via your function argument's `inputs.interactivity.interactivity_pointer`. Note that in this example, the function argument is named `interactivity`, but you may choose to name it anything, so long as you use that name to access the `interactivity_pointer`. 2. When opening or updating a modal view from a block or view event, use the `interactivity_pointer` provided as part of the `body` of the block or view event payload, _not_ from the `inputs` parameter. Your function can access it via `body.interactivity.interactivity_pointer`. You will also see an example of this in the [Opening a modal based on a Block Kit action](#open-block-kit-action) section below. In our example file [`/Block_Kit_Modals/functions/demo.ts`](https://github.com/slack-samples/deno-code-snippets/blob/main/Block_Kit_Modals/functions/demo.ts), `interactivity` is added to the function's input parameters, since the modal is opened via link trigger: ``` // /functions.demo.tsimport { DefineFunction, Schema, SlackFunction } from "deno-slack-sdk/mod.ts";export const def = DefineFunction({ callback_id: "block-kit-modal-demo", title: "Block Kit modal demo", source_file: "Block_Kit_Modals/functions/demo.ts", input_parameters: { properties: { interactivity: { type: Schema.slack.types.interactivity } }, required: ["interactivity"], }, output_parameters: { properties: {}, required: [] },});// To be continued ... ``` ## Build a modal view {#build} Modal views are constructed partially using [Block Kit](https://docs.slack.dev/block-kit/) pieces. That view will then be placed within an API call later on. Below is our example modal: ``` view: { "type": "modal", // Note that this ID can be used for dispatching view_submission and view_closed events. "callback_id": "first-page", // This option is required to be notified when this modal is closed by the user "notify_on_close": true, "title": { "type": "plain_text", "text": "My App" }, // Not all modals need a submit button, but since we want to collect input, we do "submit": { "type": "plain_text", "text": "Next" }, "close": { "type": "plain_text", "text": "Close" }, "blocks": [ { "type": "input", "block_id": "first_text", "element": { "type": "plain_text_input", "action_id": "action" }, "label": { "type": "plain_text", "text": "First" }, }, ],}, ``` Check out [Using the block suggestion handler](/tools/deno-slack-sdk/guides/creating-an-interactive-message#block-suggestion-handler) on the [Interactive messages](/tools/deno-slack-sdk/guides/creating-an-interactive-message) page to learn how to use the Block Kit element [select menu of external data source](https://docs.slack.dev/reference/block-kit/block-elements/multi-select-menu-element#external_multi_select). Its use in modals and messages is similar. ## Open a modal within your function {#open} With interactivity added to the function definition, we can open the interactive modal view. A view is opened using the [`views.open`](https://docs.slack.dev/reference/methods/views.open) method. A modal view can be opened based on either of the following, causing your function to run: * a trigger (for example, clicking on a link trigger) * a previous Block Kit action (for example, clicking a button in a message) Our example uses the first method. Some important considerations to note so that you can ensure your modal isn't left floating in the vast sea of suspended modals: 1. Take note of the `callback_id`. We'll use it to define modal view handlers that react to `view_open` or `view_closed` events later. 2. Set `notify_on_close` to `true` in order to trigger a `view_closed` event. ### Open a modal based on a trigger {#open-trigger} View this example in [demo.ts](https://github.com/slack-samples/deno-code-snippets/blob/cb432c83a539b9675e3dc7d9ec7c641c68a62a93/Block_Kit_Modals/functions/demo.ts): ``` // demo.tsexport default SlackFunction( def, // --------------------------- // The first handler function that opens a modal. // This function can be called when the workflow executes the function step. // --------------------------- async ({ inputs, client }) => { // Open a new modal with the end-user who interacted with the link trigger const response = await client.views.open({ interactivity_pointer: inputs.interactivity.interactivity_pointer, view: { "type": "modal", // Note that this ID can be used for dispatching view_submission and view_closed events. "callback_id": "first-page", // This option is required to be notified when this modal is closed by the user "notify_on_close": true, "title": { "type": "plain_text", "text": "My App" }, "submit": { "type": "plain_text", "text": "Next" }, "close": { "type": "plain_text", "text": "Close" }, "blocks": [ { "type": "input", "block_id": "first_text", "element": { "type": "plain_text_input", "action_id": "action" }, "label": { "type": "plain_text", "text": "First" }, }, ], }, }); if (response.error) { const error = `Failed to open a modal in the demo workflow. Contact the app maintainers with the following information - (error: ${response.error})`; return { error }; } return { // To continue with this interaction, return false for the completion completed: false, }; },) ``` Make sure to set the return to `completed: false`. You'll then set it to `true` later in your modal view event handler. ### Open a modal based on a Block Kit action {#open-block-kit-action} Alternatively, a modal view can be opened using a Block Kit action handler. Below is the code structure for doing so: ``` export default SlackFunction(ConfigureEventsFunctionDefinition, async ({ inputs, client }) => {// "my_button" is the action_id of the Block element from which the action originated).addBlockActionsHandler(["my_button"], async ({ body, client }) => { const openingModal = await client.views.open({ interactivity_pointer: body.interactivity.interactivity_pointer, view, }); if (openingModal.error) { return await client.functions.completeError({ function_execution_id: body.function_data.execution_id, error}); }}); ``` ## Update the modal view {#update} With your defined modal view equipped with a `callback_id`, you can implement a modal view event handler to respond to interactions with your modal view. To respond to a `view_submission` event (the action of the user clicking the **Submit** button in your modal), use [`addViewSubmissionHandler`](https://github.com/slackapi/deno-slack-sdk/blob/main/docs/functions-view-handlers.md#addviewsubmissionhandlerconstraint-handler). The handler can update or push a view in two ways: * by making a call to the [`views.update`](https://docs.slack.dev/reference/methods/views.update) API method or the [`views.push`](https://docs.slack.dev/reference/methods/views.push) API method. * by setting the [`response_action`](https://docs.slack.dev/surfaces/modals#updating_views) property on the object returned by your interactivity handler. In addition to the `view_submission` and `view_closed` events, you can also update views using the `block_actions` and `options` events via the Block Kit action and suggestion handlers, respectively. Refer to [Add a Block Kit handler to respond to Block Kit element interactions](/tools/deno-slack-sdk/guides/creating-an-interactive-message#blockkit) for more details. In the examples below, the [`addViewSubmissionHandler`](https://github.com/slackapi/deno-slack-sdk/blob/main/docs/functions-view-handlers.md#addviewsubmissionhandlerconstraint-handler) method registers a handler to push a new view on to the [view stack](https://docs.slack.dev/surfaces/modals#lifecycle). The first code snippet shows how to push a new view by calling `views.push`: ``` // ....addViewSubmissionHandler( "first-page", // The callback_id of the modal async ({ inputs, client, body }) => { const response = await client.views.push({ interactivity_pointer: body.interactivity.interactivity_pointer, view, }); },)// ... ``` The second code snippet shows how to use `response_action` to do the same thing. Both result in identical behavior! ``` // ....addViewSubmissionHandler( "first-page", // The callback_id of the modal async () => { return { response_action: "push", view, }; },)// ... ``` In our example, we'll be using the second way—updating `response_action`—to provide a second modal view when the first modal data is submitted. ### Example: updating with a new interactive modal {#update-interactive} In this example, notice how we extract the input values from the prior view using `view.state.values`. This is a property of the [view interaction payload](https://docs.slack.dev/reference/interaction-payloads/view-interactions-payload). ``` // --------------------------- // The handler that can be called when the above modal data is submitted. // It saves the inputs from the first page as private_metadata, // and then displays the second-page modal view. // --------------------------- .addViewSubmissionHandler(["first-page"], ({ view }) => { // Extract the input values from the view data const firstText = view.state.values.first_text.action.value; // Input validations if (firstText.length < 20) { return { response_action: "errors", // The key must be a valid block_id in the blocks on a modal errors: { first_text: "Must be 20 characters or longer" }, }; } // Successful. Update the modal with the second page presentation return { response_action: "update", view: { "type": "modal", "callback_id": "second-page", // This option is required to be notified when this modal is closed by the user "notify_on_close": true, "title": { "type": "plain_text", "text": "My App" }, "submit": { "type": "plain_text", "text": "Next" }, "close": { "type": "plain_text", "text": "Close" }, // Hidden string data, which is not visible to end-users // You can use this property to transfer the state of interaction // to the following event handlers. // (Up to 3,000 characters allowed) "private_metadata": JSON.stringify({ firstText }), "blocks": [ // Display the inputs from "first-page" modal view { "type": "section", "text": { "type": "mrkdwn", "text": `First: ${firstText}` }, }, // New input block to receive text { "type": "input", "block_id": "second_text", "element": { "type": "plain_text_input", "action_id": "action" }, "label": { "type": "plain_text", "text": "Second" }, }, ], }, }; }) ``` ### Example: updating with a static confirmation modal {#update-static} ``` // --------------------------- // The handler that can be called when the second modal data is submitted. // It displays the completion page view with the inputs from // the first and second pages. // --------------------------- .addViewSubmissionHandler(["second-page"], ({ view }) => { // Extract the first-page inputs from private_metadata const { firstText } = JSON.parse(view.private_metadata!); // Extract the second-page inputs from the view data const secondText = view.state.values.second_text.action.value; // Displays the third page, which tells the completion of the interaction return { response_action: "update", view: { "type": "modal", "callback_id": "completion", // This option is required to be notified when this modal is closed by the user "notify_on_close": true, "title": { "type": "plain_text", "text": "My App" }, // This modal no longer accepts further inputs. // So, the "Submit" button is intentionally removed from the view. "close": { "type": "plain_text", "text": "Close" }, // Display the two inputs "blocks": [ { "type": "section", "text": { "type": "mrkdwn", "text": `First: ${firstText}` }, }, { "type": "section", "text": { "type": "mrkdwn", "text": `Second: ${secondText}` }, }, ], }, }; }) ``` ## Success: closing a modal {#close-modal} To respond to a `view_closed` event (the action of the user clicking the **Close** button on your modal), use [`addViewClosedHandler`](https://docs.slack.dev/reference/interaction-payloads/view-interactions-payload#view_closed) and add a call to the [`functions.completeSuccess`](https://docs.slack.dev/reference/methods/functions.completeSuccess) method to explicitly mark the function as complete like this: ``` // --------------------------- // The handler that can be called when the second modal data is closed. // If your app runs some resource-intensive operations on the backend side, // you can cancel the ongoing process and/or tell the end-user // what to do next in DM and so on. // --------------------------- .addViewClosedHandler( ["first-page", "second-page", "completion"], ({ view }) => { console.log(`view_closed handler called: ${JSON.stringify(view)}`); return await client.functions.completeSuccess({ function_execution_id: body.function_data.execution_id, outputs: {}, }); }, ); ``` Remember, for an app to receive `view_closed` events, the view must set the `notify_on_close` option to `true` when it is initially opened or updated. ## Error: handling an error {#errors} If the function execution was not successful, you can add a call to the [`functions.completeError`](https://docs.slack.dev/reference/methods/functions.completeError) method to raise an error like so: ``` const response = await client.functions.completeError({ function_execution_id: body.function_data.execution_id, error: "Error completing function",}); ``` Once you have opened a modal and handled your modal views, you may decide that you'd like to display any potential data validation error messages to your users. It is important to validate the inputs you receive from the user: first, that the user is authorized to pass the input, and second, that the user is passing a value you expect to receive and nothing more. As long as your submission handler returns an error object defined [on this page](https://docs.slack.dev/surfaces/modals#displaying_errors), the error messages you include in that object will be displayed right next to the relevant form fields based on their field IDs. ## Stop a workflow {#stop-workflow} As discussed earlier, a function either completes successfully or fails with an error — and it's best practice to handle those events. However, there may be some cases in which you would like to stop a workflow early as a "quick fix" without necessarily calling [`functions.completeSuccess`](https://docs.slack.dev/reference/methods/functions.completeSuccess) or [`functions.completeError`](https://docs.slack.dev/reference/methods/functions.completeError). For example, when handling a modal view that the user closes prematurely: * the drawback with calling `functions.completeSuccess` in this scenario is that the rest of the functions in your workflow now require additional logic to handle undefined or null outputs. * the drawback with calling `functions.completeError` in this scenario is that when the user closes the modal prematurely (for example, they realize they don't have time to enter all the required details for the modal inputs), then all your admins are pinged by SlackBot with the resulting error. So, what to do instead? Well, essentially, you can do nothing at all: ``` export default SlackFunction(..., ...) .addViewClosedHandler("first-page", () => ({ client, body }) { // clean up stuff console.log('user closed modal view prematurely'); // do nothing }) ``` With the above solution, the modal view closes, an entry in your activity log is made (when `console.log` is called), and the workflow simply doesn't continue on. That said, it's generally best practice to handle function successes and errors when possible to ensure things are tidied up and there are no functions left hanging in the ether. ## Onward {#onward} You now have some shiny new modal views weaved within your app, and are on a course to providing a wonderful user experience. ✨ **To learn more about other interactivity options**, refer to the [Interactivity overview](/tools/deno-slack-sdk/guides/adding-interactivity). --- Source: https://docs.slack.dev/tools/deno-slack-sdk/guides/creating-connector-functions # Creating connector functions Connectors are step functions for workflows that behave like [Slack functions](/tools/deno-slack-sdk/guides/creating-slack-functions) for services external to Slack. They take inputs and perform work for you when added as steps to your [workflows](/tools/deno-slack-sdk/guides/creating-workflows). We recommend understanding the systems and APIs you're integrating with before setup. To protect your organization, external users (those outside your organization connected through Slack Connect) cannot use a workflow that contains connector functions built by your organization. This may manifest in a `home_team_only` warning. Refer to [this help center article](https://slack.com/help/articles/14844871922195-Slack-administration--Manage-workflow-usage-in-Slack-Connect-conversations#enterprise-grid-1) for more details. [Browse all Connector functions here](/tools/deno-slack-sdk/reference/connector-functions) --- Source: https://docs.slack.dev/tools/deno-slack-sdk/guides/creating-custom-functions # Creating custom functions Workflow apps require a paid plan Join the [Developer Program](https://api.slack.com/developer-program) and provision a sandbox with access to all Slack features for free. Custom functions are how you define custom workflow steps. They have three main components: * **Inputs**, which can come from a workflow's [trigger](/tools/deno-slack-sdk/guides/using-triggers) or the outputs of a previous step * **Logic**, which is your own code that carries out your instructions, * **Outputs**, which allows your function to pass on the result of its computations to follow-on steps in Workflow Builder To protect your organization, external users (those outside your organization connected through Slack Connect) cannot use a workflow that contains [connector steps](/tools/deno-slack-sdk/reference/connector-functions) or [workflow steps](https://docs.slack.dev/workflows/workflow-steps) built by your organization. This may manifest in a `home_team_only` warning. Refer to [this help center article](https://slack.com/help/articles/14844871922195-Slack-administration--Manage-workflow-usage-in-Slack-Connect-conversations#enterprise-grid-1) for more details. ## Define a custom function {#define} Functions are defined via the `DefineFunction` method, which is part of the [Slack SDK](https://github.com/slackapi/deno-slack-sdk) that is included with every newly-created project. Both the definition and implementation for your functions should live in the same file, so to keep your app organized, put all your function files in a `functions` folder in your app's root folder. Let's take a look at the [`greeting_function.ts`](https://github.com/slack-samples/deno-hello-world/blob/main/functions/greeting_function.ts) within the [Hello World](https://github.com/slack-samples/deno-hello-world) sample app: ``` // /slack-samples/deno-hello-world/functions/greeting_function.tsimport { DefineFunction, Schema, SlackFunction } from "deno-slack-sdk/mod.ts";export const GreetingFunctionDefinition = DefineFunction({ callback_id: "greeting_function", title: "Generate a greeting", description: "Generate a greeting", source_file: "functions/greeting_function.ts", input_parameters: { properties: { recipient: { type: Schema.slack.types.user_id, description: "Greeting recipient", }, message: { type: Schema.types.string, description: "Message to the recipient", }, }, required: ["message"], }, output_parameters: { properties: { greeting: { type: Schema.types.string, description: "Greeting for the recipient", }, }, required: ["greeting"], },}); ``` Note that we import `DefineFunction`, which is used for defining our function, and also `SlackFunction`, which we'll use to implement our function in the [Implement a custom function](#implement) section. ### Custom function fields {#fields} Field Description Required? `callback_id` A unique string identifier representing the function; max 100 characters. No other functions in your application may share a callback ID. Changing a function's callback ID is not recommended, as the function will be removed from the app and created under the new callback ID, breaking any workflows referencing the old function. Required `title` A string to nicely identify the function. Max 255 characters. Required `source_file` The relative path from the project root to the function handler file (i.e., the source file). _Remember to update this if you start nesting your functions in folders._ Required `description` A succinct summary of what your function does. Optional [`input_parameters`](#input-output) An object which describes one or more input parameters that will be available to your function. Each top-level property of this object defines the name of one input parameter available to your function. Optional [`output_parameters`](#input-output) An object which describes one or more output parameters that will be returned by your function. Each top-level property of this object defines the name of one output parameter your function makes available. Optional #### Input and output parameters {#input-output} Functions can (and generally should) declare inputs and outputs. Inputs are declared in the `input_parameters` property, and outputs are declared in the `output_parameters` property. A custom function's `input_parameters` and `output_parameters` properties have two sub-properties: * `required`, which is how you can ensure that a function requires a specific parameter. * `properties`, where you can list the specific parameters that your function accounts for. Parameters are listed in the `properties` sub-property. The value for a parameter needs to be an object with further sub-properties: * `type`: The type of the input parameter. This can be a [built-in type](/tools/deno-slack-sdk/reference/slack-types) or a [custom type](/tools/deno-slack-sdk/guides/creating-a-custom-type) that you define. * `description`: A string description of the parameter. For example, if you have an input parameter named `customer_id` that you want to be required, you can do so like this: ``` input_parameters: { properties: { customer_id: { type: Schema.types.string, description: "The customer's ID" } }, required: ["customer_id"]} ``` If your input or output parameter is a [custom type](/tools/deno-slack-sdk/guides/creating-a-custom-type) with required sub-properties, use the `DefineProperty` function to to ensure that each sub-property's required status is respected. Let's look at an example. Given an `input_parameter` of `msg_context` with three sub-properties, `message_ts`, `channel_id`, and `user_id`, this is how we would ensure that `message_ts` is required: ``` const messageAlertFunction = DefineFunction({ ... input_parameters: { properties: { msg_context: DefineProperty({ type: Schema.types.object, properties: { message_ts: { type: Schema.types.string }, channel_id: { type: Schema.types.string }, user_id: { type: Schema.types.string }, }, required: ["message_ts"] }) } }, }); ``` Object types are not supported within Workflow Builder at this time If your function will be used within Workflow Builder, we suggest not using the Object types at this time. Check out [Typescript-friendly type definitions](/tools/deno-slack-sdk/guides/creating-a-custom-type#define-property) for more details. While, strictly speaking, input and output parameters are optional, they are a common and standard way to pass data between functions and nearly any function you write will expect at least one input and pass along an output. Functions are similar in philosophy to Unix system commands: they should be minimalist, modular, and reusable. Expect the output of one function to eventually become the input of another, with no other frame of reference. After defining your custom function, declare it in your app's manifest file: ``` // /manifest.ts// Import the functionimport { GreetingFunctionDefinition } from "./functions/greeting_function.ts"// ...export default Manifest({ //... functions: [GreetingFunctionDefinition], //...}); ``` Once your function is defined in your app's manifest file, the next step is to implement the function in its respective source file. ## Implement a custom function {#implement} To keep your project tidy, implement your functions in the same source file in which you defined them. Implementation involves creating a `SlackFunction` default export. This example is again from the [`greeting_function.ts`](https://github.com/slack-samples/deno-hello-world/blob/main/functions/greeting_function.ts) within the [Hello World](https://github.com/slack-samples/deno-hello-world) sample app: ``` // /slack-samples/deno-hello-world/functions/greeting_function.ts}); // end of DefineFunctionexport default SlackFunction( // Pass along the function definition from earlier in the source file GreetingFunctionDefinition, ({ inputs }) => { // Provide any context properties, like `inputs`, `env`, or `token` // Implement your function const { recipient, message } = inputs; const salutations = ["Hello", "Hi", "Howdy", "Hola", "Salut"]; const salutation = salutations[Math.floor(Math.random() * salutations.length)]; const greeting = `${salutation}, <@${recipient}>! :wave: Someone sent the following greeting: \n\n>${message}`; // Don't forget any required output parameters return { outputs: { greeting } }; },); ``` It is important to store your environment variables, as custom functions deployed to Slack will not run with the `--allow-env` permission. When locally running your app using `slack run`, the CLI will automatically load your local `.env` file and populate the `env` function input parameter. However, when deploying your app using `slack deploy`, the values you added using `slack env add` will be available in the `env` function input parameter. Refer to [environment variables](/tools/slack-cli/guides/using-environment-variables-with-the-slack-cli) for more information. Similarly, when using a [locally running your app](/tools/deno-slack-sdk/guides/developing-locally), you can use `console.log` to emit information to the console. However, when your app is [deployed to production](/tools/deno-slack-sdk/guides/deploying-to-slack), any `console.log` commands are available via `slack activity`. Check out our [Logging](/tools/deno-slack-sdk/guides/logging-function-and-app-behavior) page for more. When composing your functions, you can: * leverage external APIs, and even store API credentials, using the CLI's [`slack env set`](/tools/slack-cli/reference/commands/slack_env_set) command * [call Slack API methods](/tools/deno-slack-sdk/guides/calling-slack-api-methods) or [third-party APIs](https://docs.slack.dev/faq#third-party) * store and retrieve data from [datastores](/tools/deno-slack-sdk/guides/using-datastores) You can also encapsulate your business logic separately from the function handler, then import what you need and build your functions that way. Function timeouts When building workflows using functions, there is a 60 second timeout for a deployed function and a 15 second timeout for a locally-run function. For deployed functions using a `block_suggestion`, `block_actions`, `view_submission`, or `view_closed` payload, there is a 10 second timeout. If a top-level custom function has not finished running within its respective time limit, you will see an error in your log. Refer to [logging](/tools/deno-slack-sdk/guides/logging-function-and-app-behavior) for more details. This error may differ when running the function locally versus a deployed function. For example, a function that calls a third-party API could complete outside of the timeout and return after Slack has already marked the function as timed out. Locally, this may result in a `token_revoked` error. If deployed, it would return an error that the timeout was reached. If an [interactivity handler function](/tools/deno-slack-sdk/guides/adding-interactivity#interactivity-handlers) times out, an error will render in the Slack client, but not in the logs. ### Function context properties {#context} Your function handler's context supports several properties that you can use by declaring them. Here are all the context properties available: Property Kind Description `env` String Represents environment variables available to your function's execution context. A locally running app gets its `env` properties populated via the local `.env` file. A deployed app gets its `env` properties populated via the CLI's [`slack env set`](/tools/slack-cli/reference/commands/slack_env_set) command. `inputs` Object Contains the input parameters you defined as part of your function definition. `client` Object An API client ready for use in your function. Useful for [calling Slack API methods](/tools/deno-slack-sdk/guides/calling-slack-api-methods). `token` String Your application's access token. `event` Object Contains the full incoming event details. `team_id` String The ID of your Slack workspace, i.e. T123ABC456. `enterprise_id` String The ID of the owning enterprise organization, i.e. "E123ABC456". Only applicable for [Slack Enterprise orgs](https://docs.slack.dev/enterprise/) customers, otherwise its value will be set to an empty string. The object returned by your function supports the following properties: Property Kind Description `error` String Indicates the error that was encountered. If present, the function will return an error regardless of what is passed to outputs. `outputs` Object Exactly matches the structure of your function definition's output\_parameters. This is required unless an error is returned. `completed` Boolean Indicates whether or not the function is completed. This defaults to `true`. ➡️ **To keep building your app**, head to the [workflows](/tools/deno-slack-sdk/guides/creating-workflows) section to learn how to add a custom function to a workflow. ➡️ **To learn how to distribute your custom function**, refer to the [custom function access guide](/tools/deno-slack-sdk/guides/controlling-access-to-custom-functions)! * * * ## Graceful errors {#error-handling} To ensure that errors in your function are handled gracefully, consider wrapping your logic in a try-catch block, and ensure you're returning an empty `outputs` property along with an `error` property: ``` import { SlackFunction } from "deno-slack-sdk/mod.ts";import type { GetCustomerNameFunction } from "../manifest.ts";import { GetCustomerInfo } from "../mycorp/get_customer_info.ts";export default SlackFunction( GetCustomerNameFunction, async ({inputs, client}) => { console.log(`Getting profile for customer ID ${inputs.customer_id}...`); let response; try { response = await GetCustomerInfo(inputs.customer_id); } catch (error) { if (error instanceof Deno.errors.NotFound) { return { error: `Could not find customer where ID == ${inputs.customer_id}!`, outputs: {}, }; } } return { outputs: { first_name: response?.first_name, last_name: response?.last_name, }, };}); ``` ``` // mycorp/get_customer_info.tsexport interface Customer { id: number; first_name: string; last_name: string;}export default function GetCustomerInfo(id: number): Customer { if (id == 1) { const customer: Customer = { id: 1, first_name: "Some", last_name: "Person", }; // Maybe here there's some third-party API we call return customer; } else { throw new Deno.errors.NotFound(); }} ``` ## Testing custom functions {#testing} During development, you may want to test your [custom functions](/tools/deno-slack-sdk/guides/creating-custom-functions) before deploying them to production. You can do this by creating a unit test for each custom function you want to validate. Since we're developing in the [Deno](/tools/deno-slack-sdk/guides/installing-deno) environment, we'll be working with the [`Deno.test` API](https://deno.land/manual/basics/testing#writing-tests). Let's go through a couple of examples from our sample apps. Using the `SlackFunctionTester`, we can specify the inputs to a function and then verify the outputs that function provides in order to ensure it is working properly. In other words, the `SlackFunctionTester` allows us to create the context for our function so that we can pass in the necessary parameters in order to test that function. Let's get started! * * * The first thing we'll do is create a new test file named after our function. For example, in the [Hello World sample app](https://github.com/slack-samples/deno-hello-world), the file containing our function is called [`greeting_function.ts`](https://github.com/slack-samples/deno-hello-world/blob/main/functions/greeting_function.ts), and the file containing our test for the function is called [`greeting_function_test.ts`](https://github.com/slack-samples/deno-hello-world/blob/main/functions/greeting_function_test.ts). We'll import our function into the test file as follows: ``` import GreetingFunction from "./greeting_function.ts"; ``` Then, we'll import `SlackFunctionTester` into the test file: ``` import { SlackFunctionTester } from "deno-slack-sdk/mod.ts"; ``` And, one more import — the specific Deno assertion method that we'll be using from the `Deno.test` API. In this case, we'll need the `assertEquals` method: ``` import { assertEquals } from "https://deno.land/std@0.153.0/testing/asserts.ts"; ``` We can initialize an instance of the `SlackFunctionTester` we mentioned earlier to create a context for our function: ``` const { createContext } = SlackFunctionTester("greeting_function"); ``` To summarize our structure, here is the original file containing our function: ``` // greeting_function.tsimport { DefineFunction, Schema, SlackFunction } from "deno-slack-sdk/mod.ts";export const GreetingFunctionDefinition = DefineFunction({ callback_id: "greeting_function", title: "Generate a greeting", description: "Generate a greeting", source_file: "functions/greeting_function.ts", input_parameters: { properties: { recipient: { type: Schema.slack.types.user_id, description: "Greeting recipient", }, message: { type: Schema.types.string, description: "Message to the recipient", }, }, required: ["message"], }, output_parameters: { properties: { greeting: { type: Schema.types.string, description: "Greeting for the recipient", }, }, required: ["greeting"], },});export default SlackFunction( GreetingFunctionDefinition, ({ inputs }) => { const { recipient, message } = inputs; const salutations = ["Hello", "Hi", "Howdy", "Hola", "Salut"]; const salutation = salutations[Math.floor(Math.random() * salutations.length)]; const greeting = `${salutation}, <@${recipient}>! :wave: Someone sent the following greeting: \n\n>${message}`; return { outputs: { greeting } }; },); ``` And here we have our test file with the items we imported and our instance of the `SlackFunctionTester`: ``` // greeting_function_test.tsimport GreetingFunction from "./greeting_function.ts";import { SlackFunctionTester } from "deno-slack-sdk/mod.ts";import { assertEquals } from "https://deno.land/std@0.153.0/testing/asserts.ts";const { createContext } = SlackFunctionTester("greeting_function");Deno.test("Greeting function test", async () => { const inputs = { message: "Welcome to the team!" }; const { outputs } = await GreetingFunction(createContext({ inputs })); assertEquals( outputs?.greeting.includes("Welcome to the team!"), true, );}); ``` Once we pass in the text we expect our function to output, we compare the two values, then check to see if the values are indeed a match. * * * Let's look at another example, this time from the [GitHub Issue sample app](https://github.com/slack-samples/deno-github-functions). Similarly to the Hello World example, we have a file containing our function called [`create_issue.ts`](https://github.com/slack-samples/deno-github-functions/blob/main/functions/create_issue.ts), and a file containing our test for the function, which is called [`create_issue_test.ts`](https://github.com/slack-samples/deno-github-functions/blob/main/functions/create_issue_test.ts). Let's look at the test file below: ``` // create_issue_test.tsimport * as mf from "https://deno.land/x/mock_fetch@0.3.0/mod.ts";import { assertEquals } from "https://deno.land/std@0.153.0/testing/asserts.ts";import { SlackFunctionTester } from "deno-slack-sdk/mod.ts";// import our original function as a handlerimport handler from "./create_issue.ts";mf.install();mf.mock("POST@/api/apps.auth.external.get", () => { return new Response(`{"ok": true, "external_token": "example-token"}`);});mf.mock("POST@/repos/slack-samples/deno-github-functions/issues", () => { return new Response( `{"number": 123, "html_url": "https://www.example.com/expected-html-url"}`, { status: 201, }, );});const { createContext } = SlackFunctionTester("create_issue");const env = { logLevel: "CRITICAL" };Deno.test("Create a GitHub issue with given inputs", async () => { const inputs = { githubAccessTokenId: {}, url: "https://github.com/slack-samples/deno-github-functions", githubIssue: { title: "The issue title", }, }; const { outputs } = await handler(createContext({ inputs, env })); // Assert whether the collection of mocked URL responses we use as inputs matches the outputs from our function. assertEquals(outputs?.GitHubIssueNumber, 123); assertEquals( outputs?.GitHubIssueLink, "https://www.example.com/expected-html-url", );}); ``` This sample makes API calls to both Slack and GitHub, and therefore requires special mocking in its test. In the test, we'll import a module called [mock fetch](https://deno.land/x/mock_fetch). This module mocks Deno's [`fetch`](https://deno.land/manual/examples/fetch_data) method, which is used to make HTTP requests. We will use `mock_fetch` to mock the responses of the Slack API. ✨ **For more information about mocking responses**, refer to [mocking](https://deno.land/manual/basics/testing/mocking#mocking) and [mock\_fetch](https://deno.land/x/mock_fetch). ### Running a test {#run-test} From the command line, run [`deno test`](https://deno.land/manual/basics/testing#running-tests) and call the file that contains your test function, as in the following example: ``` $ deno test greeting_function_test.ts ``` If you're in the base directory for your project, run this command as follows: ``` $ deno test functions/greeting_function_test.ts ``` If you want to run all of your function tests, run this command without any file names as follows: ``` $ deno test ``` ✨ **For more information about Deno's built-in test runner**, refer to [testing](https://deno.land/manual/basics/testing). ### Integrating a test into your CI/CD pipeline {#cicd-test} For more information, refer to [Setting up CI/CD with the Slack CLI](/tools/slack-cli/guides/setting-up-ci-cd-with-the-slack-cli). --- Source: https://docs.slack.dev/tools/deno-slack-sdk/guides/creating-event-triggers # Creating event triggers Workflow apps require a paid plan Join the [Developer Program](https://api.slack.com/developer-program) and provision a sandbox with access to all Slack features for free. > Invoke a workflow when a specific event happens in Slack Event triggers are a type of _automatic_ trigger, as they don't require manual activation. Instead, they're automatically invoked when a certain event happens. ## Supported events {#supported-events} A certain number of events have corresponding event triggers. Your app needs to have the proper scopes to use event triggers. Include these scopes within your app's [manifest](/tools/deno-slack-sdk/guides/using-the-app-manifest). Your app also needs to be a member of any channel where you want to listen for events. Events can be referenced in the form of `TriggerEventTypes.EVENTNAME`. Event. Reference with `TriggerEventTypes.EVENTNAME` Description Required scopes `AppMentioned` Subscribe to only the message events that mention your app or bot. [`app_mentions:read`](https://docs.slack.dev/reference/scopes/app_mentions.read) `ChannelArchived` A channel was archived. [`channels:read`](https://docs.slack.dev/reference/scopes/channels.read) `ChannelCreated` A channel was created. [`channels:read`](https://docs.slack.dev/reference/scopes/channels.read) `ChannelDeleted` A channel was deleted. [`channels:read`](https://docs.slack.dev/reference/scopes/channels.read) `ChannelRenamed` A channel was renamed. [`channels:read`](https://docs.slack.dev/reference/scopes/channels.read) `ChannelShared` A channel has been shared with an external workspace. [`channels:read`](https://docs.slack.dev/reference/scopes/channels.read) [`groups:read`](https://docs.slack.dev/reference/scopes/groups.read) `ChannelUnarchived` A channel was unarchived. [`channels:read`](https://docs.slack.dev/reference/scopes/channels.read) `ChannelUnshared` A channel has been unshared with an external workspace. [`channels:read`](https://docs.slack.dev/reference/scopes/channels.read) [`groups:read`](https://docs.slack.dev/reference/scopes/groups.read) `DndUpdated` Do not Disturb settings changed for a member. [`dnd:read`](https://docs.slack.dev/reference/scopes/dnd.read) `EmojiChanged` A custom emoji has been added or changed. [`emoji:read`](https://docs.slack.dev/reference/scopes/emoji.read) `MessagePosted` A message was sent to a channel. _A [filter](#filters) is required to listen for this event._ The no-code Workflow Builder [version](https://slack.com/help/articles/43844341409811) of this trigger is less permissive than the coded trigger discussed here because the latter requires an app install, allowing organizations to review the app before installing. [`channels:history`](https://docs.slack.dev/reference/scopes/channels.history) [`groups:history`](https://docs.slack.dev/reference/scopes/groups.history) [`im:read`](https://docs.slack.dev/reference/scopes/im.history) [`mpim:read`](https://docs.slack.dev/reference/scopes/mpim.history) `MessageMetadataPosted` Message metadata was posted. [`metadata.message:read`](https://docs.slack.dev/reference/scopes/metadata.message.read) `PinAdded` A pin was added to a channel. [`pins:read`](https://docs.slack.dev/reference/scopes/pins.read) `PinRemoved` A pin was removed from a channel. [`pins:read`](https://docs.slack.dev/reference/scopes/pins.read) `ReactionAdded` A member has added an emoji reaction. [`reactions:read`](https://docs.slack.dev/reference/scopes/reactions.read) `ReactionRemoved` A member removed an emoji reaction. [`reactions:read`](https://docs.slack.dev/reference/scopes/reactions.read) `SharedChannelInviteAccepted` A shared channel invite was accepted. [`conversations.connect:manage`](https://docs.slack.dev/reference/scopes/conversations.connect.manage) `SharedChannelInviteApproved` A shared channel invite was approved. [`conversations.connect:manage`](https://docs.slack.dev/reference/scopes/conversations.connect.manage) `SharedChannelInviteDeclined` A shared channel invite was declined. [`conversations.connect:manage`](https://docs.slack.dev/reference/scopes/conversations.connect.manage) `SharedChannelInviteReceived` A shared channel invite was sent to a Slack user. [`conversations.connect:read`](https://docs.slack.dev/reference/scopes/conversations.connect.read) `SharedChannelInviteRequested` A shared channel invite was requested to be sent. [`conversations.connect:manage`](https://docs.slack.dev/reference/scopes/conversations.connect.manage) `UserJoinedChannel` A user joined a public or private channel. [`channels:read`](https://docs.slack.dev/reference/scopes/channels.read) [`groups:read`](https://docs.slack.dev/reference/scopes/groups.read) `UserJoinedTeam` A new member has joined. [`users:read`](https://docs.slack.dev/reference/scopes/users.read) `UserLeftChannel` A user left a public or private channel. [`channels:read`](https://docs.slack.dev/reference/scopes/channels.read) [`groups:read`](https://docs.slack.dev/reference/scopes/groups.read) ### Events can activate multiple triggers {#multiple} When an event happens, all event triggers listening for that event will be invoked at roughly the same time. If you want to control which workflow runs first, you have two options: * Combine the functions of both workflows into a single workflow, invoked with a single event trigger. * Have the second workflow be invoked by the first workflow, instead of the original event trigger. ### Avoid infinite loops {#loops} Your app can respond to events _and_ be the cause of events. This can create situations where your app gets stuck in a loop. For example, if your app listens for all `message_posted` events in a channel and then posts its own message in response, it'll keep posting messages forever! That's why the `message_posted` event requires a filter. Carefully construct a [filter](#filters) to prevent boundless behavior. If your app does get stuck in an infinite loop, you can [delete the trigger](/tools/deno-slack-sdk/guides/managing-triggers#delete) and the behavior will cease. ## Create an event trigger {#create-trigger} Triggers can be added to workflows in two ways: * **You can add triggers with the CLI.** These static triggers are created only once. You create them with the Slack CLI, attach them to your app's workflow, and that's that. The trigger is defined within a trigger file. * **You can add triggers at runtime.** These dynamic triggers are created at any step of a workflow so they can incorporate data acquired from other workflow steps. The trigger is defined within a function file. * Create an event trigger with the CLI * Create an event trigger at runtime Slack CLI built-in documentation Use `slack trigger --help` to easily access information on the `trigger` command's flags and subcommands. The triggers you create when running locally (with the `slack run` command) will not work when you deploy your app in production (with the `slack deploy` command). You'll need to `create` any triggers again with the CLI. ### Create the trigger file {#create-the-trigger-file} To create an event trigger with the CLI, you'll need to create a trigger file. The trigger file contains the payload you used to define your trigger. Create a TypeScript trigger file within your app's folder with the following form: ``` import { Trigger } from "deno-slack-api/types.ts";import { TriggerEventTypes, TriggerTypes, TriggerContextData } from "deno-slack-api/mod.ts";const trigger: Trigger = { // your TypeScript payload};export default trigger; ``` Your TypeScript payload consists of the [parameters](#parameters) needed for your own use case. The following is a trigger file with a payload creating an event trigger that listens for a `reaction_added` event in a specific channel: ``` import { Trigger } from "deno-slack-api/types.ts";import { TriggerEventTypes, TriggerTypes, TriggerContextData } from "deno-slack-api/mod.ts";const trigger: Trigger = { type: TriggerTypes.Event, name: "Reactji response", description: "responds to a specific reactji", workflow: "#/workflows/myWorkflow", event: { event_type: TriggerEventTypes.ReactionAdded, channel_ids: ["C123ABC456"], filter: { version: 1, root: { statement: "{{data.reaction}} == sunglasses" } } }, inputs: { stringtoSend: { value: "how cool is that", }, channel: { value: "C123ABC456", }, },};export default trigger; ``` ### Use the trigger create command {#use-the-trigger-create-command} Once you have created a trigger file, use the following command to create the event trigger: ``` slack trigger create --trigger-def "path/to/trigger.ts" ``` If you have not used the `slack triggers create` command to create a trigger prior to running the `slack run` command, you will receive a prompt in the Slack CLI to do so. Your app needs to have the [`triggers:write`](https://docs.slack.dev/reference/scopes/triggers.write) scope to use a trigger at runtime. Include the scope within your app's [manifest](/tools/deno-slack-sdk/guides/using-the-app-manifest). The logic of a runtime trigger lies within a function's TypeScript code. Within your `functions` folder, you'll have the functions that are the steps making up your workflow. Within this folder is where you can create a trigger within the relevant `.ts` file. When you create a runtime trigger, you can leverage `inputs` acquired from functions within the workflow. Provide the workflow definition to get additional typing for the workflow and inputs fields. Create an event trigger at runtime using the `client.workflows.triggers.create` method within the relevant `function` file. ``` const triggerResponse = await client.workflows.triggers.create({ // your TypeScript payload}); ``` Your TypeScript payload consists of the [parameters](#parameters) needed for your own use case. Below is a function file with an example TypeScript payload for an event trigger. This specific TypeScript payload is for creating an event trigger that listens for a `reaction_added` event in a specific channel: ``` // functions/example_function.tsimport { DefineFunction, Schema, SlackFunction } from "deno-slack-sdk/mod.ts";import { ExampleWorkflow } from "../workflows/example_workflow.ts";import { TriggerEventTypes, TriggerTypes } from "deno-slack-api/mod.ts";export const ExampleFunctionDefinition = DefineFunction({ callback_id: "example_function_def", title: "Example function", source_file: "functions/example_function.ts",});export default SlackFunction( ExampleFunctionDefinition, ({ inputs, client }) => { const triggerResponse = await client.workflows.triggers.create({ type: TriggerTypes.Event, name: "Reactji response", description: "responds to a specific reactji", workflow: `#/workflows/${ExampleWorkflow.definition.callback_id}`, event: { event_type: TriggerEventTypes.ReactionAdded, channel_ids: ["C123ABC456"], filter: { version: 1, root: { statement: "{{data.reaction}} == sunglasses" } } }, inputs: { stringtoSend: { value: "how cool is that", }, channel: { value: "C123ABC456", }, } }); // ... ``` * * * ## Event trigger parameters {#parameters} Field Description Required? `name` The name of the trigger. Required `type` The type of trigger: `TriggerTypes.Event`. Required `workflow` Path to workflow that the trigger initiates. Required `description` The description of the trigger. Optional `event` Contains [the `event` object](#event-object). Optional `inputs` The inputs provided to the workflow. Can use with the [event response object](#response-object). Optional Event triggers are not interactive. Use a [link trigger](/tools/deno-slack-sdk/guides/creating-link-triggers) to take advantage of interactivity. ### The Event object {#event-object} Field Description Required? `event_type` The type of event; use one of the properties of `TriggerEventTypes`. Required `team_ids` An array of event-related team ID strings. Required for Enterprise orgs `all_resources` Trip the event trigger in all channels your app is present in. Defaults to `false`. Mutually exclusive with `channel_ids`. See [below](#scoping) for more details. Dependent on the [event](#channel-based-event-triggers) `channel_ids` An array of channel IDs where the event trigger will trip. Mutually exclusive with `all_resources`. See [below](#scoping) for more details. Dependent on the [event](#channel-based-event-triggers) `filter` See [trigger filters](#filters) for more details. Optional #### Scoping channel-based event triggers {#scoping} When writing a channel-based event trigger, you can pass the `channel_ids` field with a list of specific channels for the trigger to trip in. Example: ``` event: { event_type: TriggerEventTypes.ReactionAdded, channel_ids: ["C123ABC456", "C01234567", "C09876543"],} ``` Alternatively, you can set `all_resources` to `true` The `channel_ids` field will no longer be required, and the event will now trigger in all channels in the workspace the app is a part of. Example: ``` event: { event_type: TriggerEventTypes.ReactionAdded, all_resources: true,} ``` Setting `all_resources` to `true` could cause additional charges The event will trip in all channels the app is a member of in the workspace and may therefore lead to many workflow executions in workspaces with a large number of channels. #### Channel-based event triggers {#channel-based-event-triggers} The following channel-based event triggers require either the `channel_ids` or `all_resources` event object to be set: * `app_mentioned` * `call_rejected` * `channel_history_changed` * `channel_id_changed` * `channel_shared` * `channel_unshared` * `member_left_channel` * `message_metadata_posted` * `message_posted` * `pin_added` * `pin_removed` * `reaction_added` * `reaction_removed` * `user_joined_channel` ### The event response object {#response-object} An event's response object will contain additional information about that specific event instance. Property Description `data` Contains additional information dependent on event type. See [below](#data). `enterprise_id` A unique identifier for the enterprise where this event occurred. `event_id` A unique identifier for this specific event, globally unique across all workspaces. `event_timestamp` A Unix timestamp in seconds indicating when this event was dispatched. `team_id` A unique identifier for the workspace/team where this event occurred. `type` An identifier showing the `event` type #### The data property {#data} Each type of event has unique sub-properties within the `data` property. You can pass these values on to your workflows. See the example below. ##### Event types {#event-types} `app_mentioned` ``` { "team_id": "T0123ABC", "enterprise_id": "E0123ABC", "event_id": "Ev0123ABC", "event_timestamp": 1643810217.088700, "type": "event", "data": { "app_id": "A1234ABC", "channel_id": "C0123ABC", "channel_name": "cool-channel", "channel_type": "public/private/dm/mpdm", "event_type": "slack#/events/app_mentioned", "message_ts": "164432432542.2353", "text": "<@U0LAN0Z89> is it everything a river should be?", "user_id:": "U0123ABC", }} ``` `channel_archived`\* ``` { "team_id": "T0123ABC", "enterprise_id": "E0123ABC", "event_id": "Ev0123ABC", "event_timestamp": 1630623713, "type": "event", "data": { "channel_id": "C0123ABC", "channel_name": "cool-channel", "channel_type": "public/private/dm/mpdm", "event_type": "slack#/events/channel_archived", "user_id": "U0123ABC", }} ``` `channel_created`\* ``` { "team_id": "T0123ABC", "enterprise_id": "E0123ABC", "event_id": "Ev0123ABC", "event_timestamp": 1630623713, "type": "event", "data": { "channel_id": "C0123ABC", "channel_name": "fun", "channel_type": "public", "created": 1360782804, "creator_id": "U0123ABC", "event_type": "slack#/events/channel_created", }} ``` `channel_deleted`\* ``` { "team_id": "T0123ABC", "enterprise_id": "E0123ABC", "event_id": "Ev0123ABC", "event_timestamp": 1643810217.088700, "type": "event", "data": { "channel_id": "C0123ABC", "channel_name": "project_planning", "channel_type": "public/private/dm/mpdm", "event_type": "slack#/events/channel_deleted", "user_id": "U0123ABC", }} ``` `channel_renamed`\* ``` { "team_id": "T0123ABC", "enterprise_id": "E0123ABC", "event_id": "Ev0123ABC", "event_timestamp": 1643810217.088700, "type": "event", "data": { "channel_id": "C0123ABC", "channel_name": "project_planning", "channel_type": "public/private/dm/mpdm", "event_type": "slack#/events/channel_renamed", "user_id": "U0123ABC", }} ``` `channel_shared` ``` { "team_id": "T0123ABC", "enterprise_id": "E0123ABC", "event_id": "Ev0123ABC", "event_timestamp": 1643810217.088700, "type": "event", "data": { "channel_id": "C0123ABC", "channel_name": "cool-channel", "channel_type": "public/private/dm/mpdm", "connected_team_id": "E0123ABC", "event_type": "slack#/events/channel_shared", }} ``` `channel_unarchived`\* ``` { "team_id": "T0123ABC", "enterprise_id": "E0123ABC", "event_id": "Ev0123ABC", "event_timestamp": 1643810217.088700, "type": "event", "data": { "channel_id": "C0123ABC", "channel_name": "cool-channel", "channel_type": "public/private/dm/mpdm", "event_type": "slack#/events/channel_unarchived", "user_id": "U0123ABC", }} ``` `channel_unshared` ``` { "team_id": "T0123ABC", "enterprise_id": "E0123ABC", "event_id": "Ev0123ABC", "event_timestamp": 1643810217.088700, "type": "event", "data": { "channel_id": "C0123ABC", "channel_name": "cool-channel", "channel_type": "public/private/dm/mpdm", "disconnected_team_id": "E0123ABC", "event_type": "slack#/events/channel_unshared", "is_ext_shared": false, }} ``` `dnd_updated`\* ``` { "team_id": "T0123ABC", "enterprise_id": "E0123ABC", "event_id": "Ev0123ABC", "event_timestamp": 1630623713, "type": "event", "data": { "dnd_status": { "dnd_enabled": true, }, "event_type": "slack#/events/user_updated_dnd", "user_id": "U0123ABC", }} ``` `emoji_changed`\* ##### Emoji added {#emoji-added} ``` { "team_id": "T0123ABC", "enterprise_id": "E0123ABC", "event_id": "Ev0123ABC", "event_timestamp": 1630623713, "type": "event", "data": { "event_type": "slack#/events/emoji_changed", "name": "picard_facepalm", "subtype": "add", "value": "https://my.slack.com/emoji/picard_facepalm/abc123.gif" }} ``` ##### Emoji removed {#emoji-removed} ``` { "team_id": "T0123ABC", "enterprise_id": "E0123ABC", "event_id": "Ev0123ABC", "event_timestamp": 1630623713, "type": "event", "data": { "event_type": "slack#/events/emoji_changed", "names": ["picard_facepalm"], "subtype": "remove", }} ``` ##### Emoji renamed {#emoji-renamed} ``` { "team_id": "T0123ABC", "enterprise_id": "E0123ABC", "event_id": "Ev0123ABC", "event_timestamp": 1630623713, "type": "event", "data": { "event_type": "slack#/events/emoji_changed", "new_name": "captain_picard_facepalm", "old_name": "picard_facepalm", "subtype": "rename", "value": "https://my.slack.com/emoji/picard_facepalm/abc123.gif" }} ``` `user_joined_channel` ``` { "team_id": "T0123ABC", "enterprise_id": "E0123ABC", "event_id": "Ev0123ABC", "event_timestamp": 1630623713, "type": "event", "data": { "channel_id": "C0123ABC", "channel_type" : "public/private/im/mpim", "event_type": "slack#/events/user_joined_channel", "inviter_id": "U0123ABC", "user_id": "U0123ABC", }} ``` `user_left_channel` ``` { "team_id": "T0123ABC", "enterprise_id": "E0123ABC", "event_id": "Ev0123ABC", "event_timestamp": 1630623713, "type": "event", "data": { "channel_id": "C0123ABC", "channel_type" : "public/private/im/mpim", "event_type": "slack#/events/user_left_channel", "user_id": "W0123ABC", }} ``` `message_posted` ``` { "team_id": "T0123ABC", "enterprise_id": "E0123ABC", "event_id": "Ev0123ABC", "event_timestamp": 1630623713, "type": "event", "data": { "channel_id": "C0123ABC", "channel_type": "public/private/dm/mpdm", "event_type": "slack#/events/message_posted", "message_ts": "1355517523.000005", "text": "Hello world", "thread_ts": "1355517523.000006", // Nullable "user_id": "U0123ABC", }} ``` `message_metadata_posted` ``` { "team_id": "T0123ABC", "enterprise_id": "E0123ABC", "event_id": "Ev0123ABC", "event_timestamp": 1630623713, "type": "event", "data": { "app_id": "A0123ABC", "channel_id": "C0123ABC", "event_type": "slack#/events/message_metadata_posted", "message_ts": "1630708981.000001", "metadata": { "event_type": "incident_created", "event_payload": { "incident": { "id": 123, "summary": "Someone tripped over", "sev": 1 } } }, "user_id": "U0123ABC", }} ``` `pin_added` ``` { "team_id": "T0123ABC", "enterprise_id": "E0123ABC", "event_id": "Ev0123ABC", "event_timestamp": 1643810217.088700, "type": "event", "data": { "channel_id": "C0123ABC", "channel_type": "public/private/dm/mpdm", "channel_name": "project_planning", "event_type": "slack#/events/pin_added", "message_ts": "1360782804.083113", "user_id": "U0123ABC", }} ``` `pin_removed` ``` { "team_id": "T0123ABC", "enterprise_id": "E0123ABC", "event_id": "Ev0123ABC", "event_timestamp": 1643810217.088700, "type": "event", "data": { "channel_id": "C0123ABC", "channel_name": "project_planning", "channel_type": "public/private/dm/mpdm", "event_type": "slack#/events/pin_removed", "message_ts": "1360782804.083113", "user_id": "U0123ABC", }} ``` `reaction_added` ``` { "team_id": "T0123ABC", "enterprise_id": "E0123ABC", "event_id": "Ev0123ABC", "event_timestamp": 1630623713, "type": "event", "data": { "channel_id": "C0123ABC", "event_type": "slack#/events/reaction_added", "message_context": { "message_ts": "1535430114.000100", "channel_id": "C0123ABC", }, "message_ts": "1535430114.000100", "message_link": "https:\/\/example.slack.com\/archives\/C0123ABC\/p1535430114000100", "reaction": "joy", "user_id": "U0123ABC", "item_user": "U0123ABC", "parent_message_link": "https:\/\/example.slack.com\/archives\/C0123ABC\/p1535430114000100", }} ``` `reaction_removed` ``` { "team_id": "T0123ABC", "enterprise_id": "E0123ABC", "event_id": "Ev0123ABC", "event_timestamp": 12345, "type": "event", "data": { "channel_id": "C0123ABC", "event_type": "slack#/events/reaction_removed", "message_ts": "1535430114.000100", "reaction": "thumbsup", "user_id": "U0123ABC", }} ``` `shared_channel_invite_accepted`\* ``` { "team_id": "T0123ABC", "enterprise_id": "E0123ABC", "event_id": "Ev0123ABC", "event_timestamp": 1643810217.088700, "type": "event", "data": { "accepting_user": { "display_name": "John Doe", "id": "U123", "is_bot": false, "name": "John Doe", "real_name": "John Doe", "team_id": "T123", "timezone": "America/Los_Angeles", }, "approval_required": false, "channel_id": "C12345678", "channel_name": "test-slack-connect", "channel_type": "public/private/dm/mpdm", "event_type": "slack#/events/shared_channel_invite_accepted", "invite": { "date_created": 1626876000, "date_invalid": 1628085600, "id": "I0ABC123", "inviting_team": { "date_created": 1480946400, "domain": "corgis", "icon": {...}, "id": "T12345678", "is_verified": false, "name": "Corgis", }, "inviting_user": { "display_name": "John Doe", "id": "U123", "is_bot": false, "name": "John Doe", "real_name": "John Doe", "team_id": "T123", "timezone": "America/Los_Angeles", }, "recipient_email": "golden@doodle.com", "recipient_user_id": "U87654321", }, "teams_in_channel": [ { "date_created": 1626789600, "domain": "corgis", "icon": {...}, "id": "T12345678", "is_verified": false, "name": "Corgis", } ], }} ``` `shared_channel_invite_approved`\* ``` { "team_id": "T0123ABC", "enterprise_id": "E0123ABC", "event_id": "Ev0123ABC", "event_timestamp": 1643810217.0887, "type": "event", "data": { "approving_team_id": "T87654321", "approving_user": { "display_name": "John Doe", "id": "U123", "is_bot": false, "team_id": "T123", "name": "John Doe", "real_name": "John Doe", "team_id": "T12345", "timezone": "America/Los_Angeles", }, "channel_id": "C12345678", "channel_name": "test-slack-connect", "channel_type": "public/private/dm/mpdm", "event_type": "slack#/events/shared_channel_invite_approved", "invite": { "date_created": 1626876000, "date_invalid": 1628085600, "id": "I0123ABC", "inviting_team": { "date_created": 1480946400, "domain": "corgis", "icon": {...}, "id": "T12345678", "is_verified": false, "name": "Corgis", }, "inviting_user": { "display_name": "John Doe", "id": "U123", "is_bot": false, "name": "John Doe", "real_name": "John Doe", "team_id": "T123", "timezone": "America/Los_Angeles", }, "recipient_email": "golden@doodle.com", "recipient_user_id": "U87654321" }, "teams_in_channel": [ { "date_created": 1626789600, "domain": "corgis", "icon": {...}, "id": "T12345678", "is_verified": false, "name": "Corgis", } ], }} ``` `shared_channel_invite_declined`\* ``` { "team_id": "T0123ABC", "enterprise_id": "E0123ABC", "event_id": "Ev0123ABC", "event_timestamp": 1643810217.0887, "type": "event", "data": { "channel_id": "C12345678", "channel_type": "public/private/dm/mpdm", "channel_name": "test-slack-connect", "declining_team_id": "T87654321", "declining_user": { "display_name": "John Doe", "id": "U123", "is_bot": false, "name": "John Doe", "real_name": "John Doe", "team_id": "T123", "timezone": "America/Los_Angeles", }, "event_type": "slack#/events/shared_channel_invite_declined", "invite": { "date_created": 1626876000, "date_invalid": 1628085600, "id": "I0123ABC", "inviting_team": { "date_created": 1480946400, "domain": "corgis", "icon": {...}, "id": "T12345678", "is_verified": false, "name": "Corgis", }, "inviting_user": { "display_name": "John Doe", "id": "U123", "is_bot": false, "name": "John Doe", "real_name": "John Doe", "team_id": "T123", "timezone": "America/Los_Angeles", }, "recipient_email": "golden@doodle.com", "recipient_user_id": "U3472391", }, "teams_in_channel": [ { "date_created": 1626789600, "domain": "corgis", "icon": {...}, "id": "T12345678", "is_verified": false, "name": "Corgis", } ], }} ``` `shared_channel_invite_received`\* ``` { "team_id": "T0123ABC", "enterprise_id": "E0123ABC", "event_id": "Ev0123ABC", "event_timestamp": 1643810217.0887, "type": "event", "data": { "channel_id": "C12345678", "channel_name": "test-slack-connect", "channel_type": "public/private/dm/mpdm", "event_type": "slack#/events/shared_channel_invite_received", "invite": { "date_created": 1626876000, "date_invalid": 1628085600, "id": "I0123ABC", "inviting_team": { "date_created": 1480946400, "domain": "corgis", "icon": {...}, "id": "T12345678", "is_verified": false, "name": "Corgis", }, "inviting_user": { "display_name": "John Doe", "id": "U123", "is_bot": false, "name": "John Doe", "real_name": "John Doe", "team_id": "T123", "timezone": "America/Los_Angeles", }, "recipient_email": "golden@doodle.com", "recipient_user_id": "U87654321" }, }} ``` `user_joined_team`\* ``` { "team_id": "T0123ABC", "enterprise_id": "E0123ABC", "event_id": "Ev0123ABC", "event_timestamp": 1630623713, "type": "event", "data": { "event_type": "slack#/events/user_joined_team", "user": { "display_name": "John Doe", "id": "U123", "is_bot": false, "name": "John Doe", "real_name": "John Doe", "team_id": "T123", "timezone": "America/Los_Angeles", } }} ``` \*When developing with these event types in an Enterprise organization, you must include the `team_ids` field when creating workspace-based event triggers. The data returned in the event response object can be passed along to workflows. In this example, we take the `user_id`, `channel_id`, and `message_ts` from the `reaction_added` event's response object and pass them along to the `joy_workflow` in the `inputs` field, referencing them by their respective enums. ``` { type: TriggerTypes.Event, name: "Joy reactji event trigger", description: "Joy reactji trigger", workflow: "#/workflows/joy_workflow", inputs: { user: { value: TriggerContextData.Event.ReactionAdded.user_id, // Pulled from event response body and passed into the workflow }, channel: { value: TriggerContextData.Event.ReactionAdded.channel_id, // Pulled from event response body and passed into the workflow }, message_ts: { value: TriggerContextData.Event.ReactionAdded.message_ts // Pulled from event response body and passed into the workflow } }, event: { event_type: TriggerEventTypes.ReactionAdded, channel_ids: ["C123ABC456"] }} ``` ## Event trigger filters {#filters} Trigger filters allow you to define a set of conditions for a trigger which must be true in order for the trigger to activate. Filters can also prevent your app from getting stuck in an infinite loop of responses triggered by responding to events it created. Trigger filters are implemented by inserting a filter payload within your `trigger` object. The payload takes the form of an object containing blocks of conditional logic. The logical condition within each block can be one of two types: * Conditional expressions (e.g. `x < y`) * Boolean logic (e.g. `x AND y`) ### Conditional expressions {#conditional-filters} Conditional expression blocks need a single `statement` key with a string containing the comparison block. Values from the `inputs` payload can be referenced within the comparison block. Below is an example payload of a `reaction_added` event trigger that only invokes a workflow if the reaction was the `:eyes:` reaction. ``` { type: TriggerTypes.Event, name: "Reactji response", description: "responds to a specific reactji", workflow: "#/workflows/myWorkflow", event: { event_type: TriggerEventTypes.ReactionAdded, channel_ids: ["C123ABC456"], filter: { version: 1, root: { statement: "{{data.reaction}} == eyes" } } }, inputs: { stringtoSend: { value: "how cool is that", }, channel: { value: "C123ABC456", }, },}; ``` The supported operand types are `integer`, `double`, `boolean`, `string`, and `null`. The following comparators are supported. Comparator Supported types `==` all types `>` `int`, `double` `<` `int`, `double` `>=` `int`, `double` `<=` `int`, `double` `CONTAINS` string; i.e. `{{data.text}} CONTAINS 'hello'` ### Boolean logic blocks {#boolean-filters} Boolean logic blocks are made up of two key:value pairs: * An `operator` key with a string containing the comparison operator: `AND`, `OR`, or `NOT` * An `inputs` key with the child blocks The child blocks then contain additional logic. The following example filters on the `reaction` value: ``` { type: TriggerTypes.Event, name: "Reactji response", description: "responds to a specific reactji", workflow: "#/workflows/myWorkflow", event: { event_type: TriggerEventTypes.ReactionAdded, channel_ids: ["C123ABC456"], filter: { version: 1, root: { operator: "OR", inputs: [{ statement: "{{data.reaction}} == sunglasses" }, { statement: "{{data.reaction}} == smile" }], } } }, inputs: { stringtoSend: { value: "how cool is that", }, channel: { value: "C123ABC456", }, },}; ``` Nested logic blocks You can use the same boolean logic to create nested boolean logic blocks. It's boolean logic all the way down - up to a maximum of 5 nested blocks, that is. The `NOT` operator, however, must contain only one input. Also, at the moment the boolean logic block does _not_ support [short-circuit evaluation](https://en.wikipedia.org/wiki/Short-circuit_evaluation), so all arguments will be evaluated. ### Trigger filters in sample apps {#trigger-filters-in-sample-apps} ✨ [The Simple Survey App](https://github.com/slack-samples/deno-simple-survey) lets users collect feedback on specific messages. The process begins when a user reacts to a message with the `:clipboard:` reaction. This is done with a `reaction_added` event trigger filtering for the `:clipboard:` reaction. ✨ [The Daily Topic App](https://github.com/slack-samples/deno-daily-channel-topic) can reply to messages in a channel. The `message_posted` event filters out both messages by apps (to prevent recursive replies) and messages within a thread (to reply only once per thread). ## Event trigger response {#response} The response will have a property called `ok`. If `true`, then the trigger was created, and the `trigger` property will be populated. Your response will include a `trigger.id`; be sure to store it! You use that to `update` or `delete` the trigger if need be. See [trigger management](/tools/deno-slack-sdk/guides/managing-triggers). ## Onward {#onward} ➡️ With your trigger created, you can now test your app by [running your app locally](/tools/deno-slack-sdk/guides/developing-locally). ✨ Once your app is active, see [trigger management](/tools/deno-slack-sdk/guides/managing-triggers) for info on managing your triggers in your workspace. --- Source: https://docs.slack.dev/tools/deno-slack-sdk/guides/creating-functions # Creating functions Workflow apps require a paid plan Join the [Developer Program](https://api.slack.com/developer-program) and provision a sandbox with access to all Slack features for free. Functions are one of the three building blocks that make up workflow apps. You will encounter all three as you navigate the path of building your app: 1. Functions define the actions of your app. (⬅️ you are here) 2. Workflows are a combination of functions, executed in order. 3. Triggers execute workflows. There are three types of functions: * **[Slack functions](/tools/deno-slack-sdk/guides/creating-slack-functions)** enable Slack-native actions, like creating a channel or sending a message. * **[Connector functions](/tools/deno-slack-sdk/guides/creating-connector-functions)** enable actions native to services outside of Slack. Google Sheets, Dropbox and Microsoft Excel are just a few of the services with available connector functions. Connector functions cannot be used in a workflow intended for use in a Slack Connect channel. * **[Custom functions](/tools/deno-slack-sdk/guides/creating-custom-functions)** enable developer-specific actions. Pass in any desired inputs, perform any actions you can code up, and pass on outputs to other parts of your workflows. Custom functions also allow your app to create and process workflow steps that users can add in Workflow Builder. See the [Workflow Builder custom step tutorial](/tools/deno-slack-sdk/tutorials/workflow-builder-custom-step/) for instruction. --- Source: https://docs.slack.dev/tools/deno-slack-sdk/guides/creating-link-triggers # Creating link triggers Workflow apps require a paid plan Join the [Developer Program](https://api.slack.com/developer-program) and provision a sandbox with access to all Slack features for free. > Invoke a workflow from a channel in Slack Link triggers are an _interactive_ type of trigger. You typically invoke them by clicking on the associated shortcut link. A link trigger will unfurl into a button when posted in a channel. They can be invoked in other ways as well. You can: * add the link trigger as a bookmark in a channel, then select it * invoke it with a slash command via the [Shortcut menu](https://docs.slack.dev/interactivity/implementing-shortcuts#global) * create it as a [workflow button](#workflow_buttons), then click the button ## Create a link trigger {#create-trigger} Triggers can be added to workflows in two ways: * **You can add triggers with the CLI.** These static triggers are created only once. You create them with the Slack CLI, attach them to your app's workflow, and that's that. The trigger is typically defined within a trigger file, although you can create a basic link trigger without one. * **You can add triggers at runtime.** These dynamic triggers are created at any step of a workflow so they can incorporate data acquired from other workflow steps. The trigger is defined within a function file. * Create a link trigger with the CLI * Create a scheduled trigger at runtime Slack CLI built-in documentation You can use `slack trigger --help` to easily access information on the `trigger` command's flags and subcommands. The triggers you create when running locally (with the `slack run` command) will not work when you deploy your app in production (with the `slack deploy` command). You'll need to `create` any triggers again with the CLI. ### Create a basic link trigger {#create-cli} If your workflow doesn't need any parameters mapped from the trigger, such as `interactivity`, then you can create a trigger using the `trigger create` command: ``` slack trigger create --workflow "#/workflows/your_workflow" ``` ### Create a link trigger with interactivity {#create-cli-interactivity} If you need to use the `interactivity` parameter, append the `--interactivity` flag to that command: ``` slack trigger create --workflow "#/workflows/your_workflow" --interactivity ``` ### Create a link trigger with additional parameters {#create-cli-additional-parameters} If you need to pass specific values, or use other parameters, you'll need to create a link trigger using a trigger file. The trigger file contains the payload you used to define your trigger. Create a TypeScript trigger file within your app's folder with the following form: ``` import { Trigger } from "deno-slack-api/types.ts";import { ExampleWorkflow } from "../workflows/example_workflow.ts";import { TriggerContextData, TriggerTypes } from "deno-slack-api/mod.ts";const trigger: Trigger = { // your TypeScript payload};export default trigger; ``` Your TypeScript payload consists of the [parameters](#parameters) needed for your own use case; below is the trigger file from the [Deno Starter Template](https://github.com/slack-samples/deno-starter-template/blob/main/triggers/sample_trigger.ts): ``` import { Trigger } from "deno-slack-sdk/types.ts";import SampleWorkflow from "../workflows/sample_workflow.ts";import { TriggerContextData, TriggerTypes } from "deno-slack-api/mod.ts";const sampleTrigger: Trigger = { type: TriggerTypes.Shortcut, name: "Sample trigger", description: "A sample trigger", workflow: "#/workflows/sample_workflow", inputs: { interactivity: { value: TriggerContextData.Shortcut.interactivity, }, channel: { value: TriggerContextData.Shortcut.channel_id, }, user: { value: TriggerContextData.Shortcut.user_id, }, },};export default sampleTrigger; ``` Once you have created a trigger file, use the `trigger create` command to create the link trigger by pointing to a trigger file: ``` slack trigger create --trigger-def "path/to/trigger.ts" ``` If you have not used the `slack triggers create` command to create a trigger prior to running the `slack run` command, you will receive a prompt in the Slack CLI to do so. ### The CLI response {#the-cli-response} Once you've instructed Slack just how to create your link trigger using the CLI, it will respond with a shortcut link you can then copy and paste into a Slack channel or bookmarks bar. Use it to activate your workflow. Alternatively, you can use a [slash command](https://docs.slack.dev/interactivity/implementing-slash-commands). The triggers you create when running locally (with the `slack run` command) will not work when you deploy your app in production (with the `slack deploy` command). You'll need to `create` any triggers again with the CLI. When a `trigger create` command is successful, the CLI's response looks something like: ``` ⚡ Trigger successfully created! Train markovbot with words Ft0123ABC456 (shortcut) Created: 2023-01-01 12:34:56 -07:00 (8 seconds ago) Runnable by: everyone https://slack.com/shortcuts/Ft0123ABC456/abc123... ``` This response includes the trigger name, ID, and type on the first line, followed by the time of creation and access list for the trigger. The last line includes the shortcut link you'll need for invoking the trigger in Slack. You can send this to a channel or add it as a bookmark, then click it to begin the workflow! When you need to [modify, remove, or otherwise maintain the triggers you've created](/tools/deno-slack-sdk/guides/managing-triggers), the trigger ID will come in handy to specify which trigger you're updating. No need to memorize it though, since you can use `slack trigger list` to show all available triggers for your app. Your app needs to have the [`triggers:write`](https://docs.slack.dev/reference/scopes/triggers.write) scope to use a trigger at runtime. Include the scope within your app's [manifest](/tools/deno-slack-sdk/guides/using-the-app-manifest). The logic of a runtime trigger lies within a function's TypeScript code. Within your `functions` folder, you'll have the functions that are the steps making up your workflow. Within this folder is where you can create a trigger within the relevant `.ts` file. When you create a runtime trigger, you can leverage `inputs` acquired from functions within the workflow. Provide the workflow definition to get additional typing for the workflow and inputs fields. Create a link trigger at runtime using the `client.workflows.triggers.create` method within the relevant function file. ``` const triggerResponse = await client.workflows.triggers.create({ // your TypeScript payload}); ``` Your TypeScript payload consists of the [parameters](#parameters) needed for your own use case. Here's a function file with an example TypeScript payload for a link trigger: ``` // functions/example_function.tsimport { DefineFunction, Schema, SlackFunction } from "deno-slack-sdk/mod.ts";import { ExampleWorkflow } from "../workflows/example_workflow.ts";import { TriggerTypes } from "deno-slack-api/mod.ts";export const ExampleFunctionDefinition = DefineFunction({ callback_id: "example_function_def", title: "Example function", source_file: "functions/example_function.ts",});export default SlackFunction( ExampleFunctionDefinition, ({ inputs, client }) => { const triggerResponse = await client.workflows.triggers.create({ type: TriggerTypes.Shortcut, name: "My Trigger", workflow: `#/workflows/${ExampleWorkflow.definition.callback_id}`, inputs: { input_name: { value: "value", } } }); // ... ``` * * * ## Link trigger parameters {#parameters} Field Description Required? `type` The type of trigger: `TriggerTypes.Shortcut` Required `name` The name of the trigger Required `workflow` Path to workflow that the trigger initiates Required `description` The description of the trigger Optional `inputs` The inputs provided to the workflow. See [the `inputs` object](#inputs) below Optional `shortcut` Contains `button_text`, if desired Optional `shortcut.button_text` The text of the shortcut button Optional ### The inputs object {#inputs} The `inputs` of a trigger map to the inputs of a [workflow](/tools/deno-slack-sdk/guides/creating-workflows). You can pass any value as an input. You should either provide a `value` for every input, or mark the input as `customizable: true` instead. See [workflow buttons](#workflow_buttons) for info on why you might want to use `customizable: true`. There are also a specific set of input values that contain information about the trigger. Pass any of these values to provide trigger information to your workflows! Fields that take the form of `data.VALUE` can be referenced in the form of `TriggerContextData.Shortcut.VALUE` Referenced field Type Description `TriggerContextData.Shortcut.action_id` string Only available when the trigger is invoked from a [Workflow Button](#workflow_buttons)! A unique identifier for the action that invoked the trigger. `TriggerContextData.Shortcut.block_id` string Only available when the trigger is invoked from a [Workflow Button](#workflow_buttons)! A unique identifier for the block where the trigger was invoked. `TriggerContextData.Shortcut.bookmark_id` string Only available when the trigger is invoked from a channel's bookmarks bar! A unique identifier for the bookmark where the trigger was invoked. `TriggerContextData.Shortcut.channel_id` string Only available when the trigger is invoked from a channel, DM or MPDM! A unique identifier for the channel where the trigger was invoked. `TriggerContextData.Shortcut.interactivity` object A temporary token for use in building interactive UIs in the Slack client. `TriggerContextData.Shortcut.location` string Where the trigger was invoked. Can be `message`, `bookmark` or `button`. `TriggerContextData.Shortcut.message_ts` string Only available when the trigger is invoked from a channel, DM or MPDM! A unique Unix timestamp in seconds indicating when the trigger-invoking message was sent. `TriggerContextData.Shortcut.user_id` string A unique identifier for the Slack user who invoked the trigger. `TriggerContextData.Shortcut.event_timestamp` timestamp A Unix timestamp in seconds indicating when this event was dispatched. The following snippet shows a `channel_id` input being set with a value of `TriggerContextData.Shortcut.channel_id`, which is a unique identifier for the channel where the trigger was invoked. ``` ...inputs: { channel_id: { value: TriggerContextData.Shortcut.channel_id }},... ``` ## Link trigger response {#trigger-response} The response will have a property called `ok`. If `true`, then the trigger was created, and the `trigger` property will be populated. Your response will include a `trigger.id`; be sure to store it! You use that to `update` or `delete` the trigger if need be. See [trigger management](/tools/deno-slack-sdk/guides/managing-triggers). An example response of a created link trigger ``` { // If ok == true, the trigger was created ok: true, // The newly created trigger's details are here trigger: { // Your trigger's unique ID id: "Ft12345", // inputs will contain a summary of your inputs as defined in the trigger file inputs: {}, // since this is a link trigger, `outputs` will automatically contain: // {{event_timestamp}}: time when the workflow started // {{data.user_id}}: The user ID of the person who invoked the trigger // (by clicking the shortcut link or run button in Slack) // {{data.channel_id}}: The channel where the shortcut was run // {{data.interactivity}}: The trigger's interactivity context outputs: { "{{event_timestamp}}": { type: "string", name: "event_timestamp", title: "Time when workflow started", is_required: false, description: "Time when workflow started" }, "{{data.user_id}}": { type: "slack#/types/user_id", name: "user_id", title: "Person who ran this shortcut", is_required: true, description: "Person who clicked the shortcut link or run button in Slack" }, "{{data.channel_id}}": { type: "slack#/types/channel_id", name: "channel_id", title: "Channel where the shortcut was run", is_required: false, description: "Channel where the shortcut was run, if available" }, "{{data.interactivity}}": { type: "slack#/types/interactivity", name: "interactivity", title: "Interactivity context", is_required: true, description: "Interactivity context", is_hidden: true } }, // Trigger-specific information date_created: 1661894315, date_updated: 1661894315, type: "shortcut", name: "Submit a ticket to our work management system", description: "", // The shortcut URL that will activate this trigger and invoke the underlying workflow shortcut_url: "https://slack.com/shortcuts/Ft12345/caef7d773d611ddd1da81fd85de08a78", // Details about the workflow associated with this trigger workflow: { id: "Fn1234567890", callback_id: "handle_new_tickets_workflow", title: "Handle new tickets", description: "Handles a new ticket and updates the submitting user", type: "workflow", // Any workflow inputs will be included here input_parameters: [], // Any of the workflow's outputs will be included here output_parameters: [], app_id: "A1234567890", // App-specific details app: { id: "A1234567890", name: "ticket-management-app", icons: [Object], is_workflow_app: false }, date_created: 1661889787, date_updated: 1661894304, date_deleted: 0, workflow_id: "Wf01234567890" } }} ``` ## Workflow buttons {#workflow_buttons} You can also create link triggers in the form of workflow buttons! [Workflow buttons](https://docs.slack.dev/reference/block-kit/block-elements/workflow-button-element) are Block Kit elements that allow you to use link triggers with _customizable_ inputs. These customizable inputs are provided by the workflow button itself when it is used, as opposed to the inputs being provided when the link trigger is created. You can create a link trigger once, and then use that same link trigger multiple times with different values. You should use workflow buttons when your function's logic execution is about to complete but you want to provide users with follow-up actions in a message. These follow-up actions likely fit better in a separate workflow than within your interactivity handler. Distributing actions for multiple users (like a poll), or updating an object created by your function (like an incident), are common examples of when one would use workflow buttons. ### Creating the link trigger for a button {#workflow_buttons_create} Creating a link trigger to use as a workflow button is much like how you would normally [create a link trigger with the CLI](#create-cli). The only addition is that you need to specify which of the parameters are customizable. Customizable parameters can have their values provided by a workflow button that wraps a link trigger. You can set a maximum of 10 input parameters as customizable. The below example shows a sample payload used to create a link trigger that has one customizable parameter: `some_customizable_parameter`, and one non-customizable parameter: `channel_id`. ``` // In a file: ./triggers/some_workflow_trigger.tsimport { Trigger } from "deno-slack-sdk/types.ts";import SomeWorkflow from "../workflows/some_workflow.ts";import { TriggerContextData, TriggerTypes } from "deno-slack-api/mod.ts";export const someWorkflowTrigger: Trigger = { type: TriggerTypes.Shortcut, name: "Some Workflow trigger", description: "A trigger for SomeWorkflow that allows for workflow_buttons to customize the value of some_customizable_parameter", workflow: "#/workflows/some_workflow", inputs: { channel_id: { value: TriggerContextData.Shortcut.channel_id, }, some_customizable_parameter: { customizable: true, }, },}; ``` The trigger definition above is for a workflow defined like so: ``` // In a file: ./workflows/some_workflow.tsimport { DefineWorkflow, Schema } from "deno-slack-sdk/mod.ts";export const SomeWorkflow = DefineWorkflow({ callback_id: "some_workflow", title: "Some Workflow", input_parameters: { required: [], properties: { channel_id: { type: Schema.slack.types.channel_id, }, some_customizable_parameter: { type: Schema.types.string, }, }, },}); ``` Running `slack triggers create --trigger-def="./triggers/some_workflow_trigger.ts"` would output a link trigger URL (e.g. `https://slack.com/shortcuts/Ft0123ABC456/123XYZ`), which will be referenced elsewhere inside the actual `workflow_button` elements. See [below](#workflow_buttons_use) for more details. You can have both customizable and non-customizable input parameters, but only the customizable input parameters can be provided elsewhere by a [`workflow_button`](https://docs.slack.dev/reference/block-kit/block-elements/workflow-button-element) element. A non-customizable input parameter does not have a `value`, since it will be provided elsewhere by a `workflow_button` element. The values used for input parameters set as `customizable: true` may be visible client-side to end users. You should not share sensitive information or secrets via these input parameters. Input parameters marked as `customizable: true` are restricted to input parameters of types `string`, `Schema.slack.types.channel_id`, or `Schema.slack.types.user_id`. Remember, link triggers are specific to an environment and workspace. You will need to create the link trigger that uses customizable inputs in each environment and workspace you want to use workflow buttons in. ### Using the link trigger with a button {#workflow_buttons_use} After you have created a link trigger with customizable input parameters, you can use it in a [`workflow_button`](https://docs.slack.dev/reference/block-kit/block-elements/workflow-button-element), within which you will actually provide the values for the customizable input parameters. Workflow buttons are only supported in messages and message attachments, and not on views. You can use [`workflow_buttons`](https://docs.slack.dev/reference/block-kit/block-elements/workflow-button-element) with any Slack function that accepts `interactive_blocks`, like [`SendMessage`](/tools/deno-slack-sdk/reference/slack-functions/send_message) and [`SendDm`](/tools/deno-slack-sdk/reference/slack-functions/send_dm). ``` MyWorkflow.addStep(Schema.slack.functions.SendMessage, { channel_id: MyWorkflow.inputs.channel, message: `Click on this workflow button to run SomeWorkflow (not MyWorkflow!).`, interactive_blocks: [ { type: "actions", elements: [ { type: "workflow_button", text: { type: "plain_text", text: "Run Me", }, workflow: { trigger: { url: "https://slack.com/shortcuts/Ft0123ABC456/123XYZ", customizable_input_parameters: [ { name: "some_customizable_parameter", value: MyWorkflow.inputs.input_coming_from_elsewhere, }, ], }, }, }, ], }, ],}); ``` The above example is a workflow step for `MyWorkflow`, which has a button that will run a _different_ workflow (`SomeWorkflow`) via the link trigger URL `https://slack.com/shortcuts/Ft0123ABC456/123XYZ`. When `SomeWorkflow` gets run through this workflow button, the value of `MyWorkflow.inputs.input_coming_from_elsewhere` will be passed in to `SomeWorkflow`. You can also use [`workflow_buttons`](https://docs.slack.dev/reference/block-kit/block-elements/workflow-button-element) when you call [`chat.postMessage`](https://docs.slack.dev/reference/methods/chat.postMessage), [`chat.scheduleMessage`](https://docs.slack.dev/reference/methods/chat.scheduleMessage), or [`chat.postEphemeral`](https://docs.slack.dev/reference/methods/chat.postEphemeral) inside the `blocks` directly. ``` export default SlackFunction(MyFunction, async ({ inputs, token, env, client }) => { const resp = await client.chat.postMessage({ channel: inputs.channel_id, blocks: [ { type: "header", text: { type: "plain_text", text: "A header above the workflow button.", }, }, { type: "section", text: { type: "mrkdwn", text: "A section block with a workflow button accessory: ", }, accessory: { type: "workflow_button", text: { type: "plain_text", text: "Run Me", }, workflow: { trigger: { url: "https://slack.com/shortcuts/Ft0123ABC456/123XYZ", customizable_input_parameters: [ { name: "some_customizable_parameter", value: inputs.some_value, }, ], }, }, }, }, ], }); ...}); ``` ### Data validation {#workflow_buttons_data_validation} Defining customizable input parameters for your link trigger means that the values for these inputs will be provided elsewhere (where the link trigger is used in a workflow button). In your workflow, validate the inputs you receive by ensuring that any provided users are authorized to pass the input, and that the values received are ones you expect to receive and nothing more. For example: You're building a workflow that grants salary increases for individuals. Your workflow has three parameters: `approver_user`, `target_user`, `amount`. You'll want to make sure to validate inside the workflow itself that the provided `approver_user` is authorized to grant a salary increase of `amount` for the `target_user`. ## Onward {#onward} ➡️ With your trigger created, you can now test your app by [running your app locally](/tools/deno-slack-sdk/guides/developing-locally). ✨ Once your app is active, see [trigger management](/tools/deno-slack-sdk/guides/managing-triggers) for info on managing your triggers in your workspace. --- Source: https://docs.slack.dev/tools/deno-slack-sdk/guides/creating-scheduled-triggers # Creating scheduled triggers Workflow apps require a paid plan Join the [Developer Program](https://api.slack.com/developer-program) and provision a sandbox with access to all Slack features for free. > Invoke a workflow at specific time intervals Scheduled triggers are an _automatic_ type of trigger. This means that once the trigger is created, they do not require any user input. Use a scheduled trigger if you need a workflow to kick off after a delay or on an hourly, daily, weekly, or annual cadence. ## Create a scheduled trigger {#create-trigger} Triggers can be added to workflows in two ways: * **You can add triggers with the CLI.** These static triggers are created only once. You create them with the Slack CLI, attach them to your app's workflow, and that's that. The trigger is defined within a trigger file. * **You can add triggers at runtime.** These dynamic triggers are created at any step of a workflow so they can incorporate data acquired from other workflow steps. The trigger is defined within a function file. * Create a scheduled trigger with the CLI * Create a scheduled trigger at runtime Slack CLI built-in documentation Use `slack trigger --help` to easily access information on the `trigger` command's flags and subcommands. The triggers you create when running locally (with the `slack run` command) will not work when you deploy your app in production (with the `slack deploy` command). You'll need to `create` any triggers again with the CLI. ### Create the trigger file {#create-the-trigger-file} To create a scheduled trigger with the CLI, you'll need to create a trigger file. The trigger file contains the payload you used to define your trigger. Create a TypeScript trigger file within your app's folder with the following form: ``` import { Trigger } from "deno-slack-api/types.ts";import { ExampleWorkflow } from "../workflows/example_workflow.ts";import { TriggerTypes } from "deno-slack-api/mod.ts";const trigger: Trigger = { // your TypeScript payload};export default trigger; ``` Your TypeScript payload consists of the [parameters](#parameters) needed for your own use case. Below is the trigger file from the [Message Translator](https://github.com/slack-samples/deno-message-translator) app: ``` // triggers/daily_maintenance_job.tsimport { Trigger } from "deno-slack-sdk/types.ts";import workflowDef from "../workflows/maintenance_job.ts";import { TriggerTypes } from "deno-slack-api/mod.ts";/** * A trigger that periodically starts the "maintenance-job" workflow. */const trigger: Trigger = { type: TriggerTypes.Scheduled, name: "Trigger a scheduled maintenance job", workflow: `#/workflows/${workflowDef.definition.callback_id}`, inputs: {}, schedule: { // Schedule the first execution 60 seconds from when the trigger is created start_time: new Date(new Date().getTime() + 60000).toISOString(), end_time: "2037-12-31T23:59:59Z", frequency: { type: "daily", repeats_every: 1 }, },};export default trigger; ``` ### Use the trigger create command {#use-the-trigger-create-command} Once you have created a trigger file, use the following command to create the scheduled trigger: ``` slack trigger create --trigger-def "path/to/trigger.ts" ``` If you have not used the `slack triggers create` command to create a trigger prior to running the `slack run` command, you will receive a prompt in the Slack CLI to do so. Your app needs to have the [`triggers:write`](https://docs.slack.dev/reference/scopes/triggers.write) scope to use a trigger at runtime. Include the scope within your app's [manifest](/tools/deno-slack-sdk/guides/using-the-app-manifest). The logic of a runtime trigger lies within a function's TypeScript code. Within your `functions` folder, you'll have the functions that are the steps making up your workflow. Within this folder is where you can create a trigger within the relevant `.ts` file. When you create a runtime trigger, you can leverage `inputs` acquired from functions within the workflow. Provide the workflow definition to get additional typing for the workflow and inputs fields. Create a scheduled trigger at runtime using the `client.workflows.triggers.create` method within the relevant `function` file. ``` const triggerResponse = await client.workflows.triggers.create({ // your TypeScript payload}); ``` Your TypeScript payload consists of the [parameters](#parameters) needed for your own use case. Below is the function file with a TypeScript payload for a scheduled trigger from the [Daily Channel Topic](https://github.com/slack-samples/deno-daily-channel-topic) app: ``` // functions/create_scheduled_trigger.tsimport { SlackAPI } from "deno-slack-api/mod.ts";import { DefineFunction, Schema, SlackFunction } from "deno-slack-sdk/mod.ts";import { TriggerTypes } from "deno-slack-api/mod.ts";export const CreateScheduledTrigger = DefineFunction({ title: "Create a scheduled trigger", callback_id: "create_scheduled_trigger", source_file: "functions/create_scheduled_trigger.ts", input_parameters: { properties: { channel_id: { description: "The ID of the Channel to create a schedule for", type: Schema.slack.types.channel_id, }, }, required: ["channel_id"], }, output_parameters: { properties: { trigger_id: { description: "The ID of the trigger created by the Slack API", type: Schema.types.string, }, }, required: ["trigger_id"], },});export default SlackFunction( CreateScheduledTrigger, async ({ inputs, token }) => { console.log(`Creating scheduled trigger to update daily topic`); const client = SlackAPI(token, {}); const scheduleDate = new Date(); // Start schedule 1 minute in the future. Start_time must always be in the future. scheduleDate.setMinutes(scheduleDate.getMinutes() + 1); // triggers/sample_scheduled_update_topic.txt has a JSON example of the payload const scheduledTrigger = await client.workflows.triggers.create({ name: `Channel ${inputs.channel_id} Schedule`, workflow: "#/workflows/scheduled_update_topic", type: TriggerTypes.Scheduled, inputs: { channel_id: { value: inputs.channel_id }, }, schedule: { start_time: scheduleDate.toUTCString(), frequency: { type: "daily", repeats_every: 1, }, }, }); if (!scheduledTrigger.trigger) { return { error: "Trigger could not be created", }; } console.log("scheduledTrigger has been created"); return { outputs: { trigger_id: scheduledTrigger.trigger.id }, }; },); ``` ## Scheduled trigger parameters {#parameters} Field Description Required? `type` The type of trigger: `TriggerTypes.Scheduled` Required `name` The name of the trigger Required `workflow` Path to workflow that the trigger initiates Required `schedule` When and how often the trigger will activate. See the [`schedule`](#schedule) object below Required `description` The description of the trigger Optional `inputs` The inputs provided to the workflow. See the [`inputs`](#inputs) object below Optional Scheduled triggers are not interactive. Use a [link trigger](/tools/deno-slack-sdk/guides/creating-link-triggers) to take advantage of interactivity. ### The inputs object {#inputs} The `inputs` of a trigger map to the inputs of a [workflow](/tools/deno-slack-sdk/guides/creating-workflows). You can pass any value as an input. There is also a specific input value that contains information about the trigger. Pass this value to provide trigger information to your workflows! Fields that take the form of `data.VALUE` can be referenced in the form of `TriggerContextData.Shortcut.VALUE` Referenced field Type Description `TriggerContextData.Scheduled.user_id` string A unique identifier for the user who created the trigger. `TriggerContextData.Scheduled.event_timestamp` timestamp A Unix timestamp in seconds indicating when this event was dispatched. The following snippet shows a `user_id` input being set with a value of `TriggerContextData.Scheduled.user_id`, representing the user who created the trigger. ``` ...inputs: { user_id: { value: TriggerContextData.Scheduled.user_id }},... ``` ### The schedule object {#schedule} Field Description Required? `start_time` ISO date string of the first scheduled trigger Required `timezone` Timezone string to use for scheduling Optional `frequency` Details on what cadence trigger will activate. See the [`frequency`](#frequency) object below Optional `occurrence_count` The maximum number of times trigger will run Optional `end_time` If set, this trigger will not run past the provided ISO date string Optional ### The frequency object {#frequency} One-time triggers Field Description Required? `type` How often the trigger will activate: `once` Required `repeats_every` How often the trigger will repeat, respective to `frequency.type` Optional `on_week_num` The nth week of the month the trigger will repeat Optional #### Example one-time trigger {#once-example} ``` import { TriggerTypes } from "deno-slack-api/mod.ts";import { ScheduledTrigger } from "deno-slack-api/typed-method-types/workflows/triggers/scheduled.ts";import { ExampleWorkflow } from "../workflows/example_workflow.ts";const schedule: ScheduledTrigger = { name: "Sample", type: TriggerTypes.Scheduled, workflow: `#/workflows/${ExampleWorkflow.definition.callback_id}`, inputs: {}, schedule: { // Starts 60 seconds after creation start_time: new Date(new Date().getTime() + 60000).toISOString(), timezone: "asia/kolkata", frequency: { type: "once", }, },};export default schedule; ``` Hourly triggers Field Description Required? `type` How often the trigger will activate: `hourly` Required `repeats_every` How often the trigger will repeat, respective to `frequency.type` Required `on_week_num` The nth week of the month the trigger will repeat Optional #### Example hourly trigger {#hourly-example} ``` import { TriggerTypes } from "deno-slack-api/mod.ts";import { ScheduledTrigger } from "deno-slack-api/typed-method-types/workflows/triggers/scheduled.ts";import { ExampleWorkflow } from "../workflows/example_workflow.ts";const schedule: ScheduledTrigger = { name: "Sample", type: TriggerTypes.Scheduled, workflow: `#/workflows/${ExampleWorkflow.definition.callback_id}`, inputs: {}, schedule: { // Starts 60 seconds after creation start_time: new Date(new Date().getTime() + 60000).toISOString(), end_time: "2040-05-01T14:00:00Z", frequency: { type: "hourly", repeats_every: 2, }, },};export default schedule; ``` Daily triggers Field Description Required? `type` How often the trigger will activate: `daily` Required `repeats_every` How often the trigger will repeat, respective to `frequency.type` Required `on_week_num` The nth week of the month the trigger will repeat Optional #### Example daily trigger {#daily-example} ``` import { TriggerTypes } from "deno-slack-api/mod.ts";import { ScheduledTrigger } from "deno-slack-api/typed-method-types/workflows/triggers/scheduled.ts";import { ExampleWorkflow } from "../workflows/example_workflow.ts";const schedule: ScheduledTrigger = { name: "Sample", type: TriggerTypes.Scheduled, workflow: `#/workflows/${ExampleWorkflow.definition.callback_id}`, inputs: {}, schedule: { // Starts 60 seconds after creation start_time: new Date(new Date().getTime() + 60000).toISOString(), end_time: "2040-05-01T14:00:00Z", occurrence_count: 3, frequency: { type: "daily" }, },};export default schedule; ``` Weekly triggers Field Description Required? `type` How often the trigger will activate: `weekly` Required `on_days` The days of the week the trigger should activate on Required `repeats_every` How often the trigger will repeat, respective to `frequency.type` Required `on_week_num` The nth week of the month the trigger will repeat Optional #### Example weekly trigger {#weekly-example} ``` import { TriggerTypes } from "deno-slack-api/mod.ts";import { ScheduledTrigger } from "deno-slack-api/typed-method-types/workflows/triggers/scheduled.ts";import { ExampleWorkflow } from "../workflows/example_workflow.ts";const schedule: ScheduledTrigger = { name: "Sample", type: TriggerTypes.Scheduled, workflow: `#/workflows/${ExampleWorkflow.definition.callback_id}`, inputs: {}, schedule: { // Starts 60 seconds after creation start_time: new Date(new Date().getTime() + 60000).toISOString(), frequency: { type: "weekly", repeats_every: 3, on_days: ["Friday", "Monday"], }, },};export default schedule; ``` Monthly triggers Field Description Required? `type` How often the trigger will activate: `monthly` Required `on_days` The day of the week the trigger should activate on. Provide the `on_week_num` value along with this field. Required `repeats_every` How often the trigger will repeat, respective to `frequency.type` Required `on_week_num` The nth week of the month the trigger will repeat Optional #### Example monthly trigger {#monthly-example} ``` import { TriggerTypes } from "deno-slack-api/mod.ts";import { ScheduledTrigger } from "deno-slack-api/typed-method-types/workflows/triggers/scheduled.ts";import { ExampleWorkflow } from "../workflows/example_workflow.ts";const schedule: ScheduledTrigger = { name: "Sample", type: TriggerTypes.Scheduled, workflow: `#/workflows/${ExampleWorkflow.definition.callback_id}`, inputs: {}, schedule: { // Starts 60 seconds after creation start_time: new Date(new Date().getTime() + 60000).toISOString(), frequency: { type: "monthly", repeats_every: 3, on_days: ["Friday"], on_week_num: 1, }, },};export default schedule; ``` Yearly triggers Field Description Required? `type` How often the trigger will activate: `yearly` Required `repeats_every` How often the trigger will repeat, respective to `frequency.type` Required `on_week_num` The nth week of the month the trigger will repeat Optional #### Example yearly trigger {#yearly-example} ``` import { TriggerTypes } from "deno-slack-api/mod.ts";import { ScheduledTrigger } from "deno-slack-api/typed-method-types/workflows/triggers/scheduled.ts";import { ExampleWorkflow } from "../workflows/example_workflow.ts";const schedule: ScheduledTrigger = { name: "Sample", type: TriggerTypes.Scheduled, workflow: `#/workflows/${ExampleWorkflow.definition.callback_id}`, inputs: {}, schedule: { // Starts 60 seconds after creation start_time: new Date(new Date().getTime() + 60000).toISOString(), frequency: { type: "yearly", repeats_every: 2, }, },};export default schedule; ``` ## Scheduled trigger response {#response} The response will have a property called `ok`. If `true`, then the trigger was created, and the `trigger` property will be populated. Your response will include a `trigger.id`; be sure to store it! You use that to `update` or `delete` the trigger if need be. See [trigger management](/tools/deno-slack-sdk/guides/managing-triggers). ## Onward {#onward} ➡️ With your trigger created, you can now test your app by [running your app locally](/tools/deno-slack-sdk/guides/developing-locally). ✨ Once your app is active, see [trigger management](/tools/deno-slack-sdk/guides/managing-triggers) for info on managing your triggers in your workspace. --- Source: https://docs.slack.dev/tools/deno-slack-sdk/guides/creating-slack-functions # Creating Slack functions Workflow apps require a paid plan Join the [Developer Program](https://api.slack.com/developer-program) and provision a sandbox with access to all Slack features for free. Slack functions are essentially Slack-native actions, like creating a channel or sending a message. Use them alongside your [custom functions](/tools/deno-slack-sdk/guides/creating-custom-functions) in a [workflow](/tools/deno-slack-sdk/guides/creating-workflows). [Browse our inventory of Slack functions](/tools/deno-slack-sdk/reference/slack-functions). Slack functions need to be imported from the standard library built into the [Slack Deno SDK](https://github.com/slackapi/deno-slack-sdk)—all Slack functions are children of the `Schema.slack.functions` object. Just like custom functions, Slack functions can be added to steps in a workflow using the `addStep` method. Slack functions define their own inputs and outputs, as detailed for each Slack function in the catalog below. ## Slack functions catalog {#catalog} The details for each Slack function can be found in our [reference documentation](/tools/deno-slack-sdk/reference/slack-functions). ## Slack function example {#example} Here's an example of a workflow that creates a new Slack channel using the [`CreateChannel`](/tools/deno-slack-sdk/reference/slack-functions/create_channel) Slack function: ``` import { DefineWorkflow, Schema } from "deno-slack-sdk/mod.ts";// Define a workflow that can pass the parameters for the Slack functionconst myWorkflow = DefineWorkflow({ callback_id: "channel-creator", title: "Channel Creator", input_parameters: { properties: { channel_name: { type: Schema.types.string } }, required: ["channel_name"], },});const createChannelStep = myWorkflow.addStep( Schema.slack.functions.CreateChannel, { channel_name: myWorkflow.inputs.channel_name, is_private: false, },);export default myWorkflow; ``` ## Timeouts for functions {#timeouts-for-functions} When building workflows using functions, there is a 60 second timeout for a deployed function and a 15 second timeout for a locally-run function. For deployed functions using a `block_suggestion`, `block_actions`, `view_submission`, or `view_closed` payload, there is a 10 second timeout. If a function has not finished running within its respective time limit, you will see an error in your log. Refer to [logging](/tools/deno-slack-sdk/guides/logging-function-and-app-behavior) for more details. * * * ➡️ **To learn how to add a Slack function to a workflow**, head to the [workflows](/tools/deno-slack-sdk/guides/creating-workflows) section. ➡️ **To browse all Slack functions**, head to the [Slack Function reference catalog](/tools/deno-slack-sdk/reference/slack-functions) ✨ **To learn how to create your own _custom_ functions**, head to the [custom functions](/tools/deno-slack-sdk/guides/creating-custom-functions) section. --- Source: https://docs.slack.dev/tools/deno-slack-sdk/guides/creating-webhook-triggers # Creating webhook triggers Workflow apps require a paid plan Join the [Developer Program](https://api.slack.com/developer-program) and provision a sandbox with access to all Slack features for free. > Invoke a workflow when a specific URL receives a POST request Webhook triggers are an _automatic_ type of trigger that listens for a certain type of data, much like event triggers. While event triggers are used for activating a trigger based on _internal_ activity, webhooks are instead used when activating a trigger based on _external_ activity. In other words, webhook triggers are useful when tying Slack functionality together with non-Slack services. There are two steps to using a webhook trigger: 1. [Create a trigger, either via the CLI or at runtime](#create-trigger) 2. [Invoke the trigger with a POST Request](#invoke-trigger) ## Create a webhook trigger {#create-trigger} Triggers can be added to workflows in two ways: * **You can add triggers with the CLI.** These static triggers are created only once. You create them with the Slack CLI, attach them to your app's workflow, and that's that. The trigger is defined within a trigger file. * **You can add triggers at runtime.** These dynamic triggers are created at any step of a workflow so they can incorporate data acquired from other workflow steps. The trigger is defined within a function file. * Create a webhook trigger with the CLI * Create an event trigger at runtime Slack CLI built-in documentation Use `slack trigger --help` to easily access information on the `trigger` command's flags and subcommands. The triggers you create when running locally (with the `slack run` command) will not work when you deploy your app in production (with the `slack deploy` command). You'll need to `create` any triggers again with the CLI. ### Create the trigger file {#create-the-trigger-file} To create a webhook trigger with the CLI, you'll need to create a trigger file. The trigger file contains the payload you used to define your trigger. Create a TypeScript trigger file within your app's folder with the following form: ``` import { Trigger } from "deno-slack-api/types.ts";import { ExampleWorkflow } from "../workflows/example_workflow.ts";import { TriggerTypes } from "deno-slack-api/mod.ts";const trigger: Trigger = { // your TypeScript payload};export default trigger; ``` Your TypeScript payload consists of the [parameters](#parameters) needed for your own use case. The following is a TypeScript payload for creating a webhook trigger: ``` import { Trigger } from "deno-slack-api/types.ts";import { ExampleWorkflow } from "../workflows/example_workflow.ts";import { TriggerTypes } from "deno-slack-api/mod.ts";const trigger: Trigger = { type: TriggerTypes.Webhook, name: "sends 'how cool is that' to my fav channel", description: "runs the example workflow", // "myWorkflow" must be a valid callback_id of a workflow workflow: "#/workflows/myWorkflow", inputs: { stringToReverse: { value: "how cool is that", }, channel: { value: "{{data.channel}}", }, },};export default trigger; ``` ### Use the trigger create command {#use-the-trigger-create-command} Once you have created a trigger file, use the following command to create the webhook trigger: ``` slack trigger create --trigger-def "path/to/trigger.ts" ``` If you have not used the `slack triggers create` command to create a trigger prior to running the `slack run` command, you will receive a prompt in the Slack CLI to do so. Your app needs to have the [`triggers:write`](https://docs.slack.dev/reference/scopes/triggers.write) scope to use a trigger at runtime. Include the scope within your app's [manifest](/tools/deno-slack-sdk/guides/using-the-app-manifest). The logic of a runtime trigger lies within a function's TypeScript code. Within your `functions` folder, you'll have the functions that are the steps making up your workflow. Within this folder is where you can create a trigger within the relevant `.ts` file. When you create a runtime trigger, you can leverage `inputs` acquired from functions within the workflow. Provide the workflow definition to get additional typing for the workflow and inputs fields. Create a webhook trigger at runtime using the `client.workflows.triggers.create` method within the relevant `function` file. ``` const triggerResponse = await client.workflows.triggers.create({ // your TypeScript payload); ``` Your TypeScript payload consists of the [parameters](#parameters) needed for your own use case. Below is a function file with an example TypeScript payload for a webhook trigger. ``` // functions/example_function.tsimport { DefineFunction, Schema, SlackFunction } from "deno-slack-sdk/mod.ts";import { ExampleWorkflow } from "../workflows/example_workflow.ts";import { TriggerTypes } from "deno-slack-api/mod.ts";export const ExampleFunctionDefinition = DefineFunction({ callback_id: "example_function_def", title: "Example function", source_file: "functions/example_function.ts",});export default SlackFunction( ExampleFunctionDefinition, ({ inputs, client }) => { const triggerResponse = await client.workflows.triggers.create({ type: TriggerTypes.Webhook, name: "sends 'how cool is that' to my fav channel", description: "runs the example workflow", workflow: `#/workflows/${ExampleWorkflow.definition.callback_id}`, inputs: { stringToReverse: { value: "how cool is that", }, channel: { value: "{{data.channel}}", }, } }); // ... ``` * * * ## Webhook trigger parameters {#parameters} Field Description Required? `type` The type of trigger: `TriggerTypes.Webhook` Required `name` The name of the trigger Required `workflow` Path to workflow that the trigger initiates Required `description` The description of the trigger Optional `inputs` The inputs provided to the workflow Optional `webhook` Contains `filter`, if desired Optional `webhook.filter` See [trigger filters](/tools/deno-slack-sdk/guides/creating-event-triggers/#filters) Optional Webhook triggers are not interactive. Use a [link trigger](/tools/deno-slack-sdk/guides/creating-link-triggers) to take advantage of interactivity. ## Webhook trigger response {#response} The response will have a property called `ok`. If `true`, then the trigger was created, and the `trigger` property will be populated. Your response will include a `trigger.id`; be sure to store it! You use that to `update` or `delete` the trigger if need be. See [trigger management](/tools/deno-slack-sdk/guides/managing-triggers). * * * ## Invoke the trigger {#invoke-trigger} Send a POST request to invoke the trigger. Within that POST request you can send values for specific inputs. All JSON objects sent in the POST request need to be flat. Nested JSON objects will return a `parameter_validation_failed` error. **Good _flattened_ JSON object:** ``` {"channel":"C123ABC456","user":"U123ABC456"} ``` **No good, very bad _nested_ JSON object:** ``` // JSON does not support comments but we really don't want you using this code{"channel":"C123ABC456","user":{"first_name":"Jesse","last_name":"Slacksalot"}} ``` Now let's look at an entire example. ### Example POST request {#example} ``` curl \ -X POST "https://hooks.slack.com/triggers/T123ABC456/.../..." \ --header "Content-Type: application/json; charset=utf-8" \ --data '{"channel":"C123ABC456"}' ``` If the webhook was received and successfully handled, you'll get the following response: ``` { "ok":true} ``` ## Onward {#onward} ➡️ With your trigger created, you can now test your app by [running your app locally](/tools/deno-slack-sdk/guides/developing-locally). ✨ Once your app is active, see [trigger management](/tools/deno-slack-sdk/guides/managing-triggers) for info on managing your triggers in your workspace. --- Source: https://docs.slack.dev/tools/deno-slack-sdk/guides/creating-workflows # Creating workflows Workflow apps require a paid plan Join the [Developer Program](https://api.slack.com/developer-program) and provision a sandbox with access to all Slack features for free. Workflows are the combination of functions, executed in order. Remember: 1. Both [Slack functions](/tools/deno-slack-sdk/guides/creating-slack-functions) and [custom functions](/tools/deno-slack-sdk/guides/creating-custom-functions) define the actions of your app. 2. Workflows are a combination of functions, executed in order. (⬅️ you are here) 3. Triggers execute workflows. Depending on your use case, you'll want to acquaint yourself with either [Slack functions](/tools/deno-slack-sdk/guides/creating-slack-functions), [custom functions](/tools/deno-slack-sdk/guides/creating-custom-functions), or both. Then continue here to learn how to implement them in a workflow. We'll walk through defining a workflow, adding input and output parameters, adding both a Slack function and a custom function to the workflow, and declaring the workflow in your manifest. ## Defining workflows {#defining-workflows} Workflows are defined in their own files within your app's `/workflows` directory and declared in your app's [manifest](/tools/deno-slack-sdk/guides/using-the-app-manifest). Listing workflows in your manifest tells the CLI that they are implemented in your app — more on that later. Before defining your workflow, import [`DefineWorkflow`](https://github.com/slackapi/deno-slack-sdk/blob/main/src/workflows/mod.ts) at the top of your workflow file: ``` // say_hello_workflow.tsimport { DefineWorkflow } from "deno-slack-sdk/mod.ts"; ``` Then, create a **workflow definition**. This is where you set, at a minimum, the workflow's title and its unique callback ID: ``` // say_hello_workflow.tsexport const SayHelloWorkflow = DefineWorkflow({ callback_id: "say_hello_workflow", title: "Say Hello",}); ``` Definition properties Description Required? `callback_id` A unique string that identifies this particular component of your app. Required `title` The display name of the workflow that shows up in slugs, unfurl cards, and certain end-user modals. Required `description` A string description of this workflow. Optional `input_parameters` See [Defining input parameters](#defining-input-parameters). Optional In the next section, we'll look at `input_parameters` in more detail. ## Defining input parameters {#defining-input-parameters} Workflows can pass information into both functions and other workflows that are part of its workflow steps. To do this, we define what information we want to bring in to the workflow via its `input_parameters` property. A workflow's `input_parameters` property has two sub-properties: * `required`, which is how you can ensure that a workflow only executes if specific input parameters are provided. * `properties`, where you can list the specific parameters that your workflow accounts for. Any [built-in type](/tools/deno-slack-sdk/reference/slack-types) or [custom type](/tools/deno-slack-sdk/guides/creating-a-custom-type) can be used. Input parameters are listed in the `properties` sub-property. Each input parameter must include a `type` and a `description`, and can optionally include a `default` value. ``` import { DefineWorkflow, Schema } from "deno-slack-sdk/mod.ts";// Workflow definitionexport const SomeWorkflow = DefineWorkflow({ callback_id: "some_workflow", title: "Some Workflow", input_parameters: { required: [], properties: { exampleString: { type: Schema.types.string, description: "Here's an example string.", }, exampleBoolean: { type: Schema.types.boolean, description: "An example boolean.", default: true, }, exampleInteger: { type: Schema.types.integer, description: "An example integer.", }, exampleChannelId: { type: Schema.slack.types.channel_id, description: "Example channel ID.", }, exampleUserId: { type: Schema.slack.types.user_id, description: "Example user ID.", }, exampleUsergroupId: { type: Schema.slack.types.usergroup_id, description: "Example usergroup ID.", }, }, },}); ``` Denote which properties are required by listing their names as strings in the `required` property of `input_parameters`. For example, here's how we can indicate that a parameter named `exampleUserId` is required: ``` import { DefineWorkflow, Schema } from "deno-slack-sdk/mod.ts";// Workflow definitionexport const SomeWorkflow = DefineWorkflow({ callback_id: "some_workflow", title: "Some Workflow", input_parameters: { required: ["exampleUserId"], properties: { exampleUserId: { type: Schema.slack.types.user_id, description: "Example user ID.", }, }, },}); ``` If a workflow is invoked and the required input parameters are not provided, the workflow will not execute. An important distinction: `input_parameters` are used when _defining_ a workflow, whereas _retrieving_ values will use `inputs`. `inputs` is also used when implementing the logic of a custom function. Once you've defined your workflow, you can then add functionality by calling Slack functions and custom functions. This is done with the `addStep` method, which takes two arguments: * the function you want to call * the inputs (if any) you want to pass to that function. We'll see examples of how to call both types of functions in the following section. * * * ## Adding functions to workflows {#adding-functions} ### Import Schema reference {#import-schema} The first step to adding a function to a workflow is to import `Schema` from the Slack SDK. ``` // /workflows/greeting_workflow.tsimport { DefineWorkflow, Schema } from "deno-slack-sdk/mod.ts"; ``` ### Call a function with addStep {#call-function} #### Slack functions {#workflow-slack-functions} > [Slack functions](/tools/deno-slack-sdk/guides/creating-slack-functions) are essentially Slack-native actions, like creating a channel or sending a message. To use a Slack function, like [`SendMessage`](/tools/deno-slack-sdk/reference/slack-functions/send_message), let's look at an example from the [Deno Hello World](https://github.com/slack-samples/deno-hello-world) sample app. After defining the workflow, call the Slack function with your workflow's `addStep` method: ``` const GreetingWorkflow = DefineWorkflow({ callback_id: "greeting_workflow", title: "Send a greeting", description: "Send a greeting to channel", input_parameters: { properties: { interactivity: { type: Schema.slack.types.interactivity, }, channel: { type: Schema.slack.types.channel_id, }, }, required: ["interactivity"], },});const inputForm = GreetingWorkflow.addStep( Schema.slack.functions.OpenForm, { title: "Send a greeting", interactivity: GreetingWorkflow.inputs.interactivity, submit_label: "Send greeting", fields: { elements: [{ name: "recipient", title: "Recipient", type: Schema.slack.types.user_id, }, { name: "channel", title: "Channel to send message to", type: Schema.slack.types.channel_id, default: GreetingWorkflow.inputs.channel, }, { name: "message", title: "Message to recipient", type: Schema.types.string, long: true, }], required: ["recipient", "channel", "message"], }, },);//...call GreetingFunctionDefinition in greetingFunctionStep// Example: taking the string output from the greetingFunctionStep function and passing it to SendMessageGreetingWorkflow.addStep(Schema.slack.functions.SendMessage, { channel_id: inputForm.outputs.fields.channel, message: greetingFunctionStep.outputs.greeting,}); ``` ##### Using OpenForm in a workflow {#using-forms} The only Slack function that has an additional requirement is [`OpenForm`](/tools/deno-slack-sdk/reference/slack-functions/open_form). When creating a workflow that will have a step to open a form, your workflow needs to: * include the `interactivity` input parameter * have the call to `OpenForm` be its **first** step _or_ ensure the preceding step is interactive. An interactive step will generate a fresh pointer to use for opening the form; for example, use the interactive button that can be added with the [`SendMessage`](/tools/deno-slack-sdk/reference/slack-functions/send_message) Slack function immediately before opening the form. Here's an example of a basic workflow definition using `interactivity`: ``` import { DefineWorkflow, Schema } from "deno-slack-sdk/mod.ts";export const SayHelloWorkflow = DefineWorkflow({ callback_id: "say_hello_workflow", title: "Say Hello to a user", input_parameters: { properties: { interactivity: { type: Schema.slack.types.interactivity } }, required: ["interactivity"], },}); ``` ✨ Visit the [forms](/tools/deno-slack-sdk/guides/creating-a-form) section for more details and code examples of using `OpenForm` in your app. #### Custom functions {#workflow-custom-functions} > [Custom functions](/tools/deno-slack-sdk/guides/creating-custom-functions) are reusuable building blocks of automation of your own design. To use a [custom function](/tools/deno-slack-sdk/guides/creating-custom-functions) that you have already defined: 1. Import the function in your manifest, where you define the workflow: ``` import { SomeFunction } from "../functions/some_function.ts"; ``` 2. Call your function, storing its output in a variable. Here you may also pass input parameters from the workflow into the function itself: ``` import { DefineWorkflow, Schema } from "deno-slack-sdk/mod.ts";import { SomeFunction } from "../functions/some_function.ts";export const SomeWorkflow = DefineWorkflow({ callback_id: "some_workflow", title: "Some Workflow", input_parameters: { properties: { someString: { type: Schema.types.string, description: "Some string", }, channelId: { type: Schema.slack.types.channel_id, description: "Target channel", default: "C1234567", }, }, required: [], },});const myFunctionResult = SomeWorkflow.addStep(SomeFunction, { // ... Pass along workflow inputs via SomeWorkflow.inputs // ... For example, SomeWorkflow.inputs.someString}); ``` 3. Use your function in follow-on steps. For example: ``` // Example: taking the string output from a function and passing it to SendMessageSomeWorkflow.addStep(Schema.slack.functions.SendMessage, { channel_id: SomeWorkflow.inputs.channelId, message: SomeFunction.outputs.exampleOutput, // This comes from your function definition}); ``` Once you've added all steps and functions to your workflow, there's one final stop to having a fully functioning workflow — adding it to the [app manifest](/tools/deno-slack-sdk/guides/using-the-app-manifest). * * * ## Adding the workflow to the manifest {#add-workflow} The final step of using a workflow is adding it to your [manifest](/tools/deno-slack-sdk/guides/using-the-app-manifest). Declare your workflow in your app's manifest definition of your manifest file like this: ``` // manifest.tsimport { Manifest } from "deno-slack-sdk/mod.ts";import { SayHelloWorkflow } from "./workflows/say_hello_workflow.ts";export default Manifest({ name: "sayhello", description: "A deno app with an example workflow", icon: "assets/icon.png", workflows: [SayHelloWorkflow], // Add your workflow here botScopes: ["commands", "chat:write", "chat:write.public"],}); ``` The workflows guest or external users can run is based on whether those workflows run functions that are defined with certain scopes. Refer to [guests and external users](/tools/deno-slack-sdk/guides/controlling-access-to-custom-functions#guests-external) for more details. * * * ## Onward {#onward} ➡️ **To keep building your app**, head to the [triggers](/tools/deno-slack-sdk/guides/using-triggers) section to learn how to create a trigger that invokes a defined workflow. You can also learn about [creating a datastore](/tools/deno-slack-sdk/guides/using-datastores) to store and retrieve information, or building [custom types](/tools/deno-slack-sdk/guides/creating-a-custom-type) for your data. --- Source: https://docs.slack.dev/tools/deno-slack-sdk/guides/deleting-items-from-a-datastore # Deleting items from a datastore Workflow apps require a paid plan Join the [Developer Program](https://api.slack.com/developer-program) and provision a sandbox with access to all Slack features for free. There are a couple ways you can delete items from a datastore. You can: * [Delete items with `delete` and `bulkDelete`](#delete) * [Delete items automatically](#delete-automatically) Slack CLI commands You can also delete items from a datastore with the [`datastore delete`](/tools/slack-cli/reference/commands/slack_datastore_delete) and [`datastore bulk-delete`](/tools/slack-cli/reference/commands/slack_datastore_bulk-delete) Slack CLI commands. ## Delete items with delete and bulkDelete {#delete} There are two methods for deleting items in datastores: * The [`apps.datastore.delete`](https://docs.slack.dev/reference/methods/apps.datastore.delete) method is used for single items. * The [`apps.datastore.bulkDelete`](https://docs.slack.dev/reference/methods/apps.datastore.bulkDelete) method is used for multiple items. They work quite similarly. In the following examples we'll be deleting items from a datastore via their primary key. Regardless of what you named your `primary_key`, the query will always use the `id` key. Example: Using the `delete` method to delete an item by its `primary_key` ``` // Somewhere in your function:const uuid = "6db46604-7910-4684-b706-ac5929dd16ef";const response = await client.apps.datastore.delete({ datastore: "drafts", id: uuid,});if (!response.ok) { const error = `Failed to delete a row in datastore: ${response.error}`; return { error };} ``` Example: Using the `bulkDelete` method to delete an item by its `primary_key` ``` // Somewhere in your function:const uuid = "6db46604-7910-4684-b706-ac5929dd16ef";const uuid2 = "1111111-1111-1111-1111-111111111111";const response = await client.apps.datastore.bulkDelete({ datastore: "drafts", ids: [uuid,uuid2]});if (!response.ok) { const error = `Failed to delete a row in datastore: ${response.error}`; return { error };} ``` If the call was successful, the payload's `ok` property will be `true`. If it is not successful, it will be `false` and provide an error in the `errors` property. Datastore bulk API methods may _partially_ fail The `partial_failure` error message indicates that some items were successfully processed while others need to be retried. This is likely due to rate limits. Call the method again with only those failed items. You'll find a `failed_items` array within the API response. The array contains all the items that failed, in the same format they were passed in. Copy the `failed_items` array and use it in your request. ## Delete items automatically {#delete-automatically} You can set up your datastore to automatically delete records which are old and no longer relevant. This is done with the Time To Live (TTL) feature offered by AWS DynamoDB. Use it to efficiently discard data your app no longer needs. For any item, define an expiration timestamp and the item will be automatically deleted once that expiration time has passed. Notice that we didn't say _immediately deleted_. AWS only guarantees deletion of expired items _48 hours past the expiration date_. If you query your table before 48 hours have passed, do not assume all expired items have been deleted. You can read more about this within the [AWS documentation](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/ttl-expired-items.html). See below for an example on querying a database while filtering out any remaining expired items. ### Enable and utilize the TTL feature {#enable-ttl} ##### Step 1. Select an attribute to use as the expiration timestamp {#step-1-select-an-attribute-to-use-as-the-expiration-timestamp} You can use a pre-existing attribute or add a new attribute. The attribute's type _must_ be set as `Schema.slack.types.timestamp` in the datastore definition. In this example, we're using an attribute called `expire_ts`: ``` // /datastores/drafts.tsimport { DefineDatastore, Schema } from "deno-slack-sdk/mod.ts";export default DefineDatastore({ name: "drafts", primary_key: "id", attributes: { id: { type: Schema.types.string, }, expire_ts: { type: Schema.slack.types.timestamp // This line! } }, ...}); ``` ##### Step 2. Set time_to_live_attribute to the selected attribute in the datastore definition {#step-2-set-time_to_live_attribute-to-the-selected-attribute-in-the-datastore-definition} ``` // /datastores/drafts.tsimport { DefineDatastore, Schema } from "deno-slack-sdk/mod.ts";export default DefineDatastore({ name: "drafts", time_to_live_attribute: "expire_ts" // This line! primary_key: "id", attributes: { id: { type: Schema.types.string, }, expire_ts: { type: Schema.slack.types.timestamp }, message: { type: Schema.types.string, } }, ...}); ``` ##### Step 3. Set expire_ts to a value, either programmatically or manually {#step-3-set-expire_ts-to-a-value-either-programmatically-or-manually} In this example we're adding an item containing the `expire_ts` key using the [`apps.datastore.put`](https://docs.slack.dev/reference/methods/apps.datastore.put) method: ``` const expiration = const putResp = await client.apps.datastore.put< typeof DraftDatastore.definition >({ datastore: DraftDatastore.name, item: { id: draftId, expire_ts: 23456432345, message: "Congrats on the promotion Jesse!" }, }); ``` ##### Step 4. Deploy your app {#step-4-deploy-your-app} Use the `slack deploy` command. See [Deploy to Slack](/tools/deno-slack-sdk/guides/deploying-to-slack) for more information. ##### Step 5. Properly query items {#step-5-properly-query-items} As mentioned [above](#delete-automatically), expired items may not be deleted immediately. You'll likely want to filter out those expired items. Here is an example of a query that filters out any expired items that have not been automatically deleted yet: ``` const result = await client.apps.datastore.query({ datastore: "DraftDatastore", expression: "attribute_not_exists(#expire_ts) OR #expire_ts > :timestamp", expression_attributes: { "#expire_ts": "expire_ts" }, expression_values: { ":timestamp":1708448410 } //Timestamp should be the current time}); ``` To see an example of filtering out expired items via the command line, see the documentation on the [`datastore query`](/tools/slack-cli/reference/commands/slack_datastore_query) command. ### Disable the TTL feature {#disable-ttl} ##### Step 1. Remove time_to_live_attribute in the datastore definition {#step-1-remove-time_to_live_attribute-in-the-datastore-definition} We only commented it out here because showing the absence of something is a bit anticlimactic. ``` export default DefineDatastore({ name: "drafts", primary_key: "id", attributes: { id: { type: Schema.types.string, }, //expire_ts: { // type: Schema.slack.types.timestamp //} }, ...}); ``` ##### Step 2. Deploy your app. {#step-2-deploy-your-app} Use the `slack deploy` command. See [Deploy to Slack](/tools/deno-slack-sdk/guides/deploying-to-slack) for more information. ### Change the TTL attribute {#change-ttl} Due to AWS limitations, changing the TTL attribute is a bit clunky. Step 1. [Disable TTL](#disable-ttl) Step 2. Wait one hour. This wait is because AWS puts time limits on additional changes to the TTL feature. Step 3. [Enable TTL again with the new attribute](#enable-ttl) Don't forget to deploy both when disabling and enabling TTL! --- Source: https://docs.slack.dev/tools/deno-slack-sdk/guides/deploying-to-slack # Deploying to Slack Workflow apps require a paid plan Join the [Developer Program](https://api.slack.com/developer-program) and provision a sandbox with access to all Slack features for free. Your apps can be deployed to Slack's managed infrastructure by running the `slack deploy` command at the root of your project. No local development server is started when running the `slack deploy` command, and your app will have a different ID than the one created for your app if you previously ran the `slack run` command. For more details about the differences between local and deployed apps, refer to [team collaboration](/tools/deno-slack-sdk/guides/collaborating-with-teammates). Beta feature `npm` dependencies are supported but are still in beta, so ensure that you test any `npm:` specifiers when using the `slack deploy` command (the `slack run` command is not affected). ## Using the slack deploy command {#slack-deploy} When you run the `slack deploy` command and you are logged into a Slack Enterprise organization, you may also be asked to select a workspace within your organization to deploy your app to (default is all workspaces). You may also specify a workspace within your organization to grant your app access to with the `--org-workspace-grant` flag. The Slack CLI will package up your app and deploy it to the workspace you specify. At that point, anyone in your workspace will be able to find and add your app by navigating to **Apps > Manage > Browse apps**. The `slack deploy` command behavior may vary slightly based on the operating system used. ### Function access {#function-access} To make a function available so that another user (or group of users) can access workflows that reference your function after you deploy your app, you can execute the `slack function access` command. After choosing your workspace, you'll also be prompted to select which function you want to deploy, as well as who you would like to give access to your function -- app collaborators only, specific users, or everyone. Your function will then be accessible to those users the next time you deploy your app. ✨ **For more information about function access**, refer to [custom function access](/tools/deno-slack-sdk/guides/controlling-access-to-custom-functions). Workflow apps are currently not eligible for distributing to the Slack Marketplace. ## Redeploying your app {#re-deploy} If you need to make any changes to your app, you must redeploy it using the `slack deploy` command again. In addition, if administrators of your workspace have enabled [Admin-Approved Apps](https://slack.com/help/articles/222386767-Manage-app-installation-settings-for-your-workspace), it means your app will need approval before it can be deployed or redeployed to your workspace. ✨ **For more information about getting your app approved**, check out [access controls for developers](/tools/deno-slack-sdk/guides/controlling-permissions-for-admins#dev-connectors). --- Source: https://docs.slack.dev/tools/deno-slack-sdk/guides/developing-locally # Developing locally Workflow apps require a paid plan Join the [Developer Program](https://api.slack.com/developer-program) and provision a sandbox with access to all Slack features for free. As you're developing your workflow app, you can see your changes propagated to your workspace in real-time by using the `slack run` command. We refer to the workspace you develop in as your _local_ environment, and the workspace you deploy your app to as your _deployed_ environment. You are not required to deploy your app. In fact, you might never need to use the `slack deploy` command — maybe there's something you want to do just one single time, or only when you need to — you can use `slack run` for that. Otherwise, you should think of your local environment as a development environment. We even append the string `(local)` to the end of your app's name when running in this context. ## Using the slack run command {#slack-run} When you enter `slack run` from the root directory of a project and you are logged into a Slack Enterprise organization, you may also be asked to select a workspace within your organization to grant your app access to. If administrators of your workspace have enabled [Admin-Approved Apps](/tools/deno-slack-sdk/guides/controlling-permissions-for-admins), it means your app will need approval before it can be installed to your workspace. ✨ **For information about getting your app approved**, refer to [access controls for developers](/tools/deno-slack-sdk/guides/controlling-permissions-for-admins#dev-connectors). The Slack CLI will then start a local development server and establish a connection to the `(local)` version of your app. Check that your instance of the Slack CLI is logged in to the desired workspace by running `slack auth list`. To start the local development server, use the `slack run` command: ``` $ slack run ``` You'll know your development server is ready when your terminal says the following: `Connected, awaiting events` To turn off the development server, enter `Ctrl`+`c` in the command line. ## Creating link triggers in local development {#local-triggers} Link triggers are unique to each installed version of your app. This means that their "shortcut URLs" will differ across each workspace, as well as between local and [deployed](/tools/deno-slack-sdk/guides/deploying-to-slack) apps. When creating a trigger, you must select the workspace you'd like to create the trigger in. Each workspace has a development version (denoted by `(local)`), as well as a deployed version. ✨ **For more information about link triggers**, refer to [access controls for developers](/tools/deno-slack-sdk/guides/creating-link-triggers). If your app has any triggers created within that development environment, they'll be listed when you run the `slack run` command. If you only created triggers within a production environment using the `slack deploy` command, they will not appear. ## Creating triggers with the slack run command {#cli-trigger-prompt} If you have not used the `slack triggers create` command to create a trigger prior to running the `slack run` command, you will receive a prompt in the Slack CLI to do so. Let's say you've created a Slack app and tried to run the `slack run` command without first creating a trigger. The Slack CLI will ask you which workspace you'd like to run your app in, and will then prompt you to choose a trigger definition file. If you choose a file, the trigger will be created and the app will run. If you do not choose a trigger definition file or if you do not yet have one created, a trigger will not be created. No worries either way, as your app will still continue with the run operation. ## App visibility {#visibility} As discussed above, once you create an app and run it using the `slack run` command, a link trigger will be generated for your app. Once that link trigger is posted in a public channel within your workspace, other channel members can click it and interact with your app even though your app has not been deployed. --- Source: https://docs.slack.dev/tools/deno-slack-sdk/guides/developing-with-deno # Developing with Deno Workflow apps require a paid plan Join the [Developer Program](https://api.slack.com/developer-program) and provision a sandbox with access to all Slack features for free. Now that we've [installed Deno](/tools/deno-slack-sdk/guides/installing-deno), lets create our first app to get the hang of our new environment. ## Running your first Deno app {#run} Using a plain text editor, create a new file called `app.js`. In that file, include the following: ``` console.log(`Hello, world!`); ``` In your terminal, change to the directory where you wrote that file. Then execute the following command: ``` deno run app.js ``` It should respond with `Hello, world!` and then finish. Wohoo, you just wrote your first app and executed it using the Deno runtime! ### Watching your app for changes {#run-watch} During development, you'll often find you are making lots of small changes in order to get everything in tip-top shape. It's annoying to get out of your developer flow because you have to constantly stop and restart your app, which is why it's great that Deno has a built in `watch` function that will constantly check for updates to a file and automatically reload it for you. First, tell Deno to watch your `app.js` file by appending the `--watch` flag to the `run` command as follows: ``` deno run --watch app.js ``` Now, update `app.js` by adding a second line after the first `console.log`: ``` console.log(`What's up?`); ``` As soon as you save the file, you should see the output of **both** lines. ### So fetch {#fetch} Just about any web app you could think to build is going to want to pull in data from somewhere else — it's pretty much how the whole digital economy works. Deno natively supports the [Fetch API that replaced `XMLHttpRequest`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) in modern browsers, so it's pretty easy to make fetch happen. The `fetch()` method is asynchronous and [Promises-based](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise). This is one of the key differences between Deno and Node.js. Since Promises allow asynchronous functions to behave like synchronous functions, this can make life easier for developers. Whereas synchronous functions run and return a value, _asynchronous_ functions return a Promise to return a value (or an error) once it's finished calling the API, querying the database, or making a calculation. You don't need to worry about all of the particulars of the asynchronous function running in the background, you just need to write the code that gets executed once the Promise is fulfilled. Use the [`await`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await) expression for this. When the Promise returns successfully, it resolves to a [Response object](https://developer.mozilla.org/en-US/docs/Web/API/Response). The Response comes as a stream of bytes, which another `await` function that calls the [`Response.text()` method](https://developer.mozilla.org/en-US/docs/Web/API/Response/text) can finally return as a string of the body of the HTTP response. Let's see how it works by replacing the code in your `app.js` file with the following: ``` const response = await fetch('https://example.com');const body = await response.text();console.log(body); ``` If `deno run --watch app.js` is still running, it will detect the change and return a warning. This is part of [Deno's security model at work](https://deno.land/manual/getting_started/permissions), where by default, no app is allowed access to external resources like the filesystem, network access, or even sub-process and environment variables — access to these resources must be explicitly allowed when you run your app. This focus on security is one reason why [many projects](https://deno.land/showcase) are increasingly exploring Deno! You can answer `yes` to the prompt every time you run the app, or include the `--allow-net` flag when you execute the `deno run` command. If you want to limit the domains that the app has access to, include a comma-separated list of domains, such as `--allow-net=example.com` or `--allow-net=example.com,api.slack.com` as follows: ``` deno run --watch --allow-net=example.com app.js ``` You should see the source code of `example.com` in your terminal. Parsing web page source code isn't very exciting, though. How about random photos of cats and dogs? Let's try replacing all the contents of our `app.js` with the following: ``` const cat_or_dog = Deno.args[0];let url = "";switch (cat_or_dog) { case 'cat': console.log(`Meow, you're a kitty!`); const cat_response = await fetch('https://api.thecatapi.com/v1/images/search'); const cat_json = await cat_response.json(); url = cat_json[0].url; break; case 'dog': console.log(`Who's a good dog?`); const dog_response = await fetch('https://dog.ceo/api/breeds/image/random'); const dog_json = await dog_response.json(); url = dog_json.message; break;}console.log(url); ``` This script takes a single argument at runtime, assigns it to the `cat_or_dog` variable, and then retrieves a random cat or dog picture. Run it with one of the following: ``` deno run --allow-net=api.thecatapi.com,dog.ceo app.js cat ``` or ``` deno run --allow-net=api.thecatapi.com,dog.ceo app.js dog ``` Notice that the `--allow-net` flag includes the domain names to both APIs, separated by a comma. ## Third-party libraries {#third-party} Just like browsers, Deno can [import and execute scripts from remote locations](https://deno.land/manual/examples/manage_dependencies), making it possible to not only use third party libraries, but to load them from any URL. Let's say we wanted to actually load the random cat or dog pic in the user's default browser. The [Opener module](https://deno.land/x/opener/README.md) does exactly that, and it's cross-platform to boot. Import the Opener module's `open` function at the top of your script, then call it at the bottom: ``` import { open } from "https://deno.land/x/opener@v1.0.1/mod.ts";// the rest of the logic of the cat/dog random imagerawait open(url); ``` When you run the script, you'll again see a warning that Deno needs permission to run the `open` command — that's because the [source code of the Opener module](https://deno.land/x/opener@v1.0.1/mod.ts) calls the `Deno.run()` method, which executes local commands on behalf of the user executing the script. Once again, Deno's security design requires explicit permission to run another command; passing the `--allow-run` flag will allow the user to run any sub-command, and passing a comma-separated list will only allow those specific commands to run. Deno will cache third party modules locally, but you aren't required to include a `package.json` file or the equivalent of a `node_modules` directory. In fact, your working directory is kept completely clean. ## The standard library {#std-lib} Now that you've built an app and explored how to import modules, let's explore the ecosystem of third party modules and the [Deno standard library](#std-lib). Every programming language has a mechanism for allowing code to be easily shared and reused, Deno does this by leveraging [JavaScript's standard way of importing and exporting code](https://deno.land/manual/examples/manage_dependencies). The Deno project maintains a hosting service for open source modules at [deno.land/x/](https://deno.land/x). All of these modules are hosted on public GitHub repos and cached by the Deno project — in fact, every time a module is updated and tagged with a new version, that specific version is cached. This allows you to follow best practices for versioning the modules your application depends on. In addition to hosting a repository of open source modules, the Deno project also maintains a [standard library of common utilities](https://github.com/denoland/deno_std) that developers can use. Common programming tasks such as figuring out a date or time, running tests on code, writing to the filesystem, or launching an HTTP server are all part of the standard library, and these modules are audited by the Deno team to ensure they are up-to-date and do not require any other external dependencies. The standard library is located at `https://deno.land/std`, but you'll reference a specific version of the library in your apps, such as `https://deno.land/std@0.193.0` Under development The Deno standard library is still under development and parts are considered unstable. This means that if you use certain modules from the standard library, such as the [filesystem modules](https://deno.land/std/fs), you'll need to execute `deno run` with the `--unstable` flag. As the standard library matures, the plan is to version the modules alongside updates to the Deno runtime itself so it will be easier to know which version of a module to use with the version of the Deno runtime you are using. The standard library contains dozens of submodules, which are what you'll actually load for your app; you won't often import the entire standard library. For example, if your app needs to format dates, there's [the `format` submodule](https://deno.land/std/datetime#format), part of the `datetime` submodule, a part of the standard library. You would load it as follows: ``` import { format } from "https://deno.land/std@0.140.0/datetime/mod.ts"; ``` Then, you can call the `format` function: ``` // 🎈 February 12 Happy birthday, Slackbot! 🎈// That's not an error, the JavaScript Date constructor uses a zero-based `monthIndex` for months// whereas days begin with 1.// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/Dateformat(new Date(2014, 01, 12), "yyyy-MM-dd"); // 2014-02-12 ``` ## Managing versions {#versioning} The content at any URL on the web can change at any time; often, this is a powerful feature of the web and allows web sites to stay fresh. However, when you're loading, say, a block of code to be used as a dependency in an application, you want that code to be consistent and _immutable_. **Forever**. Pretty much every runtime that allows for external modules has run into this problem at some point. While there is great power in being able to load libraries, what happens if the developer makes a change that suddenly breaks your code? Or worse, what if a bad actor were to somehow gain access to where that module is hosted and [insert some malicious code](https://arstechnica.com/information-technology/2021/12/malicious-packages-sneaked-into-npm-repository-stole-discord-tokens/) that would allow the attacker access to your app? By caching every new version and making it immutable, Deno's hosted modules avoid this problem — but you have to make sure to include the version tag in the URL, like so: `https://deno.land/x/feathers@v5.0.0-pre.3/mod.ts`. If you omit the tag, you will automatically be redirected to whatever the latest version is: ``` // 🚯 will always import the latest version. AVOID.import { feathers } from "https://deno.land/x/feathers/mod.ts";// 😎 imports a specific version, DO THIS INSTEADimport { feathers } from "https://deno.land/x/feathers@v5.0.0-pre.3/mod.ts"; ``` ## Managing dependencies {#deps} As your project grows in complexity, you may want to include a list of dependencies in a single place that can be tagged to a specific version. Deno uses the convention of a `deps.js` file to store this list. Let's say we want to take our dog/cat script to the next level, with internationalization and robust testing. We're going to use the [i18next library](https://deno.land/x/i18next@v21.8.1/index.js) for managing translations and the `asserts` functionality from the standard library. Our `deps.js` file might look like this: ``` export{ assert, assertEquals,} from "https://deno.land/std@0.138.0/testing/asserts.js";export { i18next } from "https://deno.land/x/i18next@v21.8.1/index.js"; ``` The URL includes a specific version number — this is the recommended way to import libraries, instead of pulling them from the main branch and hoping nothing breaks when the library gets updated. In our script, we import them from our local `deps.js` file as follows: ``` import {assertEquals, runTests, test } from "./deps.js"import {i18next} from "./deps.js" ``` If we need to update the version or add additional libraries, we can do so from a single place. ## VSCode and the Deno plugin {#vscode-deno} If you are using VSCode with the Deno plugin and you run into an error where Deno isn't being honored properly by VSCode, this is because VSCode treats the folder that's opened as the workspace root by default If you open a parent folder, the `.vscode/settings.json` file must be moved to the root of _that_ folder. VSCode requires that `deno.enable: true` is set in that `.vscode/settings.json` file, and VSCode only honors this setting if it's in the root of the project. Other common errors you may run into are static errors when opening a parent directory that contains one or many apps inside. These include: * _An import path cannot end with a '.ts' extension. Consider importing './workflows/greeting\_workflow.js' instead_. * This error is due to Deno not being set up correctly. * _Relative import path "deno-slack-sdk/mod.ts" not prefixed with / or ./ or ../deno(import-prefix-missing)_. * This error is due to an invalid import map. These errors occur because of that first one we covered — VSCode treats the folder that’s opened as the workspace root by default, and looks for the `.vscode/settings.json` and `deno.jsonc` files there. To resolve this, open the app folder directly, or set up your own workspace config in VSCode. ## Onward {#onward} Ready to dive into developing automations? Head over to our [getting started guide](/tools/deno-slack-sdk/guides/getting-started) to start building a workflow app, or check out our [developing with TypeScript guide](/tools/deno-slack-sdk/guides/developing-with-typescript) to learn more about the language you'll be developing with. --- Source: https://docs.slack.dev/tools/deno-slack-sdk/guides/developing-with-typescript # Developing with TypeScript Workflow apps require a paid plan Join the [Developer Program](https://api.slack.com/developer-program) and provision a sandbox with access to all Slack features for free. Now that we've covered the [Deno](/tools/deno-slack-sdk/guides/installing-deno) runtime, let's talk about the language you'll use to develop: TypeScript. Just like how every chef is enamored by their own particular knife, we are enamored by TypeScript. We think you will be, too. Typescript is built upon JavaScript...and while JavaScript is arguably the most popular language in the world, it's not without its issues. It's a dynamically-typed language — meaning that any errors can sneak through until runtime. This can lead to some irksome situations, especially for more robust applications. TypeScript was created as a solution to these kinds of situations because it is a statically-typed language. There are more rules about what types are, and TypeScript will make you enforce those rules _before_ you get to runtime. You could think of TypeScript as "JavaScript but with extra steps", but with the knowledge that those extra steps are actually pretty useful, and will save you from a lot of headaches down the road. ## TypeScript for different devs {#different-devs-ts} We imagine it's likely that everyone here has previously dabbled in some other programming language. The following are a few resources to help you transition those skills to TypeScript. ### You're a JavaScript dev {#js-ts} Great! JavaScript is quite similar to Typescript. Don't overthink things; the ways you perform actions in JavaScript are the same in TypeScript, you just need to be careful about those types. Much like transitioning from free-verse poetry to haikus, you'll add some rigidity to your art, but we believe your poetry will still shine through. * [TypeScript for JavaScript Programmers](https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes.html) * [Migrating from JavaScript](https://www.typescriptlang.org/docs/handbook/migrating-from-javascript.html) * [TypeScript for JavaScript Developers in 15 minutes](https://youtu.be/JUORwadOU7s) ### You're a Java dev {#java-ts} Just like coffee, there are many types of statically-typed languages. Java is one, and TypeScript is but another. In Java, source code is compiled and then run by the Java Virtual Machine, whereas in TypeScript, source code is compiled into JavaScript code, which is then run by the JavaScript runtime. * [TypeScript for Java Programmers](https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes-oop.html) ### You're a Python dev {#python-ts} This will feel pretty different! But worry not, you're still a programmer at heart, with the knowledge of programming principles within you. Python lets you play particularly fast and loose with typing, so you'll likely spend some time figuring out how to define certain types, and that's okay. Just keep a handy cheat-sheet nearby, and soon you'll laugh at the days you kept running into errors because a value you thought was an integer was actually an array. * [TypeScript for the New Programmer](https://www.typescriptlang.org/docs/handbook/typescript-from-scratch.html) ### You're a wizard, Harry {#wizard-ts} Even wizards can brush up on their programming skills! * [TypeScript for the New Programmer](https://www.typescriptlang.org/docs/handbook/typescript-from-scratch.html) ## TypeScript resources {#typescript-resources} While we feel practice is the best way to learn, a few supplementary readings can never hurt. * [The Official TypeScript Handbook](https://www.typescriptlang.org/docs/handbook/intro.html) * [TypeScript Tooling in 5 minutes](https://www.typescriptlang.org/docs/handbook/typescript-tooling-in-5-minutes.html) * [TypeScript blog](https://devblogs.microsoft.com/typescript/) * [TypeScript Tips](https://www.totaltypescript.com/tips) --- Source: https://docs.slack.dev/tools/deno-slack-sdk/guides/following-security-best-practices # Following security best practices Workflow apps require a paid plan Join the [Developer Program](https://api.slack.com/developer-program) and provision a sandbox with access to all Slack features for free. Keeping your apps and functions secure is an important part of developing on the Slack platform. Slack’s managed hosted environment is built on the [Deno](https://deno.land/) runtime, a secure Javascript runtime. Learn more about Deno’s [permissions model](https://deno.land/manual@v1.32.1/basics/permissions). Here are some best practices to keep in mind when developing [custom functions](/tools/deno-slack-sdk/guides/creating-custom-functions), [workflows](/tools/deno-slack-sdk/guides/creating-workflows) and [triggers](/tools/deno-slack-sdk/guides/using-triggers) for Slack automations. ## Set appropriate access control levels (ACLs) {#ACL} * [Limit access](/tools/deno-slack-sdk/guides/managing-triggers#manage) to your [functions](/tools/deno-slack-sdk/guides/creating-custom-functions) and [triggers](/tools/deno-slack-sdk/guides/using-triggers) to only the intended audience. Use the `slack function access` or `slack trigger access` commands to control who can use your functions or trip your triggers. * Your app collaborators can deploy your functions and manage your workflows and triggers. Only add collaborators to your app that you trust. ## Validate input {#validate-input} * It’s always important to validate inputs to your functions. If you’ve changed the distribution of your function, keep in mind it may be used in other workflows in ways you may not anticipate. Your app collaborators may also create or update triggers to start your workflows. * When using Slack [datastores](/tools/deno-slack-sdk/guides/using-datastores), avoid [injection attacks](https://owasp.org/Top10/A03_2021-Injection/) by properly sanitizing user input when building queries, and use the `expression_values` and `expression_attributes` fields when querying for data. Also ensure that the user has read access to data from the datastore before processing it. * Always confirm the user has access to perform whatever operation your function is being asked to perform. In complex workflows, several users may participate in the various steps, so ensure you’re checking the correct user’s permissions. For example, in a contract approval workflow, anyone may be able to request an approval, but only certain approvers may actually provide an approval. * When listening to [message metadata events](/tools/deno-slack-sdk/guides/integrating-message-metadata-events), keep in mind that many apps may be posting messages with the same event types. If you’d like to listen to messages from only specific apps, use a filter on the `app_id` in the event trigger definition. ## Handle sensitive data {#handle-sensitive-data} * Secrets needed by your custom functions running on Slack-managed infrastructure should be shared with Slack using the `slack env add` command and never hard-coded in your functions. Examples are API keys, OAuth client IDs and secrets, certificates, and cryptography keys. If possible, use Slack’s [third party auth support](https://docs.slack.dev/faq#third-party) to manage OAuth-based credentials. * For local apps using `slack run`, ensure that the `.env` file containing local secrets does not end up in your source control system. * When using `slack env add`, ensure the secret does not end up in your shell’s history. This can be done using shell environment variables, or by running `slack env add` without any parameters. With no parameters, `slack env add` will prompt you for the secret’s name and value in the console, using a password display. * Be careful about collecting or logging sensitive information in workflows from users, especially passwords or personally identifiable information (PII). Data may be exposed to later workflow steps, in data exports, or in activity logs. ## Secure credentials {#secure-credentials} * The Slack CLI stores credentials in the `credentials.json` file in the `.slack` folder in your home directory. Slack will never ask you share the tokens contained in that file. * While these credentials expire and are regularly rotated, access to this file should be limited to only you. * Never share the tokens or challenge strings generated by the `slack login` flow. * Never paste a `/slackauthticket` command given to you by another user into Slack. ## Use secure libraries {#secure-libraries} * It is your responsibility to monitor and respond to security vulnerabilities in your custom function’s code and dependencies, and to deploy new versions to Slack-managed infrastructure as needed. * Keep your Slack CLI and SDKs up to date by upgrading when prompted. * Only use a Slack CLI download by following instructions found within the [Slack CLI docs](/tools/slack-cli/). ## Handle network egress {#network-egress} * Slack’s `outgoingDomains` configuration limits which domains your custom function code can use when making external network requests. Only list `outgoingDomains` if the domains are required by your functions. ## Make scopes and tokens {#scopes-tokens} * Functions are given a short-lived token that can be used to make [Slack API](/tools/deno-slack-sdk/guides/calling-slack-api-methods) calls, which use the scopes requested in the app’s manifest. We recommend only sending these tokens to Slack API endpoints, and not logging them or sending them to external systems. * Only request the scopes your functions need to do their job. Following these guidelines will get you on your way to building secure workflow apps. --- Source: https://docs.slack.dev/tools/deno-slack-sdk/guides/getting-started # Getting started with the Deno Slack SDK Workflow apps require a paid plan Join the [Developer Program](https://api.slack.com/developer-program) and provision a sandbox with access to all Slack features for free. In the following guide, you'll install the Slack CLI and authorize it in your workspace. Then, you'll use the Slack CLI to scaffold a fully-functional workflow app and run it locally. Don't have a workspace yet? You can get up and running by provisioning a sandbox with an associated workspace by following [this guide](/tools/developer-sandboxes/). Come on back when you're ready! ## Step 1: Install the Slack CLI {#install-cli} Refer to [these instructions](/tools/slack-cli/guides/installing-the-slack-cli-for-mac-and-linux) to install the latest version of the Slack CLI. The instructions will guide you through installing the Slack CLI and all required dependencies. ## Step 2: Authorize the Slack CLI {#authorize-cli} With the Slack CLI installed, authorize the Slack CLI in your workspace with the following command: ``` slack login ``` In your terminal window, you should see an authorization ticket in the form of a slash command, and a prompt to enter a challenge code: ``` $ slack login📋 Run the following slash command in any Slack channel or DM This will open a modal with user permissions for you to approve Once approved, a challenge code will be generated in Slack/slackauthticket ABC123defABC123defABC123defABC123defXYZ? Enter challenge code ``` Copy the slash command and paste it into any Slack conversation in the workspace you will be developing in. When you send the message containing the slash command, a modal will pop up, prompting you to grant certain permissions to the Slack CLI. Click the Confirm button in the modal to move to the next step. A new modal with a challenge code will appear. Copy that challenge code, and paste it back into your terminal: ``` ? Enter challenge code eXaMpLeCoDe✅ You've successfully authenticated! 🎉 Authorization data was saved to ~/.slack/credentials.json💡 Get started by creating a new app with slack create my-app Explore the details of available commands with slack help ``` Verify that your Slack CLI is set up by running `slack auth list` in your terminal window: ``` $ slack auth listmyworkspace (Team ID: T123ABC456)User ID: U123ABC456Last updated: 2023-01-01 12:00:00 -07:00Authorization Level: Workspace ``` You should see an entry for the workspace you just authorized. If you don't, get a new authorization ticket with `slack login` to try again. You're now ready to begin building workflow apps! In the next step, we'll get started with a sample app. ## Step 3: Create an app from a template {#create-app} Evaluate third-party apps Exercise caution when using third-party applications and automations (those outside of [`slack-samples`](https://github.com/slack-samples)). Review all source code created by third-parties before running `slack create` or `slack deploy`. The `create` command is how you create a workflow app. For this guide, we'll be creating a Slack app using the [Deno Starter Template](https://github.com/slack-samples/deno-starter-template) as a template: ``` slack create my-app --template https://github.com/slack-samples/deno-starter-template ``` The Slack CLI creates an app project folder and fills it with the sample app code. Once it has finished, `cd` into your new project directory: ``` cd my-app ``` Then continue to the next step. ## Step 4: Run the app in local development mode {#local-development-mode} While building your app, you can see your changes propagated to your workspace in real-time by running `slack run` within your app's directory. ``` slack run ``` When you execute `slack run`, you'll be asked to select a local environment: ``` ? Choose a local environment> Install to a new workspace or organization ``` Since you've not installed your app to any workspaces, select _Install to a new workplace_. Then select the workspace you authenticated in. The Slack CLI will attempt to list any triggers, and in this case, will inform you there are no existing triggers installed for the app. [Triggers](/tools/deno-slack-sdk/guides/using-triggers) are what cause workflows to run. A [link trigger](/tools/deno-slack-sdk/guides/creating-link-triggers) generates a _Shortcut URL_ which, when posted in a channel or added as a bookmark, becomes a link. Triggers are created from trigger definition files. The Slack CLI will then look for any trigger definition files and prompt you to select one. In this case, there is only one trigger: `sample_trigger.ts`. Select it. ``` ? Choose a trigger definition file:> triggers/sample_trigger.ts Do not create a trigger ``` Once your app's trigger is created, you will see the following output: ``` ⚡ Trigger successfully created! Sample trigger (local) Ft0123ABC456 (shortcut) Created: 2023-01-01 12:00:00 -07:00 (1 second ago) Collaborators: You! @You U123ABC456DE Can be found and used by: everyone in the workspace https://slack.com/shortcuts/Ft0123ABC456/XYZ123 ``` The Slack CLI will also start a local development server, syncing changes to your workspace's development version of your app. You'll know your local development server is up and running when your terminal window tells you it's `Connected, awaiting events`. ## Step 5: Use your app {#use} Grab the `Shortcut URL` you generated in the previous step and paste it in a public channel in your workspace. You will see the shortcut unfurl with a "Start Workflow" button. Click the button to execute the shortcut. In the modal that appears, select a channel, and enter a message. When you click the "Send message" button, you should see your message appear in the channel you specified. When you want to turn _off_ the local development server, use `Ctrl+c` in the command prompt. * * * ## Onward {#start-build} At this point your Slack CLI is fully authorized and ready to create new projects. It's time to choose the next path of adventure. We have curated a collection of sample apps. Many have tutorials. All highlight features of workflow apps. Learn how to: * design [datastores](/tools/deno-slack-sdk/guides/using-datastores) to store data with the [Virtual Running Buddies app](/tools/deno-slack-sdk/tutorials/virtual-running-buddies-app). * send [event-triggered](/tools/deno-slack-sdk/guides/creating-event-triggers) automated [messages](/tools/deno-slack-sdk/reference/slack-functions/send_message) with the [Welcome Bot app](/tools/deno-slack-sdk/tutorials/welcome-bot). * create [forms](/tools/deno-slack-sdk/guides/creating-a-form) to receive user input with the [Give Kudos app](/tools/deno-slack-sdk/tutorials/give-kudos-app). Each tutorial will expose you to many aspects of the workflow automations. If you'd rather explore the documentation on your own, here are a few places to start. You can learn how to: * [deploy your app](/tools/deno-slack-sdk/guides/deploying-to-slack) so you don't need to run it locally. * [build an app from scratch](/tools/deno-slack-sdk/guides/creating-an-app). * use workflow apps in conjunction with other services, whether that's with [third-party API calls](/faq#third-party) or [external authentication](/tools/deno-slack-sdk/guides/integrating-with-services-requiring-external-authentication). * use the [Deno Slack SDK](https://github.com/slackapi/deno-slack-sdk) in tandem with the Slack CLI to access the API, additional documentation, and code libraries. You'll first need to download and install [Deno](/tools/deno-slack-sdk/guides/installing-deno). If you're using VSCode for development, make sure to also download the [Deno extension for VSCode](/tools/deno-slack-sdk/guides/installing-deno#vscode). --- Source: https://docs.slack.dev/tools/deno-slack-sdk/guides/installing-deno # Installing Deno Workflow apps require a paid plan Join the [Developer Program](https://api.slack.com/developer-program) and provision a sandbox with access to all Slack features for free. Deno is a runtime that you'll need for developing automations apps. ## First things first — what is Deno? {#what-deno} [Deno](https://deno.land) is a runtime that allows you to execute code written using [JavaScript](https://developer.mozilla.org/en-US/docs/Web/JavaScript), [TypeScript](https://www.typescriptlang.org/), and [WebAssembly](https://webassembly.org/). It uses the open source [V8 JavaScript and WebAssembly engine](https://v8.dev/), is written in a programming language called [Rust](https://www.rust-lang.org/), and is built on another runtime called [Tokio](https://tokio.rs/). So, if you know how to write apps in one of those languages (or would like to!) Deno is how you will execute your app. ### Deno runtime {#deno-runtime} To understand what a runtime is, first let's take a look at the software programming lifecycle to get some context. At a basic level, the lifecycle is as follows: * **Develop**: source code is written and edited and an application or program is created. * **Compile**: the source code is compiled into a machine code executable. * **Link**: all of the machine code components of the application or program are connected together, including external libraries. * **Distribute**: the application or program is copied to the computers of other users; for example, via an executable. * **Install**: the user downloads the executable on their computer; their operating system places it in storage. * **Load**: the user's operating system places the executable into active memory in order to begin execution of the application or program. * **Run**: the distributed machine code is executed on the user's computer. We're interested in that last phase of this lifecycle, the _runtime_. However, it's important to note there are two concepts here that are related, but different: runtime as part of the lifecycle, and a runtime environment. Some confusion can occur since people sometimes shorten "runtime environment" to just "runtime" — but what we're talking about when we say _Deno is a runtime_ is really _Deno is a runtime environment._ So, what's a runtime environment? Essentially, it's a framework of all the hardware and software required to execute, or run, your code. A runtime environment accesses system resources, loads your application or program, and executes it, all of which is done independently of your operating system (which is also technically a runtime environment!). Why use a runtime environment? Well, because operating systems can differ significantly from one another, or even from one version to the next. Runtime environments enable cross-platform functionality for your applications or programs, allowing your code to run as smoothly as possible in a a variety of conditions. ### Why Deno? {#why-deno} Where did Deno come from? Well, for a long time, JavaScript was used almost exclusively by web browsers to add interactivity to web pages. A clever programmer named Ryan Dahl [created a way to run JavaScript on servers](https://www.youtube.com/watch?v=ztspvPYybIY). He called it Node.js, and it was built on the JavaScript engine that powered Google's web browser. Because there are a lot of JavaScript programmers in the world, Node.js grew incredibly quickly, and soon added a way to package libraries of code called the [Node Package Manager](https://www.npmjs.com/) (npm for short). Dahl soon realized the original Node.js implementation had some problems. Security wasn't built-in, and the npm ecosystem that had grown so quickly introduced vulnerabilities that were affecting millions of developers. JavaScript continued to evolve and [Dahl decided he wanted to try again with a new runtime](https://www.youtube.com/watch?v=M3BM9TB-8yA) that was secure by default, adopted modern web standards for features like including libraries of code, and came with a standard library of functionality. Enter Deno. ### Differences with Node.js {#node-differences} The biggest reason you may not want to move your existing Node.js-based applications to Deno is npm modules aren't yet fully supported. The vast ecosystem of modules that have been built over the years aren't guaranteed to work with your application by default. The Deno team is [working on some approaches to allowing npm modules](https://deno.land/manual@v1.12.2/npm_nodejs), including [the built-in standard library that should obviate the need for many npm modules](https://deno.land/manual@v1.12.2/npm_nodejs/std_node.md), and [CDNs that host npm modules](https://deno.land/manual@v1.12.2/npm_nodejs/cdns.md) in a way that Deno can use. That said, not every npm module will work with Deno automatically, particularly more complex libaries, so you may need to wait on a rewrite or roll your own. ## Installing Deno {#install} If you've written JavaScript for web browsers before, everything will feel very familiar. If you've written JavaScript or TypeScript for web _servers_ before, probably using Node.js as your runtime, most things will feel familiar with a [few key differences](#node-differences). Deno ships as a single executable with no external dependencies. Versions are available for macOS (both Intel and Apple silicon architectures), Linux, and Windows (64-bit support only for Linux and Windows). The easiest way to install is to call the [`deno_install` script](https://github.com/denoland/deno_install) remotely. On macOS and Linux: ``` curl -fsSL https://deno.land/x/install/install.sh | sh ``` On Windows: ``` iwr https://deno.land/x/install/install.ps1 -useb | iex ``` [The official installation guide](https://deno.land/manual/getting_started/installation) provides details for various other platforms. As Deno is open source, you are free to [compile from source](https://deno.land/manual/contributing/building_from_source) as well. Once installed, run `deno --version` to verify the installation was successful. The output should look something like the following: ``` $ deno --versiondeno 1.37.0* (release, x86_64-apple-darwin)v8 10.*typescript 4.* ``` The minimum version of Deno runtime required for developing workflow apps is currently at version 1.37.0 ## Installing the Deno extension for VSCode {#vscode} The installation script from our [Getting started](/tools/deno-slack-sdk/guides/getting-started#install-cli) should cover everything for you, but if you want to manually install the `vscode_deno` extension, follow these steps: 1. Within VSCode, select **Extensions** from the sidebar. 2. Enter **Deno** in the search bar. 3. Select **Deno** (a language server client for Deno, published by deno.land) and **Install**. Alternatively, you can download and install the extension from [here](https://marketplace.visualstudio.com/items?itemName=denoland.vscode-deno). Once installed, you should see a splash screen welcoming you to the extension. For additional information about configuring the extension, refer to [Using Visual Studio Code](https://docs.deno.com/runtime/manual/references/vscode_deno). ## Onward {#onward} Ready to dive into development? [Let's go](/tools/deno-slack-sdk/guides/developing-with-deno)! --- Source: https://docs.slack.dev/tools/deno-slack-sdk/guides/integrating-message-metadata-events # Integrating message metadata events Workflow apps require a paid plan Join the [Developer Program](https://api.slack.com/developer-program) and provision a sandbox with access to all Slack features for free. [Message metadata](https://docs.slack.dev/messaging/message-metadata/) can connect your workflows to events happening within Slack. By doing so you can automate tasks within your workflows. Message metadata will take the form of the custom message metadata event types you create. There are three steps to integrate a custom event type: 1. [Define](#define) the custom event type in its own file 2. [Register](#register) the custom event type in your app's manifest 3. [Use](#use) the custom event type in a trigger or function ## 1. Define a custom event type {#define} When integrating message metadata into your app, you may want some additional type safety. Custom message metadata event types ([`message_metadata_deleted`](https://docs.slack.dev/reference/events/message_metadata_deleted), [`message_metadata_posted`](https://docs.slack.dev/reference/events/message_metadata_posted), and [`message_metadata_updated`](https://docs.slack.dev/reference/events/message_metadata_updated)) provide a way for apps to validate message metadata against a schema that you define. First, let's create a new file to store the custom event type's definition. In this example, we'll create `event_types/incident.ts`. The first thing we'll do in the file is to import `DefineEvent` and `Schema` from the SDK. Then, we'll use `DefineEvent` to create our event's definition. An example definition is as follows: ``` // event_types/incident.tsimport { DefineEvent, Schema } from "deno-slack-sdk/mod.ts";const IncidentEvent = DefineEvent({ name: "my_incident_event", title: "Incident", type: Schema.types.object, properties: { id: { type: Schema.types.string }, title: { type: Schema.types.string }, summary: { type: Schema.types.string }, severity: { type: Schema.types.string }, date_created: { type: Schema.types.number }, }, required: ["id", "title", "summary", "severity"], // Set this to false to force the validation to catch any additional properties additionalProperties: false,});export default IncidentEvent; ``` For the `type` property, events can be one of two kinds: the built-in `Schema.types.object` type, or a [custom type](/tools/deno-slack-sdk/guides/creating-a-custom-type) that you define. If you go with a custom type, set the `type` property as your custom type — just don't forget to also import that custom type definition in your app's manifest. ✨ **For more information about the app manifest**, refer to [app manifest](/tools/deno-slack-sdk/guides/using-the-app-manifest). ## 2. Register a custom event type {#register} Before your app can use your custom event type, you'll need to register it with your app's manifest. To register the newly-defined custom event type, add it to the `events` array of your [manifest](/tools/deno-slack-sdk/guides/using-the-app-manifest) definition: ``` // manifest.tsimport IncidentEvent from "./event_types/incident.ts";export default Manifest({ name: "my_incident_app", description: "An app that uses a custom event type", icon: "assets/default_new_app_icon.png", workflows: [SampleWorkflow], outgoingDomains: [], datastores: [SampleDatastore], events: [IncidentEvent], // Our custom event type botScopes: [ "commands", "chat:write", "chat:write.public", "datastore:read", "datastore:write", "metadata.message:read", ],}); ``` ## 3. Use a custom event type {#use} There are two ways you can use your custom event type: * [by posting a message to Slack](#posting) * [by creating a message metadata trigger](#trigger) ### Posting a message to Slack {#posting} When you post a message to Slack using the [`metadata` parameter](https://docs.slack.dev/reference/methods/chat.postMessage), if the `event_type` matches the `name` of a custom event type specified in your app's manifest, Slack's servers will validate that all of the required parameters are provided. If the required parameters are not provided, a warning will be returned in the response. The message will still be posted, but without the message metadata since it didn't pass validation. There are two ways to post a message in Slack: * [from within a custom function](#custom-function) * [as one of your workflow's steps](#workflow-step) #### Posting a message from within a custom function {#custom-function} Here's an example of using your custom event type while calling `client.chat.postMessage()` from within a [custom function](/tools/deno-slack-sdk/guides/creating-custom-functions): ``` // functions/my_incident_function.tsimport { DefineFunction, Schema, SlackFunction } from "deno-slack-sdk/mod.ts";import IncidentEvent from "../event_types/incident.ts";export const MyFunctionDefinition = DefineFunction({ callback_id: "my_incident_function", title: "my incident function", source_file: "functions/my_incident_function.ts", input_parameters: { properties: { channel_id: { type: Schema.slack.types.channel_id }, incident_id: { type: Schema.types.string }, incident_title: { type: Schema.types.string }, incident_summary: { type: Schema.types.string }, incident_severity: { type: Schema.types.string }, incident_date: { type: Schema.slack.types.timestamp }, }, required: ["channel_id"], }, output_parameters: { properties: {}, required: [] },});export default SlackFunction( MyFunctionDefinition, async ({ inputs, client }) => { // This example assumes all required values are passed to the function's inputs const response = await client.chat.postMessage({ channel: inputs.channel_id, text: "We have an incident!", metadata: { // Our custom event type event_type: IncidentEvent, event_payload: { id: inputs.incident_id, title: inputs.incident_title, summary: inputs.incident_summary, severity: inputs.incident_severity, // This isn't required, so it doesn't need to exist to pass validation date_created: inputs.incident_date, }, }, }); if (response.error) { const error = `Failed to post a message with metadata: ${response.error}`; return { error }; } // Do something meaningful here! return { outputs: {} }; },); ``` #### Posting a message as one of your workflow's steps {#workflow-step} Here's an example of using your custom event type with the [Slack function `SendMessage`](/tools/deno-slack-sdk/reference/slack-functions/send_message) as one of your workflow's steps: ``` // workflows/my_workflow.tsimport { DefineWorkflow, Schema } from "deno-slack-sdk/mod.ts";import IncidentEvent from "../event_types/incident.ts";export const MyWorkflow = DefineWorkflow({ callback_id: "my_workflow", title: "My workflow", input_parameters: { properties: { channel_id: { type: Schema.slack.types.channel_id }, incident_id: { type: Schema.types.string }, incident_title: { type: Schema.types.string }, incident_summary: { type: Schema.types.string }, incident_severity: { type: Schema.types.string }, incident_date: { type: Schema.slack.types.timestamp }, }, required: ["channel_id"], },});// This example assumes all required values are passed to the workflow's inputsMyWorkflow.addStep(Schema.slack.functions.SendMessage, { channel_id: MyWorkflow.inputs.channel_id, message: "We have an incident!", metadata: { // Our custom event type event_type: IncidentEvent, event_payload: { id: MyWorkflow.inputs.incident_id, title: MyWorkflow.inputs.incident_title, summary: MyWorkflow.inputs.incident_summary, severity: MyWorkflow.inputs.incident_severity, // This isn't required, so it doesn't need to exist to pass validation date_created: MyWorkflow.inputs.incident_date, }, },}); ``` ### Creating a message metadata trigger {#trigger} A trigger can be created to watch for any message posted with a metadata event type matching your custom event type. When a match is found, that trigger will execute its configured workflow as in the following example: ``` // triggers/incident_metadata_posted.tsimport { Trigger } from "deno-slack-api/types.ts";import MyWorkflow from "../workflows/my_workflow.ts";import IncidentEvent from "../event_types/incident.ts";const trigger: Trigger = { type: "event", name: "Incident Metadata Posted", inputs: { incident_id: { value: "{{data.metadata.event_payload.id}}" }, incident_title: { value: "{{data.metadata.event_payload.title}}" }, incident_summary: { value: "{{data.metadata.event_payload.summary}}" }, incident_severity: { value: "{{data.metadata.event_payload.severity}}" }, incident_date: { value: "{{data.metadata.event_payload.incident_date}}" }, }, // This is the workflow that will be kicked off workflow: `#/workflows/${MyWorkflow.definition.callback_id}`, event: { event_type: "slack#/events/message_metadata_posted", // Our custom event type metadata_event_type: IncidentEvent, // The channel we're watching for message metadata being posted channel_ids: ["C123ABC456"], },};export default trigger; ``` Add the `metadata.message:read` scope [Event triggers](/tools/deno-slack-sdk/guides/creating-event-triggers) such as the one above that listen for message metadata require the `metadata.message:read` scope to be added to the `botScopes` property of your [manifest definition](/tools/deno-slack-sdk/guides/using-the-app-manifest). ✨ **For more information about custom types**, refer to [custom types](/tools/deno-slack-sdk/guides/creating-a-custom-type). ✨ **For more information about event triggers**, refer to [event triggers](/tools/deno-slack-sdk/guides/creating-event-triggers). --- Source: https://docs.slack.dev/tools/deno-slack-sdk/guides/integrating-with-services-requiring-external-authentication # Integrating with services requiring external authentication Workflow apps require a paid plan Join the [Developer Program](https://api.slack.com/developer-program) and provision a sandbox with access to all Slack features for free. You can use the Slack CLI to encrypt and to store OAuth2 credentials. This enables your app to access information from another service without exchanging passwords, but rather, tokens. ## What is OAuth2, and why should you use it? {#what-is-oauth} OAuth2 stands for Open Authorization 2.0. It is a standard protocol designed to allow apps to access resources hosted by other apps on behalf of a user. Unlike basic authorization, where you share a password with a user, OAuth2 uses access tokens to verify a user's identity. For providers that require it, Slack offers PKCE and HTTP Basic Auth support, as you will see in the [OAuth2 provider `options` properties](#options-properties) section below. The following steps guide you through integrating your app with an external service using [Google](https://developers.google.com/identity/protocols/oauth2) as an example. ### 1. Obtain your OAuth2 credentials {#credentials} The first step is to obtain your OAuth2 credentials. To do that, create a new OAuth2 credential with the external service you'll be integrating with. For our example, navigate to the [Google API Console](https://console.cloud.google.com/projectselector2/apis/dashboard?supportedpurview=project) to obtain your OAuth2 **client ID** and **client secret**. If you're asked to provide a **redirect URL**, use the following: ``` https://oauth2.slack.com/external/auth/callback ``` When you're done creating your credential, copy the credential's **client ID** and **client secret**, then head to your [manifest](/tools/deno-slack-sdk/guides/using-the-app-manifest) file (`manifest.ts`). ### 2. Define your OAuth2 provider {#define} Next, tell your app about your OAuth2 provider by defining an **OAuth2 provider** within your app. Inside your app's manifest, import `DefineOAuth2Provider`. Then, create a new provider instance. An example provider instance for Google is below. You can define it right in your manifest, or put it in its own file and import it into the manifest. ``` // manifest.tsimport { DefineFunction, DefineOAuth2Provider, DefineWorkflow, Manifest, Schema,} from "deno-slack-sdk/mod.ts";// ...// Define a new OAuth2 provider// Note: replace with your actual client ID// if you're following along and creating an OAuth2 provider for// Google.const GoogleProvider = DefineOAuth2Provider({ provider_key: "google", provider_type: Schema.providers.oauth2.CUSTOM, options: { provider_name: "Google", authorization_url: "https://accounts.google.com/o/oauth2/auth", token_url: "https://oauth2.googleapis.com/token", client_id: ".apps.googleusercontent.com", scope: [ "https://www.googleapis.com/auth/spreadsheets.readonly", "https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/userinfo.profile", ], authorization_url_extras: { prompt: "consent", access_type: "offline", }, identity_config: { url: "https://www.googleapis.com/oauth2/v1/userinfo", account_identifier: "$.email", http_method_type: "GET", }, use_pkce: false, },});// ... ``` OAuth2 provider properties are described in the tables below. #### OAuth2 provider properties {#properties} Field Type Description Required? `provider_key` `string` The unique string identifier for a provider. An app cannot have two providers with the same unique identifier. Changing unique identifiers will be treated as the deletion of a provider. Providers with active tokens cannot be deleted. Required `provider_type` `Schema.providers.oauth2` The only supported provider type value at this time is `Schema.providers.oauth2.CUSTOM`. Required `options` `object` Object with further provider-specific details. See the [table below](#options-properties). Required ##### OAuth2 provider options properties {#options-properties} Field Type Description Required? `provider_name` `string` The name of your provider. Required `client_id` `string` The client ID from your provider. Required `authorization_url` `string` An OAuth2 requirement to complete the OAuth2 flow and to direct the user to the provider consent screen. Required `scope` `array` An OAuth2 requirement to complete the OAuth2 flow and to grant only the scopes provided to the access token. Required `identity_config` `object` Used to obtain user identity by finding the account associated with the access token. See the [table below](#identity-properties) for details. Required `token_url` `string` An OAuth2 requirement to complete the OAuth2 flow and to exchange the code with an access token. Required `token_url_config` `object` An object that can further define a `use_basic_auth_scheme` object, which contains a solitary boolean property, `use_basic_auth_scheme`, that defines whether HTTP Basic Authentication should be used with the `token_url` field above. Defaults to `false`. Optional `authorization_url_extras` `object` HTTP request query parameters to attach to the `authorization_url`. Set object key names as query parameter names and object key values as query parameter values. Optional `use_pkce` `boolean` Specifies if the provider uses PKCE. Defaults to `false`. Optional #### OAuth2 provider identity_config properties {#identity-properties} Field Type Description Required? `url` `string` The endpoint the provider exposes to fetch the user identity. It is used to identify the authenticated user. Required `account_identifier` `string` The field name in the response from the above `url` field representing the user identity. Required `headers` `object` Extra HTTP headers to attach to the request to the `url` field. Set object key names as header names and object key values as header values. Note: the `Authorization` header is automatically set by Slack. Optional `http_method_type` `string` The HTTP method to employ when sending a request to the identity `url`. Defaults to `GET`. Acceptable values include `GET` or `POST`. Optional `body` `object` HTTP body parameters that the identity `url` expects. Only used if `http_method_type` is set to `POST`. Set object key names as body parameter name and object key values as body parameter values. Optional The `identity_config` field is used to extract an `external_user_id` This value is then used to allow a single user to issue multiple tokens for multiple provider accounts. If a Slack user with multiple accounts extracts the same `external_user_id` from the provider for each of their accounts, the existing token will be overwritten, and they will not be able to use multiple accounts. ### 3. Add your OAuth2 provider to your manifest {#adding-provider} In your manifest file, insert the newly-defined provider into the `externalAuthProviders` property (if that property doesn't exist yet, go ahead and create it): ``` export default Manifest({ //... // Tell your app about your OAuth2 providers here: externalAuthProviders: [GoogleProvider], //...}); ``` Now, with your OAuth2 provider defined and your manifest configured to use it, you can encrypt and store your client secret so that your app's users can utilize the OAuth2 authorization flow. ### 4. Encrypt and store your client secret {#client-secret} Your app needs to be deployed to Slack once in order to create a place to store your encrypted client secret. Run the `slack deploy` command in your terminal window: ``` slack deploy ``` This command will bring up a list of [currently authorized workspaces](/tools/deno-slack-sdk/guides/getting-started#authorize-cli). Select the workspace where your app will exist, and wait for the CLI to finish deploying. When finished, stay in your terminal window to add your client secret for the newly-defined provider, ensuring that you wrap the secret string in double quotes as follows: ``` slack external-auth add-secret --provider google --secret "GOCSPX-abc123..." ``` Running the `add-secret` command will bring up a list of workspaces available to you. Find and select the workspace you recently deployed your app to; you'll know it's the workspace you recently installed the app in by locating the item in the list with your app's name and ID (e.g., `myapp A01BC...`) rather than "App is not installed to this workspace." If you get a `provider_not_found` error, go back to your manifest file and check to make sure that you included your OAuth2 provider in the `externalAuthProviders` properties of your manifest definition. If everything was successful, the CLI will let you know: ``` ✨ successfully added external auth client secret for google ``` Great! With your app configured to interact with your defined OAuth2 provider, we can now initialize the OAuth2 sign-in flow, connecting your external provider to your Slack app. ### 5. Initialize the OAuth2 flow {#initialize-oauth-flow} Once your provider's client secret has been added, it's time to create a token for your app to interact with your OAuth2 provider with [`external-auth add`](/tools/slack-cli/reference/commands/slack_external-auth_add). Run the following command: ``` slack external-auth add ``` This will display a list of workspaces your app is deployed to. Select the one you're currently working in. Upon selection, you'll be provided a list of all providers that have been defined for this app, along with whether there's a secret and token. ``` $ slack external-auth add? Select a provider [Use arrows to move, type to filter]> Provider Key: google Provider Name: Google Client ID: .apps.googleusercontent.com Client Secret Exists? Yes Token Exists? No ``` If you have just created a provider, you'll notice that it reports no tokens existing. Let's go ahead and create a token by initializing the OAuth2 sign-in flow. Select the provider you're working on, which will open a browser window for you to complete the OAuth2 sign-in flow according to your provider's requirements. You'll know you're successful when your browser sends you to a `oauth2.slack.com` page stating that your account was successfully connected. Verify that a valid token has been created by re-running the [`external-auth add`](/tools/slack-cli/reference/commands/slack_external-auth_add) command: ``` slack external-auth add? Select a provider [Use arrows to move, type to filter]> Provider Key: google Provider Name: Google Client ID: .apps.googleusercontent.com Client Secret Exists? Yes Token Exists? Yes ``` If you see `Token Exists? Yes`, that means a valid auth token has been created, and you're ready to use OAuth2 in your app! Exit out of this command flow by entering `Ctrl+C` in your terminal — otherwise you'll be guided through the OAuth2 sign-in flow again. ### 6. Add OAuth2 to your function {#function} Your [custom functions](/tools/deno-slack-sdk/guides/creating-custom-functions) can leverage your provider's token by configuring it to receive a `Schema.slack.types.oauth2` type as an input parameter to your function's definition. Here's how that might look if we were to use the sample function from the [starter template](https://github.com/slack-samples/deno-starter-template): ``` // functions/sample_function.tsimport { DefineFunction, Schema, SlackFunction } from "deno-slack-sdk/mod.ts";export const SampleFunctionDefinition = DefineFunction({ callback_id: "sample_function", title: "Sample function", description: "A sample function", source_file: "functions/sample_function.ts", input_parameters: { properties: { message: { type: Schema.types.string, description: "Message to be posted", }, // Define token here googleAccessTokenId: { type: Schema.slack.types.oauth2, oauth2_provider_key: "google", }, user: { type: Schema.slack.types.user_id, description: "The user invoking the workflow", }, }, required: ["user"], }, output_parameters: { properties: { updatedMsg: { type: Schema.types.string, description: "Updated message to be posted", }, }, required: ["updatedMsg"], },});export default SlackFunction( SampleFunctionDefinition, // Define custom function async ({ inputs, client }) => { // Get the token: const tokenResponse = await client.apps.auth.external.get({ external_token_id: inputs.googleAccessTokenId, }); if (tokenResponse.error) { const error = `Failed to retrieve the external auth token due to ${tokenResponse.error}`; return { error }; } // If the token was retrieved successfully, use it: const externalToken = tokenResponse.external_token; // Make external API call with externalToken const response = await fetch("https://somewhere.tld/myendpoint", { headers: new Headers({ "Authorization": `Bearer ${externalToken}`, "Content-Type": "application/x-www-form-urlencoded", }), }); if (response.status != 200) { const body = await response.text(); const error = `Failed to call my endpoint! (status: ${response.status}, body: ${body})`; return { error }; } // Do something here const myApiResponse = await response.json(); const updatedMsg = `:newspaper: Message for <@${inputs.user}>!\n\n>${myApiResponse}`; return { outputs: { updatedMsg } }; },); ``` ### 7. Include OAuth2 input in a workflow step {#workflow} Next, while configuring your workflow, choose the persona whose auth you want to use when the workflow runs. To do this, pass an object with a `credential_source` to the OAuth2 input in the step configuration. Slack will automatically inject the correct token ID into the OAuth2 input property based on the selected `credential_source`; you **do not** need to provide a token ID here. #### Using end user tokens {#end-user-tokens} If you would like the workflow to use the account of the end user running the workflow, use `credential_source: "END_USER"`. The end user will be asked to authenticate with the external service in order to connect and grant Slack access to their account before running the workflow. This workflow can only be started by a [Link Trigger](/tools/deno-slack-sdk/guides/creating-link-triggers), as this is the only type of trigger guaranteed to originate directly from an end-user interaction. ``` // Somewhere in your workflow's implementation:const sampleFunctionStep = SampleWorkflow.addStep(SampleFunctionDefinition, { user: SampleWorkflow.inputs.user, googleAccessTokenId: { credential_source: "END_USER" },}); ``` #### Using developer tokens {#developer-tokens} If you would like the workflow to use the account of one of the app collaborators, use `credential_source: "DEVELOPER"`. ``` // Somewhere in your workflow's implementation:const sampleFunctionStep = SampleWorkflow.addStep(SampleFunctionDefinition, { user: SampleWorkflow.inputs.user, googleAccessTokenId: { credential_source: "DEVELOPER" },}); ``` After deploying the manifest changes above, you have to select a specific account for each of your workflows in this app. Assuming that you had run `slack external-auth add` before to add an external account, use the command `slack external-auth select-auth` as shown below: ``` slack external-auth select-auth? Select a workspace ? Choose an app environment Deployed ? Select a workflow Workflow: #/workflows/ Providers: Key: google, Name: Google, Selected Account: None? Select a provider Key: google, Name: Google, Selected Account: None? Select an external account Account: @gmail.com, Last Updated: 2023-05-30✨ Workflow #/workflows/ will use developer account @gmail.com when making calls to google APIs ``` Multiple collaborators can exist for the same app and each collaborator can create a token using the `slack external-auth add` command. To select the appropriate collaborator account to run a specific workflow, the same `slack external-auth select-auth` command can be used. However, a collaborator needs to set up their own account using `slack external-auth select-auth` command by invoking this command. i.e. a collaborator cannot use `slack external-auth select-auth` to select auth for a workflow on behalf of another collaborator for the same app. A collaborator can remove their account by running `slack external-auth remove` command. This would automatically delete the existing selected auths for each of the workflows that were using it. Therefore, in such a case, `slack external-auth select-auth` command would be needed to be invoked again before executing the relevant workflows successfully later. ### 8. Force refreshing a token programmatically {#force-refresh} If you ever want to force a refresh of your external token as a part of error handling, retry mechanism, or something similar, you can use the sample code below: ``` // Somewhere in your functions error handling and retry logic:const result = await client.apps.auth.external.get({ external_token_id: inputs.googleAccessTokenId, force_refresh: true // default force_refresh is false}); ``` ### 9. Deleting a token programmatically {#delete-programmatically} If you ever want to delete your external token programmatically, you can use the sample code below. Bear in mind that once a token is deleted, all workflows that were previously using the token will no longer work. This will _not_ revoke the token from the provider's system. It will only delete the reference to the token from Slack and prevent it from being used within the external authentication system. ``` // Somewhere in your function: await client.apps.auth.external.delete({ external_token_id: inputs.googleAccessTokenId, }); ``` ## Delete external auth tokens {#delete-tokens} If you'd like to delete your tokens and remove OAuth2 authentication from your Slack app, the following commands will allow you to do so: Command Description `$ slack external-auth remove` Choose a provider to delete tokens for from the list displayed. `$ slack external-auth remove --all` Delete all tokens for the app by specifying the `--all` flag. `$ slack external-auth remove --provider provider_name --` Delete all tokens for a provider by specifying the `--provider` flag. This will _not_ revoke the token from the provider's system. It will only delete the reference to the token from Slack and prevent it from being used within the external authentication system. ## Troubleshooting {#troubleshooting} You can view external authentication logs via `slack activity`. These logs contain information about errors encountered by users during the OAuth2 exchange and workflow execution. Below are some common errors: Error Description `access_token_exchange_failed` An error was returned from the configured `token_url`. `external_user_identity_not_found` The configured `account_identifier` was not found in user identity response. `internal_error` An internal system error happened. Please reach out to Slack if this occurs consistently. `invalid_identity_config_response` `url` in the configured `identity_config` returned an invalid response. `invalid_token_response` `token_url` returned an invalid response. `missing_client_secret` Optional client secret was found for this provider. `no_refresh_token` Token to refresh the expired access token does not exist. `oauth2_callback_error` The OAuth2 provider returned an error. `oauth2_exchange_error` There was an error while obtaining the OAuth2 token from the configured provider. `scope_mismatch_error` Slack was not able to find an OAuth2 token that matched the `scope` configured on your provider. `token_not_found` Slack was not able to find an OAuth2 token for this user and provider. ## Next Steps {#next-steps} Check out the following sample projects to see how real-world workflow apps use OAuth: * [Timesheet approval app](https://github.com/slack-samples/deno-timesheet-approval) uses Google Sheets to store information collected in a workflow form from Slack users * [Simple survey app](https://github.com/slack-samples/deno-simple-survey) uses Google Sheets to store survey responses * [GitHub functions repo](https://github.com/slack-samples/deno-github-functions) brings oft-used GitHub functionality - such as creating new issues - to Slack using functions and workflows --- Source: https://docs.slack.dev/tools/deno-slack-sdk/guides/logging-function-and-app-behavior # Logging function and app behavior When building workflow apps, you can use both function-level and app-level logging to troubleshoot and debug your app. ## Local app logs {#local-logging} When developing locally, you can log information to the console of the terminal window where you are running your development server with calls to `console.log`. Calls to `console.log` are processed while your local developer server is running. Let's use the [Virtual Running Buddies sample app](https://github.com/slack-samples/deno-virtual-running-buddies) as an example. Say you'd like to print out items that are getting stored in your datastore to verify that you're getting the type of data you expect. Within the `log_run.ts` file, we'll add a `console.log` call to the function that logs a run and stores it in the datastore as follows: ``` // log_run.tsexport default SlackFunction(LogRunFunction, async ({ inputs, client }) => { const { distance, rundate, runner } = inputs; const uuid = crypto.randomUUID(); const putResponse = await client.apps.datastore.put({ datastore: RUN_DATASTORE, item: { id: uuid, runner: runner, distance: distance, rundate: rundate, }, }); if (!putResponse.ok) { return { error: `Failed to store run: ${putResponse.error}` }; } console.log(putResponse); // add this line return { outputs: {} };}); ``` Now when you run your app locally, you'll see output similar to the following in your terminal window: ``` ✨ yourname123 of Your DevEnvConnected, awaiting events2023-03-29 07:51:07 [info] [Wf050D7QTLR5] (Trace=Tr050M6Y6QF8) Execution started for workflow 'Log a run'2023-03-29 07:51:07 [info] [Wf050D7QTLR5] (Trace=Tr050M6Y6QF8) Executing workflow step 1 of 32023-03-29 07:51:07 [info] [Fn010N] (Trace=Tr050M6Y6QF8) Function execution started for builtin function 'Open a form'2023-03-29 07:51:17 [info] [Fn010N] (Trace=Tr050M6Y6QF8) Function execution completed for function 'Open a form'2023-03-29 07:51:18 [info] [Wf050D7QTLR5] (Trace=Tr050M6Y6QF8) Execution completed for workflow step 'Open a form'2023-03-29 07:51:19 [info] [Wf050D7QTLR5] (Trace=Tr050M6Y6QF8) Executing workflow step 2 of 3{ ok: true, datastore: "running_datastore", item: { runner: "ABC123DEF45", id: "abcd1234-ef56-gh78-ij91-klmnop234567", distance: 4.5, rundate: "2023-03-29" }}2023-03-29 07:51:19 [info] [Fn050TKLQJ3V] (Trace=Tr050M6Y6QF8) Function execution started for app function 'Log a run'2023-03-29 07:51:20 [info] [Fn050TKLQJ3V] (Trace=Tr050M6Y6QF8) Function execution completed for function 'Log a run'2023-03-29 07:51:21 [info] [Wf050D7QTLR5] (Trace=Tr050M6Y6QF8) Execution completed for workflow step 'Log a run'2023-03-29 07:51:22 [info] [Wf050D7QTLR5] (Trace=Tr050M6Y6QF8) Executing workflow step 3 of 32023-03-29 07:51:22 [info] [Fn0102] (Trace=Tr050M6Y6QF8) Function execution started for builtin function 'Send a message to channel'2023-03-29 07:51:24 [info] [Fn0102] (Trace=Tr050M6Y6QF8) Function execution completed for function 'Send a message to channel'2023-03-29 07:51:25 [info] [Wf050D7QTLR5] (Trace=Tr050M6Y6QF8) Execution completed for workflow step 'Send a message to channel'2023-03-29 07:51:25 [info] [Fn051HE94GSU] (Trace=Tr050TQV7ERG) Function execution completed for function 'Log a run'2023-03-29 07:51:25 [info] [Wf050D7QTLR5] (Trace=Tr050M6Y6QF8) Execution completed for workflow 'Log a run' ``` Running `console.log` emits information that _you_ provide for your app to emit. If you want to see logs for _all_ your app's activity, you'll need to install your app and run `slack activity`. Once you run `slack activity` and select your workspace and local app environment, you'll see output similar to the following in your terminal window: ``` ✨ yourname123 of Your DevEnv2023-03-29 07:51:07 [info] [Wf050D7QTLR5] (Trace=Tr050M6Y6QF8) Execution started for workflow 'Log a run'2023-03-29 07:51:07 [info] [Wf050D7QTLR5] (Trace=Tr050M6Y6QF8) Executing workflow step 1 of 32023-03-29 07:51:07 [info] [Fn010N] (Trace=Tr050M6Y6QF8) Function execution started for builtin function 'Open a form'2023-03-29 07:51:17 [info] [Fn010N] (Trace=Tr050M6Y6QF8) Function execution completed for function 'Open a form'2023-03-29 07:51:18 [info] [Wf050D7QTLR5] (Trace=Tr050M6Y6QF8) Execution completed for workflow step 'Open a form'2023-03-29 07:51:19 [info] [Wf050D7QTLR5] (Trace=Tr050M6Y6QF8) Executing workflow step 2 of 32023-03-29 07:51:19 [info] [Fn050TKLQJ3V] (Trace=Tr050M6Y6QF8) Function execution started for app function 'Log a run'2023-03-29 07:51:20 [info] [Fn050TKLQJ3V] (Trace=Tr050M6Y6QF8) Function execution completed for function 'Log a run'2023-03-29 07:51:21 [info] [Wf050D7QTLR5] (Trace=Tr050M6Y6QF8) Execution completed for workflow step 'Log a run'2023-03-29 07:51:22 [info] [Wf050D7QTLR5] (Trace=Tr050M6Y6QF8) Executing workflow step 3 of 32023-03-29 07:51:22 [info] [Fn0102] (Trace=Tr050M6Y6QF8) Function execution started for builtin function 'Send a message to channel'2023-03-29 07:51:24 [info] [Fn0102] (Trace=Tr050M6Y6QF8) Function execution completed for function 'Send a message to channel'2023-03-29 07:51:25 [info] [Wf050D7QTLR5] (Trace=Tr050M6Y6QF8) Execution completed for workflow step 'Send a message to channel'2023-03-29 07:51:25 [info] [Fn051HE94GSU] (Trace=Tr050TQV7ERG) Function execution completed for function 'Log a run'2023-03-29 07:51:25 [info] [Wf050D7QTLR5] (Trace=Tr050M6Y6QF8) Execution completed for workflow 'Log a run' ``` ✨ **For more information about developing locally**, refer to [local development](/tools/deno-slack-sdk/guides/developing-locally). ## Deployed app logs {#deployed-logging} After your app is deployed, all calls to `console.log` will be captured remotely, and will be emitted along with the last seven days of your app's activity via the `slack activity` command **only**. Once deployed, invoke some of your app's workflows, run `slack activity`, then select your workspace and deployed app environment. In your output, you'll see all of your calls to `console.log` in addition to the workflow steps and function executions, [external auth information](/tools/deno-slack-sdk/guides/integrating-with-services-requiring-external-authentication), and any errors encountered when running your app. ✨ **For more information about deploying your app**, refer to [deploy to Slack](/tools/deno-slack-sdk/guides/deploying-to-slack). --- Source: https://docs.slack.dev/tools/deno-slack-sdk/guides/managing-triggers # Managing triggers Workflow apps require a paid plan Join the [Developer Program](https://api.slack.com/developer-program) and provision a sandbox with access to all Slack features for free. All triggers can be updated, deleted, viewed, and have their access restricted in the same way. ## Update a trigger {#update} ### Update a trigger with the CLI {#update_cli} Make an update to a pre-existing trigger with the CLI by using the `slack trigger update` command. Provide the same payload you used to create the trigger _in its entirety_, in addition to the trigger ID. ``` slack trigger update --trigger-id Ft123ABC --trigger-def "path/to/trigger.ts" ``` ### Update a trigger at runtime {#update_runtime} You can update a runtime trigger, but the trigger must be updated in its entirety. Use the same structure as `client.workflows.triggers.create()` but for `client.workflows.triggers.update` with the additional `trigger_id` parameter. ``` const triggerId = "FtABC123";const response = await client.workflows.triggers.update({ trigger_id: triggerId, type: "", name: "My trigger", workflow: "#/workflows/myworkflow", inputs: { input_name: { value: "value", } }});// Error handling example in your custom functionif (response.error) { const error = `Failed to update a trigger (id: ${triggerId}) due to ${response.error}`; return { error };} ``` ## Delete a trigger {#delete} ### Delete a trigger with the CLI {#delete_cli} You can delete a trigger with the `slack trigger delete` command. ``` slack trigger delete --trigger-id FtABC123 ``` ### Delete a trigger at runtime {#delete_runtime} Deleting a runtime trigger deletes that _specific_ trigger created in one instance of the workflow. This means that you'll need to have stored the `trigger_id` created for that instance. Your app will continue to be able to create triggers until you remove the relevant code. You can delete a runtime trigger by using `client.workflows.triggers.delete()`. ``` const response = await client.workflows.triggers.delete({ trigger_id: "FtABC123"});// Error handling example in your custom functionif (response.error) { const error = `Failed to delete a trigger due to ${response.error}`; return { error };} ``` ## List a trigger {#list} Triggers created in your local development environment will only work if your application is still running locally. You can view triggers created in your local development environment with the `slack run --show-triggers` command. Triggers created in a deployed environment will not be returned. You can use the `slack triggers list` command to view information about your app's triggers, including the trigger ID, name, type, creation, and last updated time. When you use that command, you'll be prompted to select the workspace and then the environment (either local or deployed) for the triggers to list. ## Manage access to a trigger {#manage} A newly-created trigger is accessible to anyone inside the workspace by default. You can manage who can access the trigger using the `access` Slack CLI command. ### Grant access {#grant-access} Required Flag Description Example Argument `--grant` A switch to grant access `--trigger-id` The `trigger_id` of the desired trigger `Ft123ABC` Set one of the following flags to grant access to different groups. If no flag is selected you will be prompted to select a group within the Slack CLI. Flag Description Example Argument `--app-collaborators` A switch to grant access to all app collaborators `--channels` The channel IDs of channels to be granted access `C123ABC, C456DEF` `--everyone` A switch to grant access to all workspace members `--organizations` The enterprise IDs of organizations to be granted access `E123ABC, E456DEF` `--users` The user ID of users to be granted access `U123ABC, U456DEF` `--workspaces` The team IDs of workspaces to be granted access `T123ABC, T456DEF` You can combine types of named entities (channels, organizations, users, and workspaces) in a single command. For example, the following command grants access to the trigger `FtABC123` for channel `C123ABC`, organization `E123ABC`, user `U123ABC` and workspace `T123ABC`: ``` slack trigger access --trigger-id Ft123ABC --channels C123ABC --organizations E123ABC --users U123ABC --workspaces T123ABC --grant ``` ### Revoke access {#revoke-access} Required Flag Description Example Argument `--revoke` A switch to revoke access `--trigger-id` The `trigger_id` of the desired trigger `Ft123ABC` Set one of the following flags to revoke access to different groups. If no flag is selected you will be prompted to select a group within the Slack CLI. Flag Description Example Argument `--channels` The channel IDs of channels whose access will be revoked `C123ABC, C456DEF` `--organizations` The enterprise IDs of organizations whose access will be revoked `E123ABC, E456DEF` `--users` The user IDs of users whose access will be revoked `U123ABC, U456DEF` `--workspaces` The team IDs of workspaces whose access will be revoked `T123ABC, T456DEF` The following example command revokes access to the trigger `FtABC123` for users `U123ABC` and `U456DEF` and channels `C123ABC` and `C456DEF`. ``` slack trigger access --trigger-id FtABC123 --users U123ABC, U456DEF --channels C123ABC, C456DEF --revoke ``` ## Onward {#onward} Remember, you won't be able to use any triggers unless your app is active. ✨ **If you're still testing out your app** you'll want to [run your app locally](/tools/deno-slack-sdk/guides/developing-locally). ✨ **If your app is ready to go** you'll want to [deploy it to Slack](/tools/deno-slack-sdk/guides/deploying-to-slack). --- Source: https://docs.slack.dev/tools/deno-slack-sdk/guides/retrieving-items-from-a-datastore # Retrieving items from a datastore Workflow apps require a paid plan Join the [Developer Program](https://api.slack.com/developer-program) and provision a sandbox with access to all Slack features for free. Slack CLI commands You can also retrieve items from a datastore with the [`datastore get`](/tools/slack-cli/reference/commands/slack_datastore_query), [`datastore bulk-get`](/tools/slack-cli/reference/commands/slack_datastore_bulk-get), and [`datastore query`](/tools/slack-cli/reference/commands/slack_datastore_query) Slack CLI commands. The `datastore query` command even supports exporting data to a [JSON Lines](https://jsonlines.org/) file. ## Retrieve items with get and bulkGet {#get} There are two methods for retrieving items in datastores: * The [`apps.datastore.get`](https://docs.slack.dev/reference/methods/apps.datastore.get) method is used for single items. * The [`apps.datastore.bulkGet`](https://docs.slack.dev/reference/methods/apps.datastore.bulkGet) method is used for multiple items. They work quite similarly. Regardless of what you named your `primary_key`, the query will always use the `id` key. Example: Using the `get` method to retrieve an item by its `primary_key` ``` // /functions/create_draft/interactivity_handler.ts...export const openDraftEditView: BlockActionHandler< typeof CreateDraftFunction.definition> = async ({ body, action, client }) => { if (action.selected_option.value == "edit_message_overflow") { const id = action.block_id; // Get the draft const getResp = await client.apps.datastore.get < typeof DraftDatastore.definition > ( { datastore: DraftDatastore.name, id: id, }, );... ``` If the call was successful and data was found, the `item` property in the payload will include the attributes (and their values) from the datastore definition. ``` { "ok": true, "datastore": "drafts", "item": { "id": "906dba92-44f5-4680-ada9-065149e4e930", "created_by": "U045A5X302V", "message": "This is a test message", "channels": [ "C039ARY976C" ], "channel": "C038M39A2TV", "icon": "", "username": "Slackbot", "status": "draft", }} ``` If the call was successful but no data was found, the `item` property in the payload will be blank: ``` { "ok": true, "datastore": "drafts", "item": {}} ``` Example: Using the `bulkGet` method to retrieve multiple items by their `primary_key` ``` // /functions/create_draft/interactivity_handler.ts...export const openDraftEditView: BlockActionHandler< typeof CreateDraftFunction.definition> = async ({ body, action, client }) => { if (action.selected_option.value == "edit_message_overflow") { const id = action.block_id; // Get the draft const getResp = await client.apps.datastore.bulkGet < typeof DraftDatastore.definition > ( { datastore: DraftDatastore.name, ids: [id, "41"] }, );... ``` If multiple items are returned, the `item` properties will be contained in an `items` array ``` { "ok": true, "datastore": "drafts", "items": [ { "id": "906dba92-44f5-4680-ada9-065149e4e930", "created_by": "U045A5X302V", "message": "This is a test message", "channels": [ "C039ARY976C" ], "channel": "C038M39A2TV", "icon": "", "username": "Slackbot", "status": "draft", }, { "id": "906dba92-44f5-4680-ada9-065149e4e930", "created_by": "U045A5X302V", "message": "This is a test message", "channels": [ "C039ARY976C" ], "channel": "C038M39A2TV", "icon": "", "username": "Slackbot", "status": "draft", } ]} ``` If the call was successful but no data was found, the `items` property in the payload will be blank: ``` { "ok": true, "datastore": "drafts", "items": []} ``` For both methods, if the call was unsuccessful, `ok` will be false and you'll see some information on the error. ``` { "ok": false, "error": "datastore_error", "errors": [ { "code": "datastore_config_not_found", "message": "The datastore configuration could not be found", "pointer": "/datastores" } ]} ``` It is possible to have records with undefined values, and it's important to be proactive in expecting those situations in your code. Here are some examples of how to code around a potential undefined field while retrieving an item. This example snippet supports the case where the function returns an optional output: ``` const getResponse = await client.apps.datastore.get < typeof DraftsDatastore.definition > ({ ...});const announcementId = getResponse.item.id; // this is the primary keyconst announcementIcon = getResponse.item.icon; // icon could be undefinedreturn { outputs: { id: announcementId, // id is always defined icon: announcementIcon, // icon must be an optional output of the function }} ``` This example snippet supports the case where the function assigns a default: ``` const getResponse = await client.apps.datastore.get < typeof DraftsDatastore.definition > ({ ...});const announcementId = getResponse.item.id; // this is the primary key// icon could be undefined, so use a fallbackconst announcementIcon = getResponse.item.icon ?? "n/a";return { outputs: { id: announcementId, // id is always defined icon: announcementIcon, // email is always defined }} ``` And finally, this example snippet supports the case where the function should error: ``` const getResponse = await client.apps.datastore.get < typeof DraftsDatastore.definition > ({ ...});const announcementId = getResponse.item.id; // this is the primary keyif (getResponse.item.icon) { const announcementIcon = getResponse.item.icon; return { outputs: { id: announcementId, icon: announcementIcon } }} else { return { error: "Announcement doesn't have an icon assigned" }} ``` Datastore bulk API methods may _partially_ fail The `partial_failure` error message indicates that some items were successfully processed while others need to be retried. This is likely due to rate limits. Call the method again with only those failed items. You'll find a `failed_items` array within the API response. The array contains all the items that failed, in the same format they were passed in. Copy the `failed_items` array and use it in your request. ## Find items with query {#find} If you need to find data without already knowing the item's `id`, you'll want to run a query. Querying a datastore requires knowledge of a few different components. It's also helpful to brush up on how to use [pagination](#pagination) and [filter expressions](#filter-expressions). First, let's look at the fields of a datastore query and how they might look in code, then break down the details of each bit. A Slack datastore query includes the following arguments: Parameter Description Required `datastore` A string with the name of the datastore to read the data from Required `expression` A DynamoDB filter expression, using DynamoDB's [filter expression syntax](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Scan.html#Scan.FilterExpression) Optional `expression_attributes` A map of columns used by the `expression` Optional `expression_values` A map of values used by the `expression` Optional `limit` The maximum number of entries to return, 1-1000 (both inclusive); default is `100` Optional `cursor` The string value to access the next page of results Optional Here's an example of how to query our `drafts` datastore using the [Slack CLI](/tools/slack-cli/guides/installing-the-slack-cli-for-mac-and-linux) and retrieve a list of all the announcements with messages containing "timesheet": ``` const result = await client.apps.datastore.query({ datastore: "drafts", expression: "contains (#message_term, :message)", expression_attributes: { "#message_term": "message" }, expression_values: { ":message": "timesheet" },}); ``` If that example looks wonky to you; read on while we explain. Under the hood, the [`apps.datastore.query`](https://docs.slack.dev/reference/methods/apps.datastore.query) API method is a DynamoDB scan, and thereby uses DynamoDB's [filter expression syntax](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Scan.html#Scan.FilterExpression). Let's break down that previous query example: The `expression` is the search criteria. The `expression_attributes` object is a map of the columns used for the comparison, and the `expression_values` object is a map of values. The `expression_attributes` property must always begin with a `#`, and the `expression_values` property must always begin with a `:`. To break that down further, `#message_term` seen here is a variable representing the `message` datastore attribute. So, why not just use `message` in the expression, such that it would be `expression: "message = :message"`? We do this to safeguard against anything that might break the search query, like double quotes or spaces in a name, or using DynamoDB's [reserved words](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/ReservedWords.html) as attribute names. The second such variable used in the `expression` is `:message`. We see that defined in `expression_values` as the hard-coded value of `"timesheet"`, but it's more likely that you'll use a variable here, perhaps a value obtained from a user interaction. In summary, this query searches for items in the `drafts` datastore that have a value of `"timesheet"` (represented by `:message`) in their `message` attribute (represented by `#message_term`). Let's take a look at another example, this one exploring searching a datastore by timestamp. Given this set of data in a datastore: ``` { "id": "foo5", "message": "bar5", "timestamp": 1671752648}{ "id": "foo4", "message": "bar4", "timestamp": 1670975048}{ "id": "foo3", "message": "bar3", "timestamp": 1702511048} ``` If we run the [Slack CLI](/tools/slack-cli/guides/installing-the-slack-cli-for-mac-and-linux) query: ``` slack datastore query '{ "datastore": "messages", "expression": "#timestamp between :time_start AND :time_end", "expression_attributes": {"#timestamp":"timestamp"}, "expression_values": {":time_start":1670975049,":time_end":1702511047} }' ``` We will see this object as a result: ``` { "id": "foo5", "message": "bar5", "timestamp": 1671752648} ``` You can use filter expression operators with any of the date types ([`Schema.slack.types.date`](/tools/deno-slack-sdk/reference/slack-types#date), [`Schema.slack.types.timestamp`](/tools/deno-slack-sdk/reference/slack-types#timestamp), and [`Schema.slack.types.message_ts`](/tools/deno-slack-sdk/reference/slack-types#message-ts)), so long as the values passed match the underlying type and format. Here is another example of a date query, this one using the [`Schema.slack.types.date`](/tools/deno-slack-sdk/reference/slack-types#date) field. Given this set of data in a datastore: ``` { "date": "2022-01-02", "message": "First message", "id": "1"}{ "date": "2023-04-11", "message": "Second message", "id": "2"}{ "date": "2024-01-01", "message": "Third message", "id": "3"} ``` Running this query: ``` slack datastore query '{ "datastore": "messages", "expression": "#date < :date_end", "expression_attributes": {"#date": "date"}, "expression_values": {":date_end": "2023-01-01"} }' ``` Will yield this result: ``` { "date": "2022-01-02", "message": "First message", "id": "1"} ``` ## Pagination {#pagination} It is strongly recommended to always handle pagination when implementing a query so that you can easily view all of your query results. The following code snippet from the [Virtual Running Buddies sample app](https://github.com/slack-samples/deno-virtual-running-buddies) shows how to do this: ``` export async function queryRunningDatastore( client: SlackAPIClient, expressions?: object,): Promise<{ ok: boolean; items: DatastoreItem[]; error?: string;}> { const items: DatastoreItem[] = []; let cursor = undefined; do { const runs: DatastoreQueryResponse = await client.apps.datastore.query < typeof RunningDatastore.definition > ({ datastore: RUN_DATASTORE, cursor, ...expressions, }); if (!runs.ok) { return { ok: false, items, error: runs.error }; } cursor = runs.response_metadata?.next_cursor; items.push(...runs.items); } while (cursor); return { ok: true, items };} ``` Essentially, you'll use the `cursor` parameter to retrieve the next page of your query results. If your initial query has another page of results, the `next_cursor` response parameter is the key returned that will unlock your next page of results. Use this key to query the datastore again and set `cursor` to the value of `next_cursor`. Remember that filters are applied post-hoc, so you should always be sure to check subsequent pages for results, even if the initial page has fewer results than expected. Continue to the [filter expressions](#filter-expressions) section for more context. ## Filter expressions {#filter-expressions} Because datastore `query` is a DynamoDB [scan](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Scan.html), all query expressions are essentially filter expressions: it's what you put in the value of the `expression` argument. Filter expressions are applied post-hoc. This is important to understand because it can yield some confusing results; i.e. return fewer results than requested yet have additional pages of results to be queried and [paginated](#pagination). Each query can return a maximum of 1MB of data per page of results, and returns all results of the datastore _before_ applying any filter conditions. The filter conditions are applied to each page of results individually. This is how you could end up with the first page of zero results, yet still have a cursor for a following page of results. Here is the full list of comparison operators to use in a filter expression, followed by some examples: Operator Description Example `=` True if both values are equal `a = b` `<` True if the left value is less than but not equal to the right `a < b` `<=` True if the left value is less than or equal to the right `a <= b` `>` True if the left value is greater than but not equal to the right `a > b` `>=` True if the left value is greater than or equal to the right `a >= b` `BETWEEN ... AND` True if one value is greater than or equal to one and less than or equal to another `#time_stamp BETWEEN :ts1 AND :ts2` `begins_with(str, substr)` True if a `string` begins with `substring` `begins_with("#message_term", ":message")` `contains (path, operand)` True if attribute specified by `path` is a string that contains the `operand` string `contains (#song, :inputsong)` Expressions can only contain non-primary key attributes If you try to write an expression that uses a primary key as its attribute (for example, to pull a single row from a datastore), you will receive a cryptic error. Please use [`apps.datastore.get`](#get) instead. We're hard at work on making these types of errors easier to understand! Revisiting our `drafts` datastore, here we retrieve all the announcements created by user `C123ABC456`: ``` const result = await client.apps.datastore.query({ datastore: "drafts", expression: "#announcement_creator = :user", expression_attributes: { "#announcement_creator": "created_by" }, expression_values: { ":user": "C123ABC456" },}); ``` If you wanted to verify the query before putting it in your app code, the CLI query for that same search would be: ``` slack datastore query '{ "datastore": "drafts", "expression": "#announcement_creator = :user", "expression_attributes": { "#announcement_creator": "created_by"}, "expression_values": {":user": "C123ABC456"}}' ``` Here's an example of a function that receives a string `message` via an `input` and queries for the announcement record that matches the provided message: ``` const result = await client.apps.datastore.query({ datastore: "drafts", expression: "contains (#message_term, :message)", expression_attributes: { "#message_term": "message" }, expression_values: { ":message": input.message },}); ``` You could also chain expressions together to narrow your results even further: ``` const result = await client.apps.datastore.query({ datastore: "drafts", expression: "contains (#message_term, :message) AND #announcement_creator = :creator", expression_attributes: { "#message_term": "message", "#announcement_creator": "created_by" }, expression_values: { ":message": input.message, ":creator": input.creator },}); ``` ## Count items with count {#count} As mentioned above, querying a datastore uses a DynamoDB scan to return an array of matching items for your query results. We also mentioned that each query, i.e. each DynamoDB scan, can return a maximum of 1MB of data per page of results. For that reason, if you have over 1MB of data in your datastore, multiple scans are necessary to paginate through your entire datastore. DynamoDB accomplishes this by returning a cursor to start a new scan where you left off with your previous one. Therefore if you wanted to use the `query` method to count all of the matching items in your datastore, you would need to call the `query` command several times, then manually add together the sizes of each array of matching items returned. Instead, you can use the `count` method to paginate through your datastore and sum up the count of all the items matching your query. If a query is not provided, the count will be equal to the number of items in the entire datastore. Using the DynamoDB style of syntax, the following example would retrieve the number of records from a datastore called "good\_tunes", where "You" is in the song title: ``` { "datastore": "good_tunes", "expression": "contains (#song, :keyword)", "expression_attributes": { "#song": "song" }, "expression_values": { ":keyword": "You" }} ``` The response to the request might look like the following: ``` { "ok": true, "datastore": "good_tunes", "count": 2} ``` --- Source: https://docs.slack.dev/tools/deno-slack-sdk/guides/using-datastores # Using datastores Workflow apps require a paid plan Join the [Developer Program](https://api.slack.com/developer-program) and provision a sandbox with access to all Slack features for free. Datastores are a Slack-hosted way to store data for your workflow apps. They are available for workflow apps only. Datastores are backed by [DynamoDB](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Introduction.html), a secure and performant NoSQL database. DynamoDB's data model uses three basic types of data units: tables, items, and attributes. Tables are collections of items, and items are collections of attributes. You will see how a collection of attributes comprises an item when we define a datastore later in this page. ## Initializing a datastore {#create} To initialize a datastore: 1. [Define](#define) the datastore 2. [Add](#manifest) it to your manifest ### 1. Define the datastore {#define} To keep your app tidy, datastores can be defined in their own source files just like [custom functions](/tools/deno-slack-sdk/guides/creating-custom-functions). If you don't already have one, create a `datastores` directory in the root of your project, and inside, create a source file to define your datastore. Throughout this page, we'll use the example of the [Announcement bot sample app](https://github.com/slack-samples/deno-announcement-bot/tree/main). First, we'll create a datastore called `Drafts` and define it in a file named `drafts.ts`. It will hold information about an announcement the user drafts to send to a channel: ``` // /datastores/drafts.tsimport { DefineDatastore, Schema } from "deno-slack-sdk/mod.ts";export default DefineDatastore({ name: "drafts", primary_key: "id", attributes: { id: { type: Schema.types.string, }, created_by: { type: Schema.slack.types.user_id, }, message: { type: Schema.types.string, }, channels: { type: Schema.types.array, items: { type: Schema.slack.types.channel_id, }, }, channel: { type: Schema.slack.types.channel_id, }, message_ts: { type: Schema.types.string, }, icon: { type: Schema.types.string, }, username: { type: Schema.types.string, }, status: { type: Schema.types.string, // possible statuses are draft, sent }, },}); ``` Datastores can contain three primary properties. The `primary_key` property is the only one that is required. When using additional optional properties, make sure to handle them properly to avoid running into any TypeScript errors in your code. Property Type Description Required `name` String A string to identify your datastore Optional `primary_key` String The attribute to be used as the datastore's unique key; ensure this is an actual attribute that you have defined Required `attributes` Object (see below) Properties to scaffold your datastore's columns Optional `time_to_live_attribute` String An optional attribute used as a Time To Live (TTL) feature to [delete datastore items automatically](/tools/deno-slack-sdk/guides/deleting-items-from-a-datastore#delete-automatically) when set to a property of type `Schema.slack.types.timestamp`, which represents the item's expiration. Optional Attributes can be [custom types](/tools/deno-slack-sdk/guides/creating-a-custom-type), [Slack types](/tools/deno-slack-sdk/reference/slack-types), and the following basic schema types: * array * boolean * int * number * object * string No nullable support If you use a built-in Slack type for an attribute, there is no nullable support. For example, let's say you use `channel_id` for an attribute and at some point in your app, you'd like to clear out the `channel_id` for a given item. You cannot do this with a Slack built-in type. Change the data type to be a string if you'd like to support a null or empty value. ### 2. Add the datastore to your app's manifest {#manifest} The last step in initializing your datastore is to add it to the `datastores` property in your manifest and include the required datastore bot scopes. To do that, first add the `datastores` property to your manifest if it does not exist, then list the datastores you have defined. Second, add the following datastore permission scopes to the `botScopes` property: * `datastore:read` * `datastore:write` Here's an example manifest definition for the above `drafts` datastore in the [Announcement bot sample app](https://github.com/slack-samples/deno-announcement-bot/tree/main): ``` import { Manifest } from "deno-slack-sdk/mod.ts";// Import the datastore definitionimport AnnouncementDatastore from "./datastores/announcements.ts";import DraftDatastore from "./datastores/drafts.ts";import { AnnouncementCustomType } from "./functions/post_summary/types.ts";import CreateAnnouncementWorkflow from "./workflows/create_announcement.ts";export default Manifest({ name: "Announcement Bot", description: "Send an announcement to one or more channels", icon: "assets/icon.png", outgoingDomains: ["cdn.skypack.dev"], datastores: [DraftDatastore, AnnouncementDatastore], // Add the datastore to this list types: [AnnouncementCustomType], workflows: [ CreateAnnouncementWorkflow, ], botScopes: [ "commands", "chat:write", "chat:write.public", "chat:write.customize", "datastore:read", "datastore:write", ],}); ``` Note that we've also added the required `datastore:read` and `datastore:write` bot scopes. Datastore schema changes Updates to an existing datastore that could result in data loss (such as removing a datastore or attribute from your app) may require the use of the `--force` flag when re-deploying the app. * * * ## Importing data to a datastore {#import} You can import data from a [JSON Lines](https://jsonlines.org/) file to a datastore using the [`datastore bulk-put`](/tools/slack-cli/reference/commands/slack_datastore_bulk-put) command with the `--from-file` flag. For example: ``` slack datastore bulk-put '{"datastore": "running_datastore"}' —-from-file /path/to/file.jsonl ``` See the [Add items to a datastore guide](/tools/deno-slack-sdk/guides/adding-items-to-a-datastore) for more information on the API method underlying this command. * * * ## Exporting data from a datastore {#export} You can export data from a datastore to a [JSON Lines](https://jsonlines.org/) file using the [`datastore query`](/tools/slack-cli/reference/commands/slack_datastore_query) command with the `--to-file` flag. For example: ``` slack datastore query '{"datastore": "running_datastore"}' —-to-file /path/to/file.jsonl ``` See the [Retrieve items from a datastore guide](/tools/deno-slack-sdk/guides/retrieving-items-from-a-datastore) for more information on the API method underlying this command. * * * ## Counting items in a datastore {#count} You can count the number of items in a datastore that match a query by using the [`datastore count`](/tools/slack-cli/reference/commands/slack_datastore_count) command. This command handles paginating through an entire datastore to return the number of matched items (rather than the items themselves, as with the `datastore query` command). See the [Count items in a datastore guide](/tools/deno-slack-sdk/guides/retrieving-items-from-a-datastore#count) for more information on the API method underlying this command. * * * ## Interacting with a datastore {#interact} There are two ways to interact with your app's datastore. ➡️ **To interact with your datastore through the command-line tool**, see the [datastore commands](/tools/slack-cli/reference/commands/slack_datastore) section on the commands page. ⤵️ **To interact with your datastore within a [custom function](/tools/deno-slack-sdk/guides/creating-custom-functions)**, keep reading. Interacting with your app's datastore requires hitting the `SlackAPI`. To do this from within your code, we first need to import a mechanism that will allow us to call the `SlackAPI`. That mechanism is `SlackFunction`. First we import it into our function file from the `deno-slack-sdk` package, then we add a `SlackFunction` into our code. `SlackFunction` contains a property, `client`, which allows us to call the datastore. You can find examples of this in the [Slack API methods guide](/tools/deno-slack-sdk/guides/calling-slack-api-methods) and [Add items to a datastore guide](/tools/deno-slack-sdk/guides/adding-items-to-a-datastore). In all interactions with your datastore, double and triple-check the exact spelling of the fields in the datastore definition match your query, lest you should receive an error. When interacting with your datastore, it may be helpful to first visualize its structure. In our `drafts` example, let's say we have stored the following users and their drafted announcements: id created\_by message channels channel message\_ts icon username status `906dba92-44f5-4680-ada9-065149e4e930` `U045A5X302V` `This is a test message` `["C038M39A2TV"]` `C039ARY976C` `1691513323.119209` `Slackbot` `sent` `b8457c38-4401-4dd1-b979-a0e56f7c9a3d` `BR75C7X4P90` `Remember to submit your timesheets` `["C038M39A2TV"]` `C039ARY976C` `1691520476.091369` `:robot_face:` `The Boss` `draft` `194a52d8-c75b-4eff-9f8f-4c40292cd9e7` `G98I9345NI2` `Happy Friday, team!` `["D870D2223M23"]` `D870D2223M23` `2172813323.142610` `:t-rex:` `Slackasaurus Bot` `sent` Beware of SQL injection Be sure to sanitize any strings received from a user and **never** use untrusted data in your query expressions. * * * ## Generic types for datastores {#generic-types} You can provide your datastore's definition as a generic type, which will provide some automatic typing on the arguments and response: ``` import { DefineDatastore, Schema } from "deno-slack-sdk/mod.ts";export const DraftDatastore = DefineDatastore({ name: "drafts", primary_key: "id", attributes: { id: { type: Schema.types.string, }, created_by: { type: Schema.slack.types.user_id, }, message: { type: Schema.types.string, }, channels: { type: Schema.types.array, items: { type: Schema.slack.types.channel_id, }, }, channel: { type: Schema.slack.types.channel_id, }, message_ts: { type: Schema.types.string, }, icon: { type: Schema.types.string, }, username: { type: Schema.types.string, }, status: { type: Schema.types.string, }, },}); ``` You can use the result of your `DefineDatastore()` call as the type in a function by using its `definition` property: ``` import { DraftDatastore } from "../datastores/drafts.ts";... const putResp = await client.apps.datastore.put< typeof DraftDatastore.definition >({ datastore: DraftDatastore.name, item: { id: draftId, created_by: inputs.created_by, message: inputs.message, channels: inputs.channels, channel: inputs.channel, icon: inputs.icon, username: inputs.username, status: DraftStatus.Draft, }, }); ... ``` By using typed methods, the `datastore` property (e.g. `DraftDatastore.datastore`) will enforce that its value matches the datastore definition's `name` property across methods and the `item` matches the definition's `attributes` in arguments and responses. Also, for `get()` and `delete()`, a property matching the `primary_key` will be expected as an argument. * * * ## Onward {#onward} Ready to start manipulating data with your workflows? We've got a guide for each type of activity: * [Add items to a datastore](/tools/deno-slack-sdk/guides/adding-items-to-a-datastore) * [Retrieve items from a datastore](/tools/deno-slack-sdk/guides/retrieving-items-from-a-datastore) * [Delete items from a datastore](/tools/deno-slack-sdk/guides/deleting-items-from-a-datastore) * [Delete items from a datastore automatically](/tools/deno-slack-sdk/guides/deleting-items-from-a-datastore#delete-automatically) * * * ## Deleting a datastore {#delete-datastore} If you need to delete a datastore completely, for instance you've changed the primary key, you have a couple of options. Datastores do support primary key changes, so first try using the `--force` flag on a [datastore CLI](/tools/slack-cli/reference/commands/slack_datastore) operation if the Slack CLI informs you that the datastore has changed. Otherwise, do the following: Step 1. Remove the datastore definition from the app's manifest. Step 2. Run `slack deploy`. Step 3. Modify the datastore definition to your heart's content and add it back into the app's manifest. Step 4. Run `slack deploy` again. * * * ## Troubleshooting {#troubleshooting} If you're looking to audit or query your datastore from the terminal without having to go through code, see the [datastore commands](/tools/slack-cli/reference/commands/slack_datastore). If you're getting errors, check the following: * The primary key is formatted as a string * The datastore is included in the manifest's `datastores` property * The datastore bot scopes are included in the manifest (`datastore:read` and `datastore:write`) * The spelling of the fields in your query match exactly the spelling of the fields in the datastore's definition The information stored when initializing your datastore using `slack run` will be completely separate from the information stored in your datastore when using `slack deploy`. --- Source: https://docs.slack.dev/tools/deno-slack-sdk/guides/using-environment-variables # Using environment variables with the Deno Slack SDK Storing and using environment variables in an application allows for certain variables to be maintained outside of the code of the application. You can use environment variables from within Slack [functions](/tools/deno-slack-sdk/guides/creating-custom-functions), [triggers](/tools/deno-slack-sdk/guides/using-triggers), and [manifests](/tools/deno-slack-sdk/guides/using-the-app-manifest). ## Using environment variables with a custom function {#custom-function} When accessing environment variables from within a [custom function](/tools/deno-slack-sdk/guides/creating-custom-functions), where you store them differs when the app is local versus deployed. ### Storing local environment variables {#local-env-vars} Local environment variables are stored in a `.env` file at the root of the project and made available for use in [custom functions](/tools/deno-slack-sdk/guides/creating-custom-functions) via the `env` [context property](/tools/deno-slack-sdk/guides/creating-custom-functions#context). A local `.env` file might look like this: ``` MY_ENV_VAR=asdf1234 ``` Changes to your `.env` file will be reflected when you restart your local development server. While the `.env` file should **never** be committed to source control for security reasons, you can see a sample `.env` file we've included in the [Timesheet approval sample app](https://github.com/slack-samples/deno-timesheet-approval) and the [Incident management sample app](https://github.com/slack-samples/deno-incident-management). ### Storing deployed environment variables {#deployed-env-vars} When your app is [deployed](/tools/deno-slack-sdk/guides/deploying-to-slack), it will no longer use the `.env` file. Instead, you will have to add the environment variables using the [`env set`](/tools/slack-cli/reference/commands/slack_env_set) command. Environment variables added with `env set` will be made available to your deployed app's [custom functions](/tools/deno-slack-sdk/guides/creating-custom-functions) just as they are locally; see examples in the next section. For the above example, we could run the following command before deploying our app: ``` slack env set MY_ENV_VAR shhhh ``` If your token contains non-alphanumeric characters, wrap it in quotes like this: ``` slack env set SLACK_API_URL "https://dev.slack.com/api/" ``` Your environment variables are always encrypted before being stored on our servers and will be automatically decrypted when you use them—including when listing environment variables with `slack env list`. ### Access variables from within function {#access-function} We can retrieve the `MY_ENV_VAR` environment variable from within a [custom Slack function](/tools/deno-slack-sdk/guides/creating-custom-functions) via the `env` [context property](/tools/deno-slack-sdk/guides/creating-custom-functions#context) like this: ``` // functions/my_function.tsimport { DefineFunction, SlackFunction } from "deno-slack-sdk/mod.ts";import { MyFunctionDefinition } from "functions/myfunction.ts"export default SlackFunction(MyFunctionDefinition, ({ env }) => { const myEnvVar = env["MY_ENV_VAR"]; // ... return { outputs: {} };}); ``` Environment variables also play an important part in making calls to a third-party API. Learn more about how to do that in the [FAQ](https://docs.slack.dev/faq#third-party). ## Using environment variables with a trigger or manifest {#using-trigger-manifest} Accessing environment variables from within a [trigger](/tools/deno-slack-sdk/guides/using-triggers) definition or when constructing the [manifest](/tools/deno-slack-sdk/guides/using-the-app-manifest) differs slightly from custom functions. Whether your app is being run locally or already deployed, constructing these definitions happens entirely on your machine and so the environment variables stored on your machine are used. ### Storing environment variables {#trigger-manifest-env-vars} Environment variables used in trigger or manifest definitions should be saved in the local `.env` file for your project [as shown above](#local-env-vars). The values from this file are collected and used when generating these definitions. Regardless of whether you're working with a local or deployed app, the same values from this file will be used. Read on to learn how to access these stored variables in code. ### Accessing variables from a trigger or manifest {#accessing-trigger-manifest} The Deno runtime provides a helpful `load` function to autoload environment variables as part of the `dotenv` module of the standard library. We'll leverage this to easily access our environment variables. Including this module in code will automatically import local environment variables for immediate use! Start by adding [the latest version](https://deno.land/std/dotenv/mod.ts) of this module to your `import_map.json`: test ``` { "imports": { "deno-slack-sdk/": "https://deno.land/x/deno_slack_sdk@a.b.c/", "deno-slack-api/": "https://deno.land/x/deno_slack_api@x.y.z/", "std/": "https://deno.land/std@0.202.0/" }} ``` Then, you can import the module into any file that makes use of environment variables and start accessing the environment with [`Deno.env.get("VARIABLE_NAME")`](https://examples.deno.land/environment-variables) like so: ``` // manifest.tsimport { Manifest } from "deno-slack-sdk/mod.ts";import ExampleWorkflow from "./workflows/example_workflow.ts";import "std/dotenv/load.ts";export default Manifest({ name: "Chatbot4000", displayName: Deno.env.get("CHATBOT_DISPLAY_NAME"), description: "Workflows for communicating with an imagined chatbot", icon: "assets/icon.png", workflows: [ExampleWorkflow], outgoingDomains: [ Deno.env.get("CHATBOT_API_URL")!, ], botScopes: ["commands", "chat:write"],}); ``` After including this new module, you may have to run [`deno cache manifest.ts`](https://docs.deno.com/runtime/manual/getting_started/command_line_interface#cache-and-compilation-flags) to refresh your local dependency cache. Variable values such as these are commonly used to specify [outgoing domains](/tools/deno-slack-sdk/guides/using-the-app-manifest#manifest-properties) used by functions, channel IDs for [event triggers](/tools/deno-slack-sdk/guides/creating-event-triggers#event-object), or client IDs of an [external authentication](/tools/deno-slack-sdk/guides/integrating-with-services-requiring-external-authentication#define) provider. But, don't let that limit you — environment variables can be used in so many other places! #### Requiring environment variables values {#required-manifest-variable-values} Setting values for environment variables can sometimes be forgotten, which can cause problems at runtime. Catching errors for these missing values early is often better than waiting for that runtime problem. Including a `!` with your call to `Deno.env.get()` will ensure this value is defined at the time of building a definition and will throw an error otherwise. The previous example uses this pattern to ensure an outgoing domain is always set: ``` outgoingDomains: [ Deno.env.get("CHATBOT_API_URL")!, ], ``` With this addition, running `slack deploy` without defining a value for `CHATBOT_API_URL` in the `.env` file will throw an error to give you a chance to set it before actually deploying! ## Enabling debug mode {#debug} The included environment variable `SLACK_DEBUG` can enable a basic debug mode. Set `SLACK_DEBUG` to `true` to have all function-related payloads logged. For local apps, add the following to your `.env` file: ``` SLACK_DEBUG=true ``` For deployed apps, run the following command before deployment: ``` slack env set SLACK_DEBUG true ``` ## Included local and deployed variables {#included} Slack provides two environment variables by default, `SLACK_WORKSPACE` and `SLACK_ENV`. The workspace name is specified by `SLACK_WORKSPACE` and `SLACK_ENV` provides a distinction between the `local` and `deployed` app. Use these values if you want to have different values based on the workspace or environment that the app is installed in. These variables are automatically included when generating the manifest or triggers only. For access from within a custom function, these variables can be set from the `.env` file or with the [`env set`](/tools/slack-cli/reference/commands/slack_env_set) command. A custom `WorkspaceMapSchema` can be created and used with these variables to decide which values to use for certain instances of an app. This can be used as an alternative to a local `.env` file or in conjunction with it. The following snippet works well for inclusion in your app manifest, or for triggers (for example, to change event trigger channel IDs): ``` // Custom schemas can be defined for workspace valuestype WorkspaceSchema = { channel_id: string };type WorkspaceMapSchema = { [workspace: string]: { [environment: string]: WorkspaceSchema; };};// Custom values can be set for each known workspaceexport const workspaceValues: WorkspaceMapSchema = { beagoodhost: { deployed: { channel_id: "C123ABC456", }, local: { channel_id: "C123ABC456", }, }, sandbox: { deployed: { channel_id: "C222BBB222", }, local: { channel_id: "C222BBB222", }, },};// Fallback options can also be definedexport const defaultValues: WorkspaceSchema = { channel_id: "{{data.channel_id}}",};// Included environment variables will determine which value is usedconst environment = Deno.env.get("SLACK_ENV") || "";const workspace = Deno.env.get("SLACK_WORKSPACE") || "";const { channel_id } = workspaceValues[workspace]?.[environment] ?? defaultValues; ``` --- Source: https://docs.slack.dev/tools/deno-slack-sdk/guides/using-the-app-manifest # Using the app manifest Workflow apps require a paid plan Join the [Developer Program](https://api.slack.com/developer-program) and provision a sandbox with access to all Slack features for free. An app's [manifest](https://docs.slack.dev/app-manifests/) is where you can configure its name and scopes, declare the functions your app will use, and [more](#manifest-properties). The manifest file, named `manifest.ts` is located within the root of your directory. Inside the manifest file, you will find an `export default Manifest` block that defines the app's configuration. For an example, below is an annotated version of the manifest for our [Hello World app](https://github.com/slack-samples/deno-hello-world): ``` // manifest.tsimport { Manifest } from "deno-slack-sdk/mod.ts";// Import your workflowimport GreetingWorkflow from "./workflows/greeting_workflow.ts";export default Manifest({ // This is the internal name for your app. // It can contain spaces (e.g., "My App") name: "deno-hello-world", // A description of your app that will help users decide whether to use it. description: "A sample that demonstrates using a function, workflow and trigger to send a greeting", // Your app's profile picture that will appear in the Slack client. icon: "assets/default_new_app_icon.png", // A list of all workflows your app will use. workflows: [GreetingWorkflow], // If your app communicates to any external domains, list them here. outgoingDomains: [], // e.g., myapp.tld // Bot scopes can be declared here. // For the beta, you can keep these as-is. botScopes: ["commands", "chat:write", "chat:write.public"],}); ``` ## Manifest properties {#manifest-properties} Property Type Description Required? `name` String The internal name for your app. It can contain spaces (e.g., "My App") Required `description` String A short sentence describing your application. A description of your app that will help users decide whether to use it Required `icon` String A relative path to an image asset to use for the app's icon. Your app's profile picture that will appear in the Slack client. Note this icon is only used if the app is deployed with `slack deploy`. Required `botScopes` Array of Strings A list of bot [scopes](https://docs.slack.dev/reference/scopes?token_types=Bot), or permissions, the app's functions require Required `displayName` String A custom name for the app to be displayed that's different from the `name` Optional `longDescription` String A more detailed description of your application Optional `backgroundColor` String A six digit combination of numbers and letters (the hexadecimal color code) that make up the color of your app background e.g., "#000000" is the color black Optional `functions` Array A list of all functions your app will use Optional `workflows` Array A list of all workflows your app will use Optional `outgoingDomains` Array of Strings As of [`v1.15.0`](https://docs.slack.dev/changelog/2022/12/31/output#november): if your app communicates to any external domains, list them here. If you make API calls to `slack.com`, it does not need to be explicitly listed. e.g., myapp.tld Optional `events` Array A list of all event structures that the app is expecting to be passed via [Message Metadata](https://docs.slack.dev/messaging/message-metadata/) Optional `types` Array A list of all [custom types](/tools/deno-slack-sdk/guides/creating-a-custom-type) your app will use Optional `datastores` Array A list of all [Datastores](/tools/deno-slack-sdk/guides/using-datastores) your app will use Optional `features` Object A configuration object of your app features Optional ### More on outgoing domains {#outgoing-domains} [Deno](/tools/deno-slack-sdk/guides/installing-deno) requires explicit permission to access external resources. Therefore, to make HTTP requests to external domains, you'll need to add the domain to your app manifest as an outgoing domain. Otherwise, you may run into a `PermissionDenied: Detected missing network permissions` error. ## Function definitions in the manifest {#functions} You may also see some function definitions in the manifest file. While [Slack functions](/tools/deno-slack-sdk/guides/creating-slack-functions) _can_ be defined here, to keep your code tidy, we recommend defining your functions in their own respective source files in your app's `/functions` directory. Regardless of where you define them, each function your app uses must be declared in the manifest. ## The Messages tab {#messages-tab} By default, apps created with `slack create` will include both a read-only Messages tab and an About tab within Slack. You can use the [Slack function](/tools/deno-slack-sdk/guides/creating-slack-functions) [`SendDm`](/tools/deno-slack-sdk/reference/slack-functions/send_dm) to send users direct messages from your app—which will appear for them in the app's Messages tab. Your app's Messages tab will be enabled and read-only by default. If you'd like to disable read-only mode and/or disable the Messages tab completely, add the optional `features` property to your manifest definition like this: ``` // manifest.tsimport { Manifest } from "deno-slack-sdk/mod.ts";import GreetingWorkflow from "./workflows/greeting_workflow.ts";export default Manifest({ name: "deno-hello-world", description: "A sample that demonstrates using a function, workflow and trigger to send a greeting", icon: "assets/default_new_app_icon.png", workflows: [GreetingWorkflow], outgoingDomains: [], // Add this ------ features: { appHome: { messagesTabEnabled: false, messagesTabReadOnlyEnabled: false, }, }, // --------------- botScopes: ["commands", "chat:write", "chat:write.public"],}); ``` * * * ➡️ **To keep building your new app**, head to the [Slack functions](/tools/deno-slack-sdk/guides/creating-slack-functions) section. --- Source: https://docs.slack.dev/tools/deno-slack-sdk/guides/using-triggers # Using triggers Workflow apps require a paid plan Join the [Developer Program](https://api.slack.com/developer-program) and provision a sandbox with access to all Slack features for free. > Triggers are wonderful things! Triggers are one of the three building blocks that make up workflow apps. You will encounter all three as you navigate the path of building your workflow app: 1. Functions define the actions of your app. 2. Workflows are a combination of functions, executed in order. 3. Triggers execute workflows (⬅️ you are here) Since triggers are what kick off your workflows, you need to have a workflow before you can create a trigger. Acquaint yourself with the [documentation on workflows](/tools/deno-slack-sdk/guides/creating-workflows), then head back here. We'll wait! With the knowledge of workflows within your noggin, let's take a look at how you can implement triggers into your new app. ## Understanding triggers {#understanding} You will come to many forks in this metaphorical road that is trigger implementation. There are no wrong choices; all roads lead to your own wonderful workflow. Triggers can be added to workflows in two ways: * **You can add triggers with the CLI.** These static triggers are created only once. You attach them to your app's workflow, create them with the Slack CLI, and that's that. * **You can add triggers at runtime.** These dynamic triggers are created at any step of a workflow so they can incorporate data acquired from other workflow steps. Triggers created for a locally-running app (with the `slack run` command) are distinct from triggers created for an app in a production environment (with the `slack deploy` command). ## Custom trigger paths {#custom-path} While Slack assumes your triggers will be located in the default `/triggers` directory in the root of your project, it also allows the flexibility to define them elsewhere. In order for Slack to find them, be sure to declare the alternate path in `.slack/hooks.json` with the `config.trigger-paths` property. It might look like this: ``` { "hooks": { ... }, "config": { "trigger-paths": ["my-triggers/*.ts"] }} ``` ## Trigger types {#types} There are four types of triggers, each one having its own specific implementation. In a hurry? You can create a basic [link trigger](/tools/deno-slack-sdk/guides/creating-link-triggers) with a single Slack CLI command! Trigger type Use case [Link triggers](/tools/deno-slack-sdk/guides/creating-link-triggers) Invoke a workflow from a public channel in Slack [Scheduled triggers](/tools/deno-slack-sdk/guides/creating-scheduled-triggers) Invoke a workflow at specific time intervals [Event triggers](/tools/deno-slack-sdk/guides/creating-event-triggers) Invoke a workflow when a specific event happens in Slack [Webhook triggers](/tools/deno-slack-sdk/guides/creating-webhook-triggers) Invoke a workflow when a specific URL receives a POST request ## Onward {#onward} Each type of trigger has a guide where you will learn how to create that type of trigger. Choose one to move forward: ➡️ **To learn more about link triggers,** read the [link triggers](/tools/deno-slack-sdk/guides/creating-link-triggers) documentation or explore the [Give Kudos](/tools/deno-slack-sdk/tutorials/give-kudos-app) sample app. This sample app uses a link trigger to allow users to open up a form to give a compliment to a user. ✨**To learn more about scheduled triggers,** read the [scheduled triggers](/tools/deno-slack-sdk/guides/creating-event-triggers) documentation or explore the [Virtual Running Buddies](/tools/deno-slack-sdk/tutorials/virtual-running-buddies-app) sample app. This app uses a scheduled trigger to post a weekly message to a channel about people's running activity. ✨**To learn more about event triggers,** read the [event triggers](/tools/deno-slack-sdk/guides/creating-event-triggers) documentation or explore the [Welcome Bot](/tools/deno-slack-sdk/tutorials/welcome-bot) sample app. This app uses an event trigger to send a message to a user when they join a channel. ✨**To learn more about webhook triggers,** read the [webhook triggers](/tools/deno-slack-sdk/guides/creating-webhook-triggers) documentation. --- Source: https://docs.slack.dev/tools/deno-slack-sdk/guides/utilizing-slack-and-custom-data-types # Utilizing Slack & custom data types Workflow apps require a paid plan Join the [Developer Program](https://api.slack.com/developer-program) and provision a sandbox with access to all Slack features for free. When building workflow apps, you can use a handful of Slack types. You can also [define your own custom type](/tools/deno-slack-sdk/guides/creating-a-custom-type). ## Slack types {#slack} Slack types are used in two ways: as input and output parameters of [Slack functions](/tools/deno-slack-sdk/guides/creating-slack-functions) & [custom functions](/tools/deno-slack-sdk/guides/creating-custom-functions), as well as attributes of [datastores](/tools/deno-slack-sdk/guides/using-datastores). All manifests can be written in JSON; however, declaring types in an app using the Deno Slack SDK is done differently, requiring a reference to the `Schema.slack` package for non-primitive types. The examples in the reference show both how would they appear in Typescript as they would appear in a Deno Slack SDK app and in JSON as they would be defined in a manifest. ➡️ [View the full Slack types reference catalog here](/tools/deno-slack-sdk/reference/slack-types) ## Custom types {#custom} Custom types provide a way to introduce reusable, sharable types to your workflow apps. Once registered in your manifest, you can use custom types as input or output parameters in any of your app's [functions](/tools/deno-slack-sdk/guides/creating-slack-functions), [workflows](/tools/deno-slack-sdk/guides/creating-workflows), or [datastores](/tools/deno-slack-sdk/guides/using-datastores). The possibilities are endless! ➡️ To learn how to create your own custom type, read our [Creating a custom type](/tools/deno-slack-sdk/guides/creating-a-custom-type) guide. --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions # Connectors reference catalog Service Connector Connector description Adobe Sign [`send_agreement`](/tools/deno-slack-sdk/reference/connector-functions/adobe.sign/send_agreement) Send an agreement Airtable [`add_record`](/tools/deno-slack-sdk/reference/connector-functions/airtable/add_record) Add a record Airtable [`delete_record`](/tools/deno-slack-sdk/reference/connector-functions/airtable/delete_record) Delete a record Airtable [`select_record`](/tools/deno-slack-sdk/reference/connector-functions/airtable/select_record) Select a record Airtable [`update_record`](/tools/deno-slack-sdk/reference/connector-functions/airtable/update_record) Update a record Asana [`add_task_to_section`](/tools/deno-slack-sdk/reference/connector-functions/asana/add_task_to_section) Add task to a section Asana [`create_project`](/tools/deno-slack-sdk/reference/connector-functions/asana/create_project) Create a project Asana [`create_comment`](/tools/deno-slack-sdk/reference/connector-functions/asana/create_comment) Comment on a task Asana [`create_task`](/tools/deno-slack-sdk/reference/connector-functions/asana/create_task) Create a task Asana [`update_task`](/tools/deno-slack-sdk/reference/connector-functions/asana/update_task) Update a task Atlassian Bitbucket [`create_issue`](/tools/deno-slack-sdk/reference/connector-functions/atlassian.bitbucket/create_issue) Create an issue Atlassian Bitbucket [`merge_pull_request`](/tools/deno-slack-sdk/reference/connector-functions/atlassian.bitbucket/merge_pull_request) Merge pull request Atlassian Bitbucket [`update_issue`](/tools/deno-slack-sdk/reference/connector-functions/atlassian.bitbucket/update_issue) Update an issue Box [`create_folder`](/tools/deno-slack-sdk/reference/connector-functions/box.core/create_folder) Create a folder Box [`copy_file`](/tools/deno-slack-sdk/reference/connector-functions/box.core/copy_file) Copy a file Box Sign [`create_sign_request`](/tools/deno-slack-sdk/reference/connector-functions/box.sign/create_sign_request) Create a sign request with a template Basecamp [`create_project`](/tools/deno-slack-sdk/reference/connector-functions/basecamp/create_project) Create a project Basecamp [`create_todo`](/tools/deno-slack-sdk/reference/connector-functions/basecamp/create_todo) Create a to-do Basecamp [`create_todo_list`](/tools/deno-slack-sdk/reference/connector-functions/basecamp/create_todo_list) Create a to-do list Basecamp [`mark_todo_complete`](/tools/deno-slack-sdk/reference/connector-functions/basecamp/mark_todo_complete) Mark a to-do complete Basecamp [`mark_todo_pending`](/tools/deno-slack-sdk/reference/connector-functions/basecamp/mark_todo_pending) Mark a to-do pending ClickUp [`create_task`](/tools/deno-slack-sdk/reference/connector-functions/clickup/create_task) Create a task in a folder ClickUp [`update_task`](/tools/deno-slack-sdk/reference/connector-functions/clickup/update_task) Update a task in a folder Calendly [`get_meeting_link`](/tools/deno-slack-sdk/reference/connector-functions/calendly/get_meeting_link) Get meeting link Deel [`add_time_off_request`](/tools/deno-slack-sdk/reference/connector-functions/deel/add_time_off_request) Add a time off request Deel [`create_contract`](/tools/deno-slack-sdk/reference/connector-functions/deel/create_contract) Create a new contract Dialpad [`send_sms`](/tools/deno-slack-sdk/reference/connector-functions/dialpad/send_sms) Send an SMS DocuSign [`create_envelope`](/tools/deno-slack-sdk/reference/connector-functions/docusign/create_envelope) Create an envelope DocuSign [`send_envelope`](/tools/deno-slack-sdk/reference/connector-functions/docusign/send_envelope) Send an envelope Dropbox [`copy_document`](/tools/deno-slack-sdk/reference/connector-functions/dropbox.core/copy_document) Copy a document Dropbox [`delete_document`](/tools/deno-slack-sdk/reference/connector-functions/dropbox.core/delete_document) Delete a document Dropbox [`move_document`](/tools/deno-slack-sdk/reference/connector-functions/dropbox.core/move_document) Move a document Dropbox [`create_folder`](/tools/deno-slack-sdk/reference/connector-functions/dropbox.core/create_folder) Create a folder Dropbox [`create_share_link`](/tools/deno-slack-sdk/reference/connector-functions/dropbox.core/create_share_link) Create a shared link Dropbox Sign [`send_signature_request_with_template`](/tools/deno-slack-sdk/reference/connector-functions/dropbox.sign/send_signature_request_with_template) Send signature request using a template FireHydrant [`create_incident`](/tools/deno-slack-sdk/reference/connector-functions/firehydrant/create_incident) Create an incident FireHydrant [`create_task`](/tools/deno-slack-sdk/reference/connector-functions/firehydrant/create_task) Create a task FireHydrant [`update_task`](/tools/deno-slack-sdk/reference/connector-functions/firehydrant/update_task) Update a task FireHydrant [`update_incident`](/tools/deno-slack-sdk/reference/connector-functions/firehydrant/update_incident) Update an incident Giphy [`get_translated_gif`](/tools/deno-slack-sdk/reference/connector-functions/giphy/get_translated_gif) Search for a GIF Giphy [`get_random_gif`](/tools/deno-slack-sdk/reference/connector-functions/giphy/get_random_gif) Random GIF GitHub [`create_issue`](/tools/deno-slack-sdk/reference/connector-functions/github.cloud/create_issue) Create an issue GitHub [`update_issue`](/tools/deno-slack-sdk/reference/connector-functions/github.cloud/update_issue) Update an issue GitHub Enterprise Server [`create_issue`](/tools/deno-slack-sdk/reference/connector-functions/github.enterprise_server/create_issue) Create an issue GitLab [`create_issue`](/tools/deno-slack-sdk/reference/connector-functions/gitlab/create_issue) Create an issue Google Calendar [`add_to_event`](/tools/deno-slack-sdk/reference/connector-functions/google.calendar/add_to_event) Add attendee to an event Google Calendar [`create_event`](/tools/deno-slack-sdk/reference/connector-functions/google.calendar/create_event) Create a calendar event Google Calendar [`update_event`](/tools/deno-slack-sdk/reference/connector-functions/google.calendar/update_event) Update a calendar event Google Mail [`send_email`](/tools/deno-slack-sdk/reference/connector-functions/google.mail/send_email) Send an email Google Meet [`start_meeting`](/tools/deno-slack-sdk/reference/connector-functions/google.meet/start_meeting) Start a meeting Google Sheets [`add_spreadsheet_row`](/tools/deno-slack-sdk/reference/connector-functions/google.sheets/add_spreadsheet_row) Add to spreadsheet Google Sheets [`delete_spreadsheet_row`](/tools/deno-slack-sdk/reference/connector-functions/google.sheets/delete_spreadsheet_row) Delete from a spreadsheet Google Sheets [`update_spreadsheet_row`](/tools/deno-slack-sdk/reference/connector-functions/google.sheets/update_spreadsheet_row) Update a spreadsheet Google Sheets [`select_spreadsheet_row`](/tools/deno-slack-sdk/reference/connector-functions/google.sheets/select_spreadsheet_row) Select a spreadsheet row Google Tasks [`create_tasklist`](/tools/deno-slack-sdk/reference/connector-functions/google.tasks/create_tasklist) Create a task list Google Tasks [`insert_task`](/tools/deno-slack-sdk/reference/connector-functions/google.tasks/insert_task) Insert a task Greenhouse Onboarding [`fetch_employees`](/tools/deno-slack-sdk/reference/connector-functions/greenhouse.onboarding/fetch_employees) Fetch employees Greenhouse Onboarding [`create_pending_hire`](/tools/deno-slack-sdk/reference/connector-functions/greenhouse.onboarding/create_pending_hire) Create pending hire Greenhouse Recruiting [`hire_application`](/tools/deno-slack-sdk/reference/connector-functions/greenhouse.recruiting/hire_application) Hire Application Greenhouse Recruiting [`list_candidate_activity`](/tools/deno-slack-sdk/reference/connector-functions/greenhouse.recruiting/list_candidate_activity) Candidate activity Greenhouse Recruiting [`reject_application`](/tools/deno-slack-sdk/reference/connector-functions/greenhouse.recruiting/reject_application) Reject Application Greenhouse Recruiting [`list_job_candidates`](/tools/deno-slack-sdk/reference/connector-functions/greenhouse.recruiting/list_job_candidates) List job candidates Guru [`create_card`](/tools/deno-slack-sdk/reference/connector-functions/guru/create_card) Create a card Guru [`delete_card`](/tools/deno-slack-sdk/reference/connector-functions/guru/delete_card) Delete a card Guru [`unverify_card`](/tools/deno-slack-sdk/reference/connector-functions/guru/unverify_card) Unverify a card Guru [`update_card`](/tools/deno-slack-sdk/reference/connector-functions/guru/update_card) Update a card Guru [`verify_card`](/tools/deno-slack-sdk/reference/connector-functions/guru/verify_card) Verify a card Guru [`add_comment`](/tools/deno-slack-sdk/reference/connector-functions/guru/add_comment) Add a comment Intercom [`create_article`](/tools/deno-slack-sdk/reference/connector-functions/intercom/create_article) Create an article Intercom [`create_ticket`](/tools/deno-slack-sdk/reference/connector-functions/intercom/create_ticket) Create a ticket JIRA Cloud [`edit_issue`](/tools/deno-slack-sdk/reference/connector-functions/jira.cloud/edit_issue) Edit an issue JIRA Cloud [`create_issue`](/tools/deno-slack-sdk/reference/connector-functions/jira.cloud/create_issue) Create an issue launchdarkly [`create_approval_request_update_flag`](/tools/deno-slack-sdk/reference/connector-functions/launchdarkly/create_approval_request_update_flag) Create approval request to update a feature flag's state launchdarkly [`create_feature_flag`](/tools/deno-slack-sdk/reference/connector-functions/launchdarkly/create_feature_flag) Create a boolean feature flag LaunchDarkly [`update_feature_flag_state`](/tools/deno-slack-sdk/reference/connector-functions/launchdarkly/update_feature_flag_state) Update a feature flag's state LaunchDarkly [`update_target_feature_flag`](/tools/deno-slack-sdk/reference/connector-functions/launchdarkly/update_target_feature_flag) Update a target in a feature flag LaunchDarkly [`update_target_segment`](/tools/deno-slack-sdk/reference/connector-functions/launchdarkly/update_target_segment) Update a target in a segment Lever [`create_interview`](/tools/deno-slack-sdk/reference/connector-functions/lever/create_interview) Create an interview Lever [`create_opportunity`](/tools/deno-slack-sdk/reference/connector-functions/lever/create_opportunity) Create an opportunity Lever [`create_panel`](/tools/deno-slack-sdk/reference/connector-functions/lever/create_panel) Create a panel Linear [`add_comment`](/tools/deno-slack-sdk/reference/connector-functions/linear/add_comment) Add a comment Linear [`create_issue`](/tools/deno-slack-sdk/reference/connector-functions/linear/create_issue) Create an issue Linear [`create_project`](/tools/deno-slack-sdk/reference/connector-functions/linear/create_project) Create a project Linear [`update_issue`](/tools/deno-slack-sdk/reference/connector-functions/linear/update_issue) Update an issue Loopio [`create_project`](/tools/deno-slack-sdk/reference/connector-functions/loopio/create_project) Create a project Lucid [`create_document`](/tools/deno-slack-sdk/reference/connector-functions/lucid/create_document) Create a document Mailchimp [`create_campaign`](/tools/deno-slack-sdk/reference/connector-functions/mailchimp/create_campaign) Create an email campaign Mailchimp [`add_contact`](/tools/deno-slack-sdk/reference/connector-functions/mailchimp/add_contact) Add a contact to audience Mailchimp [`get_campaign_report`](/tools/deno-slack-sdk/reference/connector-functions/mailchimp/get_campaign_report) Get campaign report Mailchimp [`send_campaign`](/tools/deno-slack-sdk/reference/connector-functions/mailchimp/send_campaign) Send a Campaign Microsoft Excel [`add_worksheet_row`](/tools/deno-slack-sdk/reference/connector-functions/microsoft.excel/add_worksheet_row) Add to worksheet Microsoft Excel [`delete_worksheet_row`](/tools/deno-slack-sdk/reference/connector-functions/microsoft.excel/delete_worksheet_row) Delete from a worksheet Microsoft Excel [`select_worksheet_row`](/tools/deno-slack-sdk/reference/connector-functions/microsoft.excel/select_worksheet_row) Select a worksheet row Microsoft Excel [`update_worksheet_row`](/tools/deno-slack-sdk/reference/connector-functions/microsoft.excel/update_worksheet_row) Update a worksheet Microsoft OneDrive [`copy_file`](/tools/deno-slack-sdk/reference/connector-functions/microsoft.onedrive/copy_file) Copy a file Microsoft OneDrive [`create_file`](/tools/deno-slack-sdk/reference/connector-functions/microsoft.onedrive/create_file) Create a file Microsoft OneNote [`update_page`](/tools/deno-slack-sdk/reference/connector-functions/microsoft.onenote/update_page) Update a page Microsoft OneNote [`create_page`](/tools/deno-slack-sdk/reference/connector-functions/microsoft.onenote/create_page) Create a page Microsoft Outlook Calendar [`create_event`](/tools/deno-slack-sdk/reference/connector-functions/microsoft.outlook.calendar/create_event) Create a calendar event Microsoft Outlook Calendar [`send_email`](/tools/deno-slack-sdk/reference/connector-functions/microsoft.outlook.email/send_email) Send an email Microsoft Teams Calls [`create_meeting`](/tools/deno-slack-sdk/reference/connector-functions/microsoft.teams/create_meeting) Create a meeting Microsoft Teams Calls [`update_meeting`](/tools/deno-slack-sdk/reference/connector-functions/microsoft.teams/update_meeting) Update a meeting Miro [`create_board`](/tools/deno-slack-sdk/reference/connector-functions/miro/create_board) Create board Miro [`copy_board`](/tools/deno-slack-sdk/reference/connector-functions/miro/copy_board) Copy board Monday [`create_board`](/tools/deno-slack-sdk/reference/connector-functions/monday/create_board) Create a board Monday [`create_group`](/tools/deno-slack-sdk/reference/connector-functions/monday/create_group) Create a group Monday [`create_item`](/tools/deno-slack-sdk/reference/connector-functions/monday/create_item) Create an item Monday [`archive_board`](/tools/deno-slack-sdk/reference/connector-functions/monday/archive_board) Archive a board Notion [`archive_page`](/tools/deno-slack-sdk/reference/connector-functions/notion/archive_page) Archive a page Notion [`create_page`](/tools/deno-slack-sdk/reference/connector-functions/notion/create_page) Create a page PagerDuty [`add_a_note`](/tools/deno-slack-sdk/reference/connector-functions/pagerduty/add_a_note) Add a note PagerDuty [`create_incident`](/tools/deno-slack-sdk/reference/connector-functions/pagerduty/create_incident) Trigger an incident PagerDuty [`escalate_incident`](/tools/deno-slack-sdk/reference/connector-functions/pagerduty/escalate_incident) Change escalation level PagerDuty [`resolve_incident`](/tools/deno-slack-sdk/reference/connector-functions/pagerduty/resolve_incident) Resolve an incident PagerDuty [`send_status_update`](/tools/deno-slack-sdk/reference/connector-functions/pagerduty/send_status_update) Create a status update PagerDuty [`update_incident`](/tools/deno-slack-sdk/reference/connector-functions/pagerduty/update_incident) Update an incident Ramp [`create_physical_card`](/tools/deno-slack-sdk/reference/connector-functions/ramp/create_physical_card) Create new physical card Ramp [`get_spend_request`](/tools/deno-slack-sdk/reference/connector-functions/ramp/get_spend_request) Get a spend request Ramp [`create_virtual_card`](/tools/deno-slack-sdk/reference/connector-functions/ramp/create_virtual_card) Create new virtual card Ramp [`unlock_card`](/tools/deno-slack-sdk/reference/connector-functions/ramp/unlock_card) Unlock a card Ramp [`terminate_card`](/tools/deno-slack-sdk/reference/connector-functions/ramp/terminate_card) Terminate a card Ramp [`create_spend_request`](/tools/deno-slack-sdk/reference/connector-functions/ramp/create_spend_request) Create spend request Ramp [`suspend_card`](/tools/deno-slack-sdk/reference/connector-functions/ramp/suspend_card) Suspend a card RingCentral [`send_sms`](/tools/deno-slack-sdk/reference/connector-functions/ringcentral/send_sms) Send a SMS Rootly [`create_cause`](/tools/deno-slack-sdk/reference/connector-functions/rootly/create_cause) Create a cause Rootly [`create_alert`](/tools/deno-slack-sdk/reference/connector-functions/rootly/create_alert) Create an alert Rootly [`update_cause`](/tools/deno-slack-sdk/reference/connector-functions/rootly/update_cause) Update a cause Salesforce [`create_record`](/tools/deno-slack-sdk/reference/connector-functions/salesforce/create_record) Create a record Salesforce [`delete_record`](/tools/deno-slack-sdk/reference/connector-functions/salesforce/delete_record) Delete a record Salesforce [`read_record`](/tools/deno-slack-sdk/reference/connector-functions/salesforce/read_record) Read a record Salesforce [`run_flow`](/tools/deno-slack-sdk/reference/connector-functions/salesforce/run_flow) Run a Flow Salesforce [`update_record`](/tools/deno-slack-sdk/reference/connector-functions/salesforce/update_record) Update a record ServiceNow [`create_incident`](/tools/deno-slack-sdk/reference/connector-functions/servicenow/create_incident) Create an incident ServiceNow [`get_incident`](/tools/deno-slack-sdk/reference/connector-functions/servicenow/get_incident) Get an incident ServiceNow [`update_incident`](/tools/deno-slack-sdk/reference/connector-functions/servicenow/update_incident) Update an incident SmartRecruiters [`create_candidate`](/tools/deno-slack-sdk/reference/connector-functions/smartrecruiters/create_candidate) Create a candidate SmartRecruiters [`create_candidate_and_assign_to_job`](/tools/deno-slack-sdk/reference/connector-functions/smartrecruiters/create_candidate_and_assign_to_job) Create a candidate and assign to job SmartRecruiters [`give_candidate_review`](/tools/deno-slack-sdk/reference/connector-functions/smartrecruiters/give_candidate_review) Provide feedback for a candidate Smartsheet [`add_row`](/tools/deno-slack-sdk/reference/connector-functions/smartsheet/add_row) Add a row to a Smartsheet Smartsheet [`delete_row`](/tools/deno-slack-sdk/reference/connector-functions/smartsheet/delete_row) Delete a row from Smartsheet Smartsheet [`select_row`](/tools/deno-slack-sdk/reference/connector-functions/smartsheet/select_row) Select a Smartsheet row Smartsheet [`update_row`](/tools/deno-slack-sdk/reference/connector-functions/smartsheet/update_row) Update a row to Smartsheet Snyk [`create_ignore`](/tools/deno-slack-sdk/reference/connector-functions/snyk/create_ignore) Ignore an issue SurveyMonkey [`copy_survey`](/tools/deno-slack-sdk/reference/connector-functions/surveymonkey/copy_survey) Copy a survey SurveyMonkey [`copy_survey_from_template`](/tools/deno-slack-sdk/reference/connector-functions/surveymonkey/copy_survey_from_template) Copy a survey from a template Travis CI [`restart_build`](/tools/deno-slack-sdk/reference/connector-functions/travisci/restart_build) Restart build Travis CI [`trigger_build`](/tools/deno-slack-sdk/reference/connector-functions/travisci/trigger_build) Trigger build Travis CI [`cancel_build`](/tools/deno-slack-sdk/reference/connector-functions/travisci/cancel_build) Cancel build Twilio [`send_sms`](/tools/deno-slack-sdk/reference/connector-functions/twilio/send_sms) Send SMS Typeform [`duplicate_form`](/tools/deno-slack-sdk/reference/connector-functions/typeform/duplicate_form) Duplicate an existing form Typeform [`create_workspace`](/tools/deno-slack-sdk/reference/connector-functions/typeform/create_workspace) Create a workspace Typeform [`get_form`](/tools/deno-slack-sdk/reference/connector-functions/typeform/get_form) Get a form Typeform [`get_form_insights`](/tools/deno-slack-sdk/reference/connector-functions/typeform/get_form_insights) Get form insights Webex [`create_meeting`](/tools/deno-slack-sdk/reference/connector-functions/webex/create_meeting) Create a meeting Workast [`create_task`](/tools/deno-slack-sdk/reference/connector-functions/workast/create_task) Create a task Wrike [`comment_on_a_folder`](/tools/deno-slack-sdk/reference/connector-functions/wrike/comment_on_a_folder) Comment on a folder Wrike [`comment_on_a_task`](/tools/deno-slack-sdk/reference/connector-functions/wrike/comment_on_a_task) Comment on a task Wrike [`create_a_folder`](/tools/deno-slack-sdk/reference/connector-functions/wrike/create_a_folder) Create a folder Wrike [`create_a_task`](/tools/deno-slack-sdk/reference/connector-functions/wrike/create_a_task) Create a task Wrike [`update_a_task`](/tools/deno-slack-sdk/reference/connector-functions/wrike/update_a_task) Update a task Zendesk [`add_tags`](/tools/deno-slack-sdk/reference/connector-functions/zendesk/add_tags) Add tags Zendesk [`create_ticket`](/tools/deno-slack-sdk/reference/connector-functions/zendesk/create_ticket) Create a ticket Zendesk [`update_ticket`](/tools/deno-slack-sdk/reference/connector-functions/zendesk/update_ticket) Update a ticket Zoom [`create_meeting`](/tools/deno-slack-sdk/reference/connector-functions/zoom/create_meeting) Create a meeting --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/adobe.sign/send_agreement # send_agreement --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/airtable/add_record # add_record --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/airtable/delete_record # delete_record --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/airtable/select_record # select_record --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/airtable/update_record # update_record --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/asana/add_task_to_section # add_task_to_section --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/asana/create_comment # create_comment --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/asana/create_project # create_project --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/asana/create_task # create_task --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/asana/update_task # update_task --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/atlassian.bitbucket/create_issue # create_issue --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/atlassian.bitbucket/merge_pull_request # merge_pull_request --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/atlassian.bitbucket/update_issue # update_issue --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/basecamp/create_project # create_project --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/basecamp/create_todo # create_todo --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/basecamp/create_todo_list # create_todo_list --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/basecamp/mark_todo_complete # mark_todo_complete --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/basecamp/mark_todo_pending # mark_todo_pending --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/box.core/copy_file # copy_file --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/box.core/create_folder # create_folder --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/box.sign/create_sign_request # create_sign_request --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/calendly/get_meeting_link # get_meeting_link --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/clickup/create_task # create_task --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/clickup/update_task # update_task --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/deel/add_time_off_request # add_time_off_request --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/deel/create_contract # create_contract --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/dialpad/send_sms # send_sms --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/docusign/create_envelope # create_envelope --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/docusign/send_envelope # send_envelope --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/dropbox.core/copy_document # copy_document --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/dropbox.core/create_folder # create_folder --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/dropbox.core/create_share_link # create_share_link --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/dropbox.core/delete_document # delete_document --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/dropbox.core/move_document # move_document --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/dropbox.sign/send_signature_request_with_template # send_signature_request_with_template --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/firehydrant/create_incident # create_incident --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/firehydrant/create_task # create_task --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/firehydrant/update_incident # update_incident --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/firehydrant/update_task # update_task --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/giphy/get_random_gif # get_random_gif --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/giphy/get_translated_gif # get_translated_gif --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/github.cloud/create_issue # create_issue --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/github.cloud/update_issue # update_issue --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/github.enterprise_server/create_issue # create_issue --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/gitlab/create_issue # create_issue --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/google.calendar/add_to_event # add_to_event --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/google.calendar/create_event # create_event --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/google.calendar/update_event # update_event --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/google.mail/send_email # send_email --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/google.meet/start_meeting # start_meeting --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/google.sheets/add_spreadsheet_row # add_spreadsheet_row --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/google.sheets/delete_spreadsheet_row # delete_spreadsheet_row --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/google.sheets/select_spreadsheet_row # select_spreadsheet_row --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/google.sheets/update_spreadsheet_row # update_spreadsheet_row --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/google.tasks/create_tasklist # create_tasklist --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/google.tasks/insert_task # insert_task --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/greenhouse.onboarding/create_pending_hire # create_pending_hire --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/greenhouse.onboarding/fetch_employees # fetch_employees --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/greenhouse.recruiting/hire_application # hire_application --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/greenhouse.recruiting/list_candidate_activity # list_candidate_activity --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/greenhouse.recruiting/list_job_candidates # list_job_candidates --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/greenhouse.recruiting/reject_application # reject_application --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/guru/add_comment # add_comment --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/guru/create_card # create_card --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/guru/delete_card # delete_card --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/guru/unverify_card # unverify_card --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/guru/update_card # update_card --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/guru/verify_card # verify_card --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/intercom/create_article # create_article --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/intercom/create_ticket # create_ticket --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/jira.cloud/create_issue # create_issue --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/jira.cloud/edit_issue # edit_issue --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/launchdarkly/create_approval_request_update_flag # create_approval_request_update_flag --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/launchdarkly/create_feature_flag # create_feature_flag --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/launchdarkly/update_feature_flag_state # update_feature_flag_state --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/launchdarkly/update_target_feature_flag # update_target_feature_flag --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/launchdarkly/update_target_segment # update_target_segment --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/lever/create_interview # create_interview --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/lever/create_opportunity # create_opportunity --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/lever/create_panel # create_panel --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/linear/add_comment # add_comment --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/linear/create_issue # create_issue --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/linear/create_project # create_project --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/linear/update_issue # update_issue --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/loopio/create_project # create_project --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/lucid/create_document # create_document --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/mailchimp/add_contact # add_contact --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/mailchimp/create_campaign # create_campaign --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/mailchimp/get_campaign_report # get_campaign_report --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/mailchimp/send_campaign # send_campaign --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/microsoft.excel/add_worksheet_row # add_worksheet_row --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/microsoft.excel/delete_worksheet_row # delete_worksheet_row --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/microsoft.excel/select_worksheet_row # select_worksheet_row --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/microsoft.excel/update_worksheet_row # update_worksheet_row --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/microsoft.onedrive/copy_file # copy_file --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/microsoft.onedrive/create_file # create_file --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/microsoft.onenote/create_page # create_page --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/microsoft.onenote/update_page # update_page --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/microsoft.outlook.calendar/create_event # create_event --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/microsoft.outlook.email/send_email # send_email --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/microsoft.teams/create_meeting # create_meeting --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/microsoft.teams/update_meeting # update_meeting --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/miro/copy_board # copy_board --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/miro/create_board # create_board --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/monday/archive_board # archive_board --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/monday/create_board # create_board --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/monday/create_group # create_group --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/monday/create_item # create_item --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/notion/archive_page # archive_page --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/notion/create_page # create_page --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/pagerduty/add_a_note # add_a_note --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/pagerduty/create_incident # create_incident --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/pagerduty/escalate_incident # escalate_incident --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/pagerduty/resolve_incident # resolve_incident --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/pagerduty/send_status_update # send_status_update --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/pagerduty/update_incident # update_incident --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/ramp/create_physical_card # create_physical_card --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/ramp/create_spend_request # create_spend_request --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/ramp/create_virtual_card # create_virtual_card --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/ramp/get_spend_request # get_spend_request --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/ramp/suspend_card # suspend_card --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/ramp/terminate_card # terminate_card --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/ramp/unlock_card # unlock_card --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/ringcentral/send_sms # send_sms --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/rootly/create_alert # create_alert --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/rootly/create_cause # create_cause --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/rootly/update_cause # update_cause --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/salesforce/create_record # create_record --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/salesforce/delete_record # delete_record --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/salesforce/read_record # read_record --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/salesforce/run_flow # run_flow --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/salesforce/update_record # update_record --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/servicenow/create_incident # create_incident --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/servicenow/get_incident # get_incident --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/servicenow/update_incident # update_incident --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/smartrecruiters/create_candidate # create_candidate --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/smartrecruiters/create_candidate_and_assign_to_job # create_candidate_and_assign_to_job --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/smartrecruiters/give_candidate_review # give_candidate_review --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/smartsheet/add_row # add_row --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/smartsheet/delete_row # delete_row --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/smartsheet/select_row # select_row --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/smartsheet/update_row # update_row --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/snyk/create_ignore # create_ignore --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/surveymonkey/copy_survey # copy_survey --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/surveymonkey/copy_survey_from_template # copy_survey_from_template --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/travisci/cancel_build # cancel_build --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/travisci/restart_build # restart_build --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/travisci/trigger_build # trigger_build --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/twilio/send_sms # send_sms --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/typeform/create_workspace # create_workspace --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/typeform/duplicate_form # duplicate_form --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/typeform/get_form # get_form --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/typeform/get_form_insights # get_form_insights --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/webex/create_meeting # create_meeting --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/workast/create_task # create_task --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/wrike/comment_on_a_folder # comment_on_a_folder --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/wrike/comment_on_a_task # comment_on_a_task --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/wrike/create_a_folder # create_a_folder --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/wrike/create_a_task # create_a_task --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/wrike/update_a_task # update_a_task --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/zendesk/add_tags # add_tags --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/zendesk/create_ticket # create_ticket --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/zendesk/remove_tags # remove_tags --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/zendesk/update_ticket # update_ticket --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/connector-functions/zoom/create_meeting # create_meeting --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/slack-functions # Slack functions catalog Slack function Description [`add_bookmark`](/tools/deno-slack-sdk/reference/slack-functions/add_bookmark) Add a bookmark to a channel [`add_pin`](/tools/deno-slack-sdk/reference/slack-functions/add_pin) Pin a message to a channel [`add_user_to_usergroup`](/tools/deno-slack-sdk/reference/slack-functions/add_user_to_usergroup) Add a user to a user group [`archive_channel`](/tools/deno-slack-sdk/reference/slack-functions/archive_channel) Archive a channel [`canvas_copy`](/tools/deno-slack-sdk/reference/slack-functions/canvas_copy) Copy a canvas [`canvas_create`](/tools/deno-slack-sdk/reference/slack-functions/canvas_create) Create a canvas [`canvas_update_content`](/tools/deno-slack-sdk/reference/slack-functions/canvas_update_content) Update a canvas [`channel_canvas_create`](/tools/deno-slack-sdk/reference/slack-functions/channel_canvas_create) Create channel canvas [`create_channel`](/tools/deno-slack-sdk/reference/slack-functions/create_channel) Create a new channel [`create_usergroup`](/tools/deno-slack-sdk/reference/slack-functions/create_usergroup) Create a new user group [`delay`](/tools/deno-slack-sdk/reference/slack-functions/delay) Pause a workflow for a specified amount of time [`invite_user_to_channel`](/tools/deno-slack-sdk/reference/slack-functions/invite_user_to_channel) Invite a user to a channel [`open_form`](/tools/deno-slack-sdk/reference/slack-functions/open_form) Open an interactive form [`remove_user_from_usergroup`](/tools/deno-slack-sdk/reference/slack-functions/remove_user_from_usergroup) Remove a user from a user group [`reply_in_thread`](/tools/deno-slack-sdk/reference/slack-functions/reply_in_thread) Reply to a message by creating or adding to a thread [`send_dm`](/tools/deno-slack-sdk/reference/slack-functions/send_dm) Send a direct message [`send_ephemeral_message`](/tools/deno-slack-sdk/reference/slack-functions/send_ephemeral_message) Send an ephemeral message (one only the recipient can see in channel) [`send_message`](/tools/deno-slack-sdk/reference/slack-functions/send_message) Send a message in a channel [`share_canvas`](/tools/deno-slack-sdk/reference/slack-functions/share_canvas) Share a canvas [`share_canvas_in_thread`](/tools/deno-slack-sdk/reference/slack-functions/share_canvas_in_thread) Share a canvas in thread [`update_channel_topic`](/tools/deno-slack-sdk/reference/slack-functions/update_channel_topic) Update a channel's topic --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/slack-functions/add_bookmark # add_bookmark --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/slack-functions/add_pin # add_pin --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/slack-functions/add_user_to_usergroup # add_user_to_usergroup --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/slack-functions/archive_channel # archive_channel --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/slack-functions/canvas_copy # canvas_copy --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/slack-functions/canvas_create # canvas_create --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/slack-functions/canvas_update_content # canvas_update_content --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/slack-functions/channel_canvas_create # channel_canvas_create --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/slack-functions/create_channel # create_channel --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/slack-functions/create_usergroup # create_usergroup --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/slack-functions/delay # delay --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/slack-functions/invite_user_to_channel # invite_user_to_channel --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/slack-functions/open_form # open_form --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/slack-functions/remove_user_from_usergroup # remove_user_from_usergroup --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/slack-functions/reply_in_thread # reply_in_thread --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/slack-functions/send_dm # send_dm --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/slack-functions/send_ephemeral_message # send_ephemeral_message --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/slack-functions/send_message # send_message --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/slack-functions/share_canvas # share_canvas --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/slack-functions/share_canvas_in_thread # share_canvas_in_thread --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/slack-functions/update_channel_topic # update_channel_topic --- Source: https://docs.slack.dev/tools/deno-slack-sdk/reference/slack-types # Slack types The examples of declaring a type are shown below in both TypeScript, as they would appear in an app built using the Deno SDK, and in JSON, as they would be defined in a manifest. All manifests can be written in JSON; however, declaring types in an app using the Deno SDK is done differently, requiring a reference to the `Schema.slack` package for non-primitive types. Name Type Description [`array`](#array) Array An array of items (based on a type that you specify). [`blocks`](#blocks) Array of Slack Blocks An array of objects that contain layout and style information about your message. [`boolean`](#boolean) Boolean A logical value, must be either `true` or `false`. [`canvas_id`](#canvasid) String A Slack canvas ID, such as `F123456AB`. [`canvas_template_id`](#canvastemplateid) String A Slack canvas template ID, such as `T5678ABC`. [`channel_id`](#channelid) String A Slack channel ID, such as `C123ABC456` or `D123ABC456`. [`date`](#date) String A string containing a date, format is displayed as `YYYY-MM-DD`. [`expanded_rich_text`](#expandedrichtext) Object A way to nicely format messages in your app. This type cannot convert other message types, e.g. blocks or strings, and is explicitly for use with canvases. [`file_id`](#fileid) Object A file ID, such as `F123ABC456`. [`integer`](#integer) Integer A whole number, such as `-1`, `0`, or `31415926535`. [`interactivity`](#interactivity) Object An object that contains context about the interactive event that led to opening of the form. [`list_id`](#list-id) String A Slack list ID, such as `F123456ABC`. [`message_context`](#message-context) Object An individual instance of a message. [`message_ts`](#message-ts) String A Slack-specific hash/timestamp necessary for referencing events like messages in Slack. [`number`](#number) Number A number that allows decimal points such as `13557523.0005`. [`oauth2`](#oauth2) Object The OAuth2 context created after authenticating with [external auth](/tools/deno-slack-sdk/guides/integrating-with-services-requiring-external-authentication). [`object`](#object) Object A custom Javascript object, like `{"site": "slack.com"}`. [`options_field`](#options-field) Object A type of `Options` parameter used in [custom steps dynamic options for Workflow Builder](/tools/bolt-python/concepts/custom-steps-dynamic-options). [`options_select`](#options-select) Object A type of `Options` parameter used in [custom steps dynamic options for Workflow Builder](/tools/bolt-python/concepts/custom-steps-dynamic-options). [`rich_text`](#rich-text) Object A way to nicely format messages in your app. This type cannot convert other message types e.g. blocks, strings. [`string`](#string) String UTF-8 encoded string, up to 4000 bytes. [`team_id`](#team_id) String A Slack team ID, such as `T1234567890`. [`timestamp`](#timestamp) Integer A Unix timestamp in seconds. Not compatible with Slack message timestamps - use [string](#string) instead. [`user_context`](#usercontext) Object Represents a user who interacted with a workflow at runtime. [`user_id`](#userid) String A Slack user ID, such as `U123ABC456` or `W123ABC456`. [`usergroup_id`](#usergroupid) String A Slack usergroup ID, such as `S123ABC456`. ## Slack types for datastores {#datastores} When defining a [datastore](/tools/deno-slack-sdk/guides/using-datastores), you can use certain Slack types for its attributes. Attributes accept only a single `type` property in the definition, instead of all the common properties listed above. The following is a list of the Slack types supported for use with datastores: * [`channel_id`](#channelid) * [`date`](#date) * [`message_ts`](#message-ts) * [`rich_text`](#rich-text) * [`timestamp`](#timestamp) * [`user_id`](#userid) * [`usergroup_id`](#usergroupid) Here's a sample datastore definition using Slack types for attributes: ``` import { DefineDatastore, Schema } from "deno-slack-sdk/mod.ts";export const MyDatastore = DefineDatastore({ name: "my_datastore", primary_key: "id", attributes: { id: { type: Schema.types.string }, channel: { type: Schema.slack.types.channel_id }, message: { type: Schema.types.string }, author: { type: Schema.slack.types.user_id }, isMember: { type: Schema.types.boolean }, },}); ``` * * * ## Array {#array} Type: `array` Property Type Description `default` The type that is being described. An optional parameter default value. `description` string An optional parameter description. `examples` An array of the type being described. An optional list of examples. `hint` string An optional parameter hint. `title` string An optional parameter title. `type` string String that defines the parameter type. `items` object The type of items in the array. Can be one of the following types: [`channel_id`](#channelid), [`user_id`](#userid), [`usergroup_id`](#usergroupid), [`timestamp`](#timestamp), [`string`](#string), [`integer`](#integer), [`number`](#number), [`boolean`](#boolean), [`list_id`](#list-id), [`canvas_id`](#canvasid), [`canvas_template_id`](#canvastemplateid), `channel_canvas_id`, [`team_id`](#team_id), [`file_id`](#fileid). `minItems` integer Minimum number of items allowed. `maxItems` integer Maximum number of items allowed. Declare an `array` of types: * Deno SDK * JSON Manifest ``` // ...{ name: "departments", title: "Your department", type: Schema.types.array, items: { type: Schema.types.string, enum: ["marketing", "design", "sales", "engineering"], }, default: ["sales", "engineering"],}// ... ``` ``` // ..."departments": { "title": "Your department", "type": "array", "items": { "type": "string", "enum": [ "marketing", "design", "sales", "engineering" ] }}// ... ``` Arrays and object types Be sure to define the array's properties in the `items` object. Untyped objects are not currently supported. In addition, you can only use an object as the item type of array if it's a custom object. Otherwise, you may receive the following error: `Unexpected schema encountered for array type: failed to match exactly one allowed schema for items - {"type":"one_of"} (failed_constraint).` Array example In this example function, we have an array of the [custom type](/tools/deno-slack-sdk/guides/creating-a-custom-type) `ChannelType` as both an `input_parameter` and `output_parameter`. See this custom type and array in action in the [Deno Archive Channel](https://github.com/slack-samples/deno-archive-channel) sample app. ``` const ChannelType = DefineType(...)export const FilterStaleChannelsDefinition = DefineFunction({ callback_id: "filter_stale_channels", title: "Filter Stale Channels", description: "Filter out any channels that have received messages within the last 6 months", source_file: "functions/filter_stale_channels.ts", input_parameters: { properties: { channels: { type: Schema.types.array, description: "The list of Channel IDs to filter", items: { type: ChannelType, }, }, }, required: ["channels"], }, output_parameters: { properties: { filtered_channels: { type: Schema.types.array, description: "The list of stale Channel IDs", items: { type: ChannelType, }, }, }, required: [], },}); ``` * * * ## Blocks {#blocks} Type: `slack#/types/blocks` Property Type Description `default` The type that is being described. An optional parameter default value. `description` string An optional parameter description. `examples` An array of the type being described. An optional list of examples. `hint` string An optional parameter hint. `title` string An optional parameter title. `type` string String that defines the parameter type. Declare an array of [Block Kit](https://docs.slack.dev/block-kit/) JSON objects. * Deno SDK * JSON manifest ``` // ...properties: { forecast: { type: Schema.slack.types.blocks, },},// ... ``` ``` // ... "input_parameters": { "forecast": { "type": "slack#/types/blocks" }}// ... ``` If you use [Block Kit builder](https://api.slack.com/tools/block-kit-builder) to build your Block Kit objects, be sure to _only_ grab the `blocks` array. For example: ``` [ { "type": "section", "text": { "type": "plain_text", "text": "This is a plain text section block.", "emoji": true } }, { "type": "image", "image_url": "example.com/png" "alt_text": "inspiration" }] ``` Blocks example In this example function, we get the current weather forecast. ``` import { DefineFunction, Schema } from "deno-slack-sdk/mod.ts";export const ForecastFunctionDefinition = DefineFunction({ callback_id: "get_forecast", title: "Weather forecast", description: "A function to get the weather forecast", source_file: "functions/weather_forecast.ts", input_parameters: { properties: { city: { type: Schema.types.string, description: "City", }, country: { type: Schema.types.string, description: "Country", }, state: { type: Schema.types.string, description: "State", }, }, required: ["city"], }, output_parameters: { properties: { forecast: { type: Schema.slack.types.blocks, description: "Weather forecast", }, }, required: ["forecast"], },}); ``` * * * ## Boolean {#boolean} Type: `boolean` Property Type Description `default` The type that is being described. For a `boolean` it would be `true` or `false`. An optional parameter default value. `description` string An optional parameter description. `examples` An array of the type being described. An optional list of examples. `hint` string An optional parameter hint. `title` string An optional parameter title. `type` string String that defines the parameter type. Declare a `boolean` type: * Deno SDK * JSON manifest ``` // ...isMember: { type: Schema.types.boolean,}// ... ``` ``` // ..."isMember": { "type": "boolean"}// ... ``` Boolean example In this example datastore definition, we use a `boolean` type to capture whether the message author holds membership in our club. ``` import { DefineDatastore, Schema } from "deno-slack-sdk/mod.ts";export const MyDatastore = DefineDatastore({ name: "my_datastore", primary_key: "id", attributes: { id: { type: Schema.types.string }, channel: { type: Schema.slack.types.channel_id }, message: { type: Schema.types.string }, author: { type: Schema.slack.types.user_id }, isMember: { type: Schema.types.boolean }, },}); ``` * * * ## Channel ID {#channelid} Type: `slack#/types/channel_id` Property Type Description `default` The type that is being described. An optional parameter default value. `description` string An optional parameter description. `examples` An array of the type being described. An optional list of examples. `hint` string An optional parameter hint. `title` string An optional parameter title. `type` string String that defines the parameter type. `choices` EnumChoice\[\] Defines labels that correspond to the `enum` values. See below. #### The choices property {#the-choices-property} The `choices` property is an array of `EnumChoice` objects. Here is a closer look at the properties of the `EnumChoice` object: Property Type Description `default` The type that is being described. For example, the default for a `boolean` would be `true` or `false`. An optional parameter default value. `description` string An optional parameter description. `examples` An array of the type being described. An optional list of examples. `hint` string An optional parameter hint. `title` string An optional parameter title. `type` string String that defines the parameter type. `value` the type that the `EnumChoice` object corresponds to — in the example below, it is `string` The value of the corresponding choice, which must map to the values present in the sibling `enum` property. `title` string The label to display for this `EnumChoice`. `description` string An optional description for this `EnumChoice`. A `choices` example using the [`string`](#string) Slack type, In the following example for the [`string`](#string) Slack type, defining the `choices` property allows us to have the label on the input form be capitalized, but the data we're going to save be lowercase. ``` import { DefineWorkflow, Schema } from "deno-slack-sdk/mod.ts";// ...const inputForm = LogFruitWorkflow.addStep( Schema.slack.functions.OpenForm, { title: "Tell us your favorite fruit", interactivity: LogFruitWorkflow.inputs.interactivity, submit_label: "Submit", fields: { elements: [{ name: "Fruit", title: "The three best fruits", type: Schema.types.string, enum: ['mango', 'strawberry', 'plum'], choices: [ {value: 'mango', title: 'Mango!', description: 'Wonderfully tropical'}, {value: 'strawberry', title: 'Strawberry!', description: 'Absolutely fantastic'}, {value: 'plum', title: 'Plum!', description: 'Tart, just the way I like em'}, ] }] required: ["Fruit"], }, },);// ... ``` Declare a `channel_id` type: * Deno SDK * JSON manifest ``` // ... input_parameters: { properties: { interactivity: { type: Schema.slack.types.interactivity, }, channel: { type: Schema.slack.types.channel_id, }, }, },// ... ``` ``` // ..."input_parameters": { "channel": { "type": "slack#/types/channel_id" }}// ... ``` * * * ## Canvas ID {#canvasid} Type: `slack#/types/canvas_id` Property Type Description `default` The type that is being described. `description` string An optional parameter description. `examples` An array of the type being described. An optional list of examples. `hint` string An optional parameter hint. `title` string An optional parameter title. `type` string String that defines the parameter type. `choices` EnumChoice\[\] Defines labels that correspond to the `enum` values. See below `render_condition` Object The `render_condition` property contains three properties of its own: the `operator` property is a string logical operator which acts on the conditions; the `is_required` property is a boolean indicating if the property is required, and the `conditions` property is an array of object conditions which specify if the field should be rendered. #### The choices property {#the-choices-property} The `choices` property is an array of `EnumChoice` objects. Here is a closer look at the properties of the `EnumChoice` object: Property Type Description `default` The type that is being described. For example, the default for a `boolean` would be `true` or `false`. An optional parameter default value. `description` string An optional parameter description. `examples` An array of the type being described. An optional list of examples. `hint` string An optional parameter hint. `title` string An optional parameter title. `type` string String that defines the parameter type. `value` the type that the `EnumChoice` object corresponds to — in the example below, it is `string` The value of the corresponding choice, which must map to the values present in the sibling `enum` property. `title` string The label to display for this `EnumChoice`. `description` string An optional description for this `EnumChoice`. A `choices` example using the [`string`](#string) Slack type, In the following example for the [`string`](#string) Slack type, defining the `choices` property allows us to have the label on the input form be capitalized, but the data we're going to save be lowercase. ``` import { DefineWorkflow, Schema } from "deno-slack-sdk/mod.ts";// ...const inputForm = LogFruitWorkflow.addStep( Schema.slack.functions.OpenForm, { title: "Tell us your favorite fruit", interactivity: LogFruitWorkflow.inputs.interactivity, submit_label: "Submit", fields: { elements: [{ name: "Fruit", title: "The three best fruits", type: Schema.types.string, enum: ['mango', 'strawberry', 'plum'], choices: [ {value: 'mango', title: 'Mango!', description: 'Wonderfully tropical'}, {value: 'strawberry', title: 'Strawberry!', description: 'Absolutely fantastic'}, {value: 'plum', title: 'Plum!', description: 'Tart, just the way I like em'}, ] }] required: ["Fruit"], }, },);// ... ``` Declare a `canvas_id` type: * Deno SDK * JSON manifest ``` // ...{ name: "project_canvas", title: "Project Canvas", type: Schema.slack.types.canvas_id}// ... ``` ``` // ..."project_canvas": { "title": "Project Canvas", "type": "slack#/types/canvas_id"}// ... ``` Canvas ID example In this example workflow, we get a canvas ID and information to update it. ``` import { Schema } from "deno-slack-sdk/mod.ts";import { CanvasWorkflow } from "../workflows/canvas.ts";const inputForm = CanvasWorkflow.addStep( Schema.slack.functions.OpenForm, { title: "Provide info to update a canvas", interactivity: CanvasWorkflow.inputs.interactivity, submit_label: "Submit", fields: { elements: [ { name: "canvas", title: "Canvas to update", type: Schema.slack.types.canvas_id }, { name: "content", title: "Content", type: Schema.slack.types.expanded_rich_text, } ], required: ["canvas", "content"], }, },); ``` * * * ## Canvas Template ID {#canvastemplateid} Type: `slack#/types/canvas_template_id` Property Type Description `default` The type that is being described. An optional parameter default value. `description` string An optional parameter description. `examples` An array of the type being described. An optional list of examples. `hint` string An optional parameter hint. `title` string An optional parameter title. `type` string String that defines the parameter type. `choices` EnumChoice\[\] Defines labels that correspond to the `enum` values. See below #### The choices property {#the-choices-property} The `choices` property is an array of `EnumChoice` objects. Here is a closer look at the properties of the `EnumChoice` object: Property Type Description `default` The type that is being described. For example, the default for a `boolean` would be `true` or `false`. An optional parameter default value. `description` string An optional parameter description. `examples` An array of the type being described. An optional list of examples. `hint` string An optional parameter hint. `title` string An optional parameter title. `type` string String that defines the parameter type. `value` the type that the `EnumChoice` object corresponds to — in the example below, it is `string` The value of the corresponding choice, which must map to the values present in the sibling `enum` property. `title` string The label to display for this `EnumChoice`. `description` string An optional description for this `EnumChoice`. A `choices` example using the [`string`](#string) Slack type, In the following example for the [`string`](#string) Slack type, defining the `choices` property allows us to have the label on the input form be capitalized, but the data we're going to save be lowercase. ``` import { DefineWorkflow, Schema } from "deno-slack-sdk/mod.ts";// ...const inputForm = LogFruitWorkflow.addStep( Schema.slack.functions.OpenForm, { title: "Tell us your favorite fruit", interactivity: LogFruitWorkflow.inputs.interactivity, submit_label: "Submit", fields: { elements: [{ name: "Fruit", title: "The three best fruits", type: Schema.types.string, enum: ['mango', 'strawberry', 'plum'], choices: [ {value: 'mango', title: 'Mango!', description: 'Wonderfully tropical'}, {value: 'strawberry', title: 'Strawberry!', description: 'Absolutely fantastic'}, {value: 'plum', title: 'Plum!', description: 'Tart, just the way I like em'}, ] }] required: ["Fruit"], }, },);// ... ``` Declare a `canvas_template_id` type: * Deno SDK * JSON manifest ``` // ...{ name: "onboarding_template", title: "Onboarding Canvas Template", type: Schema.slack.types.canvas_template_id}// ... ``` ``` // ..."onboarding_template": { "title": "Onboarding Canvas Template", "type": "slack#/types/canvas_template_id"}// ... ``` Canvas Template ID example In this example workflow, we receive a `canvas_template_id` for creating a new canvas. ``` import { Schema } from "deno-slack-sdk/mod.ts";import { CanvasWorkflow } from "../workflows/canvas.ts";const inputForm = CanvasWorkflow.addStep( Schema.slack.functions.OpenForm, { title: "Provide a template your canvas from", interactivity: CanvasWorkflow.inputs.interactivity, submit_label: "Submit", fields: { elements: [ { name: "template", title: "Canvas template", type: Schema.slack.types.canvas_template_id }, { name: "title", title: "Canvas title", type: Schema.types.string, }, { name: "owner_id", title: "Owner ID", type: Schema.slack.types.user_id } ], required: ["template", "title", "owner_id"], }, },); ``` * * * ## Date {#date} Type: `slack#/types/date` Property Type Description `default` The type that is being described. An optional parameter default value. `description` string An optional parameter description. `examples` An array of the type being described. An optional list of examples. `hint` string An optional parameter hint. `title` string An optional parameter title. `type` string String that defines the parameter type. `choices` EnumChoice\[\] Defines labels that correspond to the `enum` values. See below. #### The choices property {#the-choices-property} The `choices` property is an array of `EnumChoice` objects. Here is a closer look at the properties of the `EnumChoice` object: Property Type Description `default` The type that is being described. For example, the default for a `boolean` would be `true` or `false`. An optional parameter default value. `description` string An optional parameter description. `examples` An array of the type being described. An optional list of examples. `hint` string An optional parameter hint. `title` string An optional parameter title. `type` string String that defines the parameter type. `value` the type that the `EnumChoice` object corresponds to — in the example below, it is `string` The value of the corresponding choice, which must map to the values present in the sibling `enum` property. `title` string The label to display for this `EnumChoice`. `description` string An optional description for this `EnumChoice`. A `choices` example using the [`string`](#string) Slack type, In the following example for the [`string`](#string) Slack type, defining the `choices` property allows us to have the label on the input form be capitalized, but the data we're going to save be lowercase. ``` import { DefineWorkflow, Schema } from "deno-slack-sdk/mod.ts";// ...const inputForm = LogFruitWorkflow.addStep( Schema.slack.functions.OpenForm, { title: "Tell us your favorite fruit", interactivity: LogFruitWorkflow.inputs.interactivity, submit_label: "Submit", fields: { elements: [{ name: "Fruit", title: "The three best fruits", type: Schema.types.string, enum: ['mango', 'strawberry', 'plum'], choices: [ {value: 'mango', title: 'Mango!', description: 'Wonderfully tropical'}, {value: 'strawberry', title: 'Strawberry!', description: 'Absolutely fantastic'}, {value: 'plum', title: 'Plum!', description: 'Tart, just the way I like em'}, ] }] required: ["Fruit"], }, },);// ... ``` Declare a `date` type: * Deno SDK * JSON manifest ``` // ...fields: { elements: [ { name: "date", title: "Date Posted", type: Schema.slack.types.date, }, ],},// ... ``` ``` // ..."fields": { "elements": [ { "date_posted": { "type": "slack#/types/date" } } ]}// ... ``` Date example In this example workflow, a form requires a `date` as input, which is printed along with the message after the form is submitted. ``` import { DefineWorkflow, Schema } from "deno-slack-sdk/mod.ts";const TestReverseWorkflow = DefineWorkflow({ callback_id: "test_reverse", title: "Test reverse", input_parameters: { properties: { channel: { type: Schema.slack.types.channel_id }, interactivity: { type: Schema.slack.types.interactivity }, }, required: ["interactivity"], },});const formData = TestReverseWorkflow.addStep(Schema.slack.functions.OpenForm, { title: "Reverse string form", submit_label: "Submit form", description: "Submit a string to reverse", interactivity: TestReverseWorkflow.inputs.interactivity, fields: { required: ["channel", "stringInput", "date"], elements: [ { name: "stringInput", title: "String input", type: Schema.types.string, }, { name: "date", title: "Date Posted", type: Schema.slack.types.date, }, { name: "channel", title: "Post in", type: Schema.slack.types.channel_id, default: TestReverseWorkflow.inputs.channel, }, ], },});import { ReverseFunction } from "../functions/reverse.ts";const reverseStep = TestReverseWorkflow.addStep(ReverseFunction, { input: formData.outputs.fields.stringInput,});// Add the date parameter as a step in your workflow. The message and date will be printed side by side.TestReverseWorkflow.addStep(Schema.slack.functions.SendMessage, { channel_id: formData.outputs.fields.channel, message: reverseStep.outputs.reverseString + " " + formData.outputs.fields.date,}); ``` * * * ## Expanded Rich Text {#expandedrichtext} Type: `slack#/types/expanded_rich_text` The `expanded_rich_text` type is a superset of the [`rich_text`](#rich-text) type, and is explicitly for use with canvases. It accepts all elements that `rich_text` provides and behaves in the same way as `rich_text`, except that it also accepts the following additional sub-elements: Property Type Description `default` The type that is being described. An optional parameter default value. `description` string An optional parameter description. `examples` An array of the type being described. An optional list of examples. `hint` string An optional parameter hint. `title` string An optional parameter title. `type` string String that defines the parameter type. `rich_text_header` object The text for the header, in the form of a `plain_text` [text object](https://docs.slack.dev/reference/block-kit/composition-objects/text-object/). `rich_text_divider` object Creates a divider to place between text. `rich_text_list` object This is an expanded version of the [`rich_text_list`](https://docs.slack.dev/reference/block-kit/blocks/rich-text-block#rich_text_list) element used in `rich_text` blocks. It behaves the same, except that it accepts two new style fields: `checked` and `unchecked`. This allows for the creation of checklists. Declare an `expanded_rich_text` type: * Deno SDK * JSON manifest ``` // ...{ name: "canvas_content", title: "Canvas Content", type: Schema.slack.types.expanded_rich_text}// ... ``` ``` // ..."canvas_content": { "title": "Canvas Content", "type": "slack#/types/expanded_rich_text"}// ... ``` Expanded rich text example Here is an example payload that shows the `expanded_rich_text` type and all of its sub-elements: ``` [ { "type": "expanded_rich_text", "elements": [ { "type": "rich_text_header", "elements": [ { "type": "text", "text": "Hello world" } ], "level": 1 }, { "type": "rich_text_list", "style": "unchecked", "indent": 0, "elements": [ { "type": "rich_text_section", "elements": [ { "type": "text", "text": "one" } ] } ], "border": 0 }, { "type": "rich_text_list", "style": "checked", "indent": 1, "elements": [ { "type": "rich_text_section", "elements": [ { "type": "text", "text": "two" } ] } ], "border": 0 }, { "type": "rich_text_divider" } ] }] ``` * * * ## File ID {#fileid} Type: `slack#/types/file_id` Property Type Description `default` The type that is being described. An optional parameter default value. `description` string An optional parameter description. `examples` An array of the type being described. An optional list of examples. `hint` string An optional parameter hint. `title` string An optional parameter title. `type` string String that defines the parameter type. `allowed_filetypes_group` string If provided, a predefined subset of filetypes will be restricted for file upload when this type is used in an OpenForm function. Can either be `ALL` or `IMAGES_ONLY`. `allowed_filetypes` string\[\] If provided, only these filetypes are allowed for file upload in an `OpenForm` function. Empty arrays are not allowed. Filetypes defined here will override any restrictions set in `allowed_filetypes_group`. Declare a `file_id` type: * Deno SDK * JSON manifest ``` // ...fields: { elements: [ { title: "Enter a file", name: "image-123", type: Schema.types.array, maxItems: 1, description: "", items: { type: Schema.slack.types.file_id, allowed_filetypes_group: "ALL" } }, ],},// ... ``` ``` // ..."file": { "type": "slack#/types/file_id", "allowed_filetypes_group": "ALL"}// ... ``` ### OpenForm parameters {#openform-parameters} When using the `file_id` type in an OpenForm function, there are two additional parameters that can be utilized. Parameter Type Description `allowed_filetypes_group` `string` Can be either `ALL` or `IMAGES_ONLY`. If provided, specifies allowed predefined subset of filetypes for file in an OpenForm function. `allowed_filetypes` `array` of `strings` If provided, specifies allowed filetypes for file upload in an OpenForm function. Overrides any restrictions set in `allowed_filetypes_group`. File ID example In this example workflow, we collect a file from the user. ``` import { Schema } from "deno-slack-sdk/mod.ts";import { ImageWorkflow } from "../workflows/ImageWorkflow.ts";const getImageStep = ImageWorkflow.addStep( Schema.slack.functions.OpenForm, { title: "Submit this form", interactivity: ImageWorkflow.inputs.interactivity, fields:{ elements: [ { title: "Enter a file", name: "image-123", type: Schema.types.array, maxItems: 1, description: "", items: { type: Schema.slack.types.file_id, allowed_filetypes_group: "ALL" }, } ], required: ["image-123"], } },); ``` * * * ## Integer {#integer} Type: `integer` Property Type Description `default` The type that is being described. An optional parameter default value. `description` string An optional parameter description. `examples` An array of the type being described. An optional list of examples. `hint` string An optional parameter hint. `title` string An optional parameter title. `type` string String that defines the parameter type. `minimum` number Absolute minimum acceptable value for the integer. `maximum` number Absolute maximum acceptable value for the integer. `enum` number\[\] Constrain the available integer options to just the list of integers denoted in the `enum` property. Usage of `enum` also instructs any UI that collects a value for this parameter to render a dropdown select input rather than a free-form text input. `choices` EnumChoice\[\] Defines labels that correspond to the `enum` values. See below. #### The choices property {#the-choices-property} The `choices` property is an array of `EnumChoice` objects. Here is a closer look at the properties of the `EnumChoice` object: Property Type Description `default` The type that is being described. For example, the default for a `boolean` would be `true` or `false`. An optional parameter default value. `description` string An optional parameter description. `examples` An array of the type being described. An optional list of examples. `hint` string An optional parameter hint. `title` string An optional parameter title. `type` string String that defines the parameter type. `value` the type that the `EnumChoice` object corresponds to — in the example below, it is `string` The value of the corresponding choice, which must map to the values present in the sibling `enum` property. `title` string The label to display for this `EnumChoice`. `description` string An optional description for this `EnumChoice`. A `choices` example using the [`string`](#string) Slack type, In the following example for the [`string`](#string) Slack type, defining the `choices` property allows us to have the label on the input form be capitalized, but the data we're going to save be lowercase. ``` import { DefineWorkflow, Schema } from "deno-slack-sdk/mod.ts";// ...const inputForm = LogFruitWorkflow.addStep( Schema.slack.functions.OpenForm, { title: "Tell us your favorite fruit", interactivity: LogFruitWorkflow.inputs.interactivity, submit_label: "Submit", fields: { elements: [{ name: "Fruit", title: "The three best fruits", type: Schema.types.string, enum: ['mango', 'strawberry', 'plum'], choices: [ {value: 'mango', title: 'Mango!', description: 'Wonderfully tropical'}, {value: 'strawberry', title: 'Strawberry!', description: 'Absolutely fantastic'}, {value: 'plum', title: 'Plum!', description: 'Tart, just the way I like em'}, ] }] required: ["Fruit"], }, },);// ... ``` * Deno SDK * JSON manifest Declare an `integer` type: ``` // ... name: "meetings", title: "Number of meetings", type: Schema.types.integer,// ... ``` ``` // ..."meetings": { "title": "Number of meetings", "type": "integer"}// ... ``` Integer example In this example workflow, we check the number of meetings we have scheduled for the day. ``` import { Schema } from "deno-slack-sdk/mod.ts";import { MeetingsWorkflow } from "../workflows/meetings.ts";const inputForm = MeetingsWorkflow.addStep( Schema.slack.functions.OpenForm, { title: "Number of meetings", interactivity: MeetingsWorkflow.inputs.interactivity, submit_label: "Check meetings", fields: { elements: [ { name: "channel", title: "Channel to send results to", type: Schema.slack.types.channel_id, default: MeetingsWorkflow.inputs.channel, }, { name: "meetings", title: "Number of meetings", description: "meetings", type: Schema.types.integer, minimum: -1, maximum: 5, }, { name: "meetingdate", title: "Meeting date", type: Schema.slack.types.date, }, ], required: ["channel", "meetings", "meetingdate"], }, },); ``` * * * ## Interactivity {#interactivity} Type: `slack#/types/interactivity` Property Type Description `default` The type that is being described. An optional parameter default value. `description` string An optional parameter description. `examples` An array of the type being described. An optional list of examples. `hint` string An optional parameter hint. `title` string An optional parameter title. `type` string String that defines the parameter type. `interactivity_pointer` string A pointer used to confirm user-initiated interactivity in a function. `interactor` `user_context` Context information of the user who initiated the interactivity. Declare the `interactivity` type: * Deno SDK * JSON manifest ``` // ...properties: { interactivity: { type: Schema.slack.types.interactivity, },},// ... ``` ``` // ..."input_parameters": { "interactivity": { "type": "slack#/types/interactivity" }}// ... ``` Interactivity example In this example workflow, we specify that it is an interactive workflow. ``` import { DefineWorkflow, Schema } from "deno-slack-sdk/mod.ts";const GreetingWorkflow = DefineWorkflow({ callback_id: "greeting_workflow", title: "Send a greeting", description: "Send a greeting to channel", input_parameters: { properties: { interactivity: { type: Schema.slack.types.interactivity }, channel: { type: Schema.slack.types.channel_id }, }, required: ["interactivity"], },}); ``` * * * ## List ID {#list-id} Type: `slack#/types/list_id` Property Type Description `default` The type that is being described. An optional parameter default value. `description` string An optional parameter description. `examples` An array of the type being described. An optional list of examples. `hint` string An optional parameter hint. `title` string An optional parameter title. `type` string String that defines the parameter type. Declare the `list_id` type: * Deno SDK * JSON manifest ``` // ... name: "current_bugs", title: "Current Bug List", type: Schema.slack.types.list_id,// ... ``` ``` // ..."current_bugs": { "title": "Current Bug List", "type": "slack#/types/list_id"}// ... ``` List ID example In this example workflow, we disseminate meeting information. ``` import { Schema } from "deno-slack-sdk/mod.ts";import { MeetingsWorkflow } from "../workflows/meetings.ts";const inputForm = MeetingsWorkflow.addStep( Schema.slack.functions.OpenForm, { title: "Meeting follow-up", interactivity: MeetingsWorkflow.inputs.interactivity, submit_label: "Meeting follow-up", fields: { elements: [ { name: "channel", title: "Channel to send info to", type: Schema.slack.types.channel_id, default: MeetingsWorkflow.inputs.channel, }, { name: "meetingdate", title: "Meeting date", type: Schema.slack.types.date, }, { name: "notes", title: "Meeting notes", type: Schema.slack.types.canvas_id }, { name: "actions", title: "Meeting to-dos", description: "Action items from the meeting", type: Schema.slack.types.list_id } ], required: ["channel", "meetingdate","notes", "actions"], }, },); ``` * * * ## Message Context {#message-context} Type: `slack#/types/message_context` Property Type Description `default` The type that is being described. An optional parameter default value. `description` string An optional parameter description. `examples` An array of the type being described. An optional list of examples. `hint` string An optional parameter hint. `title` string An optional parameter title. `type` string String that defines the parameter type. The `message_context` type is used in the [`ReplyInThread`](/tools/deno-slack-sdk/reference/slack-functions/reply_in_thread) Slack function as the target message you want to reply to. For example, let's say you have a workflow step that uses the [`SendMessage`](/tools/deno-slack-sdk/reference/slack-functions/send_message) function. If you want to send a reply to that message in a follow-on step that calls the [`ReplyInThread`](/tools/deno-slack-sdk/reference/slack-functions/reply_in_thread) function, pass the return value from the first step into the `message_context` parameter of `ReplyInThread`. Here's a brief example: * Deno SDK * JSON manifest ``` // Send a message to channel with ID C123456const msgStep = GreetingWorkflow.addStep(Schema.slack.functions.SendMessage, { channel_id: "C123456", message: "This is a message to the channel.",});// Send a message as an in-thread reply to the above message by passing// the outputs' message_context propertyGreetingWorkflow.addStep(Schema.slack.functions.ReplyInThread, { message_context: msgStep.outputs.message_context, message: "This is a threaded reply to the above message.",}); ``` ``` // ..."message_context": { "type": "slack#/types/message_context"}// ... ``` You can also construct and deconstruct the `message_context` as you see fit in your app. Here is what comprises `message_context`: Property Type Description Required `message_ts` [Schema.slack.types.message\_ts](#message-ts) A Slack-specific hash/timestamp necessary for referencing events like messages in Slack. Required `channel_id` [Schema.slack.types.channel\_id](#channelid) The ID of the channel where the message is posted. Optional Any individual property on message\_context could be referenced too. See the below example where we pass `message_context.message_ts` to the `trigger_ts` property: ``` //...const message = CreateSurveyWorkflow.addStep( Schema.slack.functions.ReplyInThread, { message_context: { channel_id: CreateSurveyWorkflow.inputs.channel_id, message_ts: CreateSurveyWorkflow.inputs.parent_ts, }, message: `Your feedback is requested – <${trigger.outputs.trigger_url}|survey now>!`, },);CreateSurveyWorkflow.addStep(SaveSurveyFunctionDefinition, { channel_id: CreateSurveyWorkflow.inputs.channel_id, parent_ts: CreateSurveyWorkflow.inputs.parent_ts, reactor_id: CreateSurveyWorkflow.inputs.reactor_id, trigger_ts: message.outputs.message_context.message_ts, //Here we reference message_ts from message_context trigger_id: trigger.outputs.trigger_id, survey_stage: "SURVEY",});//... ``` * * * ## Message Timestamp {#message-ts} Type: `slack#/types/message_ts` Property Type Description `default` The type that is being described. An optional parameter default value. `description` string An optional parameter description. `examples` An array of the type being described. An optional list of examples. `hint` string An optional parameter hint. `title` string An optional parameter title. `type` string String that defines the parameter type. Declare a `message_ts` type: * Deno SDK * JSON manifest ``` //...input_parameters: { properties: { message_ts : { type: Schema.slack.types.message_ts, description: "The ts value of a message" }, },},//... ``` ``` // ..."input_parameters": { "message_ts": { "type": "slack#/types/message_ts", "description": "The ts value of a message" }}// ... ``` Message Timestamp example In this example workflow from the [Simple Survey sample app](https://github.com/slack-samples/deno-simple-survey), a `message_ts` is used as an input parameter in two functions. ``` import { DefineWorkflow, Schema } from "deno-slack-sdk/mod.ts";import { CreateGoogleSheetFunctionDefinition } from "../functions/create_google_sheet.ts";import { CreateTriggerFunctionDefinition } from "../functions/create_survey_trigger.ts";import { SaveSurveyFunctionDefinition } from "../functions/save_survey.ts";import { RemoveThreadTriggerFunctionDefinition } from "../functions/remove_thread_trigger.ts";const CreateSurveyWorkflow = DefineWorkflow({ callback_id: "create_survey", title: "Create a survey", description: "Add a request for feedback to a message", input_parameters: { properties: { channel_id: { type: Schema.slack.types.channel_id, description: "The channel containing the reacted message", }, parent_ts: { type: Schema.types.string, description: "Message timestamp of the reacted message", }, parent_url: { type: Schema.types.string, description: "Permalink to the reacted message", }, reactor_id: { type: Schema.slack.types.user_id, description: "User that added the reacji", }, }, required: ["channel_id", "parent_ts", "parent_url", "reactor_id"], },});// Step 1: Create a new Google spreadsheetconst sheet = CreateSurveyWorkflow.addStep( CreateGoogleSheetFunctionDefinition, { google_access_token_id: {}, title: CreateSurveyWorkflow.inputs.parent_ts, },);// Step 2: Create a link trigger for the surveyconst trigger = CreateSurveyWorkflow.addStep(CreateTriggerFunctionDefinition, { google_spreadsheet_id: sheet.outputs.google_spreadsheet_id, reactor_access_token_id: sheet.outputs.reactor_access_token_id,});// Step 3: Delete the prompt message and metadataCreateSurveyWorkflow.addStep(RemoveThreadTriggerFunctionDefinition, { channel_id: CreateSurveyWorkflow.inputs.channel_id, parent_ts: CreateSurveyWorkflow.inputs.parent_ts, reactor_id: CreateSurveyWorkflow.inputs.reactor_id,});// Step 4: Notify the reactor of the survey spreadsheetCreateSurveyWorkflow.addStep(Schema.slack.functions.SendDm, { user_id: CreateSurveyWorkflow.inputs.reactor_id, message: `Feedback for <${CreateSurveyWorkflow.inputs.parent_url}|this message> is being <${sheet.outputs.google_spreadsheet_url}|collected here>!`,});// Step 5: Send the survey into the reacted threadconst message = CreateSurveyWorkflow.addStep( Schema.slack.functions.ReplyInThread, { message_context: { channel_id: CreateSurveyWorkflow.inputs.channel_id, message_ts: CreateSurveyWorkflow.inputs.parent_ts, //used here as part of the message_context object }, message: `Your feedback is requested – <${trigger.outputs.trigger_url}|survey now>!`, },);// Step 6: Store new survey metadataCreateSurveyWorkflow.addStep(SaveSurveyFunctionDefinition, { channel_id: CreateSurveyWorkflow.inputs.channel_id, parent_ts: CreateSurveyWorkflow.inputs.parent_ts, reactor_id: CreateSurveyWorkflow.inputs.reactor_id, trigger_ts: message.outputs.message_context.message_ts, //Referenced here individually trigger_id: trigger.outputs.trigger_id, survey_stage: "SURVEY",});export default CreateSurveyWorkflow; ``` * * * ## Number {#number} Type: `number` Property Type Description `default` The type that is being described. An optional parameter default value. `description` string An optional parameter description. `examples` An array of the type being described. An optional list of examples. `hint` string An optional parameter hint. `title` string An optional parameter title. `type` string String that defines the parameter type. `minimum` number Absolute minimum acceptable value for the number. `maximum` number Absolute maximum acceptable value for the number. `enum` number\[\] Constrain the available number options to just the list of numbers denoted in the `enum` property. Usage of `enum` also instructs any UI that collects a value for this parameter to render a dropdown select input rather than a free-form text input. `choices` EnumChoice\[\] Defines labels that correspond to the `enum` values. See below. #### The choices property {#the-choices-property} The `choices` property is an array of `EnumChoice` objects. Here is a closer look at the properties of the `EnumChoice` object: Property Type Description `default` The type that is being described. For example, the default for a `boolean` would be `true` or `false`. An optional parameter default value. `description` string An optional parameter description. `examples` An array of the type being described. An optional list of examples. `hint` string An optional parameter hint. `title` string An optional parameter title. `type` string String that defines the parameter type. `value` the type that the `EnumChoice` object corresponds to — in the example below, it is `string` The value of the corresponding choice, which must map to the values present in the sibling `enum` property. `title` string The label to display for this `EnumChoice`. `description` string An optional description for this `EnumChoice`. A `choices` example using the [`string`](#string) Slack type, In the following example for the [`string`](#string) Slack type, defining the `choices` property allows us to have the label on the input form be capitalized, but the data we're going to save be lowercase. ``` import { DefineWorkflow, Schema } from "deno-slack-sdk/mod.ts";// ...const inputForm = LogFruitWorkflow.addStep( Schema.slack.functions.OpenForm, { title: "Tell us your favorite fruit", interactivity: LogFruitWorkflow.inputs.interactivity, submit_label: "Submit", fields: { elements: [{ name: "Fruit", title: "The three best fruits", type: Schema.types.string, enum: ['mango', 'strawberry', 'plum'], choices: [ {value: 'mango', title: 'Mango!', description: 'Wonderfully tropical'}, {value: 'strawberry', title: 'Strawberry!', description: 'Absolutely fantastic'}, {value: 'plum', title: 'Plum!', description: 'Tart, just the way I like em'}, ] }] required: ["Fruit"], }, },);// ... ``` Declare a `number` type: * Deno SDK * JSON manifest ``` // ...{ name: "distance", title: "race distance", type: Schema.types.number,}// ... ``` ``` // ..."distance": { "title": "race distance", "type": "number"}// ... ``` Number example In this example workflow, we collect a runner's distance and date of their last run. ``` import { Schema } from "deno-slack-sdk/mod.ts";import { LogRunWorkflow } from "../workflows/log_run.ts";const inputForm = LogRunWorkflow.addStep( Schema.slack.functions.OpenForm, { title: "Log a run", interactivity: LogRunWorkflow.inputs.interactivity, submit_label: "Log run", fields: { elements: [ { name: "channel", title: "Channel to send logged run to", type: Schema.slack.types.channel_id, default: LogRunWorkflow.inputs.channel, }, { name: "distance", title: "Distance (in miles)", type: Schema.types.number, description: "race distance (in miles)", minimum: 0, maximum: 26.2, }, { name: "rundate", title: "Run date", type: Schema.slack.types.date, }, ], required: ["channel", "distance", "rundate"], }, },); ``` * * * ## OAuth2 {#oauth2} Type: `slack#/types/credential/oauth2` Property Type Description `default` The type that is being described. An optional parameter default value. `description` string An optional parameter description. `examples` An array of the type being described. An optional list of examples. `hint` string An optional parameter hint. `title` string An optional parameter title. `type` string String that defines the parameter type. Declare an `oauth2` type: * Deno SDK * JSON manifest ``` // ... githubAccessTokenId: { type: Schema.slack.types.oauth2, oauth2_provider_key: "github", },// ... ``` ``` // ..."github_access_token_id": { "type": "slack#/types/credential/oauth2", "oauth2_provider_key": "github"}// ... ``` OAuth2 example In this example, we use the `oauth2` type for an input parameter in a [custom function](/tools/deno-slack-sdk/guides/creating-custom-functions). To read more about a full implementation of `oauth2`, check out [External authentication](/tools/deno-slack-sdk/guides/integrating-with-services-requiring-external-authentication). ``` export const CreateIssueDefinition = DefineFunction({ callback_id: "create_issue", title: "Create GitHub issue", description: "Create a new GitHub issue in a repository", source_file: "functions/create_issue.ts", input_parameters: { properties: { githubAccessTokenId: { type: Schema.slack.types.oauth2, oauth2_provider_key: "github", }, url: { type: Schema.types.string, description: "Repository URL", },// ... }, output_parameters: { properties: { GitHubIssueNumber: { type: Schema.types.number, description: "Issue number", }, GitHubIssueLink: { type: Schema.types.string, description: "Issue link", }, }, required: ["GitHubIssueNumber", "GitHubIssueLink"], },}); ``` * * * ## Object {#object} Type: `object` Property Type Description `default` The type that is being described. An optional parameter default value. `description` string An optional parameter description. `examples` An array of the type being described. An optional list of examples. `hint` string An optional parameter hint. `title` string An optional parameter title. `type` string String that defines the parameter type. `choices` EnumChoice\[\] Defines labels that correspond to the `enum` values. See below. #### The choices property {#the-choices-property} The `choices` property is an array of `EnumChoice` objects. Here is a closer look at the properties of the `EnumChoice` object: Property Type Description `default` The type that is being described. For example, the default for a `boolean` would be `true` or `false`. An optional parameter default value. `description` string An optional parameter description. `examples` An array of the type being described. An optional list of examples. `hint` string An optional parameter hint. `title` string An optional parameter title. `type` string String that defines the parameter type. `value` the type that the `EnumChoice` object corresponds to — in the example below, it is `string` The value of the corresponding choice, which must map to the values present in the sibling `enum` property. `title` string The label to display for this `EnumChoice`. `description` string An optional description for this `EnumChoice`. A `choices` example using the [`string`](#string) Slack type, In the following example for the [`string`](#string) Slack type, defining the `choices` property allows us to have the label on the input form be capitalized, but the data we're going to save be lowercase. ``` import { DefineWorkflow, Schema } from "deno-slack-sdk/mod.ts";// ...const inputForm = LogFruitWorkflow.addStep( Schema.slack.functions.OpenForm, { title: "Tell us your favorite fruit", interactivity: LogFruitWorkflow.inputs.interactivity, submit_label: "Submit", fields: { elements: [{ name: "Fruit", title: "The three best fruits", type: Schema.types.string, enum: ['mango', 'strawberry', 'plum'], choices: [ {value: 'mango', title: 'Mango!', description: 'Wonderfully tropical'}, {value: 'strawberry', title: 'Strawberry!', description: 'Absolutely fantastic'}, {value: 'plum', title: 'Plum!', description: 'Tart, just the way I like em'}, ] }] required: ["Fruit"], }, },);// ... ``` Object types are not supported within Workflow Builder at this time If your function will be used within Workflow Builder, we suggest not using the Object types at this time. Objects can be typed or untyped. Here we have examples of both. ### Typed Object {#typed-object} Refer to [custom types](/tools/deno-slack-sdk/guides/creating-a-custom-type) for more information about typed objects, including properties and how to use [`DefineProperty`](/tools/deno-slack-sdk/guides/creating-a-custom-type#define-property) to enforce required properties. Declare a custom `object` type: * Deno SDK * JSON manifest ``` // ...properties: { reviewer: DefineProperty({ type: Schema.types.object, properties: { login: { type: "string" }, }, }),},// ... ``` ``` // ..."input_parameters": { "reviewer": { "type": "object", "properties": { "login": { "type": "string" } } } }//... ``` Object example In this example workflow, we notify authors about updates to their file review status. ``` import { DefineWorkflow, Schema } from "deno-slack-sdk/mod.ts";export const ReviewStatusWorkflow = DefineWorkflow({ callback_id: "review_status_workflow", title: "Review status", description: "Review status", input_parameters: { properties: { action: { type: Schema.types.string, }, review_request: { type: Schema.types.object, properties: { number: { type: "integer" }, title: { type: "string" }, body: { type: "string" }, changed_files: { type: "integer" }, }, }, author: { type: Schema.types.object, properties: { login: { type: "string" }, }, }, reviewer: { type: Schema.types.object, properties: { login: { type: "string" }, }, }, }, required: ["action", "review_request", "author", "reviewer"], },}); ``` ### Untyped Object {#untyped-object} Untyped objects do not have properties defined on them. They are malleable; you can assign any kind of properties to them. In TypeScript lingo, these objects are typed as [`any`](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#any). Declare an untyped `object` type: ``` properties: { flexibleObject: { type: Schema.types.object, }}, ``` * * * ## Options_field {#options-field} Type: `object` Dynamic options in Workflow Builder can be rendered in one of two ways: as a [drop-down menu](#options-select) or as a set of fields. The input parameter is rendered as a set of fields with keys and values. The option fields are obtained from a custom step with an `options` output parameter of type [`options_field`](/tools/deno-slack-sdk/reference/slack-types#options_field). The input parameter that defines the dynamic option must be of type [`object`](/tools/deno-slack-sdk/reference/slack-types#object), as the completed set of fields in Workflow Builder will be passed to the custom step as an [untyped object](/tools/deno-slack-sdk/reference/slack-types#untyped-object) during workflow execution. Options\_field example ``` "test-field-dynamic-options": { "title": "Test dynamic field options", "description": "", "input_parameters": { "dynamic_fields": { "type": "object", "title": "Dynamic custom field options", "description": "A dynamically-populated section of input fields", "dynamic_options": { "function": "#/functions/get-field-options", "inputs": {} "selection_type": "key-value", } } }, "output_parameters": {}} ``` * * * ## Options_select {#options-select} Type: `object` Dynamic options in Workflow Builder can be rendered in one of two ways: as a drop-down menu or as a set of [fields](#options-field). The dynamic input parameter can be rendered as a drop-down menu, which will use the options obtained from a custom step with an `options` output parameter of the type [`options_select`](/tools/deno-slack-sdk/reference/slack-types#options_select). The drop-down menu UI component can be rendered in two ways: single-select, or multi-select. Options\_select example To render the dynamic input as a single-select menu, the input parameter defining the dynamic option must be of the type [`string`](/tools/deno-slack-sdk/reference/slack-types#string). ``` "step-with-dynamic-input": { "title": "Step that uses a dynamic input", "description": "This step uses a dynamic input rendered as a single-select menu", "input_parameters": { "dynamic_single_select": { "type": "string", // this must be of type string for single-select "title": "dynamic single select drop-down menu", "description": "A dynamically-populated single-select drop-down menu", "is_required": true, "dynamic_options": { "function": "#/functions/get-options", "inputs": {}, }, } }, "output_parameters": {}} ``` To render the dynamic input as a multi-select menu, the input parameter defining the dynamic option must be of the type [`array`](/tools/deno-slack-sdk/reference/slack-types#array), and its `items` must be of type [`string`](/tools/deno-slack-sdk/reference/slack-types#string). ``` "step-with-dynamic-input": { "title": "Step that uses a dynamic input", "description": "This step uses a dynamic input rendered as a multi-select menu", "input_parameters": { "dynamic_multi_select": { "type": "array", // this must be of type array for multi-select "items": { "type": "string" }, "title": "dynamic single select drop-down menu", "description": "A dynamically-populated multi-select drop-down menu", "dynamic_options": { "function": "#/functions/get-options", "inputs": {}, }, } }, "output_parameters": {}} ``` * * * ## Rich text {#rich-text} Type: `slack#/types/rich_text` Property Type Description `default` The type that is being described. An optional parameter default value. `description` string An optional parameter description. `examples` An array of the type being described. An optional list of examples. `hint` string An optional parameter hint. `title` string An optional parameter title. `type` string String that defines the parameter type. Declare a `rich_text` type: * Deno SDK * JSON manifest ``` // ...elements: [ { name: "formattedStringInput", title: "String input", type: Schema.slack.types.rich_text, },],// ... ``` ``` // ..."elements": { "formattedStringInput": { "title": "String input", "type": "slack#/types/rich_text"}}// ... ``` Rich text example In this example workflow, we collect a formatted message from the user using the `rich_text` type. ``` import { DefineWorkflow, Schema } from "deno-slack-sdk/mod.ts";const TestWorkflow = DefineWorkflow({ callback_id: "test", title: "Test", input_parameters: { properties: { channel: { type: Schema.slack.types.channel_id }, interactivity: { type: Schema.slack.types.interactivity }, }, required: ["interactivity"], },});const formData = TestWorkflow.addStep(Schema.slack.functions.OpenForm, { title: "Send Message Form", submit_label: "Send Message form", interactivity: TestWorkflow.inputs.interactivity, fields: { required: ["channel", "formattedStringInput"], elements: [ { name: "formattedStringInput", title: "String input", type: Schema.slack.types.rich_text, }, { name: "channel", title: "Post in", type: Schema.slack.types.channel_id, default: TestWorkflow.inputs.channel, }, ], },});// To share this message object with other users, embed it into a Slack function such as SendMessage.TestWorkflow.addStep(Schema.slack.functions.SendMessage, { channel_id: formData.outputs.fields.channel, message: formData.outputs.fields.formattedStringInput,}); ``` * * * ## String {#string} Type: `string` Property Type Description `default` The type that is being described. An optional parameter default value. `description` string An optional parameter description. `examples` An array of the type being described. An optional list of examples. `hint` string An optional parameter hint. `title` string An optional parameter title. `type` string String that defines the parameter type. `minLength` number Minimum number of characters comprising the string. `maxLength` number Maximum number of characters comprising the string. `enum` string\[\] Constrain the available string options to just the list of strings denoted in the `enum` property. Usage of `enum` also instructs any UI that collects a value for this parameter to render a dropdown select input rather than a free-form text input. `choices` EnumChoice\[\] Defines labels that correspond to the `enum` values. See below. `format` string Define accepted format of the string. Valid options include `url` or `email`. #### The choices property {#the-choices-property} The `choices` property is an array of `EnumChoice` objects. Here is a closer look at the properties of the `EnumChoice` object: Property Type Description `default` The type that is being described. For example, the default for a `boolean` would be `true` or `false`. An optional parameter default value. `description` string An optional parameter description. `examples` An array of the type being described. An optional list of examples. `hint` string An optional parameter hint. `title` string An optional parameter title. `type` string String that defines the parameter type. `value` the type that the `EnumChoice` object corresponds to — in the example below, it is `string` The value of the corresponding choice, which must map to the values present in the sibling `enum` property. `title` string The label to display for this `EnumChoice`. `description` string An optional description for this `EnumChoice`. A `choices` example using the [`string`](#string) Slack type, In the following example for the [`string`](#string) Slack type, defining the `choices` property allows us to have the label on the input form be capitalized, but the data we're going to save be lowercase. ``` import { DefineWorkflow, Schema } from "deno-slack-sdk/mod.ts";// ...const inputForm = LogFruitWorkflow.addStep( Schema.slack.functions.OpenForm, { title: "Tell us your favorite fruit", interactivity: LogFruitWorkflow.inputs.interactivity, submit_label: "Submit", fields: { elements: [{ name: "Fruit", title: "The three best fruits", type: Schema.types.string, enum: ['mango', 'strawberry', 'plum'], choices: [ {value: 'mango', title: 'Mango!', description: 'Wonderfully tropical'}, {value: 'strawberry', title: 'Strawberry!', description: 'Absolutely fantastic'}, {value: 'plum', title: 'Plum!', description: 'Tart, just the way I like em'}, ] }] required: ["Fruit"], }, },);// ... ``` Declare a `string` type: * Deno SDK * JSON manifest ``` // ...{ name: "notes", title: "Notes", type: Schema.types.string,},// ... ``` ``` // ..."notes": { "type": "string", "title": "notes"}// ... ``` String example In this example workflow, we use a `string` type to allow a user to add notes about their time off request. ``` import { Schema } from "deno-slack-sdk/mod.ts";import { CreateFTOWorkflow } from "../workflows/create_fto_workflow.ts";const ftoRequestData = CreateFTOWorkflow.addStep( Schema.slack.functions.OpenForm, { title: "Request dates off", description: "Hooray for vacay!", interactivity: CreateFTOWorkflow.inputs.interactivity, submit_label: "Submit request", fields: { elements: [ { name: "start_date", title: "Start date", type: Schema.slack.types.date, }, { name: "end_date", title: "End date", type: Schema.slack.types.date, }, { name: "notes", title: "Notes", description: "Anything to note?", type: Schema.types.string, long: true, // renders the input box as a multi-line text box on the form }, ], required: ["start_date", "end_date"], }, },); ``` * * * ## Team ID {#team_id} Type: `slack#/types/team_id` Property Type Description `default` The type that is being described. An optional parameter default value. `description` string An optional parameter description. `examples` An array of the type being described. An optional list of examples. `hint` string An optional parameter hint. `title` string An optional parameter title. `type` string String that defines the parameter type. `choices` EnumChoice\[\] Defines labels that correspond to the `enum` values. See below. #### The choices property {#the-choices-property} The `choices` property is an array of `EnumChoice` objects. Here is a closer look at the properties of the `EnumChoice` object: Property Type Description `default` The type that is being described. For example, the default for a `boolean` would be `true` or `false`. An optional parameter default value. `description` string An optional parameter description. `examples` An array of the type being described. An optional list of examples. `hint` string An optional parameter hint. `title` string An optional parameter title. `type` string String that defines the parameter type. `value` the type that the `EnumChoice` object corresponds to — in the example below, it is `string` The value of the corresponding choice, which must map to the values present in the sibling `enum` property. `title` string The label to display for this `EnumChoice`. `description` string An optional description for this `EnumChoice`. A `choices` example using the [`string`](#string) Slack type, In the following example for the [`string`](#string) Slack type, defining the `choices` property allows us to have the label on the input form be capitalized, but the data we're going to save be lowercase. ``` import { DefineWorkflow, Schema } from "deno-slack-sdk/mod.ts";// ...const inputForm = LogFruitWorkflow.addStep( Schema.slack.functions.OpenForm, { title: "Tell us your favorite fruit", interactivity: LogFruitWorkflow.inputs.interactivity, submit_label: "Submit", fields: { elements: [{ name: "Fruit", title: "The three best fruits", type: Schema.types.string, enum: ['mango', 'strawberry', 'plum'], choices: [ {value: 'mango', title: 'Mango!', description: 'Wonderfully tropical'}, {value: 'strawberry', title: 'Strawberry!', description: 'Absolutely fantastic'}, {value: 'plum', title: 'Plum!', description: 'Tart, just the way I like em'}, ] }] required: ["Fruit"], }, },);// ... ``` This type is not supported for use in the [OpenForm](/tools/deno-slack-sdk/reference/slack-functions/open_form) Slack function. Declare a `team_id` type: * Deno SDK * JSON manifest ``` // ...attributes: { team_id: { type: Schema.slack.types.team_id, },},// ... ``` ``` // ..."team_id": { "type": "slack#/types/team_id"}// ... ``` * * * ## Timestamp {#timestamp} Type: `slack#/types/timestamp` Property Type Description `default` The type that is being described. An optional parameter default value. `description` string An optional parameter description. `examples` An array of the type being described. An optional list of examples. `hint` string An optional parameter hint. `title` string An optional parameter title. `type` string String that defines the parameter type. `choices` EnumChoice\[\] Defines labels that correspond to the `enum` values. See below. #### The choices property {#the-choices-property} The `choices` property is an array of `EnumChoice` objects. Here is a closer look at the properties of the `EnumChoice` object: Property Type Description `default` The type that is being described. For example, the default for a `boolean` would be `true` or `false`. An optional parameter default value. `description` string An optional parameter description. `examples` An array of the type being described. An optional list of examples. `hint` string An optional parameter hint. `title` string An optional parameter title. `type` string String that defines the parameter type. `value` the type that the `EnumChoice` object corresponds to — in the example below, it is `string` The value of the corresponding choice, which must map to the values present in the sibling `enum` property. `title` string The label to display for this `EnumChoice`. `description` string An optional description for this `EnumChoice`. A `choices` example using the [`string`](#string) Slack type, In the following example for the [`string`](#string) Slack type, defining the `choices` property allows us to have the label on the input form be capitalized, but the data we're going to save be lowercase. ``` import { DefineWorkflow, Schema } from "deno-slack-sdk/mod.ts";// ...const inputForm = LogFruitWorkflow.addStep( Schema.slack.functions.OpenForm, { title: "Tell us your favorite fruit", interactivity: LogFruitWorkflow.inputs.interactivity, submit_label: "Submit", fields: { elements: [{ name: "Fruit", title: "The three best fruits", type: Schema.types.string, enum: ['mango', 'strawberry', 'plum'], choices: [ {value: 'mango', title: 'Mango!', description: 'Wonderfully tropical'}, {value: 'strawberry', title: 'Strawberry!', description: 'Absolutely fantastic'}, {value: 'plum', title: 'Plum!', description: 'Tart, just the way I like em'}, ] }] required: ["Fruit"], }, },);// ... ``` Declare a `timestamp` type: * Deno SDK * JSON manifest ``` // ...inputs: { currentTime: { value: "{{data.trigger_ts}}", type: Schema.slack.types.timestamp, },},// ... ``` ``` // ..."input_parameters": { "current_time": { "type": "slack#/types/timestamp" }}// ... ``` Timestamp example In this example trigger, we call a workflow that logs an incident and the time it occurred. ``` import { DefineWorkflow, Schema } from "deno-slack-sdk/mod.ts";import { Trigger } from "deno-slack-api/types.ts";export const MyWorkflow = DefineWorkflow({ callback_id: "my_workflow", title: "My workflow", input_parameters: { properties: { currentTime: { type: Schema.slack.types.timestamp }, interactivity: { type: Schema.slack.types.interactivity }, }, required: [], },});export const incidentTrigger: Trigger = { type: "shortcut", name: "Log an incident", workflow: `#/workflows/${MyWorkflow.definition.callback_id}`, inputs: { currentTime: { value: "{{data.trigger_ts}}" }, interactivity: { value: "{{data.interactivity}}" }, },}; ``` * * * ## User context {#usercontext} Type: `slack#/types/user_context` Using `user_context` in Workflow Builder In Workflow Builder, this input type will not have a visible input field and cannot be set manually by a builder Instead, the way the value is set is dependent on the situation: * **If the workflow starts from an explicit user action (with a link trigger, for example),** then the `user_context` will be passed from the trigger to the function input. If the workflow contains a step that alters the `user_context` value (like a message with a button), then the altered `user_context` value is passed to the function input. * **If the workflow starts from something _other_ than an explicit user action (from a scheduled trigger, for example),** then the builder of the workflow must place a step that sets the `user_context` value (like a message with a button). This value will then be passed to the input of the function. If a workflow step requires `user_context` and there is no way to ascertain the value within Workflow Builder, the workflow cannot be published. Property Type Description `default` The type that is being described. An optional parameter default value. `description` string An optional parameter description. `examples` An array of the type being described. An optional list of examples. `hint` string An optional parameter hint. `title` string An optional parameter title. `type` string String that defines the parameter type. `id` string The `user_id` of the person to which the `user_context` belongs. `secret` string A hash used internally by Slack to validate the authenticity of the `id` in the `user_context`. This can be safely ignored, since it's only used by us at Slack to avert malicious actors! Declare the `user_context` type: * Deno SDK * JSON manifest ``` // ...input_parameters: { properties: { person_reporting_bug: { type: Schema.slack.types.user_context, description: "Which user?", }, },},// ... ``` ``` // ..."input_parameters": { "person_reporting_bug": { "type": "slack#/types/user_context", "description": "Which user?" }}// ... ``` User context example In this example workflow, we use the `Schema.slack.types.user_context` type to report a bug in a system and to collect the reporter's information. ``` import { DefineWorkflow, Schema } from "deno-slack-sdk/mod.ts";const ReportBugWorkflow = DefineWorkflow({ callback_id: "report_bug", title: "Report a Bug", description: "Report a bug", input_parameters: { properties: { channel_id: { type: Schema.slack.types.channel_id, description: "Which channel?", }, person_reporting_bug: { type: Schema.slack.types.user_context, description: "Which user?", }, }, required: ["person_reporting_bug"], },});import { CreateBugFunction } from "../functions/create_bug.ts";ReportBugWorkflow.addStep( CreateBugFunction, { title: "title", summary: "summary", urgency: "S0", channel_id: ReportBugWorkflow.inputs.channel_id, creator: ReportBugWorkflow.inputs.person_reporting_bug, },); ``` * * * ## User ID {#userid} Type: `slack#/types/user_id` Property Type Description `default` The type that is being described. An optional parameter default value. `description` string An optional parameter description. `examples` An array of the type being described. An optional list of examples. `hint` string An optional parameter hint. `title` string An optional parameter title. `type` string String that defines the parameter type. `choices` EnumChoice\[\] Defines labels that correspond to the `enum` values. See below. #### The choices property {#the-choices-property} The `choices` property is an array of `EnumChoice` objects. Here is a closer look at the properties of the `EnumChoice` object: Property Type Description `default` The type that is being described. For example, the default for a `boolean` would be `true` or `false`. An optional parameter default value. `description` string An optional parameter description. `examples` An array of the type being described. An optional list of examples. `hint` string An optional parameter hint. `title` string An optional parameter title. `type` string String that defines the parameter type. `value` the type that the `EnumChoice` object corresponds to — in the example below, it is `string` The value of the corresponding choice, which must map to the values present in the sibling `enum` property. `title` string The label to display for this `EnumChoice`. `description` string An optional description for this `EnumChoice`. A `choices` example using the [`string`](#string) Slack type, In the following example for the [`string`](#string) Slack type, defining the `choices` property allows us to have the label on the input form be capitalized, but the data we're going to save be lowercase. ``` import { DefineWorkflow, Schema } from "deno-slack-sdk/mod.ts";// ...const inputForm = LogFruitWorkflow.addStep( Schema.slack.functions.OpenForm, { title: "Tell us your favorite fruit", interactivity: LogFruitWorkflow.inputs.interactivity, submit_label: "Submit", fields: { elements: [{ name: "Fruit", title: "The three best fruits", type: Schema.types.string, enum: ['mango', 'strawberry', 'plum'], choices: [ {value: 'mango', title: 'Mango!', description: 'Wonderfully tropical'}, {value: 'strawberry', title: 'Strawberry!', description: 'Absolutely fantastic'}, {value: 'plum', title: 'Plum!', description: 'Tart, just the way I like em'}, ] }] required: ["Fruit"], }, },);// ... ``` Declare a `user_id` type: * Deno SDK * JSON manifest ``` // ...{ name: "runner", title: "Runner", type: Schema.slack.types.user_id,}// ... ``` ``` // ..."runner": { "title": "Runner", "type": "slack#/types/user_id"}// ... ``` User ID example In this example workflow, we get a runner's ID and the distance of their logged run. ``` import { Schema } from "deno-slack-sdk/mod.ts";import { RunWorkflow } from "../workflows/run.ts";const inputForm = RunWorkflow.addStep( Schema.slack.functions.OpenForm, { title: "Log your run", interactivity: RunWorkflow.inputs.interactivity, submit_label: "Submit", fields: { elements: [ { name: "channel", title: "Channel to send entry to", type: Schema.slack.types.channel_id, default: RunWorkflow.inputs.channel, }, { name: "runner", title: "Runner", type: Schema.slack.types.user_id, }, { name: "distance", title: "Distance (in miles)", type: Schema.types.number, }, ], required: ["channel", "runner", "distance"], }, },); ``` * * * ## Usergroup ID {#usergroupid} Type: `slack#/types/usergroup_id` Property Type Description `default` The type that is being described. An optional parameter default value. `description` string An optional parameter description. `examples` An array of the type being described. An optional list of examples. `hint` string An optional parameter hint. `title` string An optional parameter title. `type` string String that defines the parameter type. `choices` EnumChoice\[\] Defines labels that correspond to the `enum` values. See below. #### The choices property {#the-choices-property} The `choices` property is an array of `EnumChoice` objects. Here is a closer look at the properties of the `EnumChoice` object: Property Type Description `default` The type that is being described. For example, the default for a `boolean` would be `true` or `false`. An optional parameter default value. `description` string An optional parameter description. `examples` An array of the type being described. An optional list of examples. `hint` string An optional parameter hint. `title` string An optional parameter title. `type` string String that defines the parameter type. `value` the type that the `EnumChoice` object corresponds to — in the example below, it is `string` The value of the corresponding choice, which must map to the values present in the sibling `enum` property. `title` string The label to display for this `EnumChoice`. `description` string An optional description for this `EnumChoice`. A `choices` example using the [`string`](#string) Slack type, In the following example for the [`string`](#string) Slack type, defining the `choices` property allows us to have the label on the input form be capitalized, but the data we're going to save be lowercase. ``` import { DefineWorkflow, Schema } from "deno-slack-sdk/mod.ts";// ...const inputForm = LogFruitWorkflow.addStep( Schema.slack.functions.OpenForm, { title: "Tell us your favorite fruit", interactivity: LogFruitWorkflow.inputs.interactivity, submit_label: "Submit", fields: { elements: [{ name: "Fruit", title: "The three best fruits", type: Schema.types.string, enum: ['mango', 'strawberry', 'plum'], choices: [ {value: 'mango', title: 'Mango!', description: 'Wonderfully tropical'}, {value: 'strawberry', title: 'Strawberry!', description: 'Absolutely fantastic'}, {value: 'plum', title: 'Plum!', description: 'Tart, just the way I like em'}, ] }] required: ["Fruit"], }, },);// ... ``` Declare a `usergroup_id` type: * Deno SDK * JSON manifest ``` // ...attributes: { usergroup_id: { type: Schema.slack.types.usergroup_id, },},// ... ``` ``` // ..."usergroup_id": { "type": "slack#/types/usergroup_id"}// ... ``` Usergroup ID example In this example datastore definition, we store work shift details for a team. ``` import { DefineDatastore, Schema } from "deno-slack-sdk/mod.ts";export const MyShifts = DefineDatastore({ name: "shifts", primary_key: "id", attributes: { id: { type: Schema.types.string }, team_id: { type: Schema.types.team_id }, channel: { type: Schema.slack.types.channel_id }, usergroup_id: { type: Schema.slack.types.usergroup_id }, shiftRotation: { type: Schema.types.string }, },}); ``` --- Source: https://docs.slack.dev/tools/deno-slack-sdk/tutorials/announcement-bot # Announcement bot Workflow apps require a paid plan Join the [Developer Program](https://api.slack.com/developer-program) and provision a sandbox with access to all Slack features for free. Hear ye, hear ye! In this tutorial, you will learn how to create an app for an announcement bot that helps users draft, edit, and post an announcement to a channel (or channels) in a user's workspace, all while exploring the following workflow app concepts: * [Custom](/tools/deno-slack-sdk/guides/creating-custom-functions) and [built-in](/tools/deno-slack-sdk/guides/creating-slack-functions) functions * [Datastores](/tools/deno-slack-sdk/guides/using-datastores) * [Workflows](/tools/deno-slack-sdk/guides/creating-workflows) * [Custom types](/tools/deno-slack-sdk/guides/creating-a-custom-type) * [Triggers](/tools/deno-slack-sdk/guides/creating-link-triggers) For an overview of how the final product will look and function, check out the demo video in the `README.md` of the [GitHub repo](https://github.com/slack-samples/deno-announcement-bot) for this project. Each Slack app built using the CLI begins with the same steps. Make sure you have everything you need before you call the attention of the masses to deliver your announcement. * Install the [Slack CLI](/tools/deno-slack-sdk/guides/getting-started). * Run `slack auth list` and ensure your workspace is listed. * If your workspace is not listed, address any issues by following along with the [Getting started guide](/tools/deno-slack-sdk/guides/getting-started), then come on back. ## Choose your adventure {#choose-your-adventure} Once those items are complete, you have two possible ways to proceed. ### Use a blank app {#use-a-blank-app} You can create a blank app with the Slack CLI using the following command: ``` slack create announcement-bot-app --template https://github.com/slack-samples/deno-blank-template ``` ### Use a pre-built app {#use-a-pre-built-app} Or, you can use the pre-built [Announcement Bot app](https://github.com/slack-samples/deno-announcement-bot): ``` slack create announcement-bot-app --template https://github.com/slack-samples/deno-announcement-bot ``` Whichever option you choose, be sure to have the sample app repo open for reference, since we won't cover every file here, for brevity's sake. Once your new project is ready to go, navigate to your project directory and let's get this show on the road. ## Plan your app {#plan-your-app} The best way to go about creating a workflow app is to take a bird’s eye view of it and determine: * What would you like this workflow to accomplish? * What is your goal? * How can you break that down into smaller steps and actions? This is how we will go about showing you this app’s creation, and we think it’s the best way to create workflow apps. To begin, the idea: an app that assists users in sending an announcement to a number of channels. But wait! Just in case they prematurely send it (if that’s you, check out [this help article](https://slack.com/help/articles/115005523006-Set-your-Enter-key-preference) or perhaps [this one](https://slack.com/help/articles/202395258-Edit-or-delete-messages#unsend-a-message)), let’s allow the user to preview and edit the announcement before making it final. ### How can we break this down into smaller steps? {#how-can-we-break-this-down-into-smaller-steps} If we think about the flow of the app, here's what happens. The user: * initiates a workflow * fills out a form and submits it * sees a preview where they can edit the message they drafted * sends the message * sees a summary posted by the app The first action will be handled by a [trigger](/tools/deno-slack-sdk/guides/creating-link-triggers) and the rest will be handled by [functions](/tools/deno-slack-sdk/guides/creating-custom-functions). The execution of the functions will be chained together in a [workflow](/tools/deno-slack-sdk/guides/creating-workflows), which essentially dictates which actions happen in which order. Along the way, we'll also add some visual sprinkles to sweeten the app's appearance in the form of [blocks](https://docs.slack.dev/reference/block-kit) and talk about the [app manifest](/tools/deno-slack-sdk/guides/using-the-app-manifest). Ready to unroll your proverbial scroll, gather the masses, and announce your next big message? Buckle up and let’s dive in. ## Define and implement the workflow {#define-and-implement-the-workflow} The [`create_announcement.ts`](https://github.com/slack-samples/deno-announcement-bot/blob/main/workflows/create_announcement.ts) workflow file will give us an idea of the flow of actions. The first step to creating a workflow is to define it. ``` import { DefineWorkflow, Schema } from "deno-slack-sdk/mod.ts";import { CreateDraftFunctionDefinition } from "../functions/create_draft/definition.ts";import { PostSummaryFunctionDefinition } from "../functions/post_summary/definition.ts";import { PrepareSendAnnouncementFunctionDefinition } from "../functions/send_announcement/definition.ts";const CreateAnnouncementWorkflow = DefineWorkflow({ callback_id: "create_announcement", title: "Create an announcement", description: "Create and send an announcement to one or more channels in your workspace.", input_parameters: { properties: { created_by: { type: Schema.slack.types.user_id, }, interactivity: { type: Schema.slack.types.interactivity, }, }, required: ["created_by", "interactivity"], },}); ``` This definition tells us that we will use an `interactivity` input parameter. This means that we will be requiring interaction from the user, in this case, to find out information about their announcement. Continuing on: ``` // Step 1: Open a form to create an announcement using Slack function, OpenForm// For more on Slack functions// https://docs.slack.dev/deno-slack-sdk/guides/creating-slack-functionsconst formStep = CreateAnnouncementWorkflow .addStep(Schema.slack.functions.OpenForm, { title: "Create an announcement", description: "Create a draft announcement. You will have the opportunity to preview & edit it in channel before sending.\n\n_Want to create a richer announcement? Use and paste the full payload into the message input below._", interactivity: CreateAnnouncementWorkflow.inputs.interactivity, submit_label: "Preview", fields: { elements: [{ name: "message", title: "Message", type: Schema.types.string, description: "Compose your message using plain text, mrkdwn, or blocks", long: true, }, { name: "channels", title: "Destination channel(s)", type: Schema.types.array, items: { type: Schema.slack.types.channel_id, }, description: "The channels where your announcement will be posted", }, { name: "channel", title: "Draft channel", type: Schema.slack.types.channel_id, description: "The channel where you and your team can preview & edit the announcement before sending", }, { name: "icon", title: "Custom emoji icon", type: Schema.types.string, description: "Emoji to override the default app icon. Must use the format :robot_face: to be applied correctly.", }, { name: "username", title: "Custom username", type: Schema.types.string, description: "Name to override the default app name", }], required: ["message", "channels", "channel"], }, }); ``` The first step you see here is the `OpenForm` [Slack function](/tools/deno-slack-sdk/reference/slack-functions/open_form), which handles collecting input from the user. You might be wondering why we defined this step as a variable called `formStep` instead of adding the step to the workflow, which is also a viable option. If you want to use any information collected in this function, you will need to store it in a variable to retrieve it later. We'll see this in the next step, where we'll add a step to handle drafting the announcement. ``` const draftStep = CreateAnnouncementWorkflow.addStep( CreateDraftFunctionDefinition, { created_by: CreateAnnouncementWorkflow.inputs.created_by, message: formStep.outputs.fields.message, channels: formStep.outputs.fields.channels, channel: formStep.outputs.fields.channel, icon: formStep.outputs.fields.icon, username: formStep.outputs.fields.username, },); ``` Notice how we're now accessing the information stored in the previous step's function via the `formStep.outputs.fields` property. This is how you pass data between functions. Don't worry about the particulars of `CreateDraftFunctionDefinition` just yet; we'll get to that in a bit. For now, we're just mapping out the flow of the entire app. Next, we'll send the announcement. ``` // Step 3: Send announcement(s)const sendStep = CreateAnnouncementWorkflow.addStep( PrepareSendAnnouncementFunctionDefinition, { message: draftStep.outputs.message, channels: formStep.outputs.fields.channels, icon: formStep.outputs.fields.icon, username: formStep.outputs.fields.username, draft_id: draftStep.outputs.draft_id, },); ``` As you can see, we've added another step to the workflow; this one is called `PrepareSendAnnouncementFunctionDefinition`, in which we're using data collected from both the function stored in `formStep` and `draftStep`. One final step: ``` // Step 4: Post message summary of announcementCreateAnnouncementWorkflow.addStep(PostSummaryFunctionDefinition, { announcements: sendStep.outputs.announcements, channel: formStep.outputs.fields.channel, message_ts: draftStep.outputs.message_ts,}); ``` This step sends a summary of the posted announcement using data from all three prior steps. Looking back on our code, we've defined a workflow and added steps to gather data from the user, draft the announcement, send it, and post a summary of it. Satisfied with this flow, we have one more line to add to our workflow file: ``` export default CreateAnnouncementWorkflow; ``` Awesome. Let's check out the particulars of these functions in the next section. ## Define and implement functions {#define-and-implement-functions} Now that we know the goal of what we're building and how we'll break it down, let's take a closer look at those different functions we identified in the workflow: * `OpenForm` * `CreateDraftFunctionDefinition` * `PrepareSendAnnouncementFunctionDefinition` * `PostSummaryFunctionDefinition` ### Creating a form {#creating-a-form} The `OpenForm` function is a [Slack function](/tools/deno-slack-sdk/reference/slack-functions/open_form) that allows us to collect information from the user. We saw its definition in the workflow, but let's look at it again here: ``` // This function exists in /workflows/create_announcement.tsconst formStep = CreateAnnouncementWorkflow .addStep(Schema.slack.functions.OpenForm, { title: "Create an announcement", description: "Create a draft announcement. You will have the opportunity to preview & edit it in channel before sending.\n\n_Want to create a richer announcement? Use and paste the full payload into the message input below._", interactivity: CreateAnnouncementWorkflow.inputs.interactivity, submit_label: "Preview", fields: { elements: [{ name: "message", title: "Message", type: Schema.types.string, description: "Compose your message using plain text, mrkdwn, or blocks", long: true, }, { name: "channels", title: "Destination channel(s)", type: Schema.types.array, items: { type: Schema.slack.types.channel_id, }, description: "The channels where your announcement will be posted", }, { name: "channel", title: "Draft channel", type: Schema.slack.types.channel_id, description: "The channel where you and your team can preview & edit the announcement before sending", }, { name: "icon", title: "Custom emoji icon", type: Schema.types.string, description: "Emoji to override the default app icon. Must use the format :robot_face: to be applied correctly.", }, { name: "username", title: "Custom username", type: Schema.types.string, description: "Name to override the default app name", }], required: ["message", "channels", "channel"], }, }); ``` We've defined the necessary inputs - `title`, `description`, `interactivity`, `submit_label`, and `fields`. The `fields` property represents what information we want to collect from the user. As you can see, we've required the user to minimally include `message`, `channels` to which to send the message, and a `channel` where the draft should post. ### Create a draft {#create-a-draft} Our next function, `CreateDraftFunctionDefinition`, is a [custom function](/tools/deno-slack-sdk/guides/creating-custom-functions). If you're following along in the sample code, navigate to the `/functions/create_draft` directory. For easier reading, we've split the function into three files - `definition.ts`, `handler.ts`, and `blocks.ts`. Technically speaking, you could combine the `definition.ts` and `handler.ts` files, but we like the visual separation in order to keep files shorter and easier to digest. Let's dive into the definition. ``` // /functions/create_draft/definition.tsimport { DefineFunction, Schema } from "deno-slack-sdk/mod.ts";export const CREATE_DRAFT_FUNCTION_CALLBACK_ID = "create_draft";export const CreateDraftFunctionDefinition = DefineFunction({ callback_id: CREATE_DRAFT_FUNCTION_CALLBACK_ID, title: "Create a draft announcement", description: "Creates and sends an announcement draft to channel for review before sending", source_file: "functions/create_draft/handler.ts", input_parameters: { properties: { created_by: { type: Schema.slack.types.user_id, description: "The user that created the announcement draft", }, message: { type: Schema.types.string, description: "The text content of the announcement", }, channel: { type: Schema.slack.types.channel_id, description: "The channel where the announcement will be drafted", }, channels: { type: Schema.types.array, items: { type: Schema.slack.types.channel_id, }, description: "The channels where the announcement will be posted", }, icon: { type: Schema.types.string, description: "Optional custom bot icon to use display in announcements", }, username: { type: Schema.types.string, description: "Optional custom bot emoji avatar to use in announcements", }, }, required: [ "created_by", "message", "channel", "channels", ], }, output_parameters: { properties: { draft_id: { type: Schema.types.string, description: "Datastore identifier for the draft", }, message: { type: Schema.types.string, description: "The content of the announcement", }, message_ts: { type: Schema.types.string, description: "The timestamp of the draft message in the Slack channel", }, }, required: ["draft_id", "message", "message_ts"], },}); ``` This file defines the function's six `input_parameters` and their types (four of which are required), as well as its three `output_parameters`, all of which are required. Let's check out what this function does in `handler.ts`, starting with just the first part: ``` // /functions/create_draft/handler.tsimport { SlackFunction } from "deno-slack-sdk/mod.ts";import { CreateDraftFunctionDefinition } from "./definition.ts";import { buildDraftBlocks } from "./blocks.ts";import { confirmAnnouncementForSend, openDraftEditView, prepareSendAnnouncement, saveDraftEditSubmission,} from "./interactivity_handler.ts";import { ChatPostMessageParams, DraftStatus } from "./types.ts";import DraftDatastore from "../../datastores/drafts.ts";/** * This is the handling code for the CreateDraftFunction. It will: * 1. Create a new datastore record with the draft * 2. Build a Block Kit message with the draft and send it to input channel * 3. Update the draft record with the successful sent drafts timestamp * 4. Pause function completion until user interaction */export default SlackFunction( CreateDraftFunctionDefinition, async ({ inputs, client }) => { const draftId = crypto.randomUUID(); // 1. Create a new datastore record with the draft const putResp = await client.apps.datastore.put< typeof DraftDatastore.definition >({ datastore: DraftDatastore.name, // @ts-ignore expected fix in future release - otherwise missing non-required items throw type error item: { id: draftId, created_by: inputs.created_by, message: inputs.message, channels: inputs.channels, channel: inputs.channel, icon: inputs.icon, username: inputs.username, status: DraftStatus.Draft, }, }); if (!putResp.ok) { const draftSaveErrorMsg = `Error saving draft announcement. Contact the app maintainers with the following information - (Error detail: ${putResp.error})`; console.log(draftSaveErrorMsg); return { error: draftSaveErrorMsg }; } ``` Here we see the `SlackFunction` defined with the `CreateDraftFunctionDefinition` we previously explored. `SlackFunction` is the necessary mechanism we need to use in order to interact with the [SlackAPI](/tools/deno-slack-sdk/guides/calling-slack-api-methods), via the `client` property. We haven't yet covered the datastore setup, so file that away in your brain for now; just know that it's a place where we'll store and retrieve data for this app's use. Take a minute to notice those properties on `item`. Hey those look familiar! `message`, `channels`, `channel`, `icon`, and `username` were all the inputs we collected in the `OpenForm` function step of the workflow. Next up: Build a Block Kit message with draft announcement, and send it to the input channel. ``` // 2. Build a Block Kit message with draft announcement and send it to input channel const blocks = buildDraftBlocks( draftId, inputs.created_by, inputs.message, inputs.channels, ); const params: ChatPostMessageParams = { channel: inputs.channel, blocks: blocks, text: `An announcement draft was posted`, }; if (inputs.icon) { params.icon_emoji = inputs.icon; } if (inputs.username) { params.username = inputs.username; } const postDraftResp = await client.chat.postMessage(params); if (!postDraftResp.ok) { const draftPostErrorMsg = `Error posting draft announcement to ${params.channel}. Contact the app maintainers with the following information - (Error detail: ${postDraftResp.error})`; console.log(draftPostErrorMsg); return { error: draftPostErrorMsg }; } ``` This step handles posting the draft announcement given the message we collected from the user to the draft channel they requested, through the [`postMessage`](https://docs.slack.dev/reference/methods/chat.postMessage) API method. Read more about how blocks work [over here](https://docs.slack.dev/block-kit/). Next up, let's update that draft record: ``` // 3. Update the draft record with the successful sent drafts timestamp const putResp2 = await client.apps.datastore.put< typeof DraftDatastore.definition >({ datastore: DraftDatastore.name, // @ts-expect-error expecting fix in future SDK release item: { id: draftId, message_ts: postDraftResp.ts, }, }); if (!putResp2.ok) { const draftUpdateErrorMsg = `Error updating draft announcement timestamp for ${draftId}. Contact the app maintainers with the following information - (Error detail: ${putResp2.error})`; console.log(draftUpdateErrorMsg); return { error: draftUpdateErrorMsg }; } /** * IMPORTANT! Set `completed` to false in order to pause function's complete state * since we will wait for user interaction in the button handlers below. * Steps after this step in the workflow will not execute until we * complete our function. */ return { completed: false }; },).addBlockActionsHandler( /** * These are additional interactivity handlers for events triggered * by a users interaction with Block Kit elements: */ "preview_overflow", openDraftEditView,).addViewSubmissionHandler( "edit_message_modal", saveDraftEditSubmission,).addBlockActionsHandler( "send_button", confirmAnnouncementForSend,).addViewSubmissionHandler( "confirm_send_modal", prepareSendAnnouncement,); ``` That last bit is important - we're pausing the function's completion in order to wait for an edit or confirmation to send. ### Send the announcement {#send-the-announcement} Let's take a look at the next function, located in `/functions/send_announcement`. Here's its definition: ``` // /functions/send_announcement/definition.tsimport { DefineFunction, Schema } from "deno-slack-sdk/mod.ts";import { AnnouncementCustomType } from "../post_summary/types.ts";export const SEND_ANNOUNCEMENT_FUNCTION_CALLBACK_ID = "send_announcement";export const PrepareSendAnnouncementFunctionDefinition = DefineFunction({ callback_id: SEND_ANNOUNCEMENT_FUNCTION_CALLBACK_ID, title: "Send an announcement", description: "Sends a message to one or more channels", source_file: "functions/send_announcement/handler.ts", input_parameters: { properties: { message: { type: Schema.types.string, description: "The content of the announcement", }, channels: { type: Schema.types.array, items: { type: Schema.slack.types.channel_id, }, description: "The destination channels of the announcement", }, icon: { type: Schema.types.string, description: "Optional custom bot icon to use display in announcements", }, username: { type: Schema.types.string, description: "Optional custom bot emoji avatar to use in announcements", }, draft_id: { type: Schema.types.string, description: "The datastore ID of the draft message if one was created", }, }, required: [ "message", "channels", ], }, output_parameters: { properties: { announcements: { type: Schema.types.array, items: { type: AnnouncementCustomType, }, description: "Array of objects that includes a channel ID and permalink for each announcement successfully sent", }, }, required: ["announcements"], },}); ``` Whoa, what's that `AnnouncementCustomType`?! Nothing to worry about, my dearest Slack dev. That's a [custom type](/tools/deno-slack-sdk/guides/creating-a-custom-type) that workflow apps allow us to define to suit our specialized needs. We can see its definition over in `/functions/post_summary/types.ts`: ``` // /functions/post_summary/types.tsimport { DefineType, Schema } from "deno-slack-sdk/mod.ts";export const AnnouncementCustomType = DefineType({ name: "Announcement", type: Schema.types.object, properties: { channel_id: { type: Schema.slack.types.channel_id, }, success: { type: Schema.types.boolean, }, permalink: { type: Schema.types.string, }, error: { type: Schema.types.string, }, }, required: ["channel_id", "success"],});/** * Corresponding TS typing for use elsewhere */export type AnnouncementType = { channel_id: string; success: boolean; permalink?: string; error?: string;}; ``` Let's check out the implementation of that function to see how it sends the announcement: ``` // /functions/send_announcement/handler.tsimport { SlackFunction } from "deno-slack-sdk/mod.ts";import { SlackAPIClient } from "deno-slack-api/types.ts";import { PrepareSendAnnouncementFunctionDefinition } from "./definition.ts";import { buildAnnouncementBlocks, buildSentBlocks } from "./blocks.ts";import { AnnouncementType } from "../post_summary/types.ts";import { ChatPostMessageParams, DraftStatus } from "../create_draft/types.ts";import DraftDatastore from "../../datastores/drafts.ts";import AnnouncementsDatastore from "../../datastores/announcements.ts";/** * This is the handling code for PrepareSendAnnouncementFunction. It will: * 1. Send announcement to each channel supplied * 2. Updates the status of the announcement in the */export default SlackFunction( PrepareSendAnnouncementFunctionDefinition, async ({ inputs, client }) => { // Array to gather chat.postMessage responses // deno-lint-ignore no-explicit-any const chatPostMessagePromises: Promise[] = []; // Incoming draft_id to link all announcements that are // part of the same draft. If a draft_id was not provided, // create a new identifier for this announcements. const draft_id = inputs.draft_id || crypto.randomUUID(); const blocks = buildAnnouncementBlocks(inputs.message); for (const channel of inputs.channels) { const params: ChatPostMessageParams = { channel: channel, blocks: blocks, text: `An announcement was posted`, }; if (inputs.icon) { params.icon_emoji = inputs.icon; } if (inputs.username) { params.username = inputs.username; } const announcementRes = sendAndSaveAnnouncement(params, draft_id, client); chatPostMessagePromises.push(announcementRes); } const announcements = await Promise.all(chatPostMessagePromises); // Update draft if one was created if (inputs.draft_id) { const { item } = await client.apps.datastore.put< typeof DraftDatastore.definition >({ datastore: DraftDatastore.name, // @ts-ignore expected fix in future release - otherwise missing non-required items throw type error item: { id: inputs.draft_id, status: DraftStatus.Sent, }, }); const blocks = buildSentBlocks( item.created_by, inputs.message, inputs.channels, ); await client.chat.update({ channel: item.channel, ts: item.message_ts, blocks: blocks, }); } return { outputs: { announcements: announcements } }; },); ``` Here we see the message being posted as well as updating the draft in the datastore. Continuing on: ``` /** * This method send an announcement to a channel, gets its permalink, and stores the details in the datastore * @param params parameters used in the chat.postMessage request * @param draft_id ID of the draft announcement that is being posted * @returns promise with summary */async function sendAndSaveAnnouncement( params: ChatPostMessageParams, draft_id: string, client: SlackAPIClient,): Promise { let announcement: AnnouncementType; // Send it const post = await client.chat.postMessage(params); if (post.ok) { console.log(`Sent to ${params.channel}`); // Get permalink to message for use in summary const { permalink } = await client.chat.getPermalink({ channel: params.channel, message_ts: post.ts, }); announcement = { channel_id: params.channel, success: true, permalink: permalink, }; } // There was an error sending the announcement else { console.log(`Error sending to ${params.channel}: ${post.error}`); announcement = { channel_id: params.channel, success: false, error: post.error, }; } // Save each announcement to DB even if there was an error posting await client.apps.datastore.put({ datastore: AnnouncementsDatastore.name, item: { id: crypto.randomUUID(), draft_id: draft_id, success: post.ok, error_message: post.error, channel: post.channel, message_ts: post.ts, }, }); return announcement;} ``` You might be wondering why this function returns the announcement if it's already been posted and updated in the datastore. Ah, but remember back to the app workflow when we had one final step of the flow? We post the announcement and then we post a summary of the announcement to the user who initiated the workflow. We'll use that output in our final function. Read on to see how. ### Post summary {#post-summary} Now let's head on over to `/functions/post_summary` to check out the function's definition in `definition.ts`: ``` // /functions/post_summary/definition.tsimport { DefineFunction, Schema } from "deno-slack-sdk/mod.ts";import { AnnouncementCustomType } from "./types.ts";export const POST_ANNOUNCEMENT_FUNCTION_CALLBACK_ID = "post_summary";export const PostSummaryFunctionDefinition = DefineFunction({ callback_id: POST_ANNOUNCEMENT_FUNCTION_CALLBACK_ID, title: "Post announcement summary", description: "Post a summary of all sent announcements ", source_file: "functions/post_summary/handler.ts", input_parameters: { properties: { announcements: { type: Schema.types.array, items: { type: AnnouncementCustomType, }, description: "Array of objects that includes a channel ID and permalink for each announcement successfully sent", }, channel: { type: Schema.slack.types.channel_id, description: "The channel where the summary should be posted", }, message_ts: { type: Schema.types.string, description: "Options message timestamp where the summary should be threaded", }, }, required: [ "announcements", "channel", ], }, output_parameters: { properties: { channel: { type: Schema.slack.types.channel_id, }, message_ts: { type: Schema.types.string, }, }, required: ["channel", "message_ts"], },}); ``` Notice the input parameters include an array of `announcements`. That is sourced from the `output_parameters` of the function executed prior to this one in the workflow - `PrepareSendAnnouncementFunctionDefinition`. Now that we've drafted, edited, and sent the announcement to the requested channels, we can use the last function to post a summary. Here's what that definition looks like: ``` // /functions/post_summary/definition.tsimport { DefineFunction, Schema } from "deno-slack-sdk/mod.ts";import { AnnouncementCustomType } from "./types.ts";export const POST_ANNOUNCEMENT_FUNCTION_CALLBACK_ID = "post_summary";export const PostSummaryFunctionDefinition = DefineFunction({ callback_id: POST_ANNOUNCEMENT_FUNCTION_CALLBACK_ID, title: "Post announcement summary", description: "Post a summary of all sent announcements ", source_file: "functions/post_summary/handler.ts", input_parameters: { properties: { announcements: { type: Schema.types.array, items: { type: AnnouncementCustomType, }, description: "Array of objects that includes a channel ID and permalink for each announcement successfully sent", }, channel: { type: Schema.slack.types.channel_id, description: "The channel where the summary should be posted", }, message_ts: { type: Schema.types.string, description: "Options message timestamp where the summary should be threaded", }, }, required: [ "announcements", "channel", ], }, output_parameters: { properties: { channel: { type: Schema.slack.types.channel_id, }, message_ts: { type: Schema.types.string, }, }, required: ["channel", "message_ts"], },}); ``` Refer back to the workflow definition at any point to see where this function, or any given function, is receiving its input parameters, as well as where its output parameters go. Let's continue on to the handler of this function: ``` // /functions/post_summary/handler.tsimport { SlackFunction } from "deno-slack-sdk/mod.ts";import { buildSummaryBlocks } from "./blocks.ts";import { PostSummaryFunctionDefinition } from "./definition.ts";/** * This is the handling code for PostSummaryFunction. It will: * 1. Post a message in thread to the draft announcement message * with a summary of announcement's sent * 2. Complete this function with either required outputs or an error */export default SlackFunction( PostSummaryFunctionDefinition, async ({ inputs, client }) => { const blocks = buildSummaryBlocks(inputs.announcements); // 1. Post a message in thread to the draft announcement message const postResp = await client.chat.postMessage({ channel: inputs.channel, thread_ts: inputs.message_ts || "", blocks: blocks, unfurl_links: false, }); if (!postResp.ok) { const summaryTS = postResp ? postResp.ts : "n/a"; const postSummaryErrorMsg = `Error posting announcement send summary: ${summaryTS} to channel: ${inputs.channel}. Contact the app maintainers with the following - (Error detail: ${postResp.error})`; console.log(postSummaryErrorMsg); // 2. Complete function with an error message return { error: postSummaryErrorMsg }; } const outputs = { channel: inputs.channel, message_ts: postResp.ts, }; // 2. Complete function with outputs return { outputs: outputs }; },); ``` This function handles sending a summary to the user of the announcement they drafted and sent to their selected channels. This concludes our dive into the functions of this app. Let's take a deeper look at the datastores holding our announcement data in the next section. ## Define datastores {#define-datastores} Two different datastores are needed for this application - one for drafts and one for announcements. ### Drafts datastore {#drafts-datastore} Let's first navigate to `/datastores` to check out the contents of `drafts.ts`: ``` import { DefineDatastore, Schema } from "deno-slack-sdk/mod.ts";export default DefineDatastore({ name: "drafts", primary_key: "id", attributes: { id: { type: Schema.types.string, }, created_by: { type: Schema.slack.types.user_id, }, message: { type: Schema.types.string, }, channels: { type: Schema.types.array, items: { type: Schema.slack.types.channel_id, }, }, channel: { type: Schema.slack.types.channel_id, }, message_ts: { type: Schema.types.string, }, icon: { type: Schema.types.string, }, username: { type: Schema.types.string, }, status: { type: Schema.types.string, // possible statuses are draft, sent }, },}); ``` You can read more about interacting with datastores on the [datastores page](/tools/deno-slack-sdk/guides/using-datastores), but the gist of it is that it's a Slack-hosted place to store data. In this definition, we see that this particular datastore has nine attributes - what you might consider fields in a database. Each attribute's type is listed along with its definition above. ### Announcements datastore {#announcements-datastore} The second datastore - `announcements` - is defined in `/datastores/announcements.ts` and looks like this: ``` import { DefineDatastore, Schema } from "deno-slack-sdk/mod.ts";export default DefineDatastore({ name: "announcements", primary_key: "id", attributes: { id: { type: Schema.types.string, }, draft_id: { type: Schema.types.string, }, success: { type: Schema.types.boolean, }, error_message: { type: Schema.types.string, }, channel: { type: Schema.slack.types.channel_id, }, message_ts: { type: Schema.types.string, }, },}); ``` If you refer back to `/functions/send_announcement/handler.ts` you'll see that this datastore is used to keep a record of all announcements, regardless of their success status. To play around more with datastores outside of your code, check out the datastore [commands](/tools/slack-cli/reference/commands/slack_datastore). ## Kick things off with a trigger {#kick-things-off-with-a-trigger} Triggers invoke workflows, so they're pretty important. In this app we'll be using a [link trigger](/tools/deno-slack-sdk/guides/creating-link-triggers), but you should know there are four types of [triggers](/tools/deno-slack-sdk/guides/using-triggers) available. Navigate to `/triggers/create_announcement.ts` and let's check it out. ``` import { Trigger } from "deno-slack-api/types.ts";import CreateAnnouncementWorkflow from "../workflows/create_announcement.ts";const trigger: Trigger< typeof CreateAnnouncementWorkflow.definition> = { type: "shortcut", name: "Create an announcement", description: "Create and send an announcement to one or more channels in your workspace.", workflow: "#/workflows/create_announcement", inputs: { created_by: { value: "{{data.user_id}}", }, interactivity: { value: "{{data.interactivity}}", }, },};export default trigger; ``` Next, run the trigger command in the terminal: ``` slack trigger create --trigger-def triggers/create_announcement.ts ``` After executing the command, select your app and workspace. The terminal will output a link called a "Shortcut URL", also known as your link trigger. Save that URL; we'll use it later. If you ever lose track of that URL, you can always run the command `slack triggers -info` and select your workspace to find it again. ## Report app contents in the app manifest {#report-app-contents-in-the-app-manifest} We've got one last stop to highlight before running this application, and that is the [app manifest](/tools/deno-slack-sdk/guides/using-the-app-manifest). The manifest is located in the root directory of your project. Navigating to it in the sample project, its contents look like this: ``` import { Manifest } from "deno-slack-sdk/mod.ts";import AnnouncementDatastore from "./datastores/announcements.ts";import DraftDatastore from "./datastores/drafts.ts";import { AnnouncementCustomType } from "./functions/post_summary/types.ts";import CreateAnnouncementWorkflow from "./workflows/create_announcement.ts";export default Manifest({ name: "Announcement Bot", description: "Send an announcement to one or more channels", icon: "assets/icon.png", outgoingDomains: ["cdn.skypack.dev"], datastores: [DraftDatastore, AnnouncementDatastore], types: [AnnouncementCustomType], workflows: [ CreateAnnouncementWorkflow, ], botScopes: [ "commands", "chat:write", "chat:write.public", "chat:write.customize", "datastore:read", "datastore:write", ],}); ``` The app manifest is the app's configuration. It is very important that this file is structured correctly in order for your app to run smoothly. Each function, custom type, and datastore defined in an app must be declared in the manifest file. ## Deploy your app {#deploy-your-app} Ready to see this thing in action? Let's use development mode to run this workflow in Slack. Start it off with this command in your terminal: ``` slack run ``` After you've chosen your app and assigned it to your workspace, you can switch over to the app in Slack and test it out. Remember the link trigger you created earlier? Copy and paste that URL in a message to yourself in Slack. It will unfurl into a button that you can click to initiate the workflow. ## You did it! {#you-did-it} Awww yea, you did it! You made it through the tutorial and are successfully sending announcements (but, you know, not too many...) to your workspace channels. Great job! ## Next steps {#next-steps} For your next challenge, perhaps consider creating [a bot to welcome users to your workspace](/tools/deno-slack-sdk/tutorials/welcome-bot)! --- Source: https://docs.slack.dev/tools/deno-slack-sdk/tutorials/definition-bot # Definition bot Workflow apps require a paid plan Join the [Developer Program](https://api.slack.com/developer-program) and provision a sandbox with access to all Slack features for free. Have you ever found yourself in a company or position where it feels like everyone around you is speaking in a language you don’t understand? Where the use of so many acronyms has you drowning in an alphabet soup of obscured meaning? In this tutorial, we’ll walk you through using a [trigger](/tools/deno-slack-sdk/guides/creating-link-triggers), [workflow](/tools/deno-slack-sdk/guides/creating-workflows), [custom function](/tools/deno-slack-sdk/guides/creating-custom-functions), [datastore](/tools/deno-slack-sdk/guides/using-datastores), and [modal view interactivity](/tools/deno-slack-sdk/guides/creating-an-interactive-modal) to create a workflow app that serves as a crowdsourced glossary of acronyms and team vernacular to help you talk the talk while you walk the walk. Before we begin, ensure you have the following prerequisites completed: * Install the [Slack CLI](/tools/deno-slack-sdk/guides/getting-started). * Run `slack auth list` and ensure your workspace is listed. * If your workspace is not listed, address any issues by following along with the [Getting started](/tools/deno-slack-sdk/guides/getting-started), then come on back. ## Get started {#get-started} Let’s get things started by creating a blank app via the CLI. Run the following command in your terminal. ``` slack create define-app --template https://github.com/slack-samples/deno-blank-template ``` Next, navigate to the project directory and open it in the code editor of your choice; we like Visual Studio Code. ## Plan your app {#plan-your-app} Let’s think about the flow of logic in our app. We'll need a [trigger](/tools/deno-slack-sdk/guides/using-triggers) to set things in motion, and a [modal](/tools/deno-slack-sdk/guides/creating-an-interactive-modal) to open and ask for a term that the user would like defined. From there, we'll take that term and search for it in a [datastore](/tools/deno-slack-sdk/guides/using-datastores). If it is found, we'll deliver that definition to the user in an updated modal. If it is not found, we'll ask the user if they would like to submit a definition for it. Because all of these actions will be done with view updates in the same modal, we’ll use one [function](/tools/deno-slack-sdk/guides/creating-custom-functions) with a few view handlers. We’ll also need one [workflow](/tools/deno-slack-sdk/guides/creating-workflows) and one [trigger](/tools/deno-slack-sdk/guides/creating-link-triggers). Let’s start this out by creating the [function](/tools/deno-slack-sdk/guides/creating-custom-functions), the main meat of the app. ## Write the custom function {#write-the-custom-function} Our function will handle the bulk of the logic in this app. Because all of the interaction will be within one modal pop-up, we’ll keep all the logic in one function (as opposed to breaking it out into separate functions strung together by the workflow). Create a folder called `functions` and a file within it, `term_lookup_function.ts`. First we define the function, laying out the expected inputs and outputs. ``` // term_lookup_function.tsimport { DefineFunction, Schema, SlackFunction } from "deno-slack-sdk/mod.ts";import { // don’t worry about these for now, we’ll talk about them in a later step showConfirmationView, showDefinitionSubmissionView, showDefinitionView,} from "./interactivity_handler.ts";export const TermLookupFunction = DefineFunction({ callback_id: "term_lookup_function", title: "Define a term", source_file: "functions/term_lookup_function.ts", input_parameters: { properties: { interactivity: { type: Schema.slack.types.interactivity } }, required: ["interactivity"], }, output_parameters: { properties: {}, required: [] },}); ``` The `interactivity` input parameter is essential for allowing the modal to first appear, as well as the subsequent user interactions to happen. `interactivity` gives the app permission to do these actions because the user initiated it. Without this parameter, modal interaction cannot take place. No output parameters are needed because all actions will take place within this function; we will not be passing data to another function. Now, the function implementation. Place this in the same file, after the function definition: ``` export default SlackFunction( TermLookupFunction, async ({ inputs, client }) => { const response = await client.views.open({ interactivity_pointer: inputs.interactivity.interactivity_pointer, view: { "type": "modal", "callback_id": "first-page", "notify_on_close": false, "title": { "type": "plain_text", "text": "Search for a definition" }, "submit": { "type": "plain_text", "text": "Search" }, "close": { "type": "plain_text", "text": "Close" }, "blocks": [ { "type": "input", "block_id": "term", "element": { "type": "plain_text_input", "action_id": "action" }, "label": { "type": "plain_text", "text": "Term" }, }, ], }, }); if (response.error) { const error = `Failed to open a modal in the term lookup workflow. Contact the app maintainers with the following information - (error: ${response.error})`; return { error }; } return { completed: false, }; },) ``` This implementation will create the first modal with a title, input block, submit button, and close button. Once the user enters a term in the input field and clicks “submit”, we have to handle that action in a view submission handler, which will use the `callback_id` of the modal to react. We’ll take a look at that in the next step. ## Create show definition submission view {#create-show-definition-submission-view} For ease of readability, we’ll put all of our interactivity handlers in a file separate from the main function file. Create a new file in the `functions` folder and call it `interactivity_handler.ts`. But before we get ahead of ourselves, we’ll need to look up the term the user submitted in a [datastore](/tools/deno-slack-sdk/guides/using-datastores). Let’s define that now. Back up to the root directory of your project and create a new folder called `datastores`. Add a file to it called `terms.ts`. This datastore will hold the crowdsourced terms in our app. When a user submits a term, it will be saved in the datastore, and when a user looks for a term, it will be retrieved from the datastore. Define it here: ``` // terms.tsimport { DefineDatastore, Schema } from "deno-slack-sdk/mod.ts";export const TermsDatastore = DefineDatastore({ name: "terms", primary_key: "id", attributes: { id: { type: Schema.types.string }, term: { type: Schema.types.string }, definition: { type: Schema.types.string }, },}); ``` Now we’re ready to go back to `interactivity_handler.ts` and define a view submission handler to handle what happens after a user enters a term to be defined and clicks "submit". Let’s call that `showDefinitionView`. The first step we'll need to take in this handler is look up the submitted term in our newly-defined datastore like this: ``` // interactivity_handler.tsimport { ViewSubmissionHandler } from "deno-slack-sdk/functions/interactivity/types.ts";import { TermLookupFunction } from "./term_lookup_function.ts";import { TermsDatastore } from "../datastores/terms.ts";// This handler is invoked after a user submits a term to be definedexport const showDefinitionView: ViewSubmissionHandler< typeof TermLookupFunction.definition> = async ({ view, client }) => { const termEntered = view.state.values.term.action.value; if (termEntered.length < 1) { return { response_action: "errors", errors: { term_entered: "Must be 1 character or longer" }, }; } const queryResult = await client.apps.datastore.query({ datastore: TermsDatastore.name, expression: "#term = :term", expression_attributes: { "#term": "term" }, expression_values: { ":term": termEntered }, }); ``` For some helpful guidance on how this query was constructed, check out the [Datastores](/tools/deno-slack-sdk/guides/using-datastores) page. Once the query is run, we have two possible outcomes: the term is found and we return it to the user, or the term is not found and we ask the user if they’d like to submit a definition for it. Here’s the logic for the former: ``` // interactivity_handler.ts // If the term is found, display the associated definition if (queryResult.items.length >= 1) { return { response_action: "update", view: { "type": "modal", "callback_id": "second-page", "notify_on_close": false, "title": { "type": "plain_text", "text": termEntered }, "close": { "type": "plain_text", "text": "Close" }, "private_metadata": JSON.stringify({ termEntered }), "blocks": [ { "type": "section", "text": { "type": "mrkdwn", "text": queryResult.items[0].definition, }, }, ], }, }; } ``` This modal will present the user with the definition and a close button only. We don’t provide a submit button here because the modal is only informative; there is no new data to submit. Alternatively, if the term is not found, we’ll present the user with the option to submit a definition for it: ``` // interactivity_handler.ts // If the term is not found in the datastore, ask if they'd like to add a definition if (queryResult.items.length < 1) { return { response_action: "update", view: { "type": "modal", "callback_id": "add-definition", "notify_on_close": false, "title": { "type": "plain_text", "text": termEntered }, "close": { "type": "plain_text", "text": "Close" }, "submit": { "type": "plain_text", "text": "Click here to add one" }, "private_metadata": JSON.stringify({ termEntered }), "blocks": [ { "type": "section", "text": { "type": "plain_text", "text": `There is currently no definition for ${termEntered}`, }, }, ], }, }; }}; ``` Here, we’ve changed the text of the submit button to indicate that clicking it will allow the user to submit a definition of their own. So what happens when they click it? We’ll create another view submission handler for that. Something to make note of: notice how we carry forward the term itself in `private_metadata`. Without this, we would not have access to what term we are defining, since that data was submitted in a prior modal. Also make note of the `callback_id` of the modal; we’ll use that later to call the next handler. ## Create show definition submission view {#create-show-definition-submission-view-1} Once a user elects to submit a new definition for a term that does not have one, we need a new view to handle the input of that data. This is done through another view submission handler. Let’s call this one `showDefinitionSubmissionView` and add it to the same `interactivity_handler.ts` file that we put our first handler in. ``` // interactivity_handler.ts// This handler is invoked after a user elects to add a new definitionexport const showDefinitionSubmissionView: ViewSubmissionHandler< typeof TermLookupFunction.definition> = ({ view }) => { const { termEntered } = JSON.parse(view.private_metadata!); if (termEntered.length < 1) { return { response_action: "errors", errors: { term_entered: "Must be 1 character or longer" }, }; } return { response_action: "update", view: { "type": "modal", "callback_id": "definition-submission", "notify_on_close": false, "title": { "type": "plain_text", "text": termEntered }, "submit": { "type": "plain_text", "text": "Submit" }, "close": { "type": "plain_text", "text": "Close" }, "private_metadata": JSON.stringify({ termEntered }), "blocks": [ { "type": "section", "text": { "type": "mrkdwn", "text": `Add a definition for ${termEntered}`, }, }, { "type": "input", "block_id": "definition", "element": { "type": "plain_text_input", "action_id": "action", "multiline": true, }, "label": { "type": "plain_text", "text": "Definition" }, }, { "type": "context", "elements": [ { "type": "mrkdwn", "text": "You can use Slack markdown for this field, like `*bold*` and `_italics_`.", }, ], }, ], }, };}; ``` Once the user submits the button to add a new definition, we present this modal, which provides an input block for their definition, as well as submit and close buttons. Remember the term we stored away in `private_metadata`? We can now retrieve it to use as the title for this modal. We’ll again store it in `private_metadata` so that we can use it in the subsequent modal too. Again, take note of the `callback_id`, we’ll use this later. ## Create a confirmation view {#create-a-confirmation-view} The final view submission handler to write occurs once the user submits a new definition for the term. First let’s save the submitted definition to the datastore. ``` // interactivity_handler.ts// This handler is invoked after a new definition is submittedexport const showConfirmationView: ViewSubmissionHandler< typeof TermLookupFunction.definition> = async ({ view, client }) => { const { termEntered } = JSON.parse(view.private_metadata!); const definition = view.state.values.definition.action.value; let saveSuccess: boolean; const uuid = crypto.randomUUID(); const putResponse = await client.apps.datastore.put({ datastore: TermsDatastore.name, item: { id: uuid, term: termEntered, definition: definition, }, }); if (!putResponse.ok) { console.log("Error calling apps.datastore.put:"); saveSuccess = false; return { error: putResponse.error, }; } else { saveSuccess = true; } ``` This means we two different possible outcomes: the save is successful and the user is on their way, or the save is not successful. Here is the former: ``` if (saveSuccess == true) { return { response_action: "update", view: { "type": "modal", "callback_id": "completion_successful", "notify_on_close": false, "title": { "type": "plain_text", "text": `${termEntered} added` }, "close": { "type": "plain_text", "text": "Close" }, "blocks": [ { "type": "section", "text": { "type": "mrkdwn", "text": `We've added ${termEntered} to your company definitions.`, }, }, { "type": "divider", }, { "type": "section", "text": { "type": "mrkdwn", "text": `*${termEntered}*\n${definition}`, }, }, ], }, }; } ``` And the latter: ``` else { return { response_action: "update", view: { "type": "modal", "callback_id": "completion_not_successful", "notify_on_close": false, "title": { "type": "plain_text", "text": "Add definition" }, "close": { "type": "plain_text", "text": "Close" }, "blocks": [ { "type": "section", "text": { "type": "mrkdwn", "text": "Something went wrong and the save was not successful.", }, }, ], }, }; }}; ``` This concludes the logic for the `interactivity_handler.ts` file. Next, let’s see how these handlers are wired up. ## Write a view closed handler {#write-a-view-closed-handler} Back in `functions/term_lookup_function.ts`, we need to add the handler functions we just wrote in `interactivity_handler.ts`. Here’s how that’s done: ``` // term_lookup_function.ts .addViewSubmissionHandler( ["first-page"], showDefinitionView, ) .addViewSubmissionHandler( ["add-definition"], showDefinitionSubmissionView, ) .addViewSubmissionHandler( ["definition-submission"], showConfirmationView, ) ``` The first parameter of each function is the `callback_id` of the modal they respond to. Because these are view submission handlers, when the user clicks the submit button on the modal with the `callback_id` of “first-page”, the `showDefinitionView` submission handler will be called. When the modal with the `callback_id` of “add-definition” is submitted, `showDefinitionSubmissionView` is the handler that is called, and when the modal with the `callback_id` of “definition-submission” is submitted, `showConfirmationView` is the handler that is called. Finally, we’ll add a handler for when a view is closed, a view closed handler. This one is short; add it into the same file right after the functions above. ``` // term_lookup_function.ts .addViewClosedHandler( ["first-page", "add-definition", "definition-submission"], ({ view }) => { console.log(`view_closed handler called: ${JSON.stringify(view)}`); return { completed: true }; }, ); ``` This handler takes care of what happens when the view is closed from any of the three handlers we noted in the parameters. ## Implement a workflow {#implement-a-workflow} We are now ready to create a workflow as an entry point to our function. Create a new folder at the root of the project called `workflows` and add a file named `definition_workflow.ts`. ``` // definition_workflow.tsimport { DefineWorkflow, Schema } from "deno-slack-sdk/mod.ts";import { TermLookupFunction } from "../functions/term_lookup_function.ts";export const DefinitionWorkflow = DefineWorkflow({ callback_id: "definition_workflow", title: "Definition workflow", description: "A workflow to show you definitions and add them if they don't exist.", input_parameters: { properties: { interactivity: { type: Schema.slack.types.interactivity, }, }, required: ["interactivity"], },});DefinitionWorkflow.addStep(TermLookupFunction, { interactivity: DefinitionWorkflow.inputs.interactivity,});export default DefinitionWorkflow; ``` This is a workflow with only one step. We need to collect `interactivity` as an input parameter to pass along to the function and require no outputs. Next, we’ll update our manifest to declare all that we've created thus far. ## Update the manifest {#update-the-manifest} When we created the app via the CLI initially, a bare bones `manifest.ts` file was created that looks like this: ``` // manifest.tsimport { Manifest } from "deno-slack-sdk/mod.ts";/** * The app manifest contains the app's configuration. This * file defines attributes like app name and description. * docs.slack.dev/deno-slack-sdk/guides/using-the-app-manifest */export default Manifest({ name: "define-app", description: "A blank template for building Slack apps with Deno", icon: "assets/default_new_app_icon.png", functions: [], workflows: [], outgoingDomains: [], botScopes: ["commands", "chat:write", "chat:write.public"],}); ``` We’ll add to it now by reporting our function, workflow, datastore, and necessary scopes that the datastore requires. While we’re here, let’s update the description too. ``` import { Manifest } from "deno-slack-sdk/mod.ts";import { DefinitionWorkflow } from "./workflows/definition_workflow.ts";import { TermsDatastore } from "./datastores/terms.ts";import { TermLookupFunction } from "./functions/term_lookup_function.ts";export default Manifest({ name: "define-app", description: "This project allows users to look up and add new definitions of company acronyms and terms.", icon: "assets/default_new_app_icon.png", functions: [TermLookupFunction], workflows: [DefinitionWorkflow], datastores: [TermsDatastore], outgoingDomains: [], botScopes: [ "commands", "chat:write", "chat:write.public", "datastore:read", "datastore:write", ],}); ``` The app manifest is the app's configuration. It is very important that this file is structured correctly in order for your app to run smoothly. Each function, workflow, custom type, and datastore defined in an app must be declared in the manifest file. ## Create a trigger {#create-a-trigger} This is our final step before we are able to run our app! We need to add a trigger to kick off the workflow and collect that `interactivity` parameter needed to initiate a modal’s interactivity. Create one more folder at the root of the project and call it `triggers`. Add a file to it and name it `term_definition_trigger`. In it, place the following code: ``` // term_definition_trigger.tsimport { Trigger } from "deno-slack-sdk/types.ts";import { TriggerContextData, TriggerTypes } from "deno-slack-api/mod.ts";import { DefinitionWorkflow } from "../workflows/definition_workflow.ts";const termDefinitionTrigger: Trigger = { type: TriggerTypes.Shortcut, name: "Term Definition Trigger", description: "A trigger that starts the workflow to define a user-entered term", workflow: `#/workflows/${DefinitionWorkflow.definition.callback_id}`, inputs: { interactivity: { value: TriggerContextData.Shortcut.interactivity, }, },};export default termDefinitionTrigger; ``` This is a link trigger that, upon clicking, will initiate the workflow, which will call the function, which will allow the user to search for and add company definitions to our app. To get the link for that link trigger, run the following in your terminal: ``` slack trigger create —trigger-def triggers/term_definition_trigger.ts ``` After executing the command, select your app and workspace. The terminal will output a link called a "Shortcut URL", also known as your link trigger. Save that URL; we'll use it later. If you ever lose track of that URL, you can always run the command `slack triggers -info` and select your workspace to find it again. ## Run your app {#run-your-app} While in your project’s root directory, run this command in your terminal: ``` slack run ``` Choose your app and assign it to your workspace. Then, switch over to the app in Slack and test it out. Remember the link trigger you created earlier? Copy and paste that URL in a message to yourself in Slack. It will unfurl into a button that you can click to initiate the workflow. ## Share your app {#share-your-app} Because nobody knows everything, including company jargon, this would be a great app to share with your team. Check out [Deploy to Slack](/tools/deno-slack-sdk/guides/deploying-to-slack) to discover how to share this app with your team. ## Next steps {#next-steps} For your next challenge, perhaps consider creating an app to [create an issue in GitHub](/tools/deno-slack-sdk/tutorials/github-issues-app)! --- Source: https://docs.slack.dev/tools/deno-slack-sdk/tutorials/github-issues-app # GitHub issues app Workflow apps require a paid plan Join the [Developer Program](https://api.slack.com/developer-program) and provision a sandbox with access to all Slack features for free. In this tutorial, you'll learn how to build a workflow app to create an issue in GitHub. We'll walk you through sequencing the right workflow steps together and building a function to call GitHub's APIs. Even if you're not looking to create issues on GitHub, you'll learn how you might invoke any third-party API from a function. If creating issues on GitHub is actually what you want to do, then all you'll need to do is deploy your app when you're done customizing it. By following a form with a custom function that calls [an eminent endpoint by GitHub](https://docs.github.com/en/rest/issues/issues#create-an-issue), we can create an issue and close it ourselves! Some features you’ll acquaint yourself with while building this app include: * [Functions](/tools/deno-slack-sdk/guides/creating-slack-functions): the building blocks of common Slack functionality. * [Workflows](/tools/deno-slack-sdk/guides/creating-workflows): a set of steps for calling your functions that are executed in order. * [Custom functions](/tools/deno-slack-sdk/guides/creating-custom-functions): building blocks that _you_ define! * [Triggers](/tools/deno-slack-sdk/guides/using-triggers): for kicking off your workflows. Before we begin, ensure you have the following prerequisites completed: * Install the [Slack CLI](/tools/deno-slack-sdk/guides/getting-started). * Run `slack auth list` and ensure your workspace is listed. * If your workspace is not listed, address any issues by following along with the [Getting started](/tools/deno-slack-sdk/guides/getting-started), then come on back. ## Choose your adventure {#choose-your-adventure} After you've [installed the command-line interface](/tools/deno-slack-sdk/guides/getting-started), you have two ways you can get started: ### Use a blank app {#use-a-blank-app} You can create a blank app with the Slack CLI using the following command: ``` slack create github-functions-app --template https://github.com/slack-samples/deno-blank-template ``` ### Use a pre-built app {#use-a-pre-built-app} Or, you can use the pre-built [GitHub Functions app](https://github.com/slack-samples/deno-github-functions): ``` slack create github-functions-app --template https://github.com/slack-samples/deno-github-functions ``` ### Change your directory {#change-your-directory} Once you have your new project ready to go, change into your project directory. ## Create a GitHub personal access token {#create-a-github-personal-access-token} A personal access token is required when calling the GitHub API. Create a new token for this tutorial by visiting your developer settings on [GitHub](https://github.com/settings/tokens). Since your personal access token will be used in this tutorial, all issues created from the workflow will appear to have been created by your account. ### Select required scopes {#select-required-scopes} To access public repositories, create a new personal token [on GitHub](https://github.com/settings/tokens) with the following scopes: * `public_repo`, `repo:invite` * `read:org` * `read:user`, `user:email` * `read:enterprise` To prevent `404: Not Found` errors when attempting to access private repositories, the `repo` scope must also be selected. ## Add the token to your environment variables {#add-the-token-to-your-environment-variables} When developing locally, you can store your API credentials by adding your GitHub token to your app's environment variables. To do this, create a file called `.env` in the root directory of your project, and add your token to the file as follows: ``` GITHUB_TOKEN=ghp_1234AbCd5678 ``` For more information, refer to [using environment variables with the Slack CLI](/tools/slack-cli/guides/using-environment-variables-with-the-slack-cli). ## Define the custom function {#define-the-custom-function} Defining the definitions and manifest of our app gives us a birds-eye view before we dive into building. Open your text editor (we recommend VSCode with the Deno plugin) and point to the directory we created earlier. Since the workflow we're creating revolves around creating a new GitHub issue, we'll begin by defining a [custom function](/tools/deno-slack-sdk/guides/creating-custom-functions) with the inputs we know (the repository and information about the issue) and the outputs we expect (the issue number and link). The `DefineFunction` method will allow us to define the attributes that comprise this function. Here, we'll describe the attributes seen by other people and used by workflows, as well as the input and output types. ``` // functions/create_issue/definition.tsimport { DefineFunction, Schema } from "deno-slack-sdk/mod.ts";const CreateIssueDefinition = DefineFunction({ callback_id: "create_issue", title: "Create GitHub issue", description: "Create a new GitHub issue in a repository", source_file: "functions/create_issue/mod.ts", input_parameters: { properties: { url: { type: Schema.types.string, description: "Repository URL", }, title: { type: Schema.types.string, description: "Issue Title", }, description: { type: Schema.types.string, description: "Issue Description", }, assignees: { type: Schema.types.string, description: "Assignees", }, }, required: ["url", "title"], }, output_parameters: { properties: { GitHubIssueNumber: { type: Schema.types.number, description: "Issue number", }, GitHubIssueLink: { type: Schema.types.string, description: "Issue link", }, }, required: ["GitHubIssueNumber", "GitHubIssueLink"], },});export default CreateIssueDefinition; ``` The source code for `functions/create_issue/mod.ts` is shared in Step 4, but keep this definition in mind until then! Or rush ahead and write the function! We won't mind. 😉 ## Scaffold your workflow {#scaffold-your-workflow} Start by defining the workflow and outlining the steps. We'll add functions and inputs to these steps later. ``` // workflows/create_new_issue.tsimport { DefineWorkflow, Schema } from "deno-slack-sdk/mod.ts";const CreateNewIssueWorkflow = DefineWorkflow({ callback_id: "create_new_issue_workflow", title: "Create new issue", description: "Create a new GitHub issue", input_parameters: { properties: { interactivity: { type: Schema.slack.types.interactivity, }, channel: { type: Schema.slack.types.channel_id, }, }, required: ["interactivity", "channel"], },});/* Step 1 - Open a form */// const issueFormData = CreateNewIssueWorkflow.addStep( ... );/* Step 2 - Create a new issue */// const issue = CreateNewIssueWorkflow.addStep( ... );/* Step 3 - Post the new issue to channel */// CreateNewIssueWorkflow.addStep( ... );export default CreateNewIssueWorkflow; ``` ## Make your manifest {#make-your-manifest} Import and add these definitions to your app's manifest. ``` // manifest.tsimport { Manifest } from "deno-slack-sdk/mod.ts";import CreateIssueDefinition from "./functions/create_issue/definition.ts";import CreateNewIssueWorkflow from "./workflows/create_new_issue.ts";export default Manifest({ name: "Workflows for GitHub", description: "Bringing oft-used GitHub functionality into Slack", icon: "assets/icon.png", functions: [CreateIssueDefinition], workflows: [CreateNewIssueWorkflow], outgoingDomains: [], // If your organization uses a separate GitHub Enterprise domain, add that domain to this list // so that functions can make API calls to it. outgoingDomains: ["api.github.com"], botScopes: ["commands", "chat:write", "chat:write.public"],}); ``` ## Collect user input {#collect-user-input} You can call functions in an ordered sequence by adding them to your workflow. The Slack function [`OpenForm`](/tools/deno-slack-sdk/reference/slack-functions/open_form) can be used to collect input data that is used by later steps in the workflow. ``` // workflows/create_new_issue.ts.../* Step 1 - Open a form */const issueFormData = CreateNewIssueWorkflow.addStep( Schema.slack.functions.OpenForm, { title: "Create an issue", interactivity: CreateNewIssueWorkflow.inputs.interactivity, submit_label: "Create", description: "Create a new issue inside of a GitHub repository", fields: { elements: [{ name: "url", title: "Repository URL", description: "The GitHub URL of the repository", type: Schema.types.string, }, { name: "title", title: "Issue title", type: Schema.types.string, }, { name: "description", title: "Issue description", type: Schema.types.string, }, { name: "assignees", title: "Issue assignees", description: "GitHub username(s) of the user(s) to assign the issue to (separated by commas)", type: Schema.types.string, }], required: ["url", "title"], }, },); ``` ## Call your custom function {#call-your-custom-function} The second step of the workflow calls our custom function to create an issue on GitHub. Similar to other steps, the definition of this function is provided, along with the inputs to the function. ``` // workflows/create_new_issue.tsimport CreateIssueDefinition from "../functions/create_issue/definition.ts";.../* Step 2 - Create a new issue */const issue = CreateNewIssueWorkflow.addStep(CreateIssueDefinition, { url: issueFormData.outputs.fields.url, title: issueFormData.outputs.fields.title, description: issueFormData.outputs.fields.description, assignees: issueFormData.outputs.fields.assignees,}); ``` Notice how the input of this function uses the output from the form in our previous step! The output of this step — the values to be returned from our custom function — will be used to construct and post a message with the basic details of a newly-created issue. ## Post the GitHub response {#post-the-github-response} The Slack function [`SendMessage`](/tools/deno-slack-sdk/reference/slack-functions/send_message) can be used to post details about the newly-created issue in the channel. ``` // workflows/create_new_issue.ts.../* Step 3 - Post the new issue to channel */CreateNewIssueWorkflow.addStep(Schema.slack.functions.SendMessage, { channel_id: CreateNewIssueWorkflow.inputs.channel, message: `Issue #${issue.outputs.GitHubIssueNumber} has been successfully created\n` + `Link to issue: ${issue.outputs.GitHubIssueLink}`,}); ``` ## Craft the custom function {#craft-the-custom-function} Here's where you can take input from Slack, apply custom code to it, and return it back to a workflow. Copy and paste the following code into `functions/create_issue/mod.ts`. The `mod.ts` filename is a convention to declare the entry point to your function in `functions/create_issue`. ``` // functions/create_issue/mod.tsimport { SlackFunction } from "deno-slack-sdk/mod.ts";import CreateIssueDefinition from "./definition.ts";// https://docs.github.com/en/rest/issues/issues#create-an-issueexport default SlackFunction( CreateIssueDefinition, async ({ inputs, env }) => { const headers = { Accept: "application/vnd.github+json", Authorization: "Bearer " + env.GITHUB_TOKEN, "Content-Type": "application/json", }; const { url, title, description, assignees } = inputs; try { const { hostname, pathname } = new URL(url); const [_, owner, repo] = pathname.split("/"); // https://docs.github.com/en/enterprise-server@3.3/rest/guides/getting-started-with-the-rest-api const apiURL = hostname === "github.com" ? "api.github.com" : `${hostname}/api/v3`; const issueEndpoint = `https://${apiURL}/repos/${owner}/${repo}/issues`; const body = JSON.stringify({ title, body: description, assignees: assignees?.split(",").map((assignee: string) => { return assignee.trim(); }), }); const issue = await fetch(issueEndpoint, { method: "POST", headers, body, }).then((res: Response) => { if (res.status === 201) return res.json(); else throw new Error(`${res.status}: ${res.statusText}`); }); return { outputs: { GitHubIssueNumber: issue.number, GitHubIssueLink: issue.html_url, }, }; } catch (err) { console.error(err); return { error: `An error was encountered during issue creation: \`${err.message}\``, }; } },); ``` For the curious, this function dissects input from the workflow's form, then makes a POST API request to the ["Create an issue" GitHub API endpoint](https://docs.github.com/en/rest/issues/issues#create-an-issue). The result of this API call is then returned as output as defined in the function definition (`functions/creation_issue/definition.ts`); otherwise an error is returned. ## Create a trigger {#create-a-trigger} Triggers are how workflows are invoked. Each workflow can have multiple triggers. There are four types of triggers: [link triggers](/tools/deno-slack-sdk/guides/creating-link-triggers), [scheduled triggers](/tools/deno-slack-sdk/guides/creating-event-triggers), [event triggers](/tools/deno-slack-sdk/guides/creating-event-triggers), and [webhook triggers](/tools/deno-slack-sdk/guides/creating-webhook-triggers). A link trigger is what we'll be using. Link triggers are an interactive type, which means they require a user to manually start them. Define your link trigger in a separate file in a `triggers` folder called `create_new_issue_shortcut.ts`: ``` // triggers/create_new_issue_shortcut.tsimport { Trigger } from "deno-slack-api/types.ts";import CreateNewIssueWorkflow from "../workflows/create_new_issue.ts";const createNewIssueShortcut: Trigger< typeof CreateNewIssueWorkflow.definition> = { type: "shortcut", name: "Create GitHub issue", description: "Create a new GitHub issue in a repository", workflow: "#/workflows/create_new_issue_workflow", inputs: { interactivity: { value: "{{data.interactivity}}", }, channel: { value: "{{data.channel_id}}", }, },};export default createNewIssueShortcut; ``` Run the `trigger create` command in your terminal: ``` slack trigger create --trigger-def "triggers/create_new_issue_shortcut.ts" ``` After executing this command, select your workspace and choose the _Local_ app environment. When the process completes, you'll be given a link called "shortcut URL." This is your _link trigger_ for this workflow on this workspace. Save that URL for when you start testing. ## Run your code to test and tweak {#run-your-code-to-test-and-tweak} Here's the step we're going to leave you, but this is where your development experience will begin as you alter, test, falter, alter, and test again. You're building along and your workflow should be, too. You can use development mode to run this workflow in Slack directly from the machine you're reading this from now: ``` slack run ``` After you've chosen your development app and assigned it to your workspace, you can switch over to your Slack app and try out your new workflow. In Slack, you'll want to use the _link trigger_ you created earlier. Once you paste its URL into the message box and post it, it'll unfurl and give you a button to invoke the workflow. ## Next steps {#next-steps} For your next challenge, perhaps consider creating an app your users can use to [request time off](/tools/deno-slack-sdk/tutorials/request-time-off-app)! --- Source: https://docs.slack.dev/tools/deno-slack-sdk/tutorials/give-kudos-app # Give kudos app Workflow apps require a paid plan Join the [Developer Program](https://api.slack.com/developer-program) and provision a sandbox with access to all Slack features for free. ![Bot avatar](/assets/images/pirate-parrot-cbf152b9525877fc957ac26c3c39e15f.png) Kudo Ahoy matey! Welcome aboard the Give Kudos App Tutorial! This app will allow users to give kudos and share kind words with anyone in your workspace. We're setting sail on our maiden voyage – creating and deploying a workflow app – and would love to show you the ropes. By the end of the expedition, we'll have an app to parrot personal "kudos" throughout a workspace. Nothing is more important than a crew's morale, after all! A curiosity of the waters is not the only thing you need to sail the open seas, so ensure you have the following prerequisites completed, then climb aboard to discover what it takes to be captain of your own ship – that is, developing your own Slack app! Before we begin, ensure you have the following prerequisites completed: * Install the [Slack CLI](/tools/deno-slack-sdk/guides/getting-started). * Run `slack auth list` and ensure your workspace is listed. * If your workspace is not listed, address any issues by following along with the [Getting started](/tools/deno-slack-sdk/guides/getting-started), then come on back. ## Choose your heading {#choose-your-heading} How much guidance would you like on this journey? ## Use a blank app {#use-a-blank-app} If you're ready to take the helm yourself, you can create a blank app with the Slack CLI. Don't worry, we'll be right beside you. ``` slack create give-kudos-app --template https://github.com/slack-samples/deno-blank-template ``` ## Use a pre-built app {#use-a-pre-built-app} If you'd like to follow along without steering the ship, use the pre-built [Give Kudos app](https://github.com/slack-samples/deno-give-kudos): ``` slack create give-kudos-app --template https://github.com/slack-samples/deno-give-kudos ``` Once you have your new project ready to go, change into your project directory. ## Comprehend the manifest {#comprehend-the-manifest} Before setting sail, we'll need to take inventory of our ship's cargo. The [manifest](/tools/deno-slack-sdk/guides/using-the-app-manifest) is essentially an overview summary in a file at the root of your project. It is created automatically when you create a project using the Slack CLI. An inspection of the `manifest.ts` of the [sample app](https://github.com/slack-samples/deno-give-kudos) reveals a collection of workflows, functions, and other app-related attributes. ``` // manifest.tsimport { Manifest } from "deno-slack-sdk/mod.ts";import { FindGIFFunction } from "./functions/find_gif.ts";import { GiveKudosWorkflow } from "./workflows/give_kudos.ts";export default Manifest({ name: "Kudo", description: "Brighten someone's day with a heartfelt thank you", icon: "assets/icon.png", functions: [FindGIFFunction], workflows: [GiveKudosWorkflow], outgoingDomains: [], botScopes: ["commands", "chat:write", "chat:write.public"],}); ``` The details of this expedition’s manifest are soon to follow, but first let's shine a light through the fog and get a bearing on the three fundamentals of a Slack app: [workflows](/tools/deno-slack-sdk/guides/creating-workflows), [functions](/tools/deno-slack-sdk/guides/creating-slack-functions), and [triggers](/tools/deno-slack-sdk/guides/using-triggers). ## Define the app building blocks {#define-the-app-building-blocks} A [workflow](/tools/deno-slack-sdk/guides/creating-workflows) is a collection of processes, executed in response to certain events. In nautical terms, repairing the hull is a common workflow that happens when yours truly runs the ship aground. The processes that make up a workflow are known as [functions](/tools/deno-slack-sdk/guides/creating-slack-functions). Functions can be built-in, everyday actions such as sending a message or opening a form - or made [custom](/tools/deno-slack-sdk/guides/creating-custom-functions), defined by your own logic with various inputs and outputs. On the sea, raising the sails and scrubbing the decks are oft run functions. [Triggers](/tools/deno-slack-sdk/guides/using-triggers) act as inspiration for the actions of a workflow, defining when a workflow is invoked. Certain events in Slack (such as a clicked link or reaction added) or a schedule of specified time intervals can serve as triggers for a workflow. After taking in the view from the mast, we’re ready to set sail! ## Weave a workflow {#weave-a-workflow} The goal of our expedition is to build an app that can perform the task of parroting a personalized message. To do so, a workflow called "Give kudos" is created in `workflows/give_kudos.ts`. This workflow will contain all the actions our app needs to complete that task. ``` // workflows/give_kudos.tsimport { DefineWorkflow, Schema } from "deno-slack-sdk/mod.ts";const GiveKudosWorkflow = DefineWorkflow({ callback_id: "give_kudos_workflow", title: "Give kudos", description: "Acknowledge the impact someone had on you", input_parameters: { properties: { interactivity: { type: Schema.slack.types.interactivity, }, }, required: ["interactivity"], },});// your steps go hereexport { GiveKudosWorkflow }; ``` This workflow doesn't do anything - yet. For now, just note that it'll contain steps that require user interactivity. ## Sketch the steps {#sketch-the-steps} Our task of sharing kudos can be accomplished with three actions: * collecting information about a message, * finding the right GIF, * then sharing the love in the form of a message (bottle not included). Let's look at how these actions are added as steps to a workflow. Each step is composed of a function definition as well as the input object. The input object allows the outputs of one step to become inputs to another in a chain of functions. The first step is composed of a [Slack function](/tools/deno-slack-sdk/guides/creating-slack-functions), [`OpenForm`](/tools/deno-slack-sdk/reference/slack-functions/open_form), that will, as hinted, open a form for the user. This lets our app collect the kudos users want to give. ``` // workflows/give_kudos.ts.../* Step 1. Collect message information */const kudo = GiveKudosWorkflow.addStep( Schema.slack.functions.OpenForm, { title: "Give someone kudos", interactivity: GiveKudosWorkflow.inputs.interactivity, submit_label: "Share", description: "Continue the positive energy through your written word", fields: { elements: [{ name: "doer_of_good_deeds", title: "Whose deeds are deemed worthy of a kudo?", description: "Recognizing such deeds is dazzlingly desirable of you!", type: Schema.slack.types.user_id, }, { name: "kudo_channel", title: "Where should this message be shared?", type: Schema.slack.types.channel_id, }, { name: "kudo_message", title: "What would you like to say?", type: Schema.types.string, long: true, }, { name: "kudo_vibe", title: 'What is this kudo\'s "vibe"?', description: "What sorts of energy is given off?", type: Schema.types.string, enum: [ "Appreciation for someone 🫂", "Celebrating a victory 🏆", "Thankful for great teamwork ⚽️", "Amazed at awesome work ☄️", "Excited for the future 🎉", "No vibes, just plants 🪴", ], }], required: ["doer_of_good_deeds", "kudo_channel", "kudo_message"], }, },);... ``` The form will look like this for the user wanting to give kudos. ![The form within slack](/assets/images/give-kudos-open-form-5a3978c542ee4aa9a7f9d479d199e8b1.png) The second step is composed of a [custom function](/tools/deno-slack-sdk/guides/creating-custom-functions), `FindGIFFunction`. This is a function built specifically for this app. It'll find a GIF related to the "vibe" someone gives a message. ``` // workflows/give_kudos.tsimport { FindGIFFunction } from "../functions/find_gif.ts";.../* Step 2. Find the right GIF */const gif = GiveKudosWorkflow.addStep(FindGIFFunction, { vibe: kudo.outputs.fields.kudo_vibe,});... ``` Notice how the `OpenForm` step contains the `interactivity` context of the workflow and `FindGIFFunction` uses `kudo_vibe`, an output parameter of the `OpenForm` step. The third step is composed of a Slack function, [`SendMessage`](/tools/deno-slack-sdk/reference/slack-functions/send_message), that will send a message to a specific channel. It'll combine the user's words and the chosen GIF into one spectacular message fit for even the most seasoned sailors. That message will be sent to the channel the user specified within the form from step one. ``` // workflows/give_kudos.ts.../* Step 3. Share the love */GiveKudosWorkflow.addStep(Schema.slack.functions.SendMessage, { channel_id: kudo.outputs.fields.kudo_channel, message: `*Hey <@${kudo.outputs.fields.doer_of_good_deeds}>!* Someone wanted to share some kind words with you :otter:\n` + `> ${kudo.outputs.fields.kudo_message}\n` + `${gif.outputs.URL}`,});export { GiveKudosWorkflow };... ``` And that's all you need to do for Slack functions! `FindGIFFunction` is a custom function, however. We'll have to roll up our sleeves and build that out ourselves. ## Define the custom function {#define-the-custom-function} The `FindGIFFunction` function, unique to our app, is defined in `functions/find_gif.ts`, where inputs, outputs, and other attributes are described. This definition allows `FindGIFFunction` to be used within workflows, as when sharing a kudo! ``` // functions/find_gif.tsimport { DefineFunction, Schema } from "deno-slack-sdk/mod.ts";export const FindGIFFunction = DefineFunction({ callback_id: "find_gif", title: "Find a GIF", description: "Search for a GIF that matches the vibe", source_file: "functions/find_gif.ts", input_parameters: { properties: { vibe: { type: Schema.types.string, description: "The energy for the GIF to match", }, }, required: [], }, output_parameters: { properties: { URL: { type: Schema.types.string, description: "GIF URL", }, alt_text: { type: Schema.types.string, description: "description of the GIF", }, }, required: ["URL"], },}); ``` ## Find a GIF {#find-a-gif} With the `FindGIFFunction` function defined, it can then be implemented, where input from the form is parsed and converted into a random GIF that's obtained from the GIF catalog. ``` // functions/find_gif.tsimport { SlackFunction } from "deno-slack-sdk/mod.ts";import gifs from "../assets/gifs.json" assert { type: "json" };...const getEnergy = (vibe: string): string => { if (vibe === "Appreciation for someone 🫂") return "appreciation"; if (vibe === "Celebrating a victory 🏆") return "celebration"; if (vibe === "Thankful for great teamwork ⚽️") return "thankful"; if (vibe === "Amazed at awesome work ☄️") return "amazed"; if (vibe === "Excited for the future 🎉") return "excited"; if (vibe === "No vibes, just plants 🪴") return "plants"; return "otter"; // 🦦};interface GIF { URL: string; alt_text?: string; tags: string[];}const matchVibe = (vibe: string): GIF => { const energy = getEnergy(vibe); const matches = gifs.filter((g: GIF) => g.tags.includes(energy)); const randomGIF = Math.floor(Math.random() * matches.length); return matches[randomGIF];};export default SlackFunction(FindGIFFunction, ({ inputs }) => { const { vibe } = inputs; const gif = matchVibe(vibe ?? ""); return { outputs: gif };}); ``` ## A collection of GIFs {#a-collection-of-gifs} The GIF catalog – the treasure of this expedition – is listed in `assets/gifs.json`, but could safely be stored in a [datastore](/tools/deno-slack-sdk/guides/using-datastores). To do that, you would create a [datastore](/tools/deno-slack-sdk/guides/using-datastores) and use the [CLI command](/tools/slack-cli/reference/commands/slack_datastore_put) to save all of the GIFs to it. Think of datastores as your very own treasure chests. For readability, we've only included a portion of the GIFs in the snippet below. If you want the full treasure trove, [grab it from the GitHub repo](https://github.com/slack-samples/deno-give-kudos/blob/main/assets/gifs.json). ``` // assets/gifs.json[ { "URL": "https://media2.giphy.com/media/3oEjHWXddcCOGZNmFO/giphy.gif", "alt_text": "A person wearing a banana hat says thanks a bunch", "tags": ["thankful"] }, { "URL": "https://media.giphy.com/media/3fBVaRM2c79TtXbyi6/giphy.gif", "alt_text": "The future king of the pirates smiles at you", "tags": ["amazed"] }, { "URL": "https://media.giphy.com/media/WKdPOVCG5LPaM/giphy.gif", "alt_text": "A cheerful high-five from the newsroom", "tags": ["celebration", "excited"] }, { "URL": "https://media1.giphy.com/media/Lcn0yF1RcLANG/giphy-downsized.gif", "alt_text": "Wow! A feeling of wild disbelief overwhelms the senses", "tags": ["amazed"] }, { "URL": "https://media0.giphy.com/media/rgIdiNjWC933y/giphy.gif", "alt_text": "A kingly racoon nodding over many subjects", "tags": ["excited", "amazed"] }, { "URL": "https://media0.giphy.com/media/kyLYXonQYYfwYDIeZl/giphy.gif", "alt_text": "Elmo dances in celebration", "tags": ["celebration"] }, { "URL": "https://media2.giphy.com/media/3ohs7NuHL3gjbe2uGI/giphy-downsized.gif", "alt_text": "You're noticed and appreciated <3", "tags": ["appreciation"] }, { "URL": "https://media1.giphy.com/media/xUA7aOIFDR4ZgqLy8w/giphy.gif", "alt_text": "Fern having a messy hair day", "tags": ["plants"] }, "URL": "https://media1.giphy.com/media/MbAlP79yMRysHKUyHV/giphy-downsized.gif", "alt_text": "Sleepy otter rubs checks and yawns", "tags": ["otter"] }] ``` ## Invoke with a trigger {#invoke-with-a-trigger} The workflows and functions discussed above cannot be invoked until a trigger is created, similar to how a sailor needs an order from their captain to carry out tasks. To create an entry point into our app's "Give kudos" workflow, a [link trigger](/tools/deno-slack-sdk/guides/creating-link-triggers) is defined in `triggers/give_kudos.ts`. ``` // triggers/give_kudos.tsimport { Trigger } from "deno-slack-api/types.ts";const trigger: Trigger = { type: "shortcut", name: "Give some kudos", description: "Broadcast your appreciation with kind words and a GIF", workflow: "#/workflows/give_kudos_workflow", inputs: { interactivity: { value: "{{data.interactivity}}", }, },};export default trigger; ``` The trigger is then created using the Slack CLI with the [`slack trigger create`](/tools/slack-cli/reference/commands/slack_trigger_create) command, generating a Shortcut URL that can be clicked from a channel: ``` slack trigger create --trigger-def triggers/give_kudos.ts ``` You'll be prompted to install your app to a workspace. Select your desired workspace, and then select a `Local` app environment. When you want to [deploy](/tools/deno-slack-sdk/guides/deploying-to-slack) your app later on, you would repeat this step and select a `Deployed` app environment. ## Run your app {#run-your-app} With the Shortcut URL in hand, our destination is near. Run the app in development mode to test it out on your local machine. ``` slack run ``` Use the Shortcut URL within Slack to kick off the workflow. Play around and test the waters yourself. Once you're ready to deploy to other sailors, read on! ## Deploy your app {#deploy-your-app} When you're ready to welcome others aboard, you'll `deploy` your app instead of `run` it. Run the following command in your terminal: ``` slack deploy ``` And then create the trigger again, but choose the _Deployed_ option this time: ``` slack trigger create --trigger-def triggers/give_kudos.ts ``` There's no X, but rest assured you've found the ultimate treasure - the experience of building your very own _Give Kudos_ app. ![Bot avatar](/assets/images/pirate-parrot-cbf152b9525877fc957ac26c3c39e15f.png) Kudo Hey @You! Someone wanted to share some kind words with you: 'Congratulations on finishing the tutorial!' ## End your expedition {#end-your-expedition} Returning back to harbor, we can reminisce about the journey of sharing a kudo, recalling that functions compose workflows and workflows are invoked by triggers. With these ropes, you're ready to take the seas yourself! There are many more knots to learn while on these waters, but you'll stay afloat just fine with what you now know. ### The next adventure {#the-next-adventure} The next time you set sail, perhaps consider creating [a bot to welcome users to your workspace](/tools/deno-slack-sdk/tutorials/welcome-bot). Bon voyage, captain! --- Source: https://docs.slack.dev/tools/deno-slack-sdk/tutorials/governing-slack-connect-invites # Governing Slack Connect invites Workflow apps require a paid plan Join the [Developer Program](https://api.slack.com/developer-program) and provision a sandbox with access to all Slack features for free. This guide will show you how to create a Slack workflow to automate the process of governing [Slack Connect](https://docs.slack.dev/apis/slack-connect/) invitations. ## Prerequisites {#prerequisites} You'll need to have admin access to your Slack workspace to continue! Complete the following steps to allow workflow automations to interact with your Slack Connect invitations. 1. In the Admin Dashboard, under **Slack Connect Settings**, enable the “Apply automation rules before channel invitations are sent” preference within **Slack Connect Settings** in the Admin Dashboard. 2. To allow users to request invites, in the Admin Dashboard, under **Slack Connect Settings**, then **Channels**, enable the "Sending Invitations with Permission to Post Only" or "Sending Invitations with permission to post, invite and more" preference. In addition, please consider the following before proceeding: * Only external invites sent by members of your organization to channels owned by your organization will be governed by these automation tools. * MPDM to Private Channel conversions are not considered invitations and will not be governed by automation rules. We recommend you review your policy around MPDM to Private Channel changes (under **Slack Connect Settings**). * Admins and owners and those who have the permission to approve Slack Connect invitations have implicit approval to send invitation requests, meaning their invitations will not be held and subject to automation rules. * Requests to Invite from Bots that have the [`conversations.connect:manage`](https://docs.slack.dev/reference/scopes/conversations.connect.manage) scope will implicitly be sent and will not be held and subject to automation rules. * Admin request messages will be directed to the same channels or individuals as specified under “Who can approve requests and manage channels?” and “Send requests to…” under **Approving Channel Invitations**. ## Setting up your workflow app {#setting-up} ### 1. Create a Deno Slack SDK app from the Slack CLI {#create} If you haven't yet, install and authorize the [Slack CLI](/tools/deno-slack-sdk/guides/getting-started). Then use the `create` Slack CLI command to create a workflow app. ``` slack create my-app ``` ### 2. Add the conversations.connect:manage scope {#add-scope} Scopes are added to workflows via the manifest. Modify the `botScopes` array to include the [`conversations.connect:manage`](https://docs.slack.dev/reference/scopes/conversations.connect.manage) scope. ``` botScopes: ["conversations.connect:manage"], ``` ## Creating the workflow {#workflow} ### 1. Define a function with input and output parameters for handling invite requests {#define-function} [Functions](/tools/deno-slack-sdk/guides/creating-slack-functions) are the building blocks of workflow apps. We'll be using a [custom function](/tools/deno-slack-sdk/guides/creating-custom-functions) to utilize the Slack Connect API methods. Let's define the function now. ``` import { DefineFunction, Schema } from "deno-slack-sdk/mod.ts";import { SlackFunction } from "deno-slack-sdk/mod.ts";import { SlackAPI } from "deno-slack-api/mod.ts";export const RequestInfoFunction = DefineFunction({ callback_id: "request_info_function", title: "Request to Invite", description: "Handle the requested invites", source_file: "functions/request_info.ts", input_parameters: { properties: { invite_request: { type: Schema.types.object, }, }, required: ["invite_request"], }, output_parameters: { properties: { result: { type: Schema.types.string, }, }, required: ["result"], },}); ``` ### 2. Define the main logic to handle Slack Connect Invitation Requests {#define-logic} You can filter invitees, automatically approving or denying certain requests. This is done using two Web API methods: * [`conversations.requestSharedInvite.approve`](https://docs.slack.dev/reference/methods/conversations.requestSharedInvite.approve) * [`conversations.requestSharedInvite.deny`](https://docs.slack.dev/reference/methods/conversations.requestSharedInvite.deny) In this example the filtering is based on their email domains. ``` // Define some constants to be used for domain filtering logic.const ALLOWED_EMAIL_DOMAINS = ["@slack-corp.com", "@approved-vendor.com"];const BLOCKED_EMAIL_DOMAINS = ["@danger.com"];const REQUEST_REASON_EMAIL_DOMAINS = ["@gmail.com"];const filterInvites = (invitees: any[], domains: string[]) => invitees.filter((invite: any) => domains.some((domain) => invite.email.endsWith(domain)) );const handleInvites = async (client: any, invites: any[], action: string, message?: string) => { for (const invite of invites) { const response = await client.apiCall(`conversations.requestSharedInvite.${action}`, { invite_id: invite.invite_id, message,}); }}; ``` You also have the ability to prompt the user for additional information via a form link when needed. ``` const postReasonRequestMessage = async (client: any, userId: string) => { // This code snippet sends a message to the user with a link to a form // prompting for more information. const response = await client.chat.postMessage({ channel: userId, // Use the userId of the requesting user text: "Provide more info about SC channel request", blocks: [ { "type": "header", "text": { "type": "plain_text", "text": "Slack Connect Request Details", }, }, { "type": "context", "elements": [ { "type": "mrkdwn", "text": "Provide the following details about the requested invitees.", } ], }, { "type": "actions", "elements": [ { "type": "button", "text": { type: "plain_text", text: "Start", emoji: true, }, action_id: "open_form", value: "value1_approve", style: "primary", } ], }, ], }); return { completed: false, };}; ``` Let's use these features now in a custom function. ### 3. Define the main function {#define-main-function} This function uses the filters previously created to automatically sort invites. The `BLOCKED_EMAIL_DOMAINS` values are denied, the `ALLOWED_EMAIL_DOMAINS` values are approved, and the the `REQUEST_REASON_EMAIL_DOMAINS` values are followed up with a prompt asking for additional information with the info set up in the previous instruction step using a [Block Kit actions handler](/tools/deno-slack-sdk/guides/creating-an-interactive-modal#open-block-kit-action). ``` export default SlackFunction(RequestInfoFunction, async ({ inputs, tokens, env }) => { const invitees = inputs.invite_request.target_users; const client = SlackAPI(token, { slackApiUrl: env.SLACK_API_URL }); // Filter invitees based on email domain. const denyInvites = filterInvites(invitees, BLOCKED_EMAIL_DOMAINS); const approveInvites = filterInvites(invitees, ALLOWED_EMAIL_DOMAINS); const requiresReasonInvites = filterInvites(invitees, REQUEST_REASON_EMAIL_DOMAINS); await handleInvites(client, approveInvites, "approve"); await handleInvites(client, denyInvites, "deny", "The recipients are not part of a pre-approved organization to work with."); // Auto-approve or deny based on blocked or allow domain criteria. // Prompt for more information if meets needs more info status. if (requiresReasonInvites.length) { const userId = inputs.invite_request.actor.id; await postReasonRequestMessage(client, inputs.invite_request.actor.id); } return { completed: false }; },) .addBlockActionsHandler( "open_form", async ({ inputs: _inputs, body, token, env }) => { const formMetadata = { ts: body?.message?.ts ?? "", message_channel_id: body?.channel?.id ?? "", }; // Handle button click to open a form for additional information. const client = SlackAPI(token, { slackApiUrl: env.SLACK_API_URL }); // Build form that requests a reason for the invite const view_payload = { interactivity_pointer: body.interactivity.interactivity_pointer, view: { private_metadata: JSON.stringify(formMetadata), type: "modal", title: { type: "plain_text", text: "Details about the Connecting Teams", }, submit: { type: "plain_text", text: "Submit", }, blocks: [ { "type": "section", "text": { "type": "mrkdwn", "text": "Additional information", }, }, { "type": "divider", }, { "type": "input", "block_id": "reason_block", "element": { "type": "plain_text", "action_id": "reason_input", "multiline": true, "placeholder": { "type": "plain_text", "text": "Please provide a reason for this invitation.", }, }, "label": { "type": "plain_text", "text": "Reason", }, }, ], } }; }, ) .addViewSubmissionHandler( "approval_info_modal", async ({ body, view, token, env, inputs }) => { // Handle View Submission // Update the Slackbot message to end workflow submissions const privateMetadata = JSON.parse(view.private_metadata ?? ""); const client = SlackAPI(token, { slackApiUrl: env.SLACK_API_URL, }); // Update message to user to show that submission was made with the reason const resp = await client.chat.update({ channel: privateMetadata.message_channel_id, ts: privateMetadata.ts, as_user: true, text: "Provide more info about SC channel request", blocks: [ { "type": "header", "text": { "type": "plain_text", "text": "Slack Connect Approval Details", }, }, { "type": "context", "elements": [{ "type": "mrkdwn", "text": "Thank you for your submission :white_check_mark:", }, ], }, ], }); // TODO: Handle user input in the way you want const reasonForInviteRequest = view.state.values.reason_block.reason_input.value; },); ``` ### 4. Define the workflow {#define-workflow} With the desired functionality achieved in the custom function, now it needs to be added to a workflow. The following defines a workflow and adds the `RequestInfoFunction` as a step. ``` import { DefineWorkflow, Schema } from "deno-slack-sdk/mod.ts";import { RequestInfoFunction } from "../functions/request_info.ts";const InviteRequested = DefineWorkflow({ callback_id: "invite_requested_workflow", title: "Invite Requested Workflow", description: "", input_parameters: { properties: { invite_request: { type: Schema.types.object, description: "A requested invite and its metadata", }, }, required: [], },});// This is the step that makes the workflow process that invite request as defined in the // main function.InviteRequested.addStep(RequestInfoFunction, { invite_request: InviteRequested.inputs.invite_request,});export default InviteRequested; ``` ### 5. Create the event trigger {#event-trigger} Workflows are only invoked by [triggers](/tools/deno-slack-sdk/guides/using-triggers). We'll be using an [event trigger](/tools/deno-slack-sdk/guides/creating-event-triggers) to listen for the [`shared_channel_invite_requested`](https://docs.slack.dev/reference/events/shared_channel_invite_requested) event to invoke the created workflow. ``` import { Trigger } from "deno-slack-api/types.ts";import { TriggerTypes } from "deno-slack-api/mod.ts";import InviteRequested from "../workflows/invite_requested.ts";const inviteRequestedTrigger: Trigger = { type: TriggerTypes.Event, name: "InviteRequested", description: "Gather details about the requested invite", workflow: "#/workflows/invite_requested_workflow", event: { event_type: "slack#/events/shared_channel_invite_requested", team_ids: [""],},inputs: { invite_request: { value: "{{data}}", },},};export default inviteRequestedTrigger; ``` ✅ Function ✅ Workflow ✅ Trigger And look at that, you've assembled all the parts of a workflow app! Now you'll just need to decide how to use it. ## Onward {#onward} ➡️ **To learn how to deploy your workflow app**, head over to the [Deploy your app](/tools/deno-slack-sdk/guides/deploying-to-slack) page. ✨ **To learn more about using Slack Web API methods in your workflows**, head over to the [Slack API calls](/tools/deno-slack-sdk/guides/calling-slack-api-methods) page. --- Source: https://docs.slack.dev/tools/deno-slack-sdk/tutorials/hello-world-app # Hello world app Workflow apps require a paid plan Join the [Developer Program](https://api.slack.com/developer-program) and provision a sandbox with access to all Slack features for free. In this tutorial, we're going to create an app based on the pre-built Hello World app. This is an app that will send a greeting to a channel. We'll create an app, interact with it in our workspace, then review the components that made that interaction possible. Before we begin, ensure you have the following prerequisites completed: * Install the [Slack CLI](/tools/deno-slack-sdk/guides/getting-started). * Run `slack auth list` and ensure your workspace is listed. * If your workspace is not listed, address any issues by following along with the [Getting started](/tools/deno-slack-sdk/guides/getting-started), then come on back. ## Choose your adventure {#choose-your-adventure} We can create our "Hello World" app in one of two ways: ### Use a blank app {#use-a-blank-app} You can create a blank app with the Slack CLI using the following command: ``` slack create hello-world-app --template https://github.com/slack-samples/deno-blank-template ``` ### Use a pre-built app {#use-a-pre-built-app} Or, you can use the pre-built [Hello World app](https://github.com/slack-samples/deno-hello-world): ``` slack create hello-world-app --template https://github.com/slack-samples/deno-hello-world ``` For this tutorial, we'll use the pre-built app. Once you have your new project ready to go, change into your project directory. ## Explore the app structure {#explore-the-app-structure} Let's take a look at what's inside our new "Hello World" project directory: ``` LICENSEREADME.mdassets/deno.jsoncfunctions/import_map.jsonmanifest.tsslack.jsontriggers/workflows/ ``` The first place to direct your attention are the `functions`, `triggers`, and `workflows` folders. These are where the definitions and implementations for the inner workings of your app live. The next place to look is the `manifest.ts` file. This contains your app's manifest, which is where we can configure things like bot scopes and tell our app about our workflows. Other items in the project include: * `.slack/`: a home for internal configuration files, scripts hooks, and the app SDK. _This directory must be checked into your version control._ You'll also notice a `.slack/apps.dev.json` once you begin building: this file is in `.gitignore` and **should not** be checked in to version control. * `import_map.json`: a helper file for Deno that specifies where modules should be imported from. * `assets/`: a place to store assets related with the project. This is a great place to store the icon that your app will display when users interact with it. With our project ready, it's time to take it for a spin — but before we do, we have _one more_ thing to do, which is to create the trigger that we'll use to kick off our workflow. We'll talk about triggers and the specific kind we're going to create in the next section. Onward! ## Trigger time {#trigger-time} Inside the `triggers` folder, there's a file called `greeting_trigger.ts`. This is a trigger configuration file. It's used by the CLI to create a type of [trigger](/tools/deno-slack-sdk/guides/using-triggers) called a "Link trigger." Since this is a working sample app, it comes pre-baked with working code. The only thing we need to do so that the app will work correctly is to create the one trigger it uses to kick things off. To create the trigger, use the `trigger create` command: ``` $ slack trigger create --trigger-def "triggers/greeting_trigger.ts" ``` Since you haven't installed this trigger to a workspace yet, you'll be prompted to install the trigger to a new workspace. Select an authorized workspace in which to install the app. When you select your workspace, you will be prompted to choose an app environment for the trigger. Choose the _Local_ option so you can interact with your app while developing locally. The CLI will then finish installing your trigger. Once your app's trigger is finished being installed, you'll see the following output: ``` 📚 App Manifest Created app manifest for "hello-world (local)" in "myworkspace" workspace🏠 Workspace Install Installed "hello-world (local)" app to "myworkspace" workspace Finished in 1.7s⚡ Trigger created Trigger ID: ABCD1234EFGH Trigger Type: shortcut Trigger Name: Send a greeting Trigger Created Time: 2023-03-31 10:02:15 -04:00 Trigger Updated Time: 2023-03-31 10:02:15 -04:00 URL: https://slack.com/shortcuts/ABCD1234EFGH/01d8db3db6ea1a9e05012a90028ed678 ``` See that "URL" in the output? Copy it from the terminal output — that's going to be how we start our workflow — and head to the next section to try it out! ## Starting a local development server {#starting-a-local-development-server} With our Shortcut URL in hand (or, rather, in our clipboard), paste it into any public channel in your workspace. This will unfurl into a card with a **Run** button. You will also see your shortcut in the bookmarks bar in the `workflows` folder. If you try to interact with your app right now, nothing will happen since our local development server isn't running yet. So let's get our local server running with the `run` command: ``` $ slack run ``` Once your development server is running, click the **Run** button on the unfurled card, or select your shortcut's name from the `workflows` folder in the bookmark bar to start the workflow assigned to that trigger. In the window that pops up, fill out the form and click the **Send greeting** button. In the channel from which you executed the workflow, you'll see a new message for the user you selected in the form. * * * So far we have: * created an app based on the "Hello World" app template * created a **trigger** to interact with our app, which produced a **Shortcut URL** * started a **local development server** But we've only scratched the surface. The trigger you created is configured to call a [workflow](/tools/deno-slack-sdk/guides/creating-workflows), and each workflow is configured to call one or more [functions](/tools/deno-slack-sdk/guides/creating-slack-functions). In the next section, let's dive in to see how everything is wired together! ## Take a look around: workflows {#take-a-look-around-workflows} Let's open up the trigger file `triggers/greeting_trigger.ts`, and see how it relates to the rest of the app: ``` // greeting_trigger.tsimport { Trigger } from "deno-slack-api/types.ts";import GreetingWorkflow from "../workflows/greeting_workflow.ts";const greetingTrigger: Trigger = { type: "shortcut", name: "Send a greeting", description: "Send greeting to channel", workflow: "#/workflows/greeting_workflow", inputs: { interactivity: { value: "{{data.interactivity}}", }, channel: { value: "{{data.channel_id}}", }, },};export default greetingTrigger; ``` Triggers take inputs and pass them along to an assigned workflow. Our trigger above is configured to invoke the `greeting_workflow`; notice the special string formatting for calling the workflow's name. When you create a trigger using a trigger definition like this one, your app will look for that workflow in all the workflows that you have registered in your manifest. Let's go back to the parent folder of our project and open up the `manifest.ts` file next: ``` // manifest.tsimport { Manifest } from "deno-slack-sdk/mod.ts";import GreetingWorkflow from "./workflows/greeting_workflow.ts";export default Manifest({ name: "deno-hello-world", description: "A sample that demonstrates using a function, workflow and trigger to send a greeting", icon: "assets/default_new_app_icon.png", workflows: [GreetingWorkflow], outgoingDomains: [], botScopes: ["commands", "chat:write", "chat:write.public"],}); ``` Here you can see the `workflows` property in your app's manifest. This is where you will list out all of your workflows. Notice at the top there's something that we saw in the trigger file, too: an import call to `GreetingWorkflow`. The manifest registers the workflow, and then the trigger is configured to invoke it. With that in mind, let's open up that workflow to see what's going on: ``` // workflows/greeting_workflow.tsimport { DefineWorkflow, Schema } from "deno-slack-sdk/mod.ts";import { GreetingFunctionDefinition } from "../functions/greeting_function.ts";// Here we define a new workflow called GreetingWorkflow, configuring its // required input parameters. Note how one of the input parameters is of type // `Schema.slack.types.interactivity`:const GreetingWorkflow = DefineWorkflow({ callback_id: "greeting_workflow", title: "Send a greeting", description: "Send a greeting to channel", input_parameters: { properties: { interactivity: { type: Schema.slack.types.interactivity, }, channel: { type: Schema.slack.types.channel_id, }, }, required: ["interactivity"], },});// Once the workflow is defined, we can "add steps" to the workflow with the // titular method `addStep`. In this case, we're using the Slack function // `OpenForm` to leverage that interactivity input parameter in order to // interact with the user (with a form):const inputForm = GreetingWorkflow.addStep( Schema.slack.functions.OpenForm, { title: "Send a greeting", interactivity: GreetingWorkflow.inputs.interactivity, submit_label: "Send greeting", fields: { elements: [{ name: "recipient", title: "Recipient", type: Schema.slack.types.user_id, }, { name: "channel", title: "Channel to send message to", type: Schema.slack.types.channel_id, default: GreetingWorkflow.inputs.channel, }, { name: "message", title: "Message to recipient", type: Schema.types.string, long: true, }], required: ["recipient", "channel", "message"], }, },);// After the first step, which is to send the form, we use the form data// in subsequent steps. Here, we are passing it along as inputs to // a custom function defined by `GreetingFunctionDefinition`. You'll note that // we also imported this into our workflow file. const greetingFunctionStep = GreetingWorkflow.addStep( GreetingFunctionDefinition, { recipient: inputForm.outputs.fields.recipient, message: inputForm.outputs.fields.message, },);// Finally, we're using another Slack function called `SendMessage` to // send the results of our custom function to a channel specified by the // user filling out the form:GreetingWorkflow.addStep(Schema.slack.functions.SendMessage, { channel_id: inputForm.outputs.fields.channel, message: greetingFunctionStep.outputs.greeting,});export default GreetingWorkflow; ``` The trigger invokes the workflow, and the workflow invokes one or more [custom](/tools/deno-slack-sdk/guides/creating-custom-functions) or [built-in](/tools/deno-slack-sdk/guides/creating-slack-functions) functions. The workflow is also registered in the app's manifest. Adding custom functions to your app is very similar to adding workflows, except you don't have to register them in the manifest; any functions that your workflows use are automatically registered with your app. In our next section, let's take a look at the custom function that our workflow uses. ## Take a look around: functions {#take-a-look-around-functions} Inside the `functions` folder we'll find the star of the show, our "Greeting" function, in `greeting_function.ts`. This file contains both the function definition and its implementation. At the top, just after the imports, is the definition: ``` // greeting_function.tsimport { DefineFunction, Schema, SlackFunction } from "deno-slack-sdk/mod.ts";export const GreetingFunctionDefinition = DefineFunction({ callback_id: "greeting_function", title: "Generate a greeting", description: "Generate a greeting", source_file: "functions/greeting_function.ts", input_parameters: { properties: { recipient: { type: Schema.slack.types.user_id, description: "Greeting recipient", }, message: { type: Schema.types.string, description: "Message to the recipient", }, }, required: ["message"], }, output_parameters: { properties: { greeting: { type: Schema.types.string, description: "Greeting for the recipient", }, }, required: ["greeting"], },}); ``` Notice how it looks very similar to our workflow definition; we have inputs, outputs, and the option to mark parameters required. Below that is its implementation: ``` export default SlackFunction( GreetingFunctionDefinition, ({ inputs }) => { const { recipient, message } = inputs; const salutations = ["Hello", "Hi", "Howdy", "Hola", "Salut"]; const salutation = salutations[Math.floor(Math.random() * salutations.length)]; const greeting = `${salutation}, <@${recipient}>! :wave: Someone sent the following greeting: \n\n>${message}`; return { outputs: { greeting } }; },); ``` If you go back to the workflow file, you'll see that when this function is added as a step to the workflow, the first context property we pass along is its definition. This gives us strong typing right out of the box for our custom functions. With our trigger calling our workflow, our workflow calling our functions, and our functions automating things in our workspace, we've now seen a very small sampling of what workflow apps can do! ## Wrap it up {#wrap-it-up} In this tutorial we've taken a tour of the "Hello World" sample app. This example only shows one [trigger](/tools/deno-slack-sdk/guides/using-triggers), [workflow](/tools/deno-slack-sdk/guides/creating-workflows), and [custom function](/tools/deno-slack-sdk/guides/creating-custom-functions), and is limited to essentially passing a string around. ### Next steps {#next-steps} For your next challenge, perhaps consider creating [a bot to welcome users to your workspace](/tools/deno-slack-sdk/tutorials/welcome-bot)! --- Source: https://docs.slack.dev/tools/deno-slack-sdk/tutorials/mobile-request # Build a mobile request workflow In this tutorial, we'll design a workflow that streamlines the process for employees to request an upgrade to their mobile device. The workflow will automate checking a database for each employee’s device and upgrade eligibility, then send the request to their manager for approval. Here's how it works: * This workflow uses a link trigger to kickstart the process. Running the trigger opens a form for the user to fill out with details about their request. * Once the form is submitted, the app will query a datastore to retrieve the user’s current device details, respond with the information in a thread, and send the request to the approving manager. * The manager will receive a summary with options to approve or deny the request. Once they take action, the app will update the original thread with the result. ## Create a workflow in Workflow Builder {#create-a-workflow-in-workflow-builder} To create a new workflow, you will need to open Workflow Builder in Slack. You can open Workflow Builder using one of the following methods. * **Use the message box:** In any channel, type `/workflow` and select **Create a workflow**. * **Use the sidebar:** Navigate to the left sidebar in Slack and click **More**, then **Tools**, then **Workflows**. Click **+New** then **Build Workflow** to create a new workflow. * Under **Start the workflow...**, click **Choose an event**, then select **From a link in Slack**. Next, we need to create a form in which we will collect data from the requester. * Click the button to continue, then select **Add steps** to **Collect info in a form**. * Give the form a name, like `Mobile Request Form`, and click **\+ Add question**. Enter `Which mobile device would you like?` as the question and use **Short answer** for the Question type. Click **Done**. * Repeat the process for adding the following question: `What is the urgency of this request?`. Use a dropdown for the Question type and enter a few options for Urgent, Normal, and Low. * Add one more question to the form for `Who is your manager?`. For this question, use the Slack user option as the Question type. Save the form. Next, add a step to the workflow and select **Send a message to a channel**. Select **Channel where the workflow was used** in the **Select a channel** box. Draft a message to send to the channel, and use the **Insert a variable** link at the bottom of the message composer to insert variables collected from the form. Save the step. ![workflow steps](/assets/images/workflow-steps-3aa3d3dca0a24c55c8073dc59d7ddbfe.png) ## Publish the workflow {#publish-the-workflow} Once the form and message steps are created, it’s time to publish your workflow. Give it a name—Mobile Request Workflow—and click **Finish Up** to publish it. Verify everything looks correct in the modal, then click **Publish**. After publishing, a link trigger will appear. This trigger is what users will click to initiate the workflow. You can copy this link and share it as a bookmark in a Slack channel, post it as a message, or even add it to the channel canvas. ## Set up a new project {#set-up-a-new-project} In order to add the custom steps needed to look up the requesting user and their mobile device from a datastore, we need to create a new Deno Slack SDK project. ### Install the tools {#install-the-tools} We are using the [Slack CLI](/tools/slack-cli) and [Deno Slack SDK](/tools/deno-slack-sdk) to create an app where the custom functions exist and can be pulled into Workflow Builder to be used as steps. You should be familiar with the Slack CLI and how it works before going further. If this is new for you, we recommend starting with the [Hello world app](/tools/deno-slack-sdk/tutorials/hello-world-app) first. ### Create the project {#create-the-project} Navigate to a directory where you have permission to create new files. Using the Slack CLI, run the following command to create a new project from a template: ``` slack create mobile-request-app --template slack-samples/deno-blank-template ``` This create your project from a blank template. After that, navigate to your project folder. ``` cd mobile-request-app ``` Open the project in VSCode. ``` code . ``` ## Code the app {#code-the-app} ### Manifest {#manifest} The `manifest.ts` file holds important metadata about your app, like the app name, description, and scopes it requires. Any functions you define in the project will show up in Slack’s Workflow Builder once the app is running. Edit this file by replacing the template code with what is shown here, using an image of your choice for the `icon`: ``` import { Manifest } from "deno-slack-sdk/mod.ts";import MobileDatastore from "./datastores/mobile_datastore.ts";import { ReviewRequestDefinition } from "./functions/review_request_function.ts";import { GetMobileDeviceDefinition } from "./functions/check_datastore_function.ts";export default Manifest({ name: "mobile-request-functions", description: "A set of functions to interact with an internal datastore of mobile devices.", icon: "icon.png", workflows: [], outgoingDomains: [], datastores: [MobileDatastore], functions: [GetMobileDeviceDefinition, ReviewRequestDefinition], botScopes: [ "commands", "chat:write", "chat:write.public", "datastore:read", ],}); ``` You will likely have lots of red underlines in VSCode at this point. Not to worry, we will fix those with the next steps. We have declared two functions and a datastore in this manifest, so let's write those now. ### Functions {#functions} Create a new folder in your project named `functions`, then create a new file within the folder named `check_datastore_function.ts`. In it, copy and paste this code, then save the file: ``` import { DefineFunction, Schema, SlackFunction } from "deno-slack-sdk/mod.ts";import MobileDatastore from "../datastores/mobile_datastore.ts";export const GetMobileDeviceDefinition = DefineFunction({ callback_id: "check_datastore_function", title: "Check mobile database", description: "Check internal database for a user's mobile information.", source_file: "functions/check_datastore_function.ts", input_parameters: { properties: { user: { type: Schema.types.string, description: "User id of requestor.", }, }, required: ["user"], }, output_parameters: { properties: { last_upgrade: { type: Schema.types.string, description: "The last time the user had a device upgrade.", }, mobile_device: { type: Schema.types.string, description: "The user's current mobile device", }, }, required: ["mobile_device", "last_upgrade"], },});export default SlackFunction( GetMobileDeviceDefinition, async ({ inputs, client }) => { const queryResp = await client.apps.datastore.query< typeof MobileDatastore.definition >( { datastore: MobileDatastore.name, expression: "#user = :user", expression_attributes: { "#user": "user" }, expression_values: { ":user": inputs.user }, }, ); console.log("Datastore response", queryResp); if (!queryResp.ok) { console.error("Error pulling from database!", queryResp.error); } let mobile_device; let last_upgrade; // For demonstration purposes, you'll need to seed the database manually through the CLI. // This step displays "N/A" so that the workflow doesn't break. if (queryResp.items.length === 0) { mobile_device = "N/A"; last_upgrade = "N/A"; } else { const item = queryResp.items[0]; mobile_device = item.mobile_device; last_upgrade = item.last_upgrade; } return { outputs: { mobile_device: mobile_device, last_upgrade: last_upgrade, }, }; },); ``` This function checks the user’s current device and whether it’s eligible for an upgrade. It fetches this information from a datastore. Create another file in the `functions` folder and name it `review_request_function.ts`. Copy and paste the following code into the file, then save it: ``` import { DefineFunction, Schema, SlackFunction } from "deno-slack-sdk/mod.ts";const APPROVE_ID = "approve_request";const DENY_ID = "deny_request";export const ReviewRequestDefinition = DefineFunction({ callback_id: "review_request_function", title: "Review a mobile request", description: "Sends a message to the admin within a thread to approve or deny a request", source_file: "functions/review_request_function.ts", input_parameters: { properties: { manager: { type: Schema.slack.types.user_id, description: "The user's manager", }, requester: { type: Schema.slack.types.user_id, description: "The requesting user", }, last_upgrade: { type: Schema.types.string, description: "The date of the last upgrade of a user's mobile device", }, mobile_device: { type: Schema.types.string, description: "The mobile device of the user", }, }, required: ["manager", "requester", "mobile_device", "last_upgrade"], }, output_parameters: { properties: { approval_message: { type: Schema.types.string, description: "Approval message", }, }, required: ["approval_message"], },});export default SlackFunction( ReviewRequestDefinition, async ({ inputs, client }) => { const blocks = [{ "type": "section", "text": { "type": "mrkdwn", "text": `<@${inputs.requester}> is requesting a new ${inputs.mobile_device}, their last upgrade was ${inputs.last_upgrade}`, }, }, { "type": "actions", "block_id": "approve-deny-buttons", "elements": [ { type: "button", text: { type: "plain_text", text: "Approve", }, action_id: APPROVE_ID, style: "primary", }, { type: "button", text: { type: "plain_text", text: "Deny", }, action_id: DENY_ID, style: "danger", }, ], }]; const postResponse = await client.chat.postMessage({ blocks: blocks, channel: inputs.manager, }); if (!postResponse.ok) { console.error("Error pulling from database!", postResponse.error); } return { completed: false }; },).addBlockActionsHandler( [APPROVE_ID, DENY_ID], async function ({ action, body, client }) { console.log("Incoming action handler invocation", action); const approved: boolean = action.action_id === APPROVE_ID; let approval_message = approved ? ":white_check_mark: Your request was approved! You'll be sent a new device soon." : ":x: I'm afraid that your request was denied."; // (OPTIONAL) Update the manager's message to remove the buttons and reflect the approval state. const msgUpdate = await client.chat.update({ channel: body.container.channel_id, ts: body.container.message_ts, blocks: [ { "type": "section", "text": { "type": "mrkdwn", "text": `<@${body.function_data.inputs.requester}> is requesting a new ${body.function_data.inputs.mobile_device}, their last upgrade was ${body.function_data.inputs.last_upgrade}`, }, }, { type: "context", elements: [ { type: "mrkdwn", text: `${ approved ? " :white_check_mark: You approved the request." : ":x: You denied the request." }`, }, ], }, ], }); if (!msgUpdate.ok) { console.error("Error during manager chat.update!", msgUpdate.error); } await client.functions.completeSuccess({ function_execution_id: body.function_data.execution_id, outputs: { approval_message: approval_message }, }); },); ``` This function sends the retrieved info and the user’s request to the specified manager for approval. It also includes interactive elements: the approve/deny buttons. Each of these custom functions has two key components: * **DefineFunction**: This defines the function’s structure, including the callback ID, inputs, and outputs. * **SlackFunction**: This contains the logic that will run when the function is triggered. For instance, the `GetMobileDeviceDefinition` will check a datastore to fetch device details. ### Datastore {#datastore} Create a new folder in the project called `datastores`, then create a new file in the folder named `mobile_datastore.ts`. Copy and paste the following code into this file, then save it: ``` import { DefineDatastore, Schema } from "deno-slack-sdk/mod.ts";/** * Datastores are a Slack-hosted location to store * and retrieve data for your app. * https://api.slack.com/automation/datastores */const MobileDatastore = DefineDatastore({ name: "MobileDevices", primary_key: "id", attributes: { id: { type: Schema.types.string, }, user: { type: Schema.types.string, }, mobile_device: { type: Schema.types.string, }, last_upgrade: { type: Schema.types.string, }, },});export default MobileDatastore; ``` The argument passed into the `DefineDatastore()` constructor function includes all of the information about your datastore, including the name and primary key of each record. In addition, attributes allow you to determine the properties of each record. When we interact with a datastore, we use the `client.apps.datastore.*` methods. Just like with any other datastore, you can perform the usual CRUD operations, specifying in the argument the datastore you’d like to interact with, along with the operation details. You also can perform operations directly from the terminal with the Slack CLI using the [`slack datastore`](/tools/slack-cli/reference/commands/slack_datastore) command. For a full-fledged test of the app, you will need to populate your datastore with test data. This can be done using the [`apps.datastore.put`](/reference/methods/apps.datastore.put) or [`apps.datastore.bulkPut`](/reference/methods/apps.datastore.bulkPut) API methods and using [this guide](/tools/deno-slack-sdk/guides/adding-items-to-a-datastore). Next, we need to run the app. ## Run the app {#run-the-app} Redirect your attention to your terminal and run: ``` slack run ``` This allows you to enter development mode and see changes to your code live. During the first run, you’ll be prompted to select a workspace where the app will be installed. ## Customize workflow in Workflow Builder {#customize-workflow-in-workflow-builder} ### Add custom functions to Workflow Builder {#add-custom-functions-to-workflow-builder} With the app running, it's time to add your custom functions into the workflow: 1. Open Workflow Builder and find the workflow you previously created. Click the pencil icon to edit the workflow. 2. Click **\+ Add Step**, then search for your app by name (`mobile-request-functions`) in the step sidebar. 3. Add the `Check Mobile Database` custom function. 4. When prompted for **User**, click the variable icon to the right of the field, then select `Person who used this workflow`, then save. ### Add reply in thread step {#add-reply-in-thread-step} Now that we’ve queried the datastore for the user’s information, it’s time to display that info back in Slack: 1. Click **\+ Add Step** to add another step to the workflow. Select **Messages**, then **Reply to a message in thread**. 2. Choose `Reference to the message sent` in the **Select a message to reply to** field. 3. Add a reply message using the variables from the form and the previous step, then save your changes. ![Reply to message in thread modal](/assets/images/reply-to-message-in-thread-60930fb45c414154df9dca16b300c42c.png) ### Add approval interactivity {#add-approval-interactivity} To handle approval from the manager, we’ll need interactive buttons for them to approve or deny the request. We can't use a regular Workflow Builder message for this because messages that contain interactive elements require logic to handle the button clicks. The buttons we’ll be using are considered interactive components. Block Kit interactive components are a subset of Block Kit elements, which include buttons, multi-select fields, input fields, etc. In addition to interactive components, there is also the ability to introduce and handle custom modals and their associated submissions. In this case, we’ll to add the two buttons for our manager to approve or deny our request. There are two ways of adding interactive elements to an app: through message blocks or with the dedicated `interactive_blocks` property for certain built-ins. In either case, the `action_id` property is used to uniquely identify each element to be interacted with and connect it with handler logic. We can see this in the `ReviewRequestDefinition` function defined above. ``` { "type": "actions", "block_id": "approve-deny-buttons", "elements": [ { type: "button", text: { type: "plain_text", text: "Approve", }, action_id: APPROVE_ID, style: "primary", }, { type: "button", text: { type: "plain_text", text: "Deny", }, action_id: DENY_ID, style: "danger", }, ],} ``` The `action_id`'s value of `APPROVE_ID` is used in the `addBlockActionsHandler` function just below that. ``` ...).addBlockActionsHandler( [APPROVE_ID, DENY_ID], ... ``` Within each function there is access to a `client` object. This can be used to make calls directly to the Slack Web API. We see that in the `GetMobileDevice` function. Behind the scenes, the Deno Slack SDK passes along the required token used to make such calls possible. ``` const queryResp = await client.apps.datastore.query< typeof MobileDatastore.definition >( { datastore: MobileDatastore.name, expression: "#user = :user", expression_attributes: { "#user": "user" }, expression_values: { ":user": inputs.user }, }, ); ``` Just as we did with our first custom function, we'll now add the function we just created as a step in our workflow. Direct your attention back to Workflow Builder; the project should still be running in the Slack CLI. 1. Within the edit screen of your workflow, again click **\+ Add step**. 2. Find your app by name in the step sidebar. 3. Add the `Review a mobile request` custom function as a step in your workflow. 4. For **Manager**, use the variable **Answer to: Who is your manager?**. For **Requester**, use the variable **Person who used this workflow**. For **Last upgrade**, use the variable **Last Upgrade**. For **Mobile Device**, use the variable **Mobile Device**. Save your changes. ### Respond in thread with approval {#respond-in-thread-with-approval} Once our manager acts on the request from the DM they’ve received, we want to communicate this response back to the employee in our original thread. At this step, we have a new variable available (`Approval Message`) that we obtained from the previous step as an output of our custom function. Add another step to the workflow. This time, the **Reply to a message in thread** step found in the Messages collection. Choose **Reference to the message sent** in the **Select a message to reply to** field, then add a reply message using the variables from the form and the previous step. With this last step, save the changes by clicking on the publish button on the workflow. We don’t need to update the workflow link, since it’s still the same workflow. Let’s test our complete workflow. Head back to the channel where you bookmarked the workflow, and run it! ## Deploy your app {#deploy-your-app} With everything working locally, it’s time to deploy your app to Slack’s infrastructure. This means that the functions will be available without you needing to run it from your local machine, and you can access it any time you’d like! In your terminal window, run `slack deploy`. This will package your app, initialize the datastore, and make your functions available in Slack. After deploying, you’ll need to replace the custom functions in Workflow Builder with the new ones from the deployed app because the app now lives on Slack’s servers, not on your local machine. ## Uninstall the local app {#uninstall-the-local-app} Once your app is deployed, you can remove the local version. To uninstall, run `slack delete` in your project’s root directory. You’ll be prompted to choose whether you want to remove the local version or the deployed version. An action with potential consequences to end users, the Slack CLI will ask you if you’re certain you’d like to proceed. --- Source: https://docs.slack.dev/tools/deno-slack-sdk/tutorials/open-authorization # Open authorization Workflow apps require a paid plan Join the [Developer Program](https://api.slack.com/developer-program) and provision a sandbox with access to all Slack features for free. This tutorial guides you through setting up [external authentication](/tools/deno-slack-sdk/guides/integrating-with-services-requiring-external-authentication) in an app via OAuth2. The sample app for this tutorial demonstrates multi-stage workflows for requesting and collecting feedback on messages that starts with the press of a reaction, with responses stored dynamically in a Google Sheet. Because the focus of this tutorial is setting up OAuth2, we won't be going through every workflow and function, but you can check them out in the [sample app repo on GitHub](https://github.com/slack-samples/deno-simple-survey). ✨ **First time creating a workflow app?** Try a basic app to build your confidence, such as [Hello World](/tools/deno-slack-sdk/tutorials/hello-world-app)! Before we begin, ensure you have the following prerequisites completed: * Install the [Slack CLI](/tools/deno-slack-sdk/guides/getting-started). * Run `slack auth list` and ensure your workspace is listed. * If your workspace is not listed, address any issues by following along with the [Getting started](/tools/deno-slack-sdk/guides/getting-started), then come on back. ## Choose your adventure {#choose-your-adventure} After you've [installed the command-line interface](/tools/deno-slack-sdk/guides/getting-started) you have two ways you can get started: ### Use a blank app {#use-a-blank-app} You can create a blank app with the Slack CLI using the following command: ``` slack create simple-survey-app --template https://github.com/slack-samples/deno-blank-template ``` ### Use a pre-built app {#use-a-pre-built-app} Or, you can use the pre-built [Simple Survey app](https://github.com/slack-samples/deno-simple-survey): ``` slack create simple-survey-app --template https://github.com/slack-samples/deno-simple-survey ``` Because this tutorial will not go through creating all of the files, a pre-built app might be easiest to follow along with. Once you have your new project ready to go, change into your project directory. ## Explore the app structure {#explore-the-app-structure} Let's take a look at what's inside our new "Simple Survey" project directory: ``` assets/datastores/deno.jsoncdeno.lockexternal_auth/functions/import_map.jsonLICENSEmanifest.tsREADME.mdslack.jsontriggers/workflows/ ``` The first place to direct your attention are the `datastores`, `functions`, `triggers`, and `workflows` folders. These are where the definitions and implementations for the inner workings of your app live. As you might have guessed, the `external_auth` folder is where we'll define our external authentication. The next place to look is the `manifest.ts` file. This contains your app's manifest, which is where we can configure things like bot scopes and tell our app about our workflows. We'll return to the manifest a bit later. Other items in the project include: * `.slack/`: a home for internal configuration files, scripts hooks, and the app SDK. This directory must be checked into your version control. You'll also notice a `.slack/apps.dev.json` once you begin building: this file is in `.gitignore` and should not be checked in to version control. * `import_map.json`: a helper file for Deno that specifies where modules should be imported from. * `assets/`: a place to store assets related to the project. This is a great place to store the icon that your app will display when users interact with it. ## Explore the supported workflows {#explore-the-supported-workflows} This app's functionality is centered around six workflows. While we won't go into detail on all of them, it's important to know how they all fit together so that we can understand the flow of data later on. Here are the workflows: * **Prompt survey creation**: Ask if a user wants to create a survey when a 📋 reaction is added to a message. * **Create a survey**: Respond to the reacted message with a feedback form and make a new spreadsheet to store responses. * **Respond to a survey**: Open the feedback form and store responses in the spreadsheet. * **Remove a survey**: Delete messages with survey and surveying users for reaction events. * **Event configurator**: Update the channels to survey and surveying users for reaction events. * **Maintenance job**: A daily run workflow that ensures bot user membership in channels specified for event reaction triggers. Recommended for production-grade operations. Now that we've covered the app's basic structure, let's look at hooking up OAuth2 by first preparing our Google services. ## Get your Google credentials {#get-your-google-credentials} With [external authentication](/tools/deno-slack-sdk/guides/integrating-with-services-requiring-external-authentication), you can programmatically interact with Google services and APIs from your app. The client credentials needed for these interactions can be collected from a Google Cloud project with OAuth enabled and with access to the appropriate services. ### Create a Google Cloud Project {#create-a-google-cloud-project} Begin by creating a new project from the [Google Cloud resource manager](https://console.cloud.google.com/cloud-resource-manager), then enable the Google Sheets API for this project. Next, create an OAuth consent screen for your app. The "User Type" and other required app information can be configured as you wish. No additional scopes need to be added here, and you can add test users for development if you want. Client credentials can be collected by creating an OAuth client ID with an application type of "Web application". Under the "Authorized redirect URIs" section, add [https://oauth2.slack.com/external/auth/callback](https://oauth2.slack.com/external/auth/callback), then click "Create". You'll use these newly-created client credentials in the next steps. ## Define your OAuth2 provider {#define-your-oauth2-provider} Next up, we'll define the OAuth2 provider. Open `/external_auth/google_provider.ts` to see how that's done. Take your client ID and add it as the value for `client_id` where it's marked below. ``` // google_provider.tsimport { DefineOAuth2Provider, Schema } from "deno-slack-sdk/mod.ts";/** * External authentication uses the OAuth 2.0 protocol to connect with * accounts across various services. Once authenticated, an access token * can be used to interact with the service on behalf of the user. */const GoogleProvider = DefineOAuth2Provider({ provider_key: "google", provider_type: Schema.providers.oauth2.CUSTOM, options: { "provider_name": "Google", "authorization_url": "https://accounts.google.com/o/oauth2/auth", "token_url": "https://oauth2.googleapis.com/token", "client_id": "", // TODO: Add your Client ID here! "scope": [ "https://www.googleapis.com/auth/spreadsheets", "https://www.googleapis.com/auth/userinfo.email", ], "authorization_url_extras": { "prompt": "consent", "access_type": "offline", }, "identity_config": { "url": "https://www.googleapis.com/oauth2/v1/userinfo", "account_identifier": "$.email", }, },});export default GoogleProvider; ``` Once complete, your app needs to be deployed to Slack in order to create an environment for storing your external authentication client secret and access token. Run the following in your terminal: ``` slack deploy ``` Running these commands will warn you that a client secret must be added for your OAuth2 provider. Don't worry about that for now; we'll take care of this in a future step! ## Add the provider to your app manifest {#add-the-provider-to-your-app-manifest} At the root of every app, there exists an app manifest, which defines how an app presents itself. The app [manifest](/tools/deno-slack-sdk/guides/using-the-app-manifest) is where we configure the app's name and scopes, and declare which workflows our app uses. We will also need to add our new provider as an `externalAuthProvider`. ``` // manifest.tsimport { Manifest } from "deno-slack-sdk/mod.ts";import GoogleProvider from "./external_auth/google_provider.ts";import SurveyDatastore from "./datastores/survey_datastore.ts";import ConfiguratorWorkflow from "./workflows/configurator.ts";import MaintenanceJobWorkflow from "./workflows/maintenance_job.ts";import AnswerSurveyWorkflow from "./workflows/answer_survey.ts";import CreateSurveyWorkflow from "./workflows/create_survey.ts";import RemoveSurveyWorkflow from "./workflows/remove_survey.ts";import PromptSurveyWorkflow from "./workflows/prompt_survey.ts";export default Manifest({ name: "Simple Survey", description: "Gather input and ideas at the press of a reacji", icon: "assets/default_new_app_icon.png", externalAuthProviders: [GoogleProvider], //Here is where we tell our app about the Google provider. datastores: [SurveyDatastore], workflows: [ ConfiguratorWorkflow, MaintenanceJobWorkflow, AnswerSurveyWorkflow, CreateSurveyWorkflow, PromptSurveyWorkflow, RemoveSurveyWorkflow, ], outgoingDomains: ["sheets.googleapis.com"], botScopes: [ "channels:join", "chat:write", "chat:write.public", "commands", "datastore:read", "datastore:write", "reactions:read", "triggers:read", "triggers:write", ],}); ``` Now that the provider is created and added to the manifest, we can encrypt and store the client secret. ## Encrypt and store the client secret {#encrypt-and-store-the-client-secret} With your client secret ready, run the following command in your terminal, replacing GOOGLE\_CLIENT\_SECRET with your own secret: ``` slack external-auth add-secret --provider google --secret GOOGLE_CLIENT_SECRET ``` When prompted to select an app, choose the `dev` app only if you are running locally. If everything was successful, the CLI will let you know: ``` ✨ successfully added external auth client secret for google ``` ## Initiate OAuth2 flow {#initiate-oauth2-flow} With your Google project created and the Client ID and secret set, you're ready to initiate the OAuth flow! If all the right values are in place, the following command will prompt you to choose an app, select a provider (hint: choose the Google one), then pick the Google account you want to authenticate with. This will open a browser window for you to complete the OAuth2 sign-in flow according to your provider's requirements. You'll know you're successful when your browser sends you to the oauth2.slack.com page stating that your account was successfully connected. ``` slack external-auth add ``` Verify that a token has been created by re-running the `external-auth add` command. If you see `Token Exists? Yes`, then token creation was successful. You're nearly ready to create surveys at the press of a reaction! ## Use in a custom function {#use-in-a-custom-function} If you refer back to the workflow overview we explored in Step 1 above, you'll recall that after the survey creation is prompted, the `create_survey` workflow gets kicked off. We'll now look at how that workflow gets the token and passes it through the app to be used for communicating with Google services, demonstrating how it is used in both a custom function and a workflow. Turning your attention to `create_survey.ts`, you will see that the first step in the workflow is calling the `CreateGoogleSheetFunctionDefinition`. ``` // /workflows/create_survey.ts...// Step 1: Create a new Google spreadsheetconst sheet = CreateSurveyWorkflow.addStep( CreateGoogleSheetFunctionDefinition, { google_access_token_id: {}, title: CreateSurveyWorkflow.inputs.parent_ts, },);... ``` That function's definition looks like this: ``` // /functions/create_google_sheet.tsimport { DefineFunction, Schema, SlackFunction } from "deno-slack-sdk/mod.ts";/** * Custom functions can gather OAuth access tokens from external * authentication to perform individualized actions on external APIs. */export const CreateGoogleSheetFunctionDefinition = DefineFunction({ callback_id: "create_google_sheet", title: "Create spreadsheet", description: "Create a new Google Sheet", source_file: "functions/create_google_sheet.ts", input_parameters: { properties: { google_access_token_id: { type: Schema.slack.types.oauth2, oauth2_provider_key: "google", }, title: { type: Schema.types.string, description: "The title of the spreadsheet", }, }, required: ["google_access_token_id", "title"], }, output_parameters: { properties: { google_spreadsheet_id: { type: Schema.types.string, description: "Newly created spreadsheet ID", }, google_spreadsheet_url: { type: Schema.types.string, description: "Newly created spreadsheet URL", }, reactor_access_token_id: { type: Schema.types.string, description: "The Google access token ID of the reactor", }, }, required: [ "google_spreadsheet_id", "google_spreadsheet_url", "reactor_access_token_id", ], },});... ``` This function takes an input parameter of `google_access_token_id`, which is the `Schema.slack.types.oauth2` type. Using this type indicates to the app that there must be OAuth2 provider defined inside this application and set up for this parameter. The value of the `oauth2_provider_key` property on this parameter must match the `provider_key` for an OAuth2 provider. For this app, that is "google." When the function receives the desired `oauth2` input, it can use the API client's provided `apps.auth.external.get` method to retrieve any stored third party token or credential secret, like this: ``` // create_google_sheet.ts...export default SlackFunction( CreateGoogleSheetFunctionDefinition, async ({ inputs, client }) => { // Collect Google access token const auth = await client.apiCall("apps.auth.external.get", { external_token_id: inputs.google_access_token_id, }); if (!auth.ok) { return { error: `Failed to collect Google auth token: ${auth.error}` }; } // Create spreadsheet const url = "https://sheets.googleapis.com/v4/spreadsheets"; const sheets = await fetch(url, { method: "POST", headers: { "Authorization": `Bearer ${auth.external_token}`, }, body: JSON.stringify({ properties: { title: `Slack Survey - ${inputs.title}` }, sheets: [{ properties: { title: "Responses" }, data: [{ rowData: [{ values: [ { userEnteredValue: { stringValue: "Submitted" } }, { userEnteredValue: { stringValue: "Impression" } }, { userEnteredValue: { stringValue: "Comments" } }, ], }], }], }], }), }); const body = await sheets.json(); if (body.error) { return { error: `Failed to create the survey spreadsheet: ${body.error.message}`, }; } return { outputs: { google_spreadsheet_id: body.spreadsheetId, google_spreadsheet_url: body.spreadsheetUrl, reactor_access_token_id: inputs.google_access_token_id, }, }; },); ``` Note how this function returns the `google_access_token_id` as an output, called `reactor_access_token_id`. This will be important in the next step, where we'll see how the token is used as an input to a workflow. ## Use the auth token in a workflow {#use-the-auth-token-in-a-workflow} The `CreateGoogleSheetFunctionDefinition` we explored above is called from `CreateSurveyWorkflow`. Looking back at that workflow, we see that its second step - calling the `CreateTriggerFunctionDefinition` - takes the `reactor_access_token_id` as a parameter, which we now know was an output of `CreateGoogleSheetFunctionDefinition`(also known as the OAuth2 token ID). If we dive into how `CreateTriggerFunctionDefinition` uses that, we'll see how to use the credential as a workflow input. ``` // /functions/create_survey_trigger.ts...export default SlackFunction( CreateTriggerFunctionDefinition, async ({ inputs, client }) => { const { google_spreadsheet_id, reactor_access_token_id } = inputs; const trigger = await client.workflows.triggers.create< typeof AnswerSurveyWorkflow.definition >({ type: "shortcut", name: "Survey your thoughts", description: "Share your thoughts about this post", workflow: `#/workflows/${AnswerSurveyWorkflow.definition.callback_id}`, inputs: { interactivity: { value: "{{data.interactivity}}" }, google_spreadsheet_id: { value: google_spreadsheet_id }, reactor_access_token_id: { value: reactor_access_token_id }, //Here it is used in calling a workflow }, }); if (!trigger.ok || !trigger.trigger.shortcut_url) { return { error: `Failed to create link trigger for the survey: ${trigger.error}`, }; } return { outputs: { trigger_id: trigger.trigger.id, trigger_url: trigger.trigger.shortcut_url, }, }; },); ``` After the definition of this function, we see how the `reactor_access_token_id` is used, and that is as an input to a workflow! Excellent. Turn your attention to the `/workflows/answer_survey.ts` file to see how that token ID is used in a workflow definition: ``` // answer_survey.tsimport { DefineWorkflow, Schema } from "deno-slack-sdk/mod.ts";import { SaveResponseFunctionDefinition } from "../functions/save_response.ts";/** * A workflow is a set of steps that are executed in order. * Each step in a workflow is a function. * This workflow uses interactivity. */const AnswerSurveyWorkflow = DefineWorkflow({ callback_id: "answer_survey", title: "Respond to a survey", description: "Add comments and feedback to a survey", input_parameters: { properties: { interactivity: { type: Schema.slack.types.interactivity }, google_spreadsheet_id: { type: Schema.types.string, description: "Spreadsheet ID for storing survey results", }, reactor_access_token_id: { type: Schema.types.string, description: "External authentication access token for the reactor", }, }, required: [ "interactivity", "google_spreadsheet_id", "reactor_access_token_id", ], },});... ``` The `reactor_access_token_id` is then used in the implementation of the workflow by passing it along, just like any other input parameter, to a step in the workflow: ``` // answer_survey.ts...// Step 2: Append responses to the spreadsheetAnswerSurveyWorkflow.addStep(SaveResponseFunctionDefinition, { reactor_access_token_id: AnswerSurveyWorkflow.inputs.reactor_access_token_id, google_spreadsheet_id: AnswerSurveyWorkflow.inputs.google_spreadsheet_id, impression: response.outputs.fields.impression, comments: response.outputs.fields.comments,});export default AnswerSurveyWorkflow; ``` Now you have seen how a Google token ID can be obtained from the app, used in a custom function, and used in a workflow. To complete the connection process, you need to let your app know what authenticated account you'll be using for specific workflows. For this app, only `CreateSurveyWorkflow` requires a configured external Google account, so we can set that up with our freshly-authed account. To do so, run the following: ``` slack external-auth select-auth ``` Select the workspace and app environment for your app, then select the `#/workflows/create_survey` workflow to give it access to your Google account. Select the same provider and the external account that you authenticated with above. At last — you're all set to survey. Let's run our app! ## Run your app {#run-your-app} If you embarked on this tutorial with a blank app, complete your app files using the [sample app](https://github.com/slack-samples/deno-simple-survey) as your guide. If you used a pre-built app, we're ready to run it! To see this app in action, run the following command in your terminal: ``` slack run ``` After you've chosen your app and assigned it to your workspace, you can switch over to the app in Slack and try it out. This app is triggered by reactions in Slack, so it should be ready to use once deployed (as opposed to apps that need [link triggers](/tools/deno-slack-sdk/guides/creating-link-triggers) to run). ## Next steps {#next-steps} Congratulations, you've added OAuth2 to a workflow app! Where do you go from here? Consider taking a deep dive into our [External authorization](/tools/deno-slack-sdk/guides/integrating-with-services-requiring-external-authentication) documentation. Perhaps try another tutorial, like creating a [social app to log runs with virtual running buddies](/tools/deno-slack-sdk/tutorials/virtual-running-buddies-app). --- Source: https://docs.slack.dev/tools/deno-slack-sdk/tutorials/request-time-off-app # Request time off app Workflow apps require a paid plan Join the [Developer Program](https://api.slack.com/developer-program) and provision a sandbox with access to all Slack features for free. This tutorial will guide you in creating, running, and deploying a workflow app. The Request Time Off App models how to collect user inputs as well as how to send those inputs to other users in Slack. More specifically, this app showcases one way [user interactivity](/tools/deno-slack-sdk/guides/creating-a-form) is implemented within an app. By the end, you will have a working app that can post [Block Kit](https://docs.slack.dev/block-kit/#making-things-interactive) messages, handle user interactions, and update messages in real time. ✨ **First time creating a workflow app?** Try an app to build your confidence, such as [Hello World](/tools/deno-slack-sdk/tutorials/hello-world-app)! We can break this app into 3 major parts that work together to create a symphonic harmony: 1. Functions 2. Workflows 3. Triggers Each segment will give an explanation of the components, along with some tips & tricks for orchestrating a successful path forward. Before we begin, ensure you have the following prerequisites completed: * Install the [Slack CLI](/tools/deno-slack-sdk/guides/getting-started). * Run `slack auth list` and ensure your workspace is listed. * If your workspace is not listed, address any issues by following along with the [Getting started](/tools/deno-slack-sdk/guides/getting-started), then come on back. ## Choose your adventure {#choose-your-adventure} After you've [installed the command-line interface](/tools/deno-slack-sdk/guides/getting-started) you have two ways you can get started: ### Use a blank app {#use-a-blank-app} You can create a blank app with the Slack CLI using the following command: ``` slack create request-time-off-app --template https://github.com/slack-samples/deno-blank-template ``` ### Use a pre-built app {#use-a-pre-built-app} Or, you can use the pre-built [Request Time Off app](https://github.com/slack-samples/deno-request-time-off): ``` slack create request-time-off-app --template https://github.com/slack-samples/deno-request-time-off ``` Once you have your new project ready to go, change into your project directory. ## Compose the manifest {#compose-the-manifest} The app manifest is where we define the intricacies of an app. Below is the manifest that powers the Request Time Off app: ``` import { Manifest } from "deno-slack-sdk/mod.ts";import { CreateTimeOffRequestWorkflow } from "./workflows/CreateTimeOffRequestWorkflow.ts";import { SendTimeOffRequestToManagerFunction } from "./functions/send_time_off_request_to_manager/definition.ts";export default Manifest({ name: "Request Time Off", description: "Ask your manager for some time off", icon: "assets/default_new_app_icon.png", workflows: [CreateTimeOffRequestWorkflow], functions: [SendTimeOffRequestToManagerFunction], outgoingDomains: [], botScopes: [ "commands", "chat:write", "chat:write.public", "datastore:read", "datastore:write", ],}); ``` The manifest of an app describes the most important application information, such as its `name`, `description`, `icon`, the list of [workflows](/tools/deno-slack-sdk/guides/creating-workflows) and [functions](/tools/deno-slack-sdk/guides/creating-custom-functions), and more. Read through the full [manifest documentation](/tools/deno-slack-sdk/guides/using-the-app-manifest) to learn more. ## Create a function {#create-a-function} First we will define and implement our function. [Functions](/tools/deno-slack-sdk/guides/creating-custom-functions) are reusable building blocks that accept inputs, perform calculations, and provide outputs. The code behind the app's function is stored under the `./functions/send_time_off_request_to_manager/` directory. We're working with five files inside (not including test files): 1. `block_actions.ts`: An action handler for our interactive blocks. 2. `blocks.ts`: A layout of visual blocks that is easy on the eyes. 3. `constants.ts`: Constant variables referenced throughout the app. 4. `definition.ts`: Our function definition, which houses the function's `input_parameters`, `output_parameters`, `title`, `description` and implementation source file. This is a [custom function](/tools/deno-slack-sdk/guides/creating-custom-functions) as opposed to [Slack function](/tools/deno-slack-sdk/guides/creating-slack-functions), meaning the function implementation is up to you! _Notice the `interactivity` parameter of type `Schema.slack.types.interactivity` -- one of the many built-in [Slack types](/tools/deno-slack-sdk/reference/slack-types#interactivity) available to allow your function to utilize user interaction._ 5. `mod.ts`: Our function implementation. ### Implement a function {#implement-a-function} Once you define your custom function, we'll bring it to life by completing the `mod.ts` file with various [API calls](/tools/deno-slack-sdk/guides/calling-slack-api-methods) and Block Kit [blocks](https://docs.slack.dev/reference/block-kit/blocks). Remember, the Request Time Off app collects time off start and end dates, and sends that request to a manager for approval. We can utilize [Block Kit](https://docs.slack.dev/reference/block-kit/blocks) buttons to help facilitate the decision process and to create a rich user experience. ``` import { SendTimeOffRequestToManagerFunction } from "./definition.ts";import { SlackFunction } from "deno-slack-sdk/mod.ts";import BlockActionHandler from "./block_actions.ts";import { APPROVE_ID, DENY_ID } from "./constants.ts";import timeOffRequestHeaderBlocks from "./blocks.ts";// Custom function that sends a message to the user's manager asking// for approval for the time off request. The message includes some Block Kit with two// interactive buttons: one to approve, and one to deny.export default SlackFunction( SendTimeOffRequestToManagerFunction, async ({ inputs, client }) => { console.log("Forwarding the following time off request:", inputs); // Create a block of Block Kit elements composed of several header blocks // plus the interactive approve/deny buttons at the end const blocks = timeOffRequestHeaderBlocks(inputs).concat([{ "type": "actions", "block_id": "approve-deny-buttons", "elements": [ { type: "button", text: { type: "plain_text", text: "Approve", }, action_id: APPROVE_ID, // <-- important! we will differentiate between buttons using these IDs style: "primary", }, { type: "button", text: { type: "plain_text", text: "Deny", }, action_id: DENY_ID, // <-- important! we will differentiate between buttons using these IDs style: "danger", }, ], }]); // ...continued in the next snippet ``` Now we have a message with two buttons, each using a unique `ACTION_ID` to differentiate between an approval or denial. In order to properly utilize the Block Kit buttons, we'll rely on the [`BlockActionsHandler`](https://docs.slack.dev/reference/interaction-payloads/block_actions-payload) to route the button actions. Check it out below: ``` // ...continued from the snippet above // Send the message to the manager const msgResponse = await client.chat.postMessage({ channel: inputs.manager, blocks, // Fallback text to use when rich media can't be displayed (i.e. notifications) as well as for screen readers text: "A new time off request has been submitted", }); if (!msgResponse.ok) { console.log("Error during request chat.postMessage!", msgResponse.error); } // IMPORTANT! Set `completed` to false in order to keep the interactivity // points (the approve/deny buttons) "alive" // We will set the function's complete state in the button handlers below. return { completed: false, }; }, // Create an 'actions router' which is a helper utility to route interactions // with different interactive Block Kit elements (like buttons!)).addBlockActionsHandler( // listen for interactions with components with the following action_ids [APPROVE_ID, DENY_ID], // interactions with the above components get handled by the function below BlockActionHandler,); ``` This `mods.ts` function is responsible for building a message, sending it to the selected manager, and replying with a response that is triggered by the decision of that manager. How do we connect these function steps, you may ask? Not to worry, our next step covers how to bring together the functions using a [workflow](/tools/deno-slack-sdk/guides/creating-workflows)! ## Define a workflow {#define-a-workflow} A [workflow](/tools/deno-slack-sdk/guides/creating-workflows) is a set of steps that are executed in order. Each step in a workflow can be a [function](/tools/deno-slack-sdk/guides/creating-custom-functions). Similar to functions, workflows can also optionally accept inputs and pass them further along to other functions that comprise the workflow. This app contains a single workflow stored within the `workflows/` folder. This app's workflow is composed of two functions chained sequentially as steps: 1. The workflow uses the [OpenForm Slack function](/tools/deno-slack-sdk/reference/slack-functions/open_form) to collect data from the user that started the workflow. 2. Form data is then passed to your app's [custom function](/tools/deno-slack-sdk/guides/creating-custom-functions), which is called `SendTimeOffRequestToManagerFunction`. This function is stored within the `functions/` folder. First let's define the workflow with the `DefineWorkflow` method. Make sure to set a custom `callback_id` that you can reference later on. ``` import { DefineWorkflow, Schema } from "deno-slack-sdk/mod.ts";import { SendTimeOffRequestToManagerFunction } from "../functions/send_time_off_request_to_manager/definition.ts";/** * A Workflow composed of two steps: asking for time off details from the user * that started the workflow, and then forwarding the details along with two * buttons (approve and deny) to the user's manager. */export const CreateTimeOffRequestWorkflow = DefineWorkflow({ callback_id: "create_time_off", title: "Request Time Off", description: "Create a time off request and send it for approval to your manager", input_parameters: { properties: { interactivity: { type: Schema.slack.types.interactivity, }, }, required: ["interactivity"], },}); ``` Then, place the functions in order of execution. In this case, use the Slack [`OpenForm`](/tools/deno-slack-sdk/reference/slack-functions/open_form) function to open a modal form to collect the time off request data; then use the [custom function](/tools/deno-slack-sdk/guides/creating-custom-functions) you built to send the request for approval. ``` // Step 1: opening a form for the user to input their time off details.const formData = CreateTimeOffRequestWorkflow.addStep( Schema.slack.functions.OpenForm, { title: "Time Off Details", interactivity: CreateTimeOffRequestWorkflow.inputs.interactivity, submit_label: "Submit", description: "Enter your time off request details", fields: { required: ["manager", "start_date", "end_date"], elements: [ { name: "manager", title: "Manager", type: Schema.slack.types.user_id, }, { name: "start_date", title: "Start Date", type: "slack#/types/date", }, { name: "end_date", title: "End Date", type: "slack#/types/date", }, { name: "reason", title: "Reason", type: Schema.types.string, }, ], }, },);// Step 2: send time off request details along with approve/deny buttons to managerCreateTimeOffRequestWorkflow.addStep(SendTimeOffRequestToManagerFunction, { interactivity: formData.outputs.interactivity, employee: CreateTimeOffRequestWorkflow.inputs.interactivity.interactor.id, manager: formData.outputs.fields.manager, start_date: formData.outputs.fields.start_date, end_date: formData.outputs.fields.end_date, reason: formData.outputs.fields.reason,}); ``` Voilà! Next, let's define a trigger to get the wheels in motion! ## Create a trigger {#create-a-trigger} A [trigger](/tools/deno-slack-sdk/guides/using-triggers) is a crucial finishing piece of your app. Creating a trigger sets the steps of your workflow in motion, which runs your custom & Slack functions, allowing your app to provide a pleasant experience. These triggers can be invoked by a user, or automatically as a response to an event within Slack. A [link trigger](/tools/deno-slack-sdk/guides/creating-link-triggers) is a type of trigger that generates a shortcut URL which, when posted in a channel or added as a bookmark, becomes a link. When clicked, the link trigger will run the associated workflow. To create a link trigger for our workflow, run the following command: ``` $ slack trigger create --trigger-def triggers/trigger.ts ``` After selecting a workspace and an app environment, the output provided will include the URL. Copy and paste this URL into a channel as a message, or add it as a bookmark in a channel of the workspace you selected. _Note: this link won't run the workflow until the app is either running locally or deployed! Read on to learn how to run your app locally and eventually deploy it to Slack hosting._ ## Run your app {#run-your-app} You're almost to the end! Let's use development mode to run this workflow in Slack directly from the machine you're reading this from now: ``` $ slack run ``` After you've chosen your app and assigned it to your workspace, you can switch over to the app in Slack and try it out. Use the link trigger you created previously; when you paste the shortcut URL into the message box and post them, it'll unfurl and give you a button for invoking your workflow. ## Great work! {#great-work} Congratulations! You've successfully built an approval workflow app, providing fancy buttons to all who request time off. Now that we've posted a message using Block Kit, handled the user interaction of buttons, and updated a message — you have the capability to either extend this app or to create a new one from scratch. ### Next steps {#next-steps} For your next challenge, perhaps consider creating [a social app to log runs with virtual running buddies](/tools/deno-slack-sdk/tutorials/virtual-running-buddies-app)! --- Source: https://docs.slack.dev/tools/deno-slack-sdk/tutorials/time-zone-scheduling # Scheduling meetings across time zones Coordinating meetings across different time zones can be a logistical headache. It requires switching between apps and doing mental calculations to convert time zones, which is tedious and prone to errors. Streamline this process with Slack! Using Workflow Builder, we'll create an automation that takes a proposed meeting time in your timezone, converts it to the timezone of the person you’re scheduling with, and schedules the meeting. Your timezone scheduler workflow will consist of the following steps: 1. **Built-in function**: Collect info in a form 2. **Custom function**: Timezone-aware meeting schedule 3. **Built-in function**: Send a message with the time conversion and a button to create the calendar invite 4. **Calendar connector**: Use the Google Calendar connector to create the meeting 5. **Built-in function**: Reply with a message in thread with the confirmation of the meeting creation ## Get started {#get-started} ### Set up tools {#set-up-tools} We are using the [Slack CLI](/tools/slack-cli) and [Deno Slack SDK](/tools/deno-slack-sdk) to create an app where the custom function exists and can be pulled into Workflow Builder to be used as a step. You should be familiar with the Slack CLI and how it works before going further. If this is new for you, we recommend starting with the [Hello world app](/tools/deno-slack-sdk/tutorials/hello-world-app) first. ### Create the project {#create-the-project} Navigate to a directory where you have permission to create new files. Using the Slack CLI, run the following command to create a new project from a template: ``` slack create time-zone-app --template slack-samples/deno-blank-template ``` This creates your project from a blank template. After that, navigate to your project folder. ``` cd time-zone-app ``` Open the project in VSCode. ``` code . ``` ## Code the app {#code-the-app} ### Update the manifest {#update-the-manifest} The app manifest is where your app's configurations are stored. Navigate to the `manifest.ts` file in your project and replace the template code with what is shown here. ``` import { Manifest } from "deno-slack-sdk/mod.ts";/** * The app manifest contains the app's configuration. This * file defines attributes like app name and description. */export default Manifest({ name: "Time zone scheduler", description: "Assists in scheduling meetings across time zones", icon: "app_icon.png", functions: [], workflows: [], outgoingDomains: ["timeapi.io"], botScopes: ["commands", "chat:write", "chat:write.public"],}); ``` We're going to be creating a custom function to be used as a workflow step in Workflow Builder, so we need to declare that in the manifest. Add this import to the top of the file: ``` import { TimeZoneSchedulerFunction } from "./functions/timezone_scheduler_function.ts"; ``` In the `functions` object, add the function reference, like this: ``` functions: [TimeZoneSchedulerFunction], ``` Additionally, use the icon of your choice and update the reference in the `icon` property. It is a required field. Save your changes. ### Code the custom function {#code-the-custom-function} A custom function in Slack requires two components: the function definition, `DefineFunction`, and the function logic, `SlackFunction`. Create a new folder in your project named `functions`, then create a file in that folder named `timezone_scheduler_function.ts`. Copy and paste the code below into the new file, then save it. ``` import { DefineFunction, Schema, SlackFunction } from "deno-slack-sdk/mod.ts";// Function definitionexport const TimeZoneSchedulerFunction = DefineFunction({ callback_id: "time_zone_scheduler", title: "Time Zone-Aware Meeting Scheduler", description: "Converts a proposed meeting time to the participant's time zone and calculates the end time.", source_file: "functions/timezone_scheduler_function.ts", input_parameters: { properties: { meeting_time: { type: Schema.types.string, description: "Proposed meeting time (e.g., '2023-08-12T14:30:00Z' or 'Aug 12, 2023, 2:30:00 PM')", }, user_timezone: { type: Schema.types.string, description: "User's local date and time with timezone (e.g., 'August 14th, 2024 at 11:06 PM GMT+2')", }, from_timezone: { type: Schema.types.string, description: "Time zone of the user who proposed the meeting (e.g., 'America/New_York')", }, target_timezone: { type: Schema.types.string, description: "Time zone of the meeting participant (e.g., 'Europe/London')", }, duration_minutes: { type: Schema.types.number, description: "The duration of the meeting in minutes" } }, required: [ "meeting_time", "user_timezone", "from_timezone", "target_timezone", "duration_minutes" ], }, output_parameters: { properties: { readable_time_origin: { type: Schema.types.string, description: "Readable meeting time in the user's time zone", }, readable_time_participant: { type: Schema.types.string, description: "Readable meeting time in the participant's time zone", }, calendar_meeting_time: { type: Schema.slack.types.timestamp, description: "Meeting time in the user's timezone", }, calendar_end_time: { type: Schema.slack.types.timestamp, description: "Ending meeting time" } }, required: [ "readable_time_origin", "readable_time_participant", "calendar_meeting_time", "calendar_end_time" ], },});export default SlackFunction( TimeZoneSchedulerFunction, async ({ inputs }) => { let readableTimeOrigin: string | null = null; let readableTimeParticipant: string | null = null; let calendarMeetingTime: number | null = null; let calendarEndTime: number | null = null; try { // Step 1: Correctly format the meeting time for API usage const formattedMeetingTime = formatDateTimeForAPI( inputs.meeting_time, ); const meetingConversionResult = await convertTimeZone( inputs.from_timezone, formattedMeetingTime, inputs.target_timezone, ); if ( !meetingConversionResult || !meetingConversionResult.conversionResult ) { throw new Error("Invalid DateTime format from API."); } // Step 2: Convert meeting_time from from_timezone to user timezone const calendarConversionResult = await convertTimeZone( inputs.from_timezone, formattedMeetingTime, inputs.user_timezone, ); if ( !calendarConversionResult || !calendarConversionResult.conversionResult ) { throw new Error("Invalid DateTime format from API."); } // Extract the calendar meeting time in user's timezone const userTimeZoneDate = new Date( calendarConversionResult.conversionResult.dateTime, ); calendarMeetingTime = Math.floor(userTimeZoneDate.getTime() / 1000); // Step 4: Calculate readable times const originTime = new Date(inputs.meeting_time); readableTimeOrigin = originTime.toLocaleString("en-US", { hour: "numeric", minute: "numeric", hour12: true, }); const participantDateTime = new Date( meetingConversionResult.conversionResult.dateTime, ); readableTimeParticipant = participantDateTime.toLocaleString("en-US", { hour: "numeric", minute: "numeric", hour12: true, }); // Step 5: Calculate end time for user const endTimeUser = new Date( userTimeZoneDate.getTime() + inputs.duration_minutes * 60000, ); calendarEndTime = Math.floor(endTimeUser.getTime() / 1000); } catch (error) { return { error: `Error converting time: ${error.message}`, }; } return { outputs: { readable_time_origin: readableTimeOrigin, readable_time_participant: readableTimeParticipant, calendar_meeting_time: calendarMeetingTime, calendar_end_time: calendarEndTime, }, }; },); ``` You may notice in the function logic, we call a couple of helper functions: `formatDateTimeForAPI` and `convertTimeZone`. We'll create those function files next. ### Add support files {#add-support-files} There are a few other files we need to support this app; we will create them now. Add a new folder to the project named `util`. Create a file in that folder named `api_date_formatter.ts`. This file will help to parse and format dates. Copy and paste the following code into the file, then save. ``` export const formatDateTimeForAPI = (dateTimeString: string): string => { const date = new Date(dateTimeString); if (isNaN(date.getTime())) { throw new Error(`Invalid date format: ${dateTimeString}`); } const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, "0"); const day = String(date.getDate()).padStart(2, "0"); const hours = String(date.getHours()).padStart(2, "0"); const minutes = String(date.getMinutes()).padStart(2, "0"); const seconds = String(date.getSeconds()).padStart(2, "0"); return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;} ``` Create another file in the `util` folder and name it `convert_timezone.ts`. This file converts the `from` time zone to the `to` time zone. Copy and paste the following code into it, then save. ``` /** * Converts a given date and time from one timezone to another using the Time API. * @param fromTimeZone - The original timezone of the date and time. * @param dateTime - The date and time to be converted. * @param toTimeZone - The target timezone for the conversion. * @returns A promise that resolves to the converted date and time result. */export async function convertTimeZone( fromTimeZone: string, dateTime: string, toTimeZone: string): Promise { try { const response = await fetch( "https://timeapi.io/api/Conversion/ConvertTimeZone", { method: "POST", headers: { accept: "application/json", "Content-Type": "application/json", }, body: JSON.stringify({ fromTimeZone, dateTime, toTimeZone, dstAmbiguity: "", }), } ); if (!response.ok) { throw new Error("Failed to convert time zone"); } const data = await response.json(); return data; } catch (error) { console.error("Error during time zone conversion:", error); throw error; }} ``` Be sure to add the imports for these files to the top of the `timezone_scheduler_function.ts` file, like this: ``` import { formatDateTimeForAPI } from "../util/api_date_formatter.ts";import { convertTimeZone } from "../util/convert_timezone.ts"; ``` Create one more file in the `util` folder and name it `timezones.txt`. This is the full list of available time zones. Copy and paste the following text into it, then save the file. Time zones ``` [ "Africa/Abidjan", "Africa/Accra", "Africa/Addis_Ababa", "Africa/Algiers", "Africa/Asmara", "Africa/Asmera", "Africa/Bamako", "Africa/Bangui", "Africa/Banjul", "Africa/Bissau", "Africa/Blantyre", "Africa/Brazzaville", "Africa/Bujumbura", "Africa/Cairo", "Africa/Casablanca", "Africa/Ceuta", "Africa/Conakry", "Africa/Dakar", "Africa/Dar_es_Salaam", "Africa/Djibouti", "Africa/Douala", "Africa/El_Aaiun", "Africa/Freetown", "Africa/Gaborone", "Africa/Harare", "Africa/Johannesburg", "Africa/Juba", "Africa/Kampala", "Africa/Khartoum", "Africa/Kigali", "Africa/Kinshasa", "Africa/Lagos", "Africa/Libreville", "Africa/Lome", "Africa/Luanda", "Africa/Lubumbashi", "Africa/Lusaka", "Africa/Malabo", "Africa/Maputo", "Africa/Maseru", "Africa/Mbabane", "Africa/Mogadishu", "Africa/Monrovia", "Africa/Nairobi", "Africa/Ndjamena", "Africa/Niamey", "Africa/Nouakchott", "Africa/Ouagadougou", "Africa/Porto-Novo", "Africa/Sao_Tome", "Africa/Timbuktu", "Africa/Tripoli", "Africa/Tunis", "Africa/Windhoek", "America/Adak", "America/Anchorage", "America/Anguilla", "America/Antigua", "America/Araguaina", "America/Argentina/Buenos_Aires", "America/Argentina/Catamarca", "America/Argentina/ComodRivadavia", "America/Argentina/Cordoba", "America/Argentina/Jujuy", "America/Argentina/La_Rioja", "America/Argentina/Mendoza", "America/Argentina/Rio_Gallegos", "America/Argentina/Salta", "America/Argentina/San_Juan", "America/Argentina/San_Luis", "America/Argentina/Tucuman", "America/Argentina/Ushuaia", "America/Aruba", "America/Asuncion", "America/Atikokan", "America/Atka", "America/Bahia", "America/Bahia_Banderas", "America/Barbados", "America/Belem", "America/Belize", "America/Blanc-Sablon", "America/Boa_Vista", "America/Bogota", "America/Boise", "America/Buenos_Aires", "America/Cambridge_Bay", "America/Campo_Grande", "America/Cancun", "America/Caracas", "America/Catamarca", "America/Cayenne", "America/Cayman", "America/Chicago", "America/Chihuahua", "America/Ciudad_Juarez", "America/Coral_Harbour", "America/Cordoba", "America/Costa_Rica", "America/Creston", "America/Cuiaba", "America/Curacao", "America/Danmarkshavn", "America/Dawson", "America/Dawson_Creek", "America/Denver", "America/Detroit", "America/Dominica", "America/Edmonton", "America/Eirunepe", "America/El_Salvador", "America/Ensenada", "America/Fort_Nelson", "America/Fort_Wayne", "America/Fortaleza", "America/Glace_Bay", "America/Godthab", "America/Goose_Bay", "America/Grand_Turk", "America/Grenada", "America/Guadeloupe", "America/Guatemala", "America/Guayaquil", "America/Guyana", "America/Halifax", "America/Havana", "America/Hermosillo", "America/Indiana/Indianapolis", "America/Indiana/Knox", "America/Indiana/Marengo", "America/Indiana/Petersburg", "America/Indiana/Tell_City", "America/Indiana/Vevay", "America/Indiana/Vincennes", "America/Indiana/Winamac", "America/Indianapolis", "America/Inuvik", "America/Iqaluit", "America/Jamaica", "America/Jujuy", "America/Juneau", "America/Kentucky/Louisville", "America/Kentucky/Monticello", "America/Knox_IN", "America/Kralendijk", "America/La_Paz", "America/Lima", "America/Los_Angeles", "America/Louisville", "America/Lower_Princes", "America/Maceio", "America/Managua", "America/Manaus", "America/Marigot", "America/Martinique", "America/Matamoros", "America/Mazatlan", "America/Mendoza", "America/Menominee", "America/Merida", "America/Metlakatla", "America/Mexico_City", "America/Miquelon", "America/Moncton", "America/Monterrey", "America/Montevideo", "America/Montreal", "America/Montserrat", "America/Nassau", "America/New_York", "America/Nipigon", "America/Nome", "America/Noronha", "America/North_Dakota/Beulah", "America/North_Dakota/Center", "America/North_Dakota/New_Salem", "America/Nuuk", "America/Ojinaga", "America/Panama", "America/Pangnirtung", "America/Paramaribo", "America/Phoenix", "America/Port_of_Spain", "America/Port-au-Prince", "America/Porto_Acre", "America/Porto_Velho", "America/Puerto_Rico", "America/Punta_Arenas", "America/Rainy_River", "America/Rankin_Inlet", "America/Recife", "America/Regina", "America/Resolute", "America/Rio_Branco", "America/Rosario", "America/Santa_Isabel", "America/Santarem", "America/Santiago", "America/Santo_Domingo", "America/Sao_Paulo", "America/Scoresbysund", "America/Shiprock", "America/Sitka", "America/St_Barthelemy", "America/St_Johns", "America/St_Kitts", "America/St_Lucia", "America/St_Thomas", "America/St_Vincent", "America/Swift_Current", "America/Tegucigalpa", "America/Thule", "America/Thunder_Bay", "America/Tijuana", "America/Toronto", "America/Tortola", "America/Vancouver", "America/Virgin", "America/Whitehorse", "America/Winnipeg", "America/Yakutat", "America/Yellowknife", "Antarctica/Casey", "Antarctica/Davis", "Antarctica/DumontDUrville", "Antarctica/Macquarie", "Antarctica/Mawson", "Antarctica/McMurdo", "Antarctica/Palmer", "Antarctica/Rothera", "Antarctica/South_Pole", "Antarctica/Syowa", "Antarctica/Troll", "Antarctica/Vostok", "Arctic/Longyearbyen", "Asia/Aden", "Asia/Almaty", "Asia/Amman", "Asia/Anadyr", "Asia/Aqtau", "Asia/Aqtobe", "Asia/Ashgabat", "Asia/Ashkhabad", "Asia/Atyrau", "Asia/Baghdad", "Asia/Bahrain", "Asia/Baku", "Asia/Bangkok", "Asia/Barnaul", "Asia/Beirut", "Asia/Bishkek", "Asia/Brunei", "Asia/Calcutta", "Asia/Chita", "Asia/Choibalsan", "Asia/Chongqing", "Asia/Chungking", "Asia/Colombo", "Asia/Dacca", "Asia/Damascus", "Asia/Dhaka", "Asia/Dili", "Asia/Dubai", "Asia/Dushanbe", "Asia/Famagusta", "Asia/Gaza", "Asia/Harbin", "Asia/Hebron", "Asia/Ho_Chi_Minh", "Asia/Hong_Kong", "Asia/Hovd", "Asia/Irkutsk", "Asia/Istanbul", "Asia/Jakarta", "Asia/Jayapura", "Asia/Jerusalem", "Asia/Kabul", "Asia/Kamchatka", "Asia/Karachi", "Asia/Kashgar", "Asia/Kathmandu", "Asia/Katmandu", "Asia/Khandyga", "Asia/Kolkata", "Asia/Krasnoyarsk", "Asia/Kuala_Lumpur", "Asia/Kuching", "Asia/Kuwait", "Asia/Macao", "Asia/Macau", "Asia/Magadan", "Asia/Makassar", "Asia/Manila", "Asia/Muscat", "Asia/Nicosia", "Asia/Novokuznetsk", "Asia/Novosibirsk", "Asia/Omsk", "Asia/Oral", "Asia/Phnom_Penh", "Asia/Pontianak", "Asia/Pyongyang", "Asia/Qatar", "Asia/Qostanay", "Asia/Qyzylorda", "Asia/Rangoon", "Asia/Riyadh", "Asia/Saigon", "Asia/Sakhalin", "Asia/Samarkand", "Asia/Seoul", "Asia/Shanghai", "Asia/Singapore", "Asia/Srednekolymsk", "Asia/Taipei", "Asia/Tashkent", "Asia/Tbilisi", "Asia/Tehran", "Asia/Tel_Aviv", "Asia/Thimbu", "Asia/Thimphu", "Asia/Tokyo", "Asia/Tomsk", "Asia/Ujung_Pandang", "Asia/Ulaanbaatar", "Asia/Ulan_Bator", "Asia/Urumqi", "Asia/Ust-Nera", "Asia/Vientiane", "Asia/Vladivostok", "Asia/Yakutsk", "Asia/Yangon", "Asia/Yekaterinburg", "Asia/Yerevan", "Atlantic/Azores", "Atlantic/Bermuda", "Atlantic/Canary", "Atlantic/Cape_Verde", "Atlantic/Faeroe", "Atlantic/Faroe", "Atlantic/Jan_Mayen", "Atlantic/Madeira", "Atlantic/Reykjavik", "Atlantic/South_Georgia", "Atlantic/St_Helena", "Atlantic/Stanley", "Australia/ACT", "Australia/Adelaide", "Australia/Brisbane", "Australia/Broken_Hill", "Australia/Canberra", "Australia/Currie", "Australia/Darwin", "Australia/Eucla", "Australia/Hobart", "Australia/LHI", "Australia/Lindeman", "Australia/Lord_Howe", "Australia/Melbourne", "Australia/North", "Australia/NSW", "Australia/Perth", "Australia/Queensland", "Australia/South", "Australia/Sydney", "Australia/Tasmania", "Australia/Victoria", "Australia/West", "Australia/Yancowinna", "Brazil/Acre", "Brazil/DeNoronha", "Brazil/East", "Brazil/West", "Canada/Atlantic", "Canada/Central", "Canada/Eastern", "Canada/Mountain", "Canada/Newfoundland", "Canada/Pacific", "Canada/Saskatchewan", "Canada/Yukon", "CET", "Chile/Continental", "Chile/EasterIsland", "CST6CDT", "Cuba", "EET", "Egypt", "Eire", "EST", "EST5EDT", "Etc/GMT", "Etc/GMT-0", "Etc/GMT-1", "Etc/GMT-10", "Etc/GMT-11", "Etc/GMT-12", "Etc/GMT-13", "Etc/GMT-14", "Etc/GMT-2", "Etc/GMT-3", "Etc/GMT-4", "Etc/GMT-5", "Etc/GMT-6", "Etc/GMT-7", "Etc/GMT-8", "Etc/GMT-9", "Etc/GMT+0", "Etc/GMT+1", "Etc/GMT+10", "Etc/GMT+11", "Etc/GMT+12", "Etc/GMT+2", "Etc/GMT+3", "Etc/GMT+4", "Etc/GMT+5", "Etc/GMT+6", "Etc/GMT+7", "Etc/GMT+8", "Etc/GMT+9", "Etc/GMT0", "Etc/Greenwich", "Etc/UCT", "Etc/Universal", "Etc/UTC", "Etc/Zulu", "Europe/Amsterdam", "Europe/Andorra", "Europe/Astrakhan", "Europe/Athens", "Europe/Belfast", "Europe/Belgrade", "Europe/Berlin", "Europe/Bratislava", "Europe/Brussels", "Europe/Bucharest", "Europe/Budapest", "Europe/Busingen", "Europe/Chisinau", "Europe/Copenhagen", "Europe/Dublin", "Europe/Gibraltar", "Europe/Guernsey", "Europe/Helsinki", "Europe/Isle_of_Man", "Europe/Istanbul", "Europe/Jersey", "Europe/Kaliningrad", "Europe/Kiev", "Europe/Kirov", "Europe/Kyiv", "Europe/Lisbon", "Europe/Ljubljana", "Europe/London", "Europe/Luxembourg", "Europe/Madrid", "Europe/Malta", "Europe/Mariehamn", "Europe/Minsk", "Europe/Monaco", "Europe/Moscow", "Europe/Nicosia", "Europe/Oslo", "Europe/Paris", "Europe/Podgorica", "Europe/Prague", "Europe/Riga", "Europe/Rome", "Europe/Samara", "Europe/San_Marino", "Europe/Sarajevo", "Europe/Saratov", "Europe/Simferopol", "Europe/Skopje", "Europe/Sofia", "Europe/Stockholm", "Europe/Tallinn", "Europe/Tirane", "Europe/Tiraspol", "Europe/Ulyanovsk", "Europe/Uzhgorod", "Europe/Vaduz", "Europe/Vatican", "Europe/Vienna", "Europe/Vilnius", "Europe/Volgograd", "Europe/Warsaw", "Europe/Zagreb", "Europe/Zaporozhye", "Europe/Zurich", "GB", "GB-Eire", "GMT", "GMT-0", "GMT+0", "GMT0", "Greenwich", "Hongkong", "HST", "Iceland", "Indian/Antananarivo", "Indian/Chagos", "Indian/Christmas", "Indian/Cocos", "Indian/Comoro", "Indian/Kerguelen", "Indian/Mahe", "Indian/Maldives", "Indian/Mauritius", "Indian/Mayotte", "Indian/Reunion", "Iran", "Israel", "Jamaica", "Japan", "Kwajalein", "Libya", "MET", "Mexico/BajaNorte", "Mexico/BajaSur", "Mexico/General", "MST", "MST7MDT", "Navajo", "NZ", "NZ-CHAT", "Pacific/Apia", "Pacific/Auckland", "Pacific/Bougainville", "Pacific/Chatham", "Pacific/Chuuk", "Pacific/Easter", "Pacific/Efate", "Pacific/Enderbury", "Pacific/Fakaofo", "Pacific/Fiji", "Pacific/Funafuti", "Pacific/Galapagos", "Pacific/Gambier", "Pacific/Guadalcanal", "Pacific/Guam", "Pacific/Honolulu", "Pacific/Johnston", "Pacific/Kanton", "Pacific/Kiritimati", "Pacific/Kosrae", "Pacific/Kwajalein", "Pacific/Majuro", "Pacific/Marquesas", "Pacific/Midway", "Pacific/Nauru", "Pacific/Niue", "Pacific/Norfolk", "Pacific/Noumea", "Pacific/Pago_Pago", "Pacific/Palau", "Pacific/Pitcairn", "Pacific/Pohnpei", "Pacific/Ponape", "Pacific/Port_Moresby", "Pacific/Rarotonga", "Pacific/Saipan", "Pacific/Samoa", "Pacific/Tahiti", "Pacific/Tarawa", "Pacific/Tongatapu", "Pacific/Truk", "Pacific/Wake", "Pacific/Wallis", "Pacific/Yap", "Poland", "Portugal", "PRC", "PST8PDT", "ROC", "ROK", "Singapore", "Turkey", "UCT", "Universal", "US/Alaska", "US/Aleutian", "US/Arizona", "US/Central", "US/East-Indiana", "US/Eastern", "US/Hawaii", "US/Indiana-Starke", "US/Michigan", "US/Mountain", "US/Pacific", "US/Samoa", "UTC", "W-SU", "WET", "Zulu"] ``` The last file we'll create is a `slack.json` file. Create this in the project (the root of the project, not within folder) and paste the code here into it, then save: ``` { "hooks": { "get-hooks": "deno run -q --allow-read --allow-net https://deno.land/x/deno_slack_hooks@1.3.1/mod.ts" }} ``` This allows your app to access an external source - the time conversion API. ### Run the app {#run-the-app} In your terminal window, run the following command: ``` slack run ``` Select the workspace you'd like to run the app in. If successful, you'll see a message that says `Connected awaiting events`. ## Build the workflow in Workflow Builder {#build-the-workflow-in-workflow-builder} To create a new workflow, you will need to open Workflow Builder in Slack. You can open Workflow Builder using one of the following methods. * **Use the message box:** In any channel, type `/workflow` and select **Create a workflow**. * **Use the sidebar:** Navigate to the left sidebar in Slack and click **More**, then **Tools**, then **Workflows**. Click **+New** then **Build Workflow** to create a new workflow. Under **Start the workflow...**, click **Choose an event**, then select **From a link in Slack**. The first step of the workflow is to collect info in a form. * Click the button to continue, then select **Add steps** to **Collect info in a form**. * Give the form a name, like `Time zone scheduler`, and click **\+ Add question**. Enter `From which time zone?` as the question and use **Short answer** for the Question type. Click **Done**. * Repeat the process for adding the following questions and types: * `Date and time of the meeting?` → date/time picker * `How long is your meeting (in minutes)?` → number * `To which time zone?` → short answer * `What is your meeting about?` → short answer * `Email of your contact` → short answer * `Your time zone when running the workflow` → short answer Save the form. ### Add custom function {#add-custom-function} With your custom function now available in Workflow Builder (after running `slack run`), add it as the second step in the workflow by clicking **\+ Add steps**, then searching for your app by name in the left sidebar search. Select **Time Zone-Aware Meeting Scheduler** as the step. We need to connect the info we collected on the previous step to pass it as input of our function. For that we’re going to use variables. Click on the `{}` on the right side of the inputs and choose the following: * **Meeting time**: `{}Answer to: Date and time of the meeting?` Then click on the caret and choose the local compact format (that’s the format we expect in the code of our custom function). * **User timezone**: `{}Answer to: Your timezone when running the workflow` * **From timezone**: `{}Answer to: From which timezone` * **Target timezone**: `{}Answer to: To which timezone` * **Duration in minutes**: `{}Answer to: How long is your meeting (in minutes)?` Save your changes. ### Send a message {#send-a-message} Now, inform the user of the time conversion results. Add another step to the workflow and in the sidebar, search for **Messages** > **Send a message to a person**. You can craft your own message using all the variables available from previous steps. Here’s an example: ![Send message step](/assets/images/send-message-237f57c0d633b32222fc0aa7ffbec04f.png) Enable the **Include a button** option and customize the button label. The workflow will pause here and continue once the user clicks the button. Save your changes to continue. ### Use the Google Calendar connector {#use-the-google-calendar-connector} Add another step to the workflow and in the sidebar, search for **Google Calendar** > **Create a calendar event** > **Add just the step**. Confirm by clicking the **Set Up** button. Fill in the event details using the variables from previous steps. ![Calendar connector step](/assets/images/calendar-connector-ef257ebebd5994ea3dcadbde544adcbb.png) ### Confirm with a message in thread {#confirm-with-a-message-in-thread} To finish, let the user know the event has been successfully scheduled. Add another step to the workflow and in the sidebar, search for **Messages** > **Reply to a message in thread**. Using the available variables, craft a message that says: Calendar invite sent for `{} Answer to: What's your meeting about?`. You can see it here `{} Event link` . Where `{} Answer to: What's your meeting about?` and `{} Event link` are variables added by clicking the variable button. ## Test your function {#test-your-function} Your workflow is now complete! Click the **Finish Up** button in Workflow Builder, name your workflow, click **Publish**, and copy the generated link trigger. Share the link in your test Slack channel or add it as a bookmark or to your channel canvas. ## Deploy your function {#deploy-your-function} In the process of developing your function, you ran it on your local machine. As soon as you shut that environment down or turn off your computer, the function will stop running. Deploying your function to infrastructure managed by Slack ensures that the function is always available. To deploy your function to production, run: ``` slack deploy ``` Choose the appropriate workspace for deployment. Remember, the local version (`slack run`) and the production version (`slack deploy`) are separate. Update Workflow Builder to use the production function and reconnect input variables. --- Source: https://docs.slack.dev/tools/deno-slack-sdk/tutorials/virtual-running-buddies-app # Virtual running buddies app Workflow apps require a paid plan Join the [Developer Program](https://api.slack.com/developer-program) and provision a sandbox with access to all Slack features for free. In this tutorial, you'll learn how to create a social app to log runs with your virtual running buddies. We'll pace you along the path to design a [datastore](/tools/deno-slack-sdk/guides/using-datastores), craft a [custom type](/tools/deno-slack-sdk/guides/creating-a-custom-type), tailor [triggers](/tools/deno-slack-sdk/guides/using-triggers), wire [workflows](/tools/deno-slack-sdk/guides/creating-workflows), and form [functions](/tools/deno-slack-sdk/guides/creating-slack-functions). Now that you're familiar with the course map, let's head to the start line. ✨ **First time creating a workflow app?** Try a basic app to build your confidence, such as [Hello World](/tools/deno-slack-sdk/tutorials/hello-world-app)! Before we begin, ensure you have the following prerequisites completed: * Install the Slack CLI. * Run `slack auth list` and ensure your workspace is listed. * If your workspace is not listed, address any issues by following along with the Getting started, then come on back. * * * ## Step 1: Create an app to complete your warmup {#warmup} Each Slack app built using the CLI begins with the same steps. Make sure you have everything you need before you show up at the start line. After you've [installed the command-line interface](/tools/deno-slack-sdk/guides/getting-started), you have two ways you can get started. * Use a blank app * Use a pre-built app You can create a blank app with the Slack CLI using the following command: ``` slack create virtual-running-buddies-app --template https://github.com/slack-samples/deno-blank-template ``` Or, you can use the pre-built [Virtual Running Buddies app](https://github.com/slack-samples/deno-virtual-running-buddies): ``` slack create virtual-running-buddies-app --template https://github.com/slack-samples/deno-virtual-running-buddies ``` Once you have your new project ready to go, change into your project directory and get to the start line. * * * ## Step 2: Map your course with your manifest {#map-course} Determining the definitions and manifest of your app allows you to create a course map of where you want to go. Open your text editor (we recommend VSCode with the [Deno plugin](https://marketplace.visualstudio.com/items?itemName=denoland.vscode-deno)) and point to the directory you created with the `slack` command. Import and add the following definitions to your [app's manifest](/tools/deno-slack-sdk/guides/using-the-app-manifest): ``` import { Manifest } from "deno-slack-sdk/mod.ts";import RunningDatastore from "./datastores/run_data.ts";import LogRunWorkflow from "./workflows/log_run_workflow.ts";import DisplayLeaderboardWorkflow from "./workflows/display_leaderboard_workflow.ts";import { RunnerStatsType } from "./types/runner_stats.ts";export default Manifest({ name: "my-run-app", description: "Log runs with virtual running buddies!", icon: "assets/icon.png", workflows: [LogRunWorkflow, DisplayLeaderboardWorkflow], outgoingDomains: [], datastores: [RunningDatastore], types: [RunnerStatsType], botScopes: [ "commands", "chat:write", "chat:write.public", "datastore:read", "datastore:write", "channels:read", "triggers:write", ],}); ``` You'll notice a lot of stuff we haven't talked about yet, but not to worry! We'll cover everything in the following steps. * * * ## Step 3: Define a datastore {#datastore} We need a way to store all the information that our running buddies log, including their user ID (`runner`), how long they ran (`distance`), and the date they ran (`rundate`). Enter [datastores](/tools/deno-slack-sdk/guides/using-datastores)! Datastores have three main properties: * `name`: to identify your datastore. * `primary_key`: the attribute to be used as the datastore's primary key (ensure this is an actual attribute that you have defined), which we'll use for querying information later. For more information, refer to [querying the datastore](/tools/deno-slack-sdk/guides/retrieving-items-from-a-datastore). * `attributes`: to scaffold your datastore's columns. For what we need, the following datastore will do the trick. Let's create a `datastores` folder and add a file called `run_data.ts` with our datastore definition: ``` import { DefineDatastore, Schema } from "deno-slack-sdk/mod.ts";export const RUN_DATASTORE = "running_datastore";const RunningDatastore = DefineDatastore({ name: RUN_DATASTORE, primary_key: "id", attributes: { id: { type: Schema.types.string, }, runner: { type: Schema.slack.types.user_id, }, distance: { type: Schema.types.number, }, rundate: { type: Schema.slack.types.date, }, },});export default RunningDatastore; ``` We already did this earlier when we defined our manifest — but if you hadn't yet, you would need to import your datastore within the manifest file: `import RunningDatastore from "./datastores/run_data.ts";` And then, register it: `datastores: [RunningDatastore],` Additionally, for any datastore, you'll need to add the following bot scopes to your manifest: * `datastore:read` * `datastore:write` * * * ## Step 4: Craft a custom type {#custom-type} The next thing we'll do is define a custom type for our runners' recent runs. Since we're going to be passing this information to our datastore, workflows, and functions, having our own custom reusable type will make our lives a little easier. ✨ **For more information about custom types and how to define them**, refer to [custom types](/tools/deno-slack-sdk/guides/creating-a-custom-type). We'll create a new `types` folder with a file called `runner_stats.ts` with our custom type definition: ``` import { DefineType, Schema } from "deno-slack-sdk/mod.ts";export const RunnerStatsType = DefineType({ title: "Runner Stats", description: "Information about the recent runs for a runner", name: "runner_stats", type: Schema.types.object, properties: { runner: { type: Schema.slack.types.user_id }, weekly_distance: { type: Schema.types.number }, total_distance: { type: Schema.types.number }, }, required: ["runner", "weekly_distance", "total_distance"],}); ``` Just like with our datastore, we'll also verify that we imported our custom type into our manifest earlier: `import { RunnerStatsType } from "./types/runner_stats.ts";` And then, make sure it's registered: `types: [RunnerStatsType],` * * * ## Step 5: Wire your workflows {#workflows} Next, let's define our workflows — we'll have two of them. Don't worry about functions yet; we'll get to those next. For now, you can just import and call them as shown in our workflows. For our first workflow, we need three steps to accomplish the following: 1. Allow a runner on our team to log their run details. The Slack function [`OpenForm`](/tools/deno-slack-sdk/reference/slack-functions/open_form) is used to collect data. 2. Save those details to the datastore we defined earlier. 3. Post a message of encouragement to the runner. Every runner loves a good cheering section! Let's create a `workflows` folder and add a file called `log_run_workflow.ts` with the following workflow definition: ``` import { DefineWorkflow, Schema } from "deno-slack-sdk/mod.ts";import { LogRunFunction } from "../functions/log_run.ts";const LogRunWorkflow = DefineWorkflow({ callback_id: "log_run_workflow", title: "Log a run", description: "Collect and store info about a recent run", input_parameters: { properties: { interactivity: { type: Schema.slack.types.interactivity }, channel: { type: Schema.slack.types.channel_id }, user_id: { type: Schema.slack.types.user_id }, }, required: ["interactivity", "channel", "user_id"], },});// Step 1: Collect run information with a formconst inputForm = LogRunWorkflow.addStep( Schema.slack.functions.OpenForm, { title: "Log your run", interactivity: LogRunWorkflow.inputs.interactivity, submit_label: "Submit run", fields: { elements: [{ name: "runner", title: "Runner", type: Schema.slack.types.user_id, default: LogRunWorkflow.inputs.user_id, }, { name: "distance", title: "Distance (in miles)", type: Schema.types.number, minimum: 0, }, { name: "rundate", title: "Run date", type: Schema.slack.types.date, default: new Date().toISOString().split("T")[0], // YYYY-MM-DD }, { name: "channel", title: "Channel to send entry to", type: Schema.slack.types.channel_id, default: LogRunWorkflow.inputs.channel, }], required: ["channel", "runner", "distance", "rundate"], }, },);// Step 2: Save run info to the datastoreLogRunWorkflow.addStep(LogRunFunction, { runner: inputForm.outputs.fields.runner, distance: inputForm.outputs.fields.distance, rundate: inputForm.outputs.fields.rundate,});// Step 3: Post a message about the runLogRunWorkflow.addStep(Schema.slack.functions.SendMessage, { channel_id: inputForm.outputs.fields.channel, message: `:athletic_shoe: <@${inputForm.outputs.fields.runner}> submitted ${inputForm.outputs.fields.distance} mile(s) on ${inputForm.outputs.fields.rundate}. Keep up the great work!`,});export default LogRunWorkflow; ``` * * * For our second workflow, we need to generate our leaderboard. To do this, our workflow contains the following steps: 1. Gather our team's runs. 2. Gather each individual's runs. 3. Format our leaderboard. 4. Post the leaderboard to our channel. ``` import { DefineWorkflow, Schema } from "deno-slack-sdk/mod.ts";import { CollectTeamStatsFunction } from "../functions/collect_team_stats.ts";import { CollectRunnerStatsFunction } from "../functions/collect_runner_stats.ts";import { FormatLeaderboardFunction } from "../functions/format_leaderboard.ts";const DisplayLeaderboardWorkflow = DefineWorkflow({ callback_id: "display_leaderboard_workflow", title: "Display the leaderboard", description: "Show team statistics and highlight the top runners from the past week", input_parameters: { properties: { channel: { type: Schema.slack.types.channel_id }, interactivity: { type: Schema.slack.types.interactivity }, }, required: ["channel", "interactivity"], },});// Step 1: Gather team stats from the past weekconst teamStats = DisplayLeaderboardWorkflow.addStep( CollectTeamStatsFunction, {},);// Step 2: Collect individual runner statsconst runnerStats = DisplayLeaderboardWorkflow.addStep( CollectRunnerStatsFunction, {},);// Step 3: Format the leaderboard messageconst leaderboard = DisplayLeaderboardWorkflow.addStep( FormatLeaderboardFunction, { team_distance: teamStats.outputs.weekly_distance, percent_change: teamStats.outputs.percent_change, runner_stats: runnerStats.outputs.runner_stats, },);// Step 4: Post the leaderboard message to channelDisplayLeaderboardWorkflow.addStep(Schema.slack.functions.SendMessage, { channel_id: DisplayLeaderboardWorkflow.inputs.channel, message: `${leaderboard.outputs.teamStatsFormatted}\n\n${leaderboard.outputs.runnerStatsFormatted}`,});export default DisplayLeaderboardWorkflow; ``` Our workflows also need to be imported into our manifest, so let's just double-check the following lines are there: `import LogRunWorkflow from "./workflows/log_run_workflow.ts";` `import DisplayLeaderboardWorkflow from "./workflows/display_leaderboard_workflow.ts";` And that they are registered: `workflows: [LogRunWorkflow, DisplayLeaderboardWorkflow],` * * * ## Step 6: Form your functions {#functions} Following fast, functions: we'll fashion four. The first function will store our collected run data in our datastore (so don't forget to import it first): ``` import { DefineFunction, Schema, SlackFunction } from "deno-slack-sdk/mod.ts";import { RUN_DATASTORE } from "../datastores/run_data.ts";export const LogRunFunction = DefineFunction({ callback_id: "log_run", title: "Log a run", description: "Record a run in the datastore", source_file: "functions/log_run.ts", input_parameters: { properties: { runner: { type: Schema.slack.types.user_id, description: "Runner", }, distance: { type: Schema.types.number, description: "Distance", }, rundate: { type: Schema.slack.types.date, description: "Run date", }, }, required: ["runner", "distance", "rundate"], }, output_parameters: { properties: {}, required: [], },});export default SlackFunction(LogRunFunction, async ({ inputs, client }) => { const { distance, rundate, runner } = inputs; const uuid = crypto.randomUUID(); const putResponse = await client.apps.datastore.put({ datastore: RUN_DATASTORE, item: { id: uuid, runner: runner, distance: distance, rundate: rundate, }, }); if (!putResponse.ok) { return { error: `Failed to store run: ${putResponse.error}` }; } return { outputs: {} };}); ``` ✨ **For more information about how data is stored and successful vs. unsuccessful payloads**, refer to [creating or updating an item](/tools/deno-slack-sdk/guides/adding-items-to-a-datastore). * * * Our second function calculates weekly and all-time total distance statistics for an individual runner. We'll query the datastore to get a runner's logged run details, calculate some statistics for that runner, and then return an array containing all of that information: ``` import { DefineFunction, Schema, SlackFunction } from "deno-slack-sdk/mod.ts";import RunningDatastore, { RUN_DATASTORE } from "../datastores/run_data.ts";import { RunnerStatsType } from "../types/runner_stats.ts";export const CollectRunnerStatsFunction = DefineFunction({ callback_id: "collect_runner_stats", title: "Collect runner stats", description: "Gather statistics of past runs for all runners", source_file: "functions/collect_runner_stats.ts", input_parameters: { properties: {}, required: [], }, output_parameters: { properties: { runner_stats: { type: Schema.types.array, items: { type: RunnerStatsType }, description: "Weekly and all-time total distances for runners", }, }, required: ["runner_stats"], },});export default SlackFunction(CollectRunnerStatsFunction, async ({ client }) => { // Query the datastore for all the data we collected const runs = await client.apps.datastore.query< typeof RunningDatastore.definition >({ datastore: RUN_DATASTORE }); if (!runs.ok) { return { error: `Failed to retrieve past runs: ${runs.error}` }; } const runners = new Map(); const startOfLastWeek = new Date(); startOfLastWeek.setDate(startOfLastWeek.getDate() - 6); // Add run statistics to the associated runner runs.items.forEach((run) => { const isRecentRun = run.rundate >= startOfLastWeek.toLocaleDateString("en-CA", { timeZone: "UTC" }); // Find existing runner record or create new one const runner = runners.get(run.runner) || { runner: run.runner, total_distance: 0, weekly_distance: 0 }; // Add run distance to the runner's totals runners.set(run.runner, { runner: run.runner, total_distance: runner.total_distance + run.distance, weekly_distance: runner.weekly_distance + (isRecentRun && run.distance), }); }); // Return an array with runner stats return { outputs: { runner_stats: [...runners.entries()].map((r) => r[1]) }, };}); ``` ✨ **For more information about how data is retrieved and successful vs. unsuccessful payloads**, refer to [retrieving a single item](/tools/deno-slack-sdk/guides/retrieving-items-from-a-datastore#get) and [querying the datastore](/tools/deno-slack-sdk/guides/retrieving-items-from-a-datastore#query). * * * Our third function calculates the weekly and all-time total distance for the whole team, as well as the percentage difference between this week's runs and the previous week's runs. Similar to the query for individual runners, we'll query the datastore to get the team's logged run details, calculate statistics for the team, and then return all of that information: ``` import { DefineFunction, Schema, SlackFunction } from "deno-slack-sdk/mod.ts";import { SlackAPIClient } from "deno-slack-api/types.ts";import RunningDatastore, { RUN_DATASTORE } from "../datastores/run_data.ts";export const CollectTeamStatsFunction = DefineFunction({ callback_id: "collect_team_stats", title: "Collect team stats", description: "Gather and compare run data from the last week", source_file: "functions/collect_team_stats.ts", input_parameters: { properties: {}, required: [], }, output_parameters: { properties: { weekly_distance: { type: Schema.types.number, description: "Total number of miles ran last week", }, percent_change: { type: Schema.types.number, description: "Percent change of miles ran compared to the prior week", }, }, required: ["weekly_distance", "percent_change"], },});export default SlackFunction(CollectTeamStatsFunction, async ({ client }) => { const today = new Date(); // Collect runs from the past week (days 0-6) const lastWeekStartDate = new Date(new Date().setDate(today.getDate() - 6)); const lastWeekDistance = await distanceInWeek(client, lastWeekStartDate); if (lastWeekDistance.error) { return { error: lastWeekDistance.error }; } // Collect runs from the prior week (days 7-13) const priorWeekStartDate = new Date(new Date().setDate(today.getDate() - 13)); const priorWeekDistance = await distanceInWeek(client, priorWeekStartDate); if (priorWeekDistance.error) { return { error: priorWeekDistance.error }; } // Calculate percent difference between totals of last week and the prior week const weeklyDiff = lastWeekDistance.total - priorWeekDistance.total; let percentageDiff = 0; if (priorWeekDistance.total != 0) { percentageDiff = 100 * weeklyDiff / priorWeekDistance.total; } return { outputs: { weekly_distance: Number(lastWeekDistance.total.toFixed(2)), percent_change: Number(percentageDiff.toFixed(2)), }, };});// Sum all logged runs in the seven days following startDateasync function distanceInWeek( client: SlackAPIClient, startDate: Date,): Promise<{ total: number; error?: string }> { const endDate = new Date(startDate); endDate.setDate(endDate.getDate() + 6); const runs = await client.apps.datastore.query< typeof RunningDatastore.definition >({ datastore: RUN_DATASTORE, expression: "#date BETWEEN :start_date AND :end_date", expression_attributes: { "#date": "rundate" }, expression_values: { ":start_date": startDate.toLocaleDateString("en-CA", { timeZone: "UTC" }), ":end_date": endDate.toLocaleDateString("en-CA", { timeZone: "UTC" }), }, }); if (!runs.ok) { return { total: 0, error: `Failed to retrieve past runs: ${runs.error}` }; } const total = runs.items.reduce((sum, entry) => (sum + entry.distance), 0); return { total };} ``` * * * Our final function generates our ordered leaderboard, as well as a formatted message with all of our queried data and calculated statistics: ``` import { DefineFunction, Schema, SlackFunction } from "deno-slack-sdk/mod.ts";import { RunnerStatsType } from "../types/runner_stats.ts";export const FormatLeaderboardFunction = DefineFunction({ callback_id: "format_leaderboard", title: "Format leaderboard message", description: "Format team and runner stats for a sharable message", source_file: "functions/format_leaderboard.ts", input_parameters: { properties: { team_distance: { type: Schema.types.number, description: "Total number of miles ran last week for the team", }, percent_change: { type: Schema.types.number, description: "Percent change of miles ran compared to the prior week for the team", }, runner_stats: { type: Schema.types.array, items: { type: RunnerStatsType }, description: "Weekly and all-time total distances for runners", }, }, required: ["team_distance", "percent_change", "runner_stats"], }, output_parameters: { properties: { teamStatsFormatted: { type: Schema.types.string, description: "A formatted message with team stats", }, runnerStatsFormatted: { type: Schema.types.string, description: "An ordered leaderboard of runner stats", }, }, required: ["teamStatsFormatted", "runnerStatsFormatted"], },});export default SlackFunction(FormatLeaderboardFunction, ({ inputs }) => { const teamStatsFormatted = `Your team ran *${inputs.team_distance} miles* this past week: a ${inputs.percent_change}% difference from the prior week.`; const runnerStatsFormatted = inputs.runner_stats.sort((a, b) => b.weekly_distance - a.weekly_distance ).map((runner) => ` - <@${runner.runner}> ran ${runner.weekly_distance} miles last week (${runner.total_distance} total)` ).join("\n"); return { outputs: { teamStatsFormatted, runnerStatsFormatted }, };}); ``` Whew! Don't slow down now...we're almost there! * * * ## Step 7: Tailor your triggers {#triggers} Triggers invoke workflows. There are four types of available triggers, but we'll only be using two: [link triggers](/tools/deno-slack-sdk/guides/creating-link-triggers) and [scheduled triggers](/tools/deno-slack-sdk/guides/creating-scheduled-triggers). For this app, we'll need three triggers, two of which will be link triggers. This means that they require a user to manually trigger them. First, we'll create a `triggers` folder and define a link trigger for collecting our team's runs called `log_run_trigger.ts`: ``` import { Trigger } from "deno-slack-api/types.ts";import LogRunWorkflow from "../workflows/log_run_workflow.ts";const LogRunTrigger: Trigger = { type: "shortcut", name: "Log a run", description: "Save the details of a recent run", workflow: `#/workflows/${LogRunWorkflow.definition.callback_id}`, inputs: { interactivity: { value: "{{data.interactivity}}", }, channel: { value: "{{data.channel_id}}", }, user_id: { value: "{{data.user_id}}", }, },};export default LogRunTrigger; ``` Run the `trigger create` command in terminal: ``` slack trigger create --trigger-def triggers/log_run_trigger.ts ``` After executing this command, select your app and workspace. Once completed, you'll be given a link called "Shortcut URL." This is your link trigger for this workflow on this workspace. Save that URL for when you start testing, since that's how you'll invoke this particular trigger. You can also use the `slack triggers -info` command and select your workspace to grab that URL again later, or click the `/` icon within Slack to open the `Run workflow` menu and select your trigger. * * * Second, we'll need a trigger to display our leaderboard. Create another link trigger in that same `triggers` folder called `display_leaderboard_trigger.ts` and define it as follows: ``` import { Trigger } from "deno-slack-api/types.ts";import DisplayLeaderboardWorkflow from "../workflows/display_leaderboard_workflow.ts";const DisplayLeaderboardTrigger: Trigger< typeof DisplayLeaderboardWorkflow.definition> = { type: "shortcut", name: "Display the leaderboard", description: "Show stats for the team and individual runners", workflow: `#/workflows/${DisplayLeaderboardWorkflow.definition.callback_id}`, inputs: { interactivity: { value: "{{data.interactivity}}", }, channel: { value: "{{data.channel_id}}", }, },};export default DisplayLeaderboardTrigger; ``` Run the `trigger create` command in the terminal again and save the Shortcut URL for our second link trigger: ``` slack trigger create --trigger-def triggers/display_leaderboard_trigger.ts ``` * * * Finally, we'll create a scheduled trigger in our `triggers` folder to post a message to a channel with our stats on a weekly basis. We'll call this one `display_weekly_stats.ts`: ``` import { Trigger } from "deno-slack-api/types.ts";import DisplayLeaderboardWorkflow from "../workflows/display_leaderboard_workflow.ts";const DisplayWeeklyStats: Trigger< typeof DisplayLeaderboardWorkflow.definition> = { type: "scheduled", name: "Display weekly stats", description: "Display weekly running stats on a schedule", workflow: `#/workflows/${DisplayLeaderboardWorkflow.definition.callback_id}`, inputs: { interactivity: { value: "{{data.interactivity}}", }, channel: { value: "{{data.channel_id}}", }, }, schedule: { start_time: new Date(new Date().getTime() + 60000).toISOString(), timezone: "EDT", frequency: { type: "weekly", on_days: ["Thursday"], repeats_every: 1, }, },};export default DisplayWeeklyStats; ``` Since this is a scheduled trigger, we won't have a Shortcut URL for this one since there's nothing to invoke manually. Use the following command to create the [scheduled trigger](/tools/deno-slack-sdk/guides/creating-event-triggers): `slack trigger create --trigger-def triggers/display_weekly_stats.ts` Make sure that the app has the `triggers:write` scope added to the manifest! * * * ## Step 8: Cross the finish line {#finish} You're almost to the finish line! Let's use development mode to run this workflow in Slack directly from the machine you're reading this from now: ``` slack run ``` After you've chosen your app and assigned it to your workspace, you can switch over to the app in Slack and give it a spin. Use the link triggers you created previously; when you paste the Shortcut URLs into the box and post them as messages, they'll unfurl and give you buttons for invoking our workflows. Here is an example of the message displayed after logging a run: ![Log a run](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAlUAAAA9CAMAAACz4GyeAAAAaVBMVEX////7/Pz66bSGhYR0c3TQ0ND22trxyf4dHB3i4+Pa2tslf+JFRUW4uLg1NDXy8vInJidjYmOamZlVVVXEw8MyiuX39Pmjo6OsrKySkZDp6uripjoecdqUwfFjmtThtVLTuogZVJcuarAYdh6bAAAACXBIWXMAAAsTAAALEwEAmpwYAAAQMElEQVR42u2c23bbuJKGP5DgCRRF0ZblxKt7Lub9n2rWTBIfZYriESAwF5Is2bGTTu9pJ5nN/0oCiSIOP6sKhQJhwoQJEyZMmDBhwoQJEyZMmDBhwoQJEyZMmDBhws+FOP5w37nP/eSm5sMbF8JymsdfC/6BNd/ljBD/aENkOgAkwrcv2TTGGsB7q+popnn8teB95/rVn1f7X/+sqlrKEICFH728FMhhmqffC/KblPJFq+N3acdI8Nal+yxWn57+paWEFBqHySnlNIW/MKu+0kQiXopWE3fdwTj+k8oqb2nfuubqIDk2a5z1jpEqd60dq2wTnTYrOKNqAFCZrjRcAt3O7Vr2FfBh0wAk8xs3Tf4/7le9oFTyMQyNiaUU0tiXbouaDyuRiTjz7QcyVVT4Z/MagHMl5soUquNMaeufpcNsZg6O0ocoCIoa/8yINJw3aj7IIhiXNZAN6Zp8nAVCewNBLrN5k2XxvEYWOVZ0sFNm+WOERUbaydb1YZc7C4cGLruu3TlhaugG59uru8i5x12XBpP10I0O4KLP6mny35VVf+ZhaIilFA/CSDPfhH9uxPzEuTF+H7aB9gcPXCvaxNPDbmajNuiE7PRZPYxZu2gG1es4303gZal93S+3i8ZLvcETxvieGLzaArFuLQGB7fGGpTGea1zkOjsknu4tDAdWbc9t7SNln9A7fN9P+iOrek/bdFYD5622q6Tu5vdNs2dcuxwG8Fw6gKoif2LV+/lV4g/RdhBDByiAThj3p2HxXyc2YyyzQZZF40o/GEiaZP1BQNkAQXviiaUPiR3CZAH2+ubS+1S0PZD6fdwVdxBs/GBWOiiDi8+ZptZ+AB26u2rY+LIladJ7ET210uQP5BvJnYs34SAWt9HmZY80H/jyBRg0cZRF97tmr4tqAPDa1Q29N838u7FqFZ1S6uBfPcSXDiQfP51U08HQOY8uH5wJBgy63mmT5JoTVpWuienNGiTu+vzSoIHScM6tAINHlzQAPbN1uGG1Ju9J7caQqO2sFAy4Y3OWg4OsxS2bELBJf+oeFY8Z0uN+Z6c3KV1K4FkHJPreyUDDxShYrvU08+/CqpU01I7kdklHoxoUjQLaWMUOgwkeXxXQFu28lhViAeVpWPUEFxZqlsP2zfXkal1yE+wcsKjHr4HZ3byb1XFJ3od7WokuXGAdXJQA7oWiulOktxYNkAxhif5C5UsNzL1H5t5noKuTx6iZZv5dWGUfg/I/mtBduI697VPsiNWpb4UgfGpCuDuWdD7bZ+zaKblhUOviwKKBizuArIlLwCFYljIvbwJuZqYaueIT6eCoHI0fPD2rcpwYxJdo8rtwt5ZMdHjKHHF/kaBdXkKZtfOHaeJ5nyioTrlTUjiaBkUTNTQNqkEkCYDZviHhThKPz9eIMfL8tZl33k4byaLpYgH4sqFywD0xftyN5wEgkLOi2QoeMURwxmFLxnQOXGd2wYIw5IVyFNukhSQh0WErIFihhALO3eeHh4fYAmzn8TTv78Mq0eRrscC57kEpmkatFUoBtG6n08q3Ajwd2+cbceUYD7r4aupU3KZRCCCdkfUnIJLxLu5gQqM+jbHpFzFUi9C0xjnaBNnAQPJi0/IZVU9+X7IFkgSdRYuFz9mDHMIS2CnJuxbAPXya5p132V0WsRyD8j+5Vo2K+kbRgGpUoyA2oQH5Sb4ep8xF533lply9NnHi42cHFK0Z5yUEPiLc7EnhB3pEPC0Ico5EvXoIK3fcXd4FZfehWeFe2V0+2SgXFzfTHP9Ev6oYykXZJABrRYNqVENDo5pkH74uXmeVWqf3XxW+qg3cU6k78OCJD5dNen9ywylPBpLNy7LnC4a3tZebSPVT/aq6zNeipNhRCdXQgKJo1C7gI97ap7Oq/bFHGrUzaBdKhUcaSv+t++/GaXPld82vWm39JxO4M2jFGkA1LKSB4CaY0pgm8GM7Ns2FaBZVJMMKDUrvg+taF6mzQKv9fhquCT8YW68b1qLcr7aaYq0aVNOgAGmglNFXWQ3dV6YpfIcEuind6vdhlYu0aualCAEK1jTQAE3TfTCvBUFzG4mXa7+Q98h3ChdMTvhvoqtuP1Z6URq/WMP64FUB/GEAQfH5BalSo9920/5RlPk0cb9LhnHdsRa3QFGwIxYFBRgJ9EH9klT1z/OzpnXDb8OqKMjFvCMsWBcUxU5brSkw8DL/N7GBI8pXrNTP0BuTrvpdWOVuF71O3ehYF7DekaqgCF/Jw7qSqtRdWa7ztf9rbP+LHyTaSvzYPeJy/+NS/H6TLFbvLPIk9ii8sB9GPbMJJAlrICHhv2cAVnFCnygZ+n7RA75nTk5a+e805P34siSw4dde3qVYVqxqIH9preNh1+ygUEpp+2pVPz3Gdy/r/cpTFN9KIs0yPx0tiMtYDRc1KKeyfAssI5nOnoZQObX/dyXzo8Cn4oOcPc60BYIi/fFXeFXD2fCjzkr+vQoHkWeL7XdObkVNLuYt4RrWayiKgoPL/gVxewwjBGVf5kg4Taj7FS2gB3QJ5G+yPevDMEy+f6StPWwDpN/aSfjgh6JKFfitDJJGoEJE+HgJ2XYjqu3Bj1AhYrPNALVtjzlET8VPcvbYxIBKH+9+fKi65G+Mr/rLuxmb7ju5oG63Chz9nf0rAArP/WEAvNMOuaRa3SBy6FOM/mV952s25L2AV5TRYVTc9s2qJ1iag3bsvOXbc9vIzwjTo2y74aq5/DJEpqpkDU6PFfFBnUaeqUAKtzTb+Khjn4oPcp55HVth/8b714v/szMyf6/+wQSGdHQF6y5FCGcBitvgpI9paLfzoe66Tgfj0NqfYgETt9Afq+x8C9n51vdEpsW853JI7aILPO+sIfCsyjpdJI03T8I+Pwt0oiHw8oXtd61WxZFVfjzX6ZClJtYEniXyWvZ1+vhx/7NP9ilByi50oknc/CzQ8x5A2LSGrLc26qAyQRtnFYhgwJ3VkO3DxquaAZJR2LlfhcYdjdWu2B3kPClOOeRdKGsO7UHZReH3XA6+8HenvjkMBf5HrLcjq8o6XRBYzy3yGlgO3sXeiiq70LMoyTLrPHcQuxxy1RHoIE06wD9vENK35GNk8rPAnDXgx8tgjGyP8GeDF3XfY9W8zoyqF3LsKNZpknSbtk0dL/drRD8Xrj1vxapejVEQLqr3ZlVek3vG17Id7QCjHXzP366wmtBv52aMc9kIu/I6as/2/dw0fZENhDPn6aVMgvvR7lnVhVnSHezqllingfPzeuV1RF57ta+z2Dhmo1aNRc87gCDRWeiNVkhR5vg7EbaGq8EfFn4HXHa9roC0s9iaq8CI3daDc9qCkWfbbc2RVcfig5wjqxI58gCH9iyF9kt/UYcinbUkA8BhKMjt9lz6u2fVnu2lhHTjq5YzPSix8yivrPaTfghF542r+V7sZZ/EvXFpyqZ3gB90LI21FDS5I5DNvCXvbBNK2wt/Edavs+rUfXhaBa45F2etc5E69145jeLEtd/zMOeWm6gf+tufYd2CIa3G00zhePwcmhn45iGR8fU6tABaEI5jxDh+uubhy+dqwPn31yY9qOdZ7Ozh1QrHcZv513awR0O4q+PliHqoHgzkuxGJtub6XkS7Byft05JItP4WrwKwuzvPvBVA8GDDFpHnuVA7nzQcTpawL4tF65/YZtPXs/KkPd3WVAYPoodrk4QvxubRXD/a4an7DfgP43bgyqqxMjIBsHasgliUmNh8Poi9qR4+1ylVaMfRAYlb8ZgoQd+4LQ/Xa2WvIK9NA/ixuYHqu99ZuKgj1uJOnJ21zkW9V7omOpyViZ7R6q6wVBd7exjnPyEKqsPx7Jkj2oAXB1DBAzUEL/VmYn0pF7GoT/OvHr7cPcRy34EALuwNzdHb3NehD3GJ7wdAuDN2JgGEEdDAw3FwfR05bAawy6f+YNtPADoazQdmfd+vyt0ehHcMAn5d7OvTU9nyzHTBsT0rk0spvVsoHYiXK+IlOH9+UmBLmHtU3Z2UdSwB7r2PVJ2E8O7YTedWanWyzZrQCtVzLlzConFQdQNIB3iFmpdw4b7LqluaXFy0LthTqivLZp9fXNy657QKLXdOiJsyGtA/YQ1Yimxwl8+Lv6M0OyHExs6/au79Nzqwq0Mk4DFe+BJEBDDfnz2cv1z9qPETbDTAbQB86OReellVmm0URTcKH7jqjmz4qngn54hPo58kT+3pkUKI9vLHw0xCiDoB0NvmPMjb591Mii463bu/Ea20Tb0+Z0sPcEF/jM0MwGP43fOAbhP3WrpReuuo6bogcEUtpHklZc+JlpXmEbhZaS1+Ql5dWeLXu2jUU2e64FukytaAZvHSqJ93x1Vs/3qdEnBfyFh92lG3nDdAq8rgBal0CyidtAgVQtbJO0BJ3e4+AlYC5awFxpPVnyuFOy3eyzkdcWP91c2hPXOtjxm3e2/t2VAIV732iRQ07Lbmcze4bvOim/5mJDutEY11i5eshSNgCxWHN8F+PjPJW1/H8J9tCwsV9sOq63XdW98rRnXdu0218Ld67F/sHAvqeRsYWG2R59W7rwGv0PhpG7dL5YVu8L3hYpiLLclg8T1hUL2dmY6IQBbjEAbaH8+b1cwbjO+rdO87ZdbPQqN2zY9lzUL3IOPtzHREXnuoc+EGcSn7KLxD7aKWqV/4BenW94TePRCQs0QqhR7iREgXmD7TmVVKNTpIhCqGdO8qRZzL0CZ3IERonkb2qfggR808vV8D0vvS9Yf2pCZrr5pEJ9IuZ1ptAQ5DQUyRtTbQO5GBLKztIbG9zoRvlL2ogXPjBrHKq8T2cBCr5nXuG8NsPBs1gD8knnZR7A+MXMwjk2yIZQ2J7VsTLmrfc3/h+1UNZijLJpCLIPu8uXVEhbz5n8ZFX7vs7kvKUkA3drdX724BH8NsHrfcmcdBJkC87PzefN3F1uhwfZ9ameigjjZmZBVYF+1fgzHzeyG/8JauOtTxTTC/F3MxusDsXsR12vRd+uXFR7i6x+12K8G3octNKTTr7Xa7TRixfZMfbq+CthfJZ0iiyASR/6L4SY7f+qc9cd6hPet0iBoRQBBt+mQn9mkosNsHFTTH7h/9qy6K5eoW4NFP59Tb592M+tloYmjNg5cDlIltcDUJ6KB+MN1p/twZDyTJX/iCo/j4qAlcUUe3+1MF4qInpd5c3L5Cyku450I36OIGIHyvyMINsOpLgNUh2eqt8zT5xkFeAuTRDcDq5Ma9lG88K7oBisog5iXI7GmS8m/V/Ko1Yv7s9jcqvyh+5a59H3bdKipzcst+KBbR9UlhvnGvtaKIP0PgP31RYCd2JT99o3nPRX3jw5//Eg2EW9rgHndxz+7VeC9W8e5JOErufZC5+ZVO0xfVK8m3i+j6+zU/1skot+3Iz/iC4zfhxF2szy+4zc71//NMmOZwaDb+pT7REC5fKaz+yoH/z+5+vBPjP3zG5u+HIzOim9Vj++4WcAK//Mmtf4VXMabd562/C6a89X8DVk1nbCZMmDBhwoQJEyZMmDBhwoQJEyZMmDBhwr8T/hdBOjFI/UTWGwAAAABJRU5ErkJggg==) Here is an example of a message displayed after generating the leaderboard: ![Display leaderboard](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAnQAAAB5CAMAAACHtkwKAAAAb1BMVEX///8uLS766bT6+vri4uLQ0NBvb2/xyf7o9foeHR708vRiYWKbm5s7OjsthuT22tp8fHzBwcFIR0hWVVaSkpLs7O21tbaIh4fS2+GkpKQfdd2sq6zjqDwZZaVblsaLueE5fbOlyOZ8qs7Tr2fYv4703bsWAAAACXBIWXMAAAsTAAALEwEAmpwYAAAZeklEQVR42u2d63arOrOmH5DEGRt8ij2z+vt67+77v6Q9xu4eMycnsRNjzJn+AXbsxJnnlTXXar0/ohiBJEShUpX0FqChoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhofHbwTj+Iai/ePLX8v98eMU7GVaqn+XfBuL4/7r98sltK9o/szGRmwFIIZrXwmZME4B3q68b/Sz/NjC/4RznP5wPaUucd62JZPg6yxaZflb/SKE7qzudf//Lerj44im/CgXvCvezzKdHcwIDCMNwAkYYGvop/pNGOuH8+1/Wah2z+pD5WvN+PbU40qwTywKKYls6+MXWCk7PlYvjBOJgIV8SgHgGcDnvzxNaDD4W8v2sub1iRWzyEK/mN68zC7W1DNVUZT40KsNcEpn2NQBjVMlzyFMdWM06Es3Wtp7L/rrLtansayLxPEpVu5oX6nFQiE0N2Jn3gGfVqgVQhm1Z17GRtymR6absXt6FUo0eIboVdk7wUDB6PGqZMh6OErisEs/00j4BEFumS1h6nTJ/1lLwewidmNqrNCU2mxVgYjOc/hfOi4n4IHesHFlljkjbnSOQOf3TS8kcGewAufMoc0pRVd3jZvFI6uxmd2UunzMH+SB38tGpwjVQUoOzgwqIM9Sjc7FpCVOZ84yyt/u6l+NNZ8SWZmdcqGOZ87fGSwLkRnl38bhPAAZWnxevQFVaCH4H9Srm//bTFfE4Xj30Cm/FJvv0afS/jjVRZcrKNQVNOsQiwqwuJ5PJHAjC42m/Z1Y4tpxMJgvuRkFbswW8ws2IgLAdkAvARxLvqDY2YDB4nPCc16xx8Kr2RfUa4X0uDNiOi7plFZnl8UyzKdqj5EJxv4TVeJ8AWI/BGmC79cDfaiH4y4XujMTxEDP/TwBGxw6U2qKpFXgJO9VgkW232wYI7sqj8zZ1CcrYbrc59U21CLGBTZ2OeAYeeXxxF1rIEgnejnRUw9SnFDaC+sXIGGa+jw22aSWAipr749lfepy0LUAc3xwlctXceAqIxjGxXWoh+EvVqxhtGytNo8JtVq9E024BjPMz/cpuBqlc0Yzg9p3hM7KgRDnZ+j0btT5yGuYWjULxnIZOuMkKvPbQ0t0gI29B3gLEyy/d3h2AajdHCdNyLV15DzxVcrvRMvDXjnQDQRaNC2G9kjkaVnlnOJ4vxSKlhdXnz5/7KVLyarGD5efPn+9wKtH6exF/ZNA54Tqnr80Kk8qjglpS3t42TYrNjmFNidVfNqLdbPL9mGt//RaVX6QvCeIBx9nspkAamZae0v3V6vXBp6hntdEfPfgi2niEDTy8U8rKJTtdElAZ7jh/c2JCe9FNooQ/dDIDcK2aQQ3cYVOBGOeAixj5SSZYU7GFCfsh6TG/hzZ/zG8BHh6+LHAeyledzKlO4w68+yRJXAPgwXK1CPy1QidsVqJGGKuHY4kD1mZDDiT37xTTMDxd/EynWVX4b877lDV3826tK91JuQSUwL4HKLNKLsOsakcZ3I3krpFuTWXilvBM9I13FAUqmu0Tw55aZer7l30CWAbA0xqgTpZaBPhLF/yFEdrP6f+sn4Ag4fAXiLcOYP+f9rw28nbO8LUnj+m5BypGSwC/qQsvBSUprby3Pi+eB7dHl3m8yPE0kWn9Awv+otaP+Dc3JOLqIUrb/Zys/xskwGraqcp3pu1e672ROc6eWndHnZSDSKUvw2V1fNmxGFW09etjp7bMe8aJxm8+p9smrMRtp1j7PwFJEJzdB3U6V3/6vmqF36neyPetFymt1u+dv6r0LpJ/5khX04b2c4ogSEgIkqOJXTfQjd9ZMrr+3mrvzo6GX3CZldqdxj9yPx3NqHkapJNGdWo1KIqigKIgCJpu9i30s9f4xWuv25KVSBXBYVYXJBAkNEcTPQ2NXzinqxkHE/sGkSQkQZAEBEnwMqm391pWQ+PXOYfzmgcfRRAksJe5IIFhb97qDtP45Qv+24SVSOnGuYQkIAmSoB/hnnR3afxyQ4JWxWJQP4emAisJsEgCiiJIbADLTupWd5nGLx7p8poHv1YcL0cEQb8E1f5WzlZPgHx/47PwfvXr6f3pt/Mxw8wvr+i7e+ZVC8wmY2dYHdPQIik6OyKxABorPXhMIicDmDnBu4sEwvypYVEMlZ8BTF0p33pqZOHXrhnszj7AEvx077f2v4FI5n3dFeR/gVk7IzR7Cz/2h93LOjGaw/3H/jDBH3ie53lOFvuek4FQg6OWyWhrRTtQ7sBohrn//b6pC9ssv+E2vfZrI4cV7b6rYj/9uZGus1+Xe1ncL0bIfofIi/G6zWeAt6zfXzEX859686O8Ut1+lfTsGpfzrgNnLk43tnwd8597+y83riylApBr2S0IzrbW4eF2Bw3DMIw6TabrZ7WWMFFndidKGTyn7pP8fsbl4q71/tzb/NN2Dh/r18Njszvj1TlabS/rnQCL4v2ig59plxSp7NeDk7MPoCon772xJ36dsv0GKttPeoKexf1dIUeAGHi3XfuX/qHe/mByf39/n3plNSpvvAvi9LWMFA9wkd2WtKr8/nnMg1otf81tFg8fakiA2cjioF8BklDaZQMQ/l9796IwR+vpdr6dPXKZi/kGgmgLk2Abx9lguAVvcp+5bUPsNLME5j4jL2WRT0q3llL0mqU7+5A5bPYZs9K3cnsL47YZVkanNxaF6VpBaHm10QiHTKF2yIFvBhli7Hp+3gKqzAKrVqWclrMEz68aLnMx3gLIaearUVuyP3SZi/G2u6KFRZjs78TOWBRikIE3rMKqwSpBzPztPO/0aN9ogLZsISwzGFZN2c0NzBdbf38QkHLX1NaOQZvvmsOm5VkpTNPeXrZFvC0DyzI9EW376vv+7H7IaTs0Rm0JsdO4dbvvXWAhLDHdRNWioZFTRnmDnHqhqo1xd8W+Y2RjDppBBvv7A+KxE2IXyIE5NNSsLSB2mphyX/35nunTfc/8sNC1yu/s1xZIPCV3om6sBuA5PDZe08UTSt3iF8K+90vCKgOnyiT21kohopaByILN0Knqen7rl0a7o1Vby3CcsCqNptPb9tYaHDIlZp+RFNsgt7ewKRp3L3SyKVSa2QZ1sDNNa6dQu/kmr/zdaDvc1btkmAETtYutVIkAsY3Stm2bSTryN1EKiDJ0nK29wxKhkYZZl+OqXWylLYQ3ovWapkDWWePDaDfdTBsxbLfzxCpBqbS067YF9ncE0LQw3Xpb4jywDVnC7GEQ7jfsHA4CMkgwnEGQ5YF30PuTpJ2K0t7mZumqXWxZpemIpK++68/+hyhdKe6tkqD0w41T9L0LMNwJhySXZjbOVRoXjdGIUu4ypxH2vVVy6BiZR6VZGg19mQDy0WnVU5SJtm7KLDdLgtK3clOW+8d5rmf2Jex75ieGvmmspvzrf3z69K//+Pfg06dPg/+c/e8//vjjjz9ieaoPRiN3TuxKUG7MOAJGEYG79yC7lzDt/kg8iF2J78PCjcHoggZ0Z+8zxxwygAt31lfkevtYJzNiNxSMXCncEZ47wncVjEaExuH9mYRAYHhgjZCu6trWmRVuDHM3xgPG1j5n0sexmLoxs7GhWITMXAlxSBQKuLAIDKKRB2q23+7X3fx+Nj0GEbrRZGRMkW54cWF03LPDQWDuKsBbBN6FfzC9PXcCwp1h+RAYXcMO1Qdu/PJDuTEsXG/qLrrm9r0L4FuAMQZGFuCPUK7qjixc8XKb+17elwkQGFNYuJ5yLwDL70oWxsXhcZ7rmUMT+575OYb/ShTCVuu8DBKnEqtV28/IT+cEm519g+VWUGbWy96nbHWyzW1pWU+EpEKCASXcs4FRf1a2gkPmBvC+OAUr2WDVJC+7rFIG4/FqR+54F6cOlBS6N5w8t+K9SG7gJitJ8eZZeJIDLD2Lba5GPHsYmWlZReWZCMuqAnD8WqRQ9jtk9o3u3wfTBLOq1vdPQUmUObe3F+kMjg4Cj14JpNeJnXrWYBD1NI+nV76oDA7Vk62OfrCBe4qq2z946N1jtgpiFwHGTkALow3cn2xK63v5pcx+u8895eGGKgqoverwOM/1zKGEvmd+XOjqbRJM7GVd7SUuScZ937xaBFO4kJcAQS+Ob3wkirZt2yoXkTd8V+d/MfOryBzH+RRS+zwrdf6UxCEbeidTinjsbOw3OeSpSN2o9JpH8rht26JKEW3bPj9D5lbVe42O6t0SJlkJtRA8qyXcyAaODsJc9FvDputsHTw+dgab4nWzHXipntc/AMX2uHdPr7b7gXj0FaviVZkd7vcVdPy5L/bMoYTTnvmRkS5vefBrQ+wlro3qL4SocaspTCu32xPw5sRHKMuyTENRPwzfK+OLmV/DyLn9/PnzHfX9ZoR/5ukBrIpg9WJMm443zcTjtnmTQ1a5o+XDypQl7qopy7IkKcuyLCvgXqzl+UZH9a4CMkcA5abTWqJbxzkchE3vDhTNuJzl0MQdkf31bCiDQ/VdLx//AMzukn3vnl6dZgVQZMtDWeef3KsyGR4Jv4kB1PkXe+alhJOe+U5izmFVfzW9flqtErtto2fr8fHq5tZxHsfbt3YHT1k6n6fZE7JU3qmjWFaerMxQoiKv3KTvh7Y7mxkvEorFFC4XJebi3RtocBeLsUU0UVUvdHY1nXPc4zPJyunV9liogXxYYzKvDzmHKyp3m1OrrYCnQHgintA2C+EtZgCJGgiC6E2jo6dBGMeKZymmXtC0PKSBJ0Ww4uLi5SCq6Qe6CffcmUKYjwCltKbSev2u7Kvvevn4B3DjhZLZ5b53X3XJ+OnSu3wan753px3zukxnJmZ5/fL4bswqnk6qI4fYmZ45KiFRA8HFxQ+PdPXWxqhIkratLesxT+s6j9vkv+37c4tg6YXz9OTIlGYkd/4JCzW4a6e0iXIrmRZl6Nnv0f3OZprrhnxdweO6ol6/69ZchazXrS/Yymf7c0+ITLbecY83dVjvXfx5I9tdXdZP4aY95LxcUTguRM4WUj9srcqkdBKrLVa9k1zQPolXjVa585hlmUO9C5PWckoqx2hVuEspy5eDWP2KQZyaUHqeZ3dKKTMT13dfjXT76rtePv4BYObKre72vfu6S8ZX7dV4xZuR7uU2X5eZra1NfjxYeUmW5OHxEPy2Z45LkIgf3N3dLcuJqVJqLqee2C/TCeFNp0e/zy/mee9kTLtCv2gx/9yypfe6kDdNmR6sV9lnet5JjvelRcWTddH9wfONPqxDdtf0Pep9ZbXV+/qa5usyxGn7X5f9XledryAwzizJet+w3HraRB1z7SyUK3UnnFk8Mn678K//ILS23od67l2c6T7Q0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0OAf/L3XH8LwI4LihzqkhRa6E5kLP6Cd1jbXoVy10LHfMRB+zKdYhf52Nf8ffWT494Ae6LTQ8dfGl9LQQvc7j3Tn4yH9WAym46tk/CNN0Z80/gXjh4H9MZ/X+mKM7anleeJM8LyFMUyrc188kabRfH/kJrWPzQQgVQZi6PRTWunG23Phqo6LrbzCiHXk5n/GzmEvKzeNccaK/h53zk5+Z4ifLXhRvueRDJyHs+GqXhXbapn7h6hXu8rKlZu/HbaTzfJbo0d5zve1J/JKpHNoVlia9blwVa+LbSq9Yf7vI3QVxAsZXeBN/EjBQsWWFR/HoOv1XCQDa8bMjySoqMv0JlYkgUvLOqIDxJEfCFCWFQjpMQ4koC6A+AKYLSCOrAXA4lAXlx1XtM3BaNb9LG+W9+RmvMVqGSi4tKxL6IvtawJS09JC97dpqQ1lPkhrIZ4HZTXl2TEtlXWk9MYVc1WNu6HmyfXjZVQPzNpj1nHUp6KdynrO5Hk63R5Ie4tKutsJSlpRMTCeyf0J0D4rMJ8l7DYEazsqLPBza1pddldddfry0YNyXe8/vlePJn0gm8ZRfoD/bHlXPl2xfU3dvFDr1x9m5Hlf+jzcn6Ved05xT11ce44F1gMYs1uA2iqfpLFvTnuLlRsPc8dOD3F31jUXj7TiM2xfvhW14SJl2DwmFCiSNUA1ULfT+1GezJ/KafLpM1MhR5vnKomfAS6vulCbwjqO9PPk1PVOhStgiRt9Js7yCtXEK0WyRnQ19Z8B3+qR7gdlrm3b6sPt7C7GE9NS5icxntTtpyLY7Mm+IQxkys2Lg2Mfeek0SFNdC1lXPO3Gl8euELNEeo5N5Vb7sEgvEY7Mxz7SnH0cLUgIv7jfHsd8OY1m1dcEHD7JoYXu+5EahvEX8ZYXo8Z4paJ873O9rKvB+xLbRV46DdIkfc9uoRzWV7ujwElPu2nhF+m0LQ5hkV4iHA2y0T4QmXHsN1oD0e5FoE+jWfU1dTmGFrofvTBN0/QvsV5nN88PRcy5zye+6/A9RF46CdI0UJutAG7W5ugo3lMp28dk6bVpdgiL9BKfaC2fe0tEHlkkLSHQyPpMNKvjmgCeN1ro/k7WayftQUv8Kua838yEF2a37127j7x0HL4JVi7eFpSCVtISyk4wvAcvxXpwK/ZhkY7iE5XRsnPD5ds+fIcQ1LUx92YP7Uu4qkM0q5ZQir6mzqEy1kL3d7JeuzFENWFrn+bdRUurNeW7k8x95KXj8E3gr0PHh9gxmmRFVWd11xuPjg+VY8I+LNJxfKLrYRf5KHoS4Fl5ankgm6d26Scv4aoO0ayqOqvNviYAtavQW5v+LlubXoLPTh/POIq9r+y36/Onx2HvxWh5nOednS/0Vwj7Va5oPn0+c15n89SHv12x+5rAUlstdHo/3Q8bNA/FD1wV+jfaT6cN+B/F3Y/ZQ1rmfsVI9yHb1ckqvYtTCx0fyMvRzBz0frrj6f2g0DKn8cEURKF+RTtKrTz1SPfBModo9Tez0c7hb4T6RQ1R+lloodPQ0EKnoYXud5qXim9nHvIdfEWx+OYWeD9CSNSGBD+7pBH1q2EXYeCpjPmbLx333z6OQiv70i6Sd9o5t936DWsQlO9nyj5HHnzLPDylBp5HdfGyVW+eFHgj2x205TmS4tGv+ZpJUWtT6GNHuk/TuTOfAXxqzPY7rYLZN4wo4Y0lQ/fMhrl3GQfBGcH9vptKWoRTSjvt/Urzkzd0cUqweUjmWpo+dO11zhJYzLM1pbr6EwR/mkTXTCsnffvV4fc+Spz4XxfDL9vTlcF46abMNt3+k/zV5xdPUF9kWpo+Ur3OzSuAJJQJvujWxVxjy2LgDjfQpz5bopFANEN/uAGikTsUGRdNOwwSKkAOzKGh1NDw64Z4vBuIST+SVVvIhcwAFvmkdGspxSDD86tGiBIWhRhkoKRwOz1nWAWxI+y6RQ7EyGNyn7ltA1OrapADs0QNMmKnmSWwT40gQQ3MEpiWKV64AqsqWkCVWWDVrZwyyhtmlSGmG69raphAUWtX4weq14grovl8enFNhFF+inppvmiR5Sf4VCGqi/5bmGtwhCznEKnKaFR0LPdGXu3KvOcYllcDqdYdcS8tQQqv+17qs7O2jDAcq0xR9JSDniPY0QlfcQy9Gp+2pwbC8smBcR6DUR44hvsUUJWVdt8wheUNQhmDGiAO8X2UWg1KpUhL/GZPh+wcjdoT8IEURA8iJ1vPDRp3ffepVHPjGuAW+NRAae417ry5AYwrFtXi2mnuYOZwNRUvGjm+5cAxJEuxDlpsspVP/VpZUxSLtXO9JyACs54j2NMJOeEYlk6xBnCjbtflUCSk5hrKqddzDOM+rWB+22/OdDp1GqV5t/B7NwluYZgU1/jhYzKuj5sKqRGttDx9GAWxarHMNfU1AFdLSRUdBsGaBb1omPPmDmAH1xjUAjDqt2vBe44hKVgHE/E+aGt54OHcs4HR4SnvOYKndMKe+aekCI5frm3qzZO88ZSz2nMM9ykUt+N+Q3DbkQXXZXlkv4hdBBh73tehqUCgF1U+kIIoG8yWhQDzGuD6piM0RLO5dTLdbtqv1n//imN4FAlkmcTRe83ZcwRP6YQ98y91bEsdWchNFlTDyg1GZrrnGO5T2Ixasdf1neRWslKvmBoeozd0SFGVWpz+ZPV6ZEfWgoaLlkVtnsiSY16xgJ2K1t3DvpvNb/YjYNuRCs9sLpndiGr/UMm6Kdsif6qhtN9rjXvTh6+5QUT1uj86aNb4wBKO3G/U0TowMNdBtH1UlEBZ9CmMqyxcHyI+Wd4aCnX8stlAke3eNNVAcws/0JDIG+6kuDEMccViehFFM3HdaTeoYS0cWFx0k6wZ4BE54hqzveCiVSBO91edcAz7kW65CqWMqndfkT1HsKcTcsIxnM4gbzpqYCcf5eoZWe6KA8dwnwLLLO8YreMAonwxnQ9l08eGms4ZP116l09j2AhvftzUUaZpXh8odOt2xtUV11fXcG0aSpmdy8ps5nMB3DTTadvpqqy9gHqqyOBWNdNGXUFbTi+OR61jjmHv/KqkqZTpvMsv2HMEezohJxxDURuGmXfUwM78ZVhzE3vLA8fwkALlaKkAqp3iLrpJnrJd3ctwsvVW46v2aryCtmgfj5u61fEi+KhNnA7Ap6bNwTY6q/XHcORbPc8x/Eq8np4jeEJF3DP/+jxxNsjdnjt4wk4E8MviVWM6J/GequilR7lS6TndxwodkVeZjUzX/BKh+00w327+bEKiFrqfELpfgN9wFclL/6yTtdBpodP4exkSv2oioydEWuj44A/ZaDaYhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhobGd+D/AbX3hOHtE8/qAAAAAElFTkSuQmCC) ## Next steps {#next-steps} Congratulations, you made it! 🎉 Thinking about signing up for your next race? For your next challenge, perhaps consider creating an app your users can use to [request time off](/tools/deno-slack-sdk/tutorials/request-time-off-app)! --- Source: https://docs.slack.dev/tools/deno-slack-sdk/tutorials/weather-workflow # Check the weather using a custom step in Workflow Builder In this tutorial, we'll create a workflow that tells us what the weather is like in our locale using a custom function to call a third-party API and Workflow Builder in Slack. Workflow Builder creates workflows that automate routine tasks and processes in Slack through the use of steps. These steps include Slack-native functions (creating a channel, sending a message, opening a form, etc.) and custom functions, which you define. Your workflow in Slack will contain: * **Built-in function**: Collect info in a form * **Custom function**: Weather function that calls a third-party API * **Built-in function**: Send an ephemeral message with the output of the custom function ## Get started {#get-started} ### Set up tools {#set-up-tools} We are using the [Slack CLI](/tools/slack-cli) and [Deno Slack SDK](/tools/deno-slack-sdk) to create an app where the custom function exists and can be pulled into Workflow Builder to be used as a step. You should be familiar with the Slack CLI and how it works before going further. If this is new for you, we recommend starting with the [Hello world app](/tools/deno-slack-sdk/tutorials/hello-world-app) first. ### Create the project {#create-the-project} Next, navigate to a directory where you have permission to create new files. Using the Slack CLI, run the following command to create a new project from a template: ``` slack create weather-app --template slack-samples/deno-function-template ``` This creates your project from a blank template. After that, navigate to your project folder. ``` cd weather-app ``` Open the project in VSCode. ``` code . ``` ### Create an account with OpenWeather {#create-an-account-with-openweather} Create an account with [openweathermap.org](https://openweathermap.org/), which is where we’ll be pulling weather information from. Follow the prompts on [this page](https://home.openweathermap.org/users/sign_up) and once you’ve created an account, navigate to the API keys tab to retrieve your API key. Set this key as an environment variable by creating a file in your project named `.env` and putting the key in there, so that it looks like this: ``` WEATHER_API_KEY= ``` Save the file. ## Code the custom step {#code-the-custom-step} In order to get the weather via the OpenWeather API, we'll need to code a custom step. We’ll be calling the OpenWeather API to get weather information from around the world. The specific API method that should be called is the [current weather data by city name](https://openweathermap.org/current#name). As for outputs, you’ll want the `weather.description`, which gives us a description of the weather in plain language. Within your project, the foundation has already been laid for you to create your function. This function will show up in Workflow Builder as a step that you can add to any workflow. Copy and paste the code below into the `sample_function.ts` file in your project. Rename the file to `weather_function.ts`, then save your changes. ``` import { DefineFunction, Schema, SlackFunction } from "deno-slack-sdk/mod.ts";export const WeatherFunctionDefinition = DefineFunction({ callback_id: "weather_function", title: "Weather function", description: "Weather function that checks a location for the weather ", source_file: "functions/weather_function.ts", // These input parameters correspond to fields within your form, which are passed along to this step. input_parameters: { properties: { location: { type: Schema.types.string, description: "Location of where we're getting the weather", }, }, required: ["location"], }, // Your function will produce a single string output that can be used in your workflow. output_parameters: { properties: { weather_result: { type: Schema.types.string, description: "The weather of the inputted location", }, }, required: ["weather_result"], },});export default SlackFunction( WeatherFunctionDefinition, async ({ env, inputs, }) => { // Call this OpenWeather API method to retrieve the `weather.description` parameter from the JSON result. const location = inputs.location; const api_key = process.env.WEATHER_API_KEY; const resp = await fetch( `https://api.openweathermap.org/data/2.5/weather?q=${location}&appid=${api_key}`, ); const weather_json = await resp.json(); // Use this to view the JSON object if needed! // console.log(weather_json) const weather_result = weather_json.weather[0].description; return { outputs: { weather_result, }, }; },); ``` ### Update the manifest {#update-the-manifest} The `manifest.ts` file declares everything about your app, from which functions it hosts to which domains that your app will call. There are a few things that we need to change here in order for you to be able to find your step within Workflow Builder. * Change the name of your app to something like `-weather-function`. e.g. `my-weather-function`. * Within the `outgoingDomains` array, add the `api.openweathermap.org` domain. * Add an icon of your choice and update the filepath for it. ``` //manifest.tsimport { Manifest } from "deno-slack-sdk/mod.ts";import { WeatherFunctionDefinition } from "./functions/weather_function.ts";export default Manifest({ name: "my-weather-function", description: "A custom function for finding the weather based on location.", icon: "icon.png", workflows: [], functions: [ WeatherFunctionDefinition ], outgoingDomains: ["api.openweathermap.org"], datastores: [], botScopes: [ "commands", "chat:write", "chat:write.public", ]}); ``` ### Run your app {#run-your-app} Now that you’ve finished coding your app, let's test it in the wild. Run your app locally by using the following command: ``` slack run ``` After following the prompts, a successful `slack run` execution will show the message `Connected, awaiting events` in the terminal window. This means that your app is up and ready to go. Your step is now available in Workflow Builder. ## Create a workflow with Workflow Builder {#create-a-workflow-with-workflow-builder} Now that we have everything in place, let’s start building our workflow. To create a new workflow, you will need to open Workflow Builder in Slack. You can open Workflow Builder using one of the following methods. * **Use the message box:** In any channel, type `/workflow` and select **Create a workflow**. * **Use the sidebar:** Navigate to the left sidebar in Slack and click **More**, then **Tools**, then **Workflows**. Click **+New** then **Build Workflow** to create a new workflow. Under **Start the workflow...**, click **Choose an event**, then select **From a link in Slack**. It might make more sense that this workflow runs on a schedule, but for ease of testing, we’ll use a link. The next page you’ll see is your workflow’s main page. Take a moment here to update your workflow name. ### Add a form workflow step {#add-a-form-workflow-step} The first step that you’ll add to your workflow is a form. Click **\+ Add step**, then select **Collect info in a form**. Name your form, then add a question. Ask your users for the location for which they want the weather and select **Short answer** for the answer type. After you add this question to your form, you’ll be able to access it as a variable in subsequent steps. Save your changes. ### Add the custom step {#add-the-custom-step} Next we'll add the custom step we coded to your workflow. Click the button to add another step, then search for the function name in the left sidebar. Select the **Weather function** as a step. For the location, click the variable field `{}` to the right and select **Answer to: Which location would you like the weather for?**. Save your changes. ### Send a message with your weather result {#send-a-message-with-your-weather-result} The last step is to send your user a message to notify them of the weather result. Add another step to the workflow and search for **Messages** > **Send a “only visible to you” message**. For the **Select a channel** field, choose **Channel where the workflow was used**. For the **Select a member of the channel** field, choose **Person who used this workflow**. Add a message in the text box including the input and output variables. It might look something like this: The weather for **Answer to Which location would you like the weather for?** today is **Weather Result**. Save your changes. Publish your workflow by clicking the green **Finish Up** button, then the **Publish** button. Once your workflow is published, it will give you a link to trigger the workflow. Copy and paste this link into a test channel, then click the button to run the workflow. ## Deploy the app {#deploy-the-app} We’ve used `slack run` to run our app on our local machine, but as soon as you shut that environment down or turn off your computer, the function will stop running. Deploying your function to infrastructure managed by Slack ensures that the function is always available. To deploy your function to production, run the following command: ``` slack deploy ``` Choose the appropriate workspace for deployment. The local version (`slack run`) and the production version (`slack deploy`) are separate apps. Update the workflow in Workflow Builder to use the production function and reconnect input variables. --- Source: https://docs.slack.dev/tools/deno-slack-sdk/tutorials/welcome-bot # Welcome bot Workflow apps require a paid plan Join the [Developer Program](https://api.slack.com/developer-program) and provision a sandbox with access to all Slack features for free. ![Bot avatar](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAWAAAAFgCAMAAACyv1N+AAAAb1BMVEUAAAAqERA8Ghl6imcEAgINBwcWDg37iYk2xfB7jGhSJCN/OjloLy4dHhyoT06URUQtMCs9QTlMUEdbYFW9W1nmd3ZnbWDab26ampdxgGLOZmXwf36qqqd/f332hIR0dHJSx+pxxt2NjYugub2NwM2ZSazLAAAACXBIWXMAAAsTAAALEwEAmpwYAAAdA0lEQVR42u2daXejurKGSyAmTyHtjN39/3/Zvrs7+yRpx5hZw/3QGSgBNrYROLG01l7rpE9i8EPxqqpUKhEfzNA5LIPAADaAzTCADWAD2AwD2AA2gM0wgA1gMwxgA9gANsMANoANYDMMYAPYADbDADaAzTCADWAD2AwD2AA2gM0wgA1gMwxgA9gANsMANoANYDMMYAPYADbDADaAzTCADWAD2AwD2AA2gM0wgA1gMwxgA9gANsMANoANYDMMYAPYADbDADaAzTCADWAD2AwD2AA2gM3oddCRr39fsglPOUgASQCIHQin8PjvLwOYjNbD/YfkkeCQA0gA+XY7f//zbGLN8tk/BvCBZpt6a5aDlMAaXyEGQAkQmJBFHvwygPcbV0XJcikZUAC2VboYUEI827X83wZwt3FnrcpcSraVLcbMKAGfLuj/GcA7x/dVmUlGu7FVIJOJ6z4awNuUIS0yCbAn3YpgEDK5+IR2PAzgOx7FB9Ot+JPW9FL8NoBrTkOcZvIoum+QGSW+GzwawMjfXUUC+sD7rhWzy38N4He8fzaiL7ofjGdz+7cBDAA//0Q94/2LmNEgmP46e8D38aozXvoew3X97SBY/HPegG+fU0bZLlIECEAJ5O/PEpy/qQnWgfGnsGJtgMNssx0vJQTKwJJUzsrg4d1dZrwgjCTSASnZTsTfyO/zBPzzeb0tZKOEMJ/Ywar9E5YxkxndDpkymIf/O0fA1w9FO16HlAEJoi6fMy/LrZApMAi//XtugK/iqFUdHMImVviwx6d5XKa0lTEFBt8u/++sAN/+ajNfapVTKz9AzzdbGFNg8+t/zwfwj+eoxdtyWODMDg1zw41MadkaQV8sfp0J4OuHgrbgPch4q1ohEipYixGHsxPNUNi9rnrei6eyka8D/mXCj/twLm4lIUTU/x8haJLdJF/fgq82q0b1dXgQ9mRgHmtWCsrot8nvL27B1//FlIkm673e9GVeXIZQOvWLCLBSebf+yoDv5WMJ9W9OZbBI+3x7C3HDGxFbiQiTrwv4Klo34p0s0rzne07FUjQgFjSRdxF8UQ0O46hhdnMcmuu57+U6tlldiJ3vD1/Tgq8f07r9Uulzrum+UxFCSYUqxCIJ868IePHYMLO7zkWm8c4LMSH1hyryefnlAN+LlxKazLfQe+9cOnVVEuVpEe6hfPXnai3r6usumf67Z3OrbiAv069lwVerTT24INrN99WIL0GqQnxaNnw04DBZCVHLDQz2FXNRlwnBTojwsYC/r+rRk+MvBnT4xbSUJ0z4SMDf/xc1uNZsUFeJhXWZYCfjrR0H+Pq5ztca3HqKuagRLm6TLwD4+iFVfX3qX8aDf4lcejXCpxI1HwO4Ibnu+MunMb6GcJSYg2bsW/LJATfwdV0Wj/M9hEO4EjWT682nBtzAl8yK0b6ICCTOewguy88MuIGvNR9z6mY1wuw+/ryAr1dpje/IolcnLBbZZwX8/TlSM1mTy7EXbJg60wl7fBk+DHC4qvlAk+nL6NYiHOytCQ7lpwR8lajGSv3LPz3d0RWdUc/z5s58f8URHs6LCDq6N3zIktGPp1WN77celmqWMSMie9u3TAB8S26twGwyGPxU6Dz89eks+O4lEr3zDYHkkeAsl2+ZGymlZIJHwndn3eeqeSaxSNj5pwMsI6byLY/zhxZEvoiSA1cSn0IIDlLwyPUmHTnlE+xKiLF9m/0B3z6VNb5H3cJcrgUTXLQKKwfJY9/ptn7K1YnOKj8X4HCd1vQhPgrvi+TtdN+jMpG5Lu820RX4T39Enwnwj2iNE2jUXR6hv1flixCiEzeQmd8pcJhhC+BwtflEgHmk8PAvj9gk4T8JLrr+sgAZTTuspOZTfIt8mnyeVeVbdYKbXB5RNums9xPIUqbO1e5fyzz8V5v7TwM4/KUAsaaH872yc7nv0n6R/1nu/q2AoB837LNIxM91jAWYLA+fQBbr4oAvLmQUFrtFAgnPqCmJvQDzyEJM3CPUbREXBxmWsJObdHfaB9WtiUnyKQB//x8ur3bcw13M25c2vtR6H6KZcHS5M+pw0WYOTpebTwD4x7MS5/tHaFvRxJeKv+Hb23j7J5Uw7PQ8OEUmzJ3sE3T++7PBKXb3omGCmxfgdtBlPy0b9sUyoJZnEyB/m9XxXDAGUKs7Zvkkq5V/A6C00IQVqCIQTj+bdv0b32XDBOeJ2JEE2MTfwTj8X4MN0sC2qP0fuG9GfgOFFDxtMHXrGuXY/DJ1JClJdauNg0RhvF1enSXixxOeWpxZUovK8lJwwbnNCnf7S1yPjSmdB8EfWWS2/X5rWVYyvnA8Udu6ZVdDiXnMueCCgy1ePgIRByV92EV86oDFBs051FedpeVvXorXJBjhbNtE5G1UYu5suhIZ53Y9aij5BRUET3iCLd9Fwl/Zr9WdQlD+/mSxCgtrrHi5K2DVg4CFAnD5qzL3CIskWwhbSrhNF9Ok4DbYduN8yNkFLZW/eP8EP60kOQX5IIwdCcfKThrw/R8sCERdz7n9164+AWERIds+bPkHp8WcxZpD4bbeIhScTdFuD8qc10/3IiX9y9+2H3FWvQHh5ycN2HrBAhGUtR0TTMmVgyNaUwrYnZ6ndtFsvG83WdhsRotq3oe9RhuZreboif+KvFqvRpk7UjRndaxiZ9hDUxMCXlFzhMpk3vJpuOKGztOi3Xxfr1dA4qDfsf9OWrS+l0u8gZzxasPcOIXTBXwXF8jQCVVTwA07aMHO2rr5IL6zFwB3Z2RWKFsvGAcAWCQNDq796nmupvgVO2HAPEK7MJzaApnHGpxVFjcnvlL0sCzX3c0XwC1cC/1acgsASYPAMfnmhNtONaAq708W8P0K43OW9RxXY5T40rL7qvq9p53LXd1JNe6kCQCkTddl1GvSCFaeLOA42iEQkDTevGyWPSKrMCy3663+R6rvkSgBPNqYDXmb3B4n6MUhpwr4aoUEgk5rDo/X7IowGTb/e/X6dlf7BZc4igjzZk9QJg0aASw5VcAJnuFEsPWlR1qSNa+tVRTCszvn35S8iQCQsuXBvq4ruax6Kf7zNAF/f0Ym587rRZRtL1/jitBthqMI6Fy0jcUkvYK24Mx5nebWQfW1STYnCfjuGaU0qZs1reO05eqanAhFWbvf63/I2h151dr7jrxPF9VXwDlJwBwrMN+nWFCKxv5IXYy/0YIRL7HJ257r+7/b6Oslpwj4LkIG7EyaMr175VEKufXN3yYQllULNWDrNDpHIizuTxCwXCFXiE+OvmKBQBB3nz9EBQ9kt3hjR20UT3gH4PtnbEXzxm0C/j6fTg6XQrfNG1Ev8f7SSVp5mBk7PcC4Ew+l2V4fQrYrJADQ/bx/GwtT6x9/iHD1V0rv5AD/VOroWnKbkuz6oi1hBuzH9z+CP9xvy7XO3v5HgK62OTnAf7ABT/O2fsstFmzvok7s/SQCTbhgtVyXvec3VgGa5e5ODPAPnEXjbb/cVoFdhn3eqgtACNJguxkw9Vu+n6QnBniFguRWA4ZV0HjnNHjYJczefv4HAgyk5cFWp1E5dixn7WHAi/Zpu9GULGdnrgfsw2+dsZYHyyreBq3eQ+GdFmBswM6WzG3IGhui7sr1AIU9UhEAYGNHGGjT7dPputq2uHq16O6UAP/EzRL5dJtD32CsJNhthPZxd5/TBhNGb5pHRq4U3gJ4jWzL2bojcFn34Chv/Da3x9wtXnsWALQeL+M3rRrLMSiKEwJ8j9fOtgfJD3OrxnfWnDzqtQAknzq1635rrUugkP44HcACTXFOsH0vfeYo39Rq2TqIbYj8d8S9ZwBQOsqrw5VNp1O0MBexkwF8h7MQO7M8zHNxZ1CmoZfjf3W5uXTxguFcSfc9TdGk+vzjVABz1HBjlwEDQOl85FioFQxhKhwAHq98t1KBVm+6Ul2YYxBFpwIYv0wtgqp06nxdlnSIf6ep1A4nIwoAgIcymLgUAKhrufd1XZpxdaUJTqHCPXxECux1aSqQw5x7Aix5EbW7zLY8rL5+S24+A5+7AJbtrxrKrB+DvGorkXMaFpyjL88WHSd1VnJesqhjvv2AsxJp483njHNeZs1WMMOJ4+j7KQD+ESMDnurqNkc6rxgdPnBATcth57kWwCv01Jml7fruAN8RmTCD1Wp8wHdFtZqPBifWeH5vE3bGEwmrzUfbnRWDo9c8hxoT5EjQYkiRaJ7How5ZsXHGQe71ep4WSCSskS34Ck1xHbJiI4lu55rqTLGj6HpcwAlarOS9LvyM0z5UWTUsV1djAr4vkY82eTwZC1YyRd0/K1cS1lF8NyJghhbrma/3DvYyaQkHrjeFGDCNkhEBb/DKZXQyeNWxRzemRywSjK3C0QDjKc5ydEYWcr8/voFOFVtdRELEP8cCjDc8sQudCbEBhyISLHoeDTCgetWH/nO4Pf31ft6sIhJAo9txAIdIIXRPcQM+HUUkWPkrHAUwOrim/ymuxw2Xcu8O2TjeKLL7MQCjPA8hetNn8mY/Cz4u6xQqPRFW6xEAhyisZLO+L/jYm+bQvUv5nuYuzmusr4cHXGCFWJ2QqLp4YxyRe18uc52hQ2YV8B0Kk4mj/4quTtlVx4Ua0MVDA84ylKicgd4Mac73CvrQzkZywM2pAR2NpgMDxpusphqaYR1hhPjmSnKQr6bIcBQOC7is+hCWDZprGyQ/PFImBz19hiuBWBHfDwk4zKu1cuWF7jWU/cwZN0Y60B0JFF8tfRkSMD4ec6rjHHO0JYnJg32OQ5e61zOiiMT1gIB5dRohWhQCn34ji30ylnkfYl4LmR9+DAb4B0pDa/EhoGpA9Ag3mNLDD5WiuMnd82CAGY4yHrUb8B6xcqH0AyOHuyMh1hrY3A4FOJKgN8oAgLWvyGrnSCOU+Pi4g2/haeEqebUfwwC+Q06arkylVd2gnR+cgSRHRPFKyKxTJBDgDDlpRFMeAtnhPo4w8tLgqMf/DW3aYbC5HgRwtTUM7bfepK1GQcoDk5X0qOqcB1yWzcrVzyEAlxUnjWly0mqOcNHdiRA97RGt+2o0fR4A8D1qkFjqKqmclVU/TXZ2Igqc6pHHHgBKB8lJVAGnSIIDbW3jUSTFO/tpN9iJOHbfsbKHUWb6AaPuXETb8np1TWMfNwLPccGxyz1YJBjEt9oBl1JbTWWbnwZQ8MMW5KweTglGIlE+/9QM+A5LsL6KKdzNTBSHzHFUHn8fWCRo+kczYDaMBOOIt+ssV5vj7L43OjMWXekFbCMvWGOFU7XFAKPJzUFzXB8GgMMNKhK9gKNBvGA1G8E6SoQ6x/URZuIeAQw2oVbA6ByBUudSFW4ELLtJMN4p0s8uiwznPEWuE/A9r+ZayQMMU9BPO81yBRQoV0l7esFQ4pKxONQIOEWPb6KRL1TPSWPQTYSxBPdVtfyEnGGqwYStSoctOowEA6z8/f20qgRTYH0ZQMioXhP+ALyRTHuyvVmEOxDGEsygt6rlxwk5wCk/CDAKlMuJ3gN+kAizmy41J2XPYcZ7G2IUz21+6AKM4ji9cxyAh0Q427myXKgS3GMYNCfoya10AbZRbYheA4Y16ptail3rci4USa+ptKqrVjVhVhb3mgDH1UAZJGg+6RJrRLhnO/he90ZiE86ZJsBo2tHrRCh9fRlkOxUixKfv9Xp72ITFRhNgtD9ZY67yNSxHFyjFfnEy69fHQSe1M3alB7Csemml9qYg2A/coRFqnNzzzpwJOkkoK/QAFuzgzurHagRAWuzlpPV8e+spPWQVdj/Ad9BbzcH+GkEZL7Y7aQxAY1Gij8/nuNIBmKJIdICOIAQ15EuW2500pBC87z7LUTVe7lkjrMbDXKR+wBfIKItyW6xR4KPTJ71vb6tOc0yWOgBX14sYGaCZ/BNuD85Ct2sY59haS2qBsZ8aADtDWzA4SIfSvNWEXSjQZivW/8agVTXxTnOhAbA1qBsMADBDzhEr24/+VRQieNLddPRFA+AUHys5AGDU951BFraYsOpDaPEhL6qr66UOC0anRpaPAwBG6xpAS95iwjUfQsfr9cRQ0vJn/4Dl8E1Jouo0x2jcbMJFTSG0VMSgI4X7PPTIajh+c6gDafA0V7YFGwMoBIBNWMUT9jQ3phuoy0nGFBN2iyaFyPUrBMAUecIbrRIx3IlKAQpRW1R4EIWAp1JTW5YB22TCjhN6WlR4iTKVjq7VWDQhSM2Ah6L+iBr7NplwUetDONVW6l8h3OMZ11ZTMg1GauwbX9RNGK9lgLZm5xIFAtnXkAhYKyZcqvOcGiY79jCNlNkXAYwXEwDSbIcTzLX1fEd7n/LJVwG8Vna+x3Nkwi4s8Tw40daIa7ZlQ/UnBqzsy6aiqJZI1KY4S9/tcqJnxWFswIojweIJmueUKY7qOw5BaErXvgL+PRphXBsGZVGZ52pTnM4kCVKFVK8FiyEJK0f9Vea5Agpe7ntk1eGZERR26AQ88Jl2Shk/iydvJuzCMsFT3Ao+3bBGbptc3zcv3kSigKJEXjG3Bmr9wnR8LtUSx3QZudJQ8q35oOqjgdYTf9CKAxFaLZjJYU0YV/mw8q9I1Hw01xmuW7+jWSJuBwW8XuDvUxShW4ALYYkXcrSe3IXaI3hJ74Dtai2Ekw5rwpnSZyvNwAWAHK3E0mCt95AkqmXV12o6QVvKoVMSFj7kIg5qNcG6ljI+1iGZVolwNIXi3UbkYBsuyrkLBT4wZar3PFkkCvav3gEXXnUnCBs+nsM/p0VRcKTAXG9Qfys1le9a7+ucbNTDRGZKC8J4EW7QLWk+lTGpPkxiaSj+s6t6LwYHnCuLbXKDD3Z2NZ/Wm8oKYTLpH/Bv0noc1yhJH4aDOKCagx/0znqlhkgOlaw6i+EJf9v2Xuo2YEiwHmkAPK8SliMcQ6t6EqC/0+7HCCkqlPulY6dnNaMvOYwgEu1W6gSayxGzauFNr7UXHxJR2T/MQI4AGKa8zYS55q29wGW1wH+mA/D/2Xoy+nudzmu1GfB6SAn2LC1p0KoGM2c+BuG8RYa1G/AChVb2P1oAOxXCVKbjLNA54xgw2ghPLT2JfGvUdM+7DI9hwEiCqTXVAzioznIsvYVxZNgdwYBxYZrXq3HZ749u47FKhOxwPgph7oClBuoX2r3yvHJN0W/1kAUtIixgpDoJT8l2u/oNeOkwXRJcBVwVYQbxSIChFHhtgS21XzJGXTupLsA+alVEvbEIX/Oqz2TNH2DIOa7XXCUG/Nuhp6ARsKpOdJQOUEMgKnMc82ba6i1CC62LLcYinFcKAi1ngAuiZ9hrmIEBSw+llJKxAFcOahnEgFEc1/MchwD/QoHqSMEcbq/O6SARur45Dhf/hahAi4546vqVeK2FGCIxjWpOyEwj4H+R5yDK8QA/3JHBDFipMyl0HnfmOqjZ64gm/DR3Aeh0kJUVhuIcXyfgADf2jcYDDHngAB/mCZPDDyvfF/AjXtm16YiEM8+dDvSE5XCHpl6in2USjki4HGptWxKNLc0UI42CakpNjBjPAYAYyFG0+EeljcWnsdaTwdHqPbBixHlusGFXI2X5fKXTgiFBWWEguf31AYdpZWITgt9FGgHDLLOQSDjiywNOmV39ysVmfpHo28blYqelTOZf34SVNb/y6WVxpc2Ck6BAazZUyC8PWF0fo1ksg2mmoU3CXxPGx14Rl311wDnFuxUY0AjcWWj9o8GCIUhxebBgYfHVCbtc2RkngJbphi2DpH/AV2slyXMGIsFdJur7tUX2Iv3ri+PWXEnd0XVUL8X5+iJx+0+Ts0QZgDN1Lf93nxYMLlMqEwSb8C8OOL6JZdNaHRUiiwkLL6MeATuFukeDki8vEmkj4b8gijTh3jTc9GbBuQpYgPvlw410KdsTLyJLC7EMo54suAYYhLJh+yuO7KYgW5SQZ/FBZtwNMJB8mX11wrHw7W0b2ESWkvJyXzVuAEyL+mWEzb9+QAf8RpT2NjEsspR5+4V4HQGDgDPI+kDMLiXfilhkpfTDi80xgEnWdIUz8NUAAHJxaZd0K+IizfhknhwM+K7lYCF6BjIMAJCzW2lvn9J5VkBXpagDvnhu7lhzHjIMAJAwGRJOdymFd9lFKeqA7aRlKj0Hb/ij7falZW/z2kAUWcYnV9H+gOebNl/lHLzhilLIxQ4z5lnBvF2LH3XAJGt1BnlxFhPdhxnfSmfrjCeykvvX0X6AG72094nuJj0jwpAwccM4FQBtmEVRFt7lYrNHuhJlK6nSBY+6l49wbsMTsc2AtsujO7mwfnW14J9VJ8Jx7VItthdnB5iLpeBb+ALPEj65XncDbFWdCJtexhK9HUK650cYMmH7zraGiyIr8qBxvqsBvogqgKm3nhRK9l06Z0gYgIsrxqnYIsaM+w2Ia4BlNZdmhymfCoUwOSNnrTpS8V3YpNziGZfcr8V3NcB21YkQBQBzFcK8mJ4nYYiZXFhb/DYhcunNsq2A7+JKqof6EgCEoz6183KH1RBPtiuFEKLIJgixWjpl5/VNj9PaL0UenO1YMyeYuKylfyhjInoOwnbAuFHE3x/W35ROgOysCQPkfDFrbY74F/H3NomYICfC/SsFxVRpQyVo/vXLfXYpBWtTCmHJInoTChUwciJEmL23elFcCTs5Xx1+zcx/FyWFFim25JsWq4CrmQj60eZWOMqatkWyMycMMbN9ApZosmIhBPmLuA74wwWzKs1VhIedNSHo2RMG4NK3QYpWocgmoQL4Lq64GJZdmfGuc8WGDeHXAI+XlmWJFqFILDUfCS2tKR4uXcXYmYx8MOOpvPJ4m0MBQrFgN65kNGy0gJ1cxLb6oM40L1Gf7wrmWKJ5utt2zA6Gv7oTqnddFNTwBYAH5rqiuWuhtWU7iFpT+XRXe0hl7hi8r57spBGxtc+23advQv0IltkLQxcAALJGxNaWQ9Wg1oUvqhOG4skzcNsR77n1OfomqHp+qzDOxBbE++4tj76pOsyoTIwQtyK2up9Q1aYSDMrMXhq0zYgt2PeYr+i6rsOQ/DIy0YzY2tpepXGslrVDFxiViXNlyDYgViI5O5XVRfvmEDC/adgmI0RkUhNoUeJv1dVBDVQeuOc01QbQWwO2gvinSxzVgn1kwU7r6rFwiFqGIWgpX4wRQ3VXzaUsrS2b77dFdSyoNY1jwETqmLiuMtblzcE9lrJ5gzMB5ebRuBPII1AAe92PsM6vG09wkrFtQuf2hhz7nNS3Ao8UTVlmx/MjQ7Y50OB7nYpdBo3NYMvN2jGRXTNg6e1lz9m8+aC3YvPLIG4sPNnYlZfeomL35tOypZtmsfGnheFbW7Z3q6vKluzg6wVt23uL1HeMW1yri6hsEUDL9u0e8U3ZtqGskNSb5AYwoHYnH6ujVml3O9DVadtAzUsRe+dtxtuyaUx2zC2wO9dtPdclzmw6B+MHvwFGS8ldX+8nCJjV5uAVBU0CEkRGIgAAvlXLV93u/bbZUm7ZvQCljF3bmebGgu1DA7sVzBkrtjSiT6i1mUjbW5834KIqEXKvzS45BMwutzb731Br41u2DFZnC9hHsTLZ78OyZcIttuNEhQQctvEtSeVZCEZtr3J1q/LbFoI9RrhJ7E52TwkLaH5+kxxU22R2dITxptMbxmiHmkshoDyHDXdW/R8qZ2TLA9Ynnti9Z3UrupTr+flZcFDtx+ESftCe06XgtFPlsCXOzoLRYVTywCh3xe4nE7e5wzYacQhn5kVgP21fN+JjPAL4bmKXO0SWpmdnwbR6LOJRB8Hk5fVksuP8O0ng3DR4U+3PLNjtMU3MMyZD6WxzKtqKh76wBeN/cY5N0USML712O2bzs7NgCOOKG2H10L69EPIGHNaUFqbBBs7OgqWG4+uesvLen04sdaudG56fHwwXxZ7Lct1GwoW8Fa5DmftW1Ub9+fP55SIA/E0l4UOu+k4vLnLCSCIBAufi4cvzhf8H4wFZuBJAi84AAAAASUVORK5CYII=) Welcome Bot Welcome to the Welcome Bot tutorial! We hope you enjoy your stay here! What a nice message to read! It sure would be nice if everyone joining a Slack channel received such a message! In this tutorial you'll learn how to create a Slack app that sends a friendly welcome message, similar to the one at the top of this page, to a user when they join a channel. A user in the channel will be able to create the custom message from a form. Before we begin, ensure you have the following prerequisites completed: * Install the [Slack CLI](/tools/deno-slack-sdk/guides/getting-started). * Run `slack auth list` and ensure your workspace is listed. * If your workspace is not listed, address any issues by following along with the [Getting started](/tools/deno-slack-sdk/guides/getting-started), then come on back. ## Create a blank project {#create-a-blank-project} Create a blank app with the Slack CLI using the following command: ``` slack create welcome-bot-app --template https://github.com/slack-samples/deno-blank-template ``` A new app folder will be created. Once you have your new project ready to go, change into your project directory. You'll be bouncing between a few folders, so we recommend using an editor that streamlines switching between files. Welcome to your Slack app! There may not be a welcoming message, but do not fret, you can make yourself at home here. Slack apps are built around their flexibility; don't be afraid to run wild! For now though, just make three folders within your app folder. Each folder will contain a fundamental building block of a Slack app: * `functions` * `workflows` * `triggers` With the setup complete, you can get building! ## Alternatively, create an app from the template {#alternatively-create-an-app-from-the-template} If you want to follow along without placing the code yourself, use the pre-built [Welcome Bot app](https://github.com/slack-samples/deno-welcome-bot): ``` slack create welcome-bot-app --template https://github.com/slack-samples/deno-welcome-bot ``` Once you have your new project ready to go, change into your project directory. ## Create the app manifest {#create-the-app-manifest} The app manifest provides a sneak peak at what you'll be building throughout the rest of this tutorial. The recipe for the Welcome Bot app calls for: * two workflows, imported from their files: * `MessageSetupWorkflow` * `SendWelcomeMessageWorkflow` * one datastore imported from its file: * `WelcomeMessageDatastore` * and six scopes: * [`chat:write`](https://docs.slack.dev/reference/scopes/chat.write) * [`chat:write.public`](https://docs.slack.dev/reference/scopes/chat.write.public) * [`datastore:read`](https://docs.slack.dev/reference/scopes/datastore.read) * [`datastore:write`](https://docs.slack.dev/reference/scopes/datastore.write) * [`channels:read`](https://docs.slack.dev/reference/scopes/channels.read) * [`triggers:write`](https://docs.slack.dev/reference/scopes/triggers.write) * [`triggers:read`](https://docs.slack.dev/reference/scopes/triggers.read) Put that all together and your `manifest.ts` file will look like: ``` // /manifest.tsimport { Manifest } from "deno-slack-sdk/mod.ts";import { WelcomeMessageDatastore } from "./datastores/messages.ts";import { MessageSetupWorkflow } from "./workflows/create_welcome_message.ts";import { SendWelcomeMessageWorkflow } from "./workflows/send_welcome_message.ts";export default Manifest({ name: "Welcome Message Bot", description: "Quick way to setup automated welcome messages for channels in your workspace.", icon: "assets/default_new_app_icon.png", workflows: [MessageSetupWorkflow, SendWelcomeMessageWorkflow], outgoingDomains: [], datastores: [WelcomeMessageDatastore], botScopes: [ "chat:write", "chat:write.public", "datastore:read", "datastore:write", "channels:read", "triggers:write", "triggers:read", ],}); ``` We've provided you all this upfront to streamline the tutorial, but you would likely build up your manifest as you add workflows and datastores to your app. ## Define a workflow for setting up the welcome message {#define-a-workflow-for-setting-up-the-welcome-message} In this step we'll be creating a [workflow](/tools/deno-slack-sdk/guides/creating-workflows) named `MessageSetupWorkflow`. This workflow will contain the functions needed for someone in the channel to create a welcome message with a form. Create a file named `create_welcome_message.ts` within the `workflows` folder. There you'll add the following workflow definition: ``` // /workflows/create_welcome_message.tsimport { DefineWorkflow, Schema } from "deno-slack-sdk/mod.ts";import { WelcomeMessageSetupFunction } from "../functions/create_welcome_message.ts";/** * The MessageSetupWorkflow opens a form where the user creates a * welcome message. The trigger for this workflow is found in * `/triggers/welcome_message_trigger.ts` */export const MessageSetupWorkflow = DefineWorkflow({ callback_id: "message_setup_workflow", title: "Create Welcome Message", description: " Creates a message to welcome new users into the channel.", input_parameters: { properties: { interactivity: { type: Schema.slack.types.interactivity, }, channel: { type: Schema.slack.types.channel_id, }, }, required: ["interactivity"], },}); ``` The `input_parameters` you need are `interactivity` and `channel`. The `interactivity` parameter enables interactive elements, like the form you'll set up next. ## Add a form for the user to specify the welcome message {#add-a-form-for-the-user-to-specify-the-welcome-message} You add functions to workflows by using the `addStep` method. In this case, you'll be adding the form the user will interact with. This is done using a [Slack function](/tools/deno-slack-sdk/guides/creating-slack-functions). Slack functions give you the ability to add common Slack functionality without the need to do so from scratch. The Slack function to use here is the [`OpenForm`](/tools/deno-slack-sdk/reference/slack-functions/open_form) function. Add it to your `create_welcome_message.ts` workflow like so: ``` // /workflows/create_welcome_message.ts/** * This step uses the OpenForm Slack function. The form has two * inputs -- a welcome message and a channel id for that message to * be posted in. */const SetupWorkflowForm = MessageSetupWorkflow.addStep( Schema.slack.functions.OpenForm, { title: "Welcome Message Form", submit_label: "Submit", description: ":wave: Create a welcome message for a channel!", interactivity: MessageSetupWorkflow.inputs.interactivity, fields: { required: ["channel", "messageInput"], elements: [ { name: "messageInput", title: "Your welcome message", type: Schema.types.string, long: true, }, { name: "channel", title: "Select a channel to post this message in", type: Schema.slack.types.channel_id, default: MessageSetupWorkflow.inputs.channel, }, ], }, },); ``` This creates a form that will show the following fields: * "Your welcome message", where the user provides the message as a string of text * "Select a channel to post this message in", where the user provides the channel for the desired channel. The user can then submit the form. ## Add a confirmation ephemeral message when submitting the form {#add-a-confirmation-ephemeral-message-when-submitting-the-form} When the user submits the form, they'll want confirmation that it is submitted. You can do this by using the Slack [`SendEphemeralMessage`](/tools/deno-slack-sdk/reference/slack-functions/send_ephemeral_message) function. Add the following step to your `create_welcome_message.ts` workflow: ``` // /workflows/create_welcome_message.ts/** * This step takes the form output and passes it along to a custom * function which sets the welcome message up. * See `/functions/setup_function.ts` for more information. */MessageSetupWorkflow.addStep(WelcomeMessageSetupFunction, { message: SetupWorkflowForm.outputs.fields.messageInput, channel: SetupWorkflowForm.outputs.fields.channel, author: MessageSetupWorkflow.inputs.interactivity.interactor.id,});/** * This step uses the SendEphemeralMessage Slack function. * An ephemeral confirmation message will be sent to the user * creating the welcome message, after the user submits the above * form. */MessageSetupWorkflow.addStep(Schema.slack.functions.SendEphemeralMessage, { channel_id: SetupWorkflowForm.outputs.fields.channel, user_id: MessageSetupWorkflow.inputs.interactivity.interactor.id, message: `Your welcome message for this channel was successfully created! :white_check_mark:`,});export default MessageSetupWorkflow; ``` This function takes the provided `message` text and sends it to the specified user and channel, both pulled from the `OpenForm` function step above. Wonderful! Now let's build functionality to handle that welcome message once its submitted by a user. ## Create a datastore to store the welcome message {#create-a-datastore-to-store-the-welcome-message} The message data needs to be accessible at a later time (when a user joins the channel), so it needs to be stored somewhere, like a [datastore](/tools/deno-slack-sdk/guides/using-datastores). Within your `datastores` folder, create a file named `messages.ts`. Within it, define the datastore: ``` // /datastores/messages.tsimport { DefineDatastore, Schema } from "deno-slack-sdk/mod.ts";export const WelcomeMessageDatastore = DefineDatastore({ name: "messages", primary_key: "id", attributes: { id: { type: Schema.types.string, }, channel: { type: Schema.slack.types.channel_id, }, message: { type: Schema.types.string, }, author: { type: Schema.slack.types.user_id, }, },}); ``` Each `attribute` is a type of information you want to store. In this case, it's the information from the form submission. Next, you'll fill the datastore with that information. ## Create a custom function to send the message to the datastore {#create-a-custom-function-to-send-the-message-to-the-datastore} Within your `functions` folder, create a file named `create_welcome_message.ts`. This is where you'll define this [custom function](/tools/deno-slack-sdk/guides/creating-custom-functions). The custom function you'll add here will take the form input the user provided and store that information in the created datastore. Add the function definition to the `create_welcome_message.ts` file: ``` // /functions/create_welcome_message.tsimport { DefineFunction, Schema, SlackFunction } from "deno-slack-sdk/mod.ts";import { SlackAPIClient } from "deno-slack-sdk/types.ts";import { SendWelcomeMessageWorkflow } from "../workflows/send_welcome_message.ts";import { WelcomeMessageDatastore } from "../datastores/messages.ts";/** * This custom function will take the initial form input, store it * in the datastore and create an event trigger to listen for * user_joined_channel events in the specified channel. */export const WelcomeMessageSetupFunction = DefineFunction({ callback_id: "welcome_message_setup_function", title: "Welcome Message Setup", description: "Takes a welcome message and stores it in the datastore", source_file: "functions/create_welcome_message.ts", input_parameters: { properties: { message: { type: Schema.types.string, description: "The welcome message", }, channel: { type: Schema.slack.types.channel_id, description: "Channel to post in", }, author: { type: Schema.slack.types.user_id, description: "The user ID of the person who created the welcome message", }, }, required: ["message", "channel"], },}); ``` This function provides three `properties` as `input_parameters`. These are the three pieces of information you want to pass to the datastore: the welcome message, the channel to post in, and the user ID of the person who created the message. ## Add the custom function's functionality {#add-the-custom-functions-functionality} The actual functionality involves taking those input parameters and putting them into a datastore. Put this right below your function definition within `create_welcome_message.ts`: ``` // /functions/create_welcome_message.tsexport default SlackFunction( WelcomeMessageSetupFunction, async ({ inputs, client }) => { const { channel, message, author } = inputs; const uuid = crypto.randomUUID(); // Save information about the welcome message to the datastore const putResponse = await client.apps.datastore.put< typeof WelcomeMessageDatastore.definition >({ datastore: WelcomeMessageDatastore.name, item: { id: uuid, channel, message, author }, }); if (!putResponse.ok) { return { error: `Failed to save welcome message: ${putResponse.error}` }; } // Search for any existing triggers for the welcome workflow const triggers = await findUserJoinedChannelTrigger(client, channel); if (triggers.error) { return { error: `Failed to lookup existing triggers: ${triggers.error}` }; } // Create a new user_joined_channel trigger if none exist if (!triggers.exists) { const newTrigger = await saveUserJoinedChannelTrigger(client, channel); if (!newTrigger.ok) { return { error: `Failed to create welcome trigger: ${newTrigger.error}`, }; } } return { outputs: {} }; },); ``` ## Add the custom function to the workflow {#add-the-custom-function-to-the-workflow} Add the custom function you created as a step in the workflow. This connection allows you to use inputs and outputs from previous steps, which is how you'll get the specific pieces of information. Pivot back to your `create_welcome_message.ts` workflow file. Add the following step: ``` // /workflows/create_welcome_message.ts/** * This step takes the form output and passes it along to a custom * function which sets the welcome message up. * See `/functions/setup_function.ts` for more information. */MessageSetupWorkflow.addStep(WelcomeMessageSetupFunction, { message: SetupWorkflowForm.outputs.fields.messageInput, channel: SetupWorkflowForm.outputs.fields.channel, author: MessageSetupWorkflow.inputs.interactivity.interactor.id,});export default MessageSetupWorkflow; ``` Now you've created a workflow that will: * let a user fill out a form with information for a welcome message * store the welcome message information in a datastore ## Create the link trigger {#create-the-link-trigger} You need to create a [trigger](/tools/deno-slack-sdk/guides/using-triggers) that will start the workflow, which provides a user the form to fill out. This app will use a specific type of trigger called a [link trigger](/tools/deno-slack-sdk/guides/creating-link-triggers). Link triggers kick off workflows when a user clicks on their link. Within your triggers folder, create a file named `create_welcome_message_shortcut.ts`. Place this trigger definition within that file: ``` // triggers/create_welcome_message_shortcut.tsimport { Trigger } from "deno-slack-api/types.ts";import MessageSetupWorkflow from "../workflows/create_welcome_message.ts";import { TriggerContextData, TriggerTypes } from "deno-slack-api/mod.ts";/** * This link trigger prompts the MessageSetupWorkflow workflow. */const welcomeMessageTrigger: Trigger = { type: TriggerTypes.Shortcut, name: "Setup a Welcome Message", description: "Creates an automated welcome message for a given channel.", workflow: `#/workflows/${MessageSetupWorkflow.definition.callback_id}`, inputs: { interactivity: { value: TriggerContextData.Shortcut.interactivity, }, channel: { value: TriggerContextData.Shortcut.channel_id, }, },};export default welcomeMessageTrigger; ``` This defines a trigger that will kick off the provided workflow, `message_setup_workflow`, along with an added bonus: it'll pass along the channel ID of the channel it was started in. ## Create the event trigger to start a second workflow {#create-the-event-trigger-to-start-a-second-workflow} The workflow to send a message to a user needs to be invoked _after_ the message is created in the workflow. It also needs to be invoked whenever a new user joins the channel. This calls for using a different type of trigger: an [event trigger](/tools/deno-slack-sdk/guides/creating-event-triggers). Event triggers are only invoked when a certain event happens. In this case, our event is `user_joined_channel`. Think of your `setup` function as priming everything needed for that message to send. The final piece to set up is this trigger. Since it runs at a certain point in a workflow, you'll actually place it within a function file. Place it within the `/functions/create_welcome_message.ts` file: ``` // /functions/create_welcome_message.ts/** * findUserJoinedChannelTrigger returns if the user_joined_channel trigger * exists for the "Send Welcome Message" workflow in a channel. */export async function findUserJoinedChannelTrigger( client: SlackAPIClient, channel: string,): Promise<{ error?: string; exists?: boolean }> { // Collect all existing triggers created by the app const allTriggers = await client.workflows.triggers.list({ is_owner: true }); if (!allTriggers.ok) { return { error: allTriggers.error }; } // Find user_joined_channel triggers for the "Send Welcome Message" // workflow in the specified channel const joinedTriggers = allTriggers.triggers.filter((trigger) => ( trigger.workflow.callback_id === SendWelcomeMessageWorkflow.definition.callback_id && trigger.event_type === "slack#/events/user_joined_channel" && trigger.channel_ids.includes(channel) )); // Return if any matching triggers were found const exists = joinedTriggers.length > 0; return { exists };}/** * saveUserJoinedChannelTrigger creates a new user_joined_channel trigger * for the "Send Welcome Message" workflow in a channel. */export async function saveUserJoinedChannelTrigger( client: SlackAPIClient, channel: string,): Promise<{ ok: boolean; error?: string }> { const triggerResponse = await client.workflows.triggers.create< typeof SendWelcomeMessageWorkflow.definition >({ type: "event", name: "User joined channel", description: "Send a message when a user joins the channel", workflow: `#/workflows/${SendWelcomeMessageWorkflow.definition.callback_id}`, event: { event_type: "slack#/events/user_joined_channel", channel_ids: [channel], }, inputs: { channel: { value: channel }, triggered_user: { value: "{{data.user_id}}" }, }, }); if (!triggerResponse.ok) { return { ok: false, error: triggerResponse.error }; } return { ok: true };} ``` This trigger passes the event-related `channel` and `triggered_user` values on to your soon-to-be workflow. With those accessible, you can now build out your next workflow. ## Create a workflow for sending the welcome message {#create-a-workflow-for-sending-the-welcome-message} This second workflow will retrieve the message from the datastore and send it to the channel when a new user joins that channel. Navigate back to your `workflows` folder, and create a new file `send_welcome_message.ts`. Within that file place the workflow definition: ``` // /workflows/send_welcome_message.tsimport { DefineWorkflow, Schema } from "deno-slack-sdk/mod.ts";import { SendWelcomeMessageFunction } from "../functions/send_welcome_message.ts";/** * The SendWelcomeMessageWorkFlow will retrieve the welcome message * from the datastore and send it to the specified channel, when * a new user joins the channel. */export const SendWelcomeMessageWorkflow = DefineWorkflow({ callback_id: "send_welcome_message", title: "Send Welcome Message", description: "Posts an ephemeral welcome message when a new user joins a channel.", input_parameters: { properties: { channel: { type: Schema.slack.types.channel_id, }, triggered_user: { type: Schema.slack.types.user_id, }, }, required: ["channel", "triggered_user"], },}); ``` This workflow will have two inputs: `channel` and `triggered_user`, both acquired from the trigger invocation. ## Create a custom function that sends the welcome message {#create-a-custom-function-that-sends-the-welcome-message} Navigate to the `functions` folder, and create a new file called `send_welcome_message.ts`. Within that file add the definition for a function that uses the inputs `channel` and `triggered_user`: ``` // /functions/send_welcome_message.tsimport { DefineFunction, Schema, SlackFunction } from "deno-slack-sdk/mod.ts";import { WelcomeMessageDatastore } from "../datastores/messages.ts";/** * This custom function will pull the stored message from the datastore * and send it to the joining user as an ephemeral message in the * specified channel. */export const SendWelcomeMessageFunction = DefineFunction({ callback_id: "send_welcome_message_function", title: "Sending the Welcome Message", description: "Pull the welcome messages and sends it to the new user", source_file: "functions/send_welcome_message.ts", input_parameters: { properties: { channel: { type: Schema.slack.types.channel_id, description: "Channel where the event was triggered", }, triggered_user: { type: Schema.slack.types.user_id, description: "User that triggered the event", }, }, required: ["channel", "triggered_user"], },}); ``` ## Add the custom function's functionality {#add-the-custom-functions-functionality-1} With the function defined, add the actual functionality right after: ``` // /functions/send_welcome_message.tsexport default SlackFunction(SendWelcomeMessageFunction, async ( { inputs, client },) => { // Querying datastore for stored messages const messages = await client.apps.datastore.query< typeof WelcomeMessageDatastore.definition >({ datastore: WelcomeMessageDatastore.name, expression: "#channel = :mychannel", expression_attributes: { "#channel": "channel" }, expression_values: { ":mychannel": inputs.channel }, }); if (!messages.ok) { return { error: `Failed to gather welcome messages: ${messages.error}` }; } // Send the stored messages ephemerally for (const item of messages["items"]) { const message = await client.chat.postEphemeral({ channel: item["channel"], text: item["message"], user: inputs.triggered_user, }); if (!message.ok) { return { error: `Failed to send welcome message: ${message.error}` }; } } return { outputs: {}, };}); ``` This creates a function that: * queries the datastore for stored messages * posts an ephemeral message using the `message` item from the datastore with a matching `channel` channel ID value to the user with the `triggered_user` user ID. ## Add the custom function to the workflow {#add-the-custom-function-to-the-workflow-1} With the custom function built, add it to your `send_welcome_message.ts` workflow as a step: ``` // /workflows/send_welcome_message.tsSendWelcomeMessageWorkflow.addStep(SendWelcomeMessageFunction, { channel: SendWelcomeMessageWorkflow.inputs.channel, triggered_user: SendWelcomeMessageWorkflow.inputs.triggered_user,}); ``` And with that, you have created the two workflows that contain all the functionality you need to send a custom ephemeral message to a user joining a new channel. ## Run your Slack app {#run-your-slack-app} For now, you'll want to [locally install the app](/tools/deno-slack-sdk/guides/developing-locally) to the workspace. From the command line, within your app's root folder, run the following command: ``` slack run ``` Proceed through the prompts until you have a local server running in that terminal instance. It's installed! You can't use it quite yet though. ## Invoke the link trigger {#invoke-the-link-trigger} Within a terminal located within that folder, you'll need to create that initial link trigger. You can open a new terminal tab or cancel your running server and restart later if you'd like. You can do that with the `slack trigger create` command. Make it so. ``` slack trigger create --trigger-def triggers/create_welcome_message_shortcut.ts ``` Since you haven't installed this trigger to a workspace yet, you'll be prompted to install the trigger to a new workspace. Then select an authorized workspace in which to install the app. When you select your workspace, you will be prompted to choose an app environment for the trigger. Choose the _Local_ option so you can interact with your app while developing locally. The CLI will then finish installing your trigger. Once your app's trigger is finished being installed, you will see the following output: ``` 📚 App Manifest Created app manifest for "welcomebot (local)" in "myworkspace" workspace⚠️ Outgoing domains No allowed outgoing domains are configured If your function makes network requests, you will need to allow the outgoing domains Learn more about upcoming changes to outgoing domains: https://docs.slack.dev/changelog🏠 Workspace Install Installed "welcomebot (local)" app to "myworkspace" workspace Finished in 1.5s⚡ Trigger created Trigger ID: Ft0123ABC456 Trigger Type: shortcut Trigger Name: Setup a Welcome Message Shortcut URL: https://slack.com/shortcuts/Ft0123ABC456/XYZ123... ``` Copy the URL, paste, and post it in a channel to kick off the first workflow and create a message. ## Deploy your Slack app {#deploy-your-slack-app} When you're ready to make the app accessible to others, you'll want to [deploy it](/tools/deno-slack-sdk/guides/deploying-to-slack) instead of running it: ``` slack deploy ``` And then create the trigger again, but choosing the _Deployed_ option this time: ``` slack trigger create --trigger-def triggers/create_welcome_message_shortcut.ts ``` Other than that, the steps are the same. ## Pause and reflect {#pause-and-reflect} Congratulations! You've successfully built your friendly neighborhood welcome bot, providing a cozy presence to all who enter your desired channel. ### Next steps {#next-steps} For your next challenge, perhaps consider creating [an app that creates an issue in GitHub](/tools/deno-slack-sdk/tutorials/github-issues-app)! --- Source: https://docs.slack.dev/tools/deno-slack-sdk/tutorials/workflow-builder-custom-step # Create a custom step for Workflow Builder with the Deno Slack SDK Workflow apps require a paid plan Join the [Developer Program](https://api.slack.com/developer-program) and provision a sandbox with access to all Slack features for free. Custom functions in apps can be added as workflow steps in Workflow Builder. In this tutorial, you will define and implement a [custom function](/tools/deno-slack-sdk/guides/creating-custom-functions), then wire it up as a workflow step in our no-code automation platform [Workflow Builder](https://slack.com/help/articles/360035692513-Guide-to-Workflow-Builder). When finished, you'll be ready to build custom, scalable functions for anyone using Workflow Builder in your workspace. You'll build three things in this tutorial: * A custom function * A workflow app * A workflow in Workflow Builder The custom function will take a user-supplied string—the name of a composite color—and use a switch statement to return a new string—the meaning of the color—based on its value. The function will collect the input string from the user once the workflow is started, then return the result to the user in the form of an ephemeral message. An _ephemeral_ message in Slack is one that is only visible to the user. To protect your organization, external users (those outside your organization connected through Slack Connect) cannot use a workflow that contains connector functions built by your organization. This may manifest in a `home_team_only` warning. Refer to [this help center article](https://slack.com/help/articles/14844871922195-Slack-administration--Manage-workflow-usage-in-Slack-Connect-conversations#enterprise-grid-1) for more details. Skip to the code If you'd rather skip the tutorial and just head straight to the code, create a new app and use our [function sample](https://github.com/slack-samples/deno-function-template) as a template. The sample custom function provided in the template will be a good place to start exploring! ### The road ahead {#the-road-ahead} 1. We'll sketch out what we want the function to do and how users will integrate it in their workflows. 2. You'll write the custom function and deploy the app so you can use the step in Workflow Builder. 3. You'll find the workflow step in Workflow Builder and use it as a step in a workflow that you can run from inside Slack. Ready? Let's get started! ## Install & authorize the Slack CLI {#install--authorize-the-slack-cli} > Have you installed the CLI? Are you authorized in a workspace? If you answered 'yes' to both questions, skip this step! You'll need to have the Slack CLI **installed** and **authorized** to begin this tutorial. If you need help, follow the [Quickstart](/tools/deno-slack-sdk/guides/getting-started) guide and you'll be ready to build. You'll also need a development workspace where you have permission to install apps. Please note that the features in this tutorial require that the workspace be part of a [paid Slack plan](https://slack.com/pricing). ## Create a new app {#create-a-new-app} When you deploy custom functions for Workflow Builder, users will be able to search for your deployed app and then include any steps you've provided for them. Let's get our new app project started so we can define and then implement our custom function. With your CLI authorized, go to your terminal and create a new app with the blank template: 1. Run the command `slack create meaning-of-color`. This will tell the CLI you want to create a new workflow app named `meaning-of-color`. 2. When prompted to select a template to build from, select the **Blank template**. 3. When the CLI is finished setting up your project, follow the instructions in your terminal to `cd` into your project's directory. VS Code If you're using VS Code, once you `cd` into your project's directory, open it up with VS code by executing `code .`. ## Create a new custom function {#create-a-new-custom-function} Create a new folder called `functions`. In the `functions` folder, create a new file called `interpret_color.ts` where you'll will define and implement a custom Slack function. Start the file off by importing the necessary modules: ``` import { DefineFunction, Schema, SlackFunction } from "deno-slack-sdk/mod.ts"; ``` Next, define a new custom function named `InterpretColorFunction`. It should have a single string as both an input and output parameter: ``` export const InterpretColorFunction = DefineFunction({ callback_id: "interpret_color_function", source_file: "functions/interpret_color.ts", title: "Interpret Color", input_parameters: { properties: { input_string: { type: Schema.types.string, }, }, required: ["input_string"], }, output_parameters: { properties: { result: { type: Schema.types.string, }, }, required: ["result"], },}); ``` The `title` property will be used to identify this function for users in Workflow Builder. With the function defined, the next step is to implement it. This function will take the `input_string` and run it through a switch statement, then return a new string based on the matching case: ``` export default SlackFunction( InterpretColorFunction, ({ inputs }) => { const input_string = inputs.input_string; switch (input_string) { case "orange": return { outputs: { input_string, result: "Orange is the color of ambition", }, }; case "green": return { outputs: { input_string, result: "Green is the color of collaboration", }, }; case "purple": return { outputs: { input_string, result: "Purple is the color of harmony", }, }; default: return { outputs: { input_string, result: "That's not a color I recognize", }, }; } },); ``` The function is aware of three composite colors: orange, green, and purple. If a user supplies one of those composite colors as input, the function will return a fun meaning of that color. If the user supplies a color that is not known (i.e., not orange, green, or purple), then they'll get the default case message. In every return statement, the function includes both the required output (the `result` string) as well as the original input (the `input_string` string), which is not a required output parameter. This is a design decision that you can make depending on your use case; include the inputs in the return value if you want them available as inputs in follow-on workflow steps. ## Configure the app's manifest {#configure-the-apps-manifest} Your app's manifest is where you configure which functions your app should care about, among other things. To do that, import your custom function and add it to your manifest's `functions` property. Start by opening `manifest.ts` and importing `InterpretColorFunction`: ``` import { Manifest } from "deno-slack-sdk/mod.ts";// Add this:import { InterpretColorFunction } from "./functions/interpret_color.ts"; ``` Next, configure your app's manifest. For this tutorial, update the `name`, `description`, and `functions` property: ``` export default Manifest({ name: "Meaning of Color App", description: "The meaning of colors", icon: "assets/default_new_app_icon.png", functions: [InterpretColorFunction], workflows: [], outgoingDomains: [], botScopes: ["commands", "chat:write", "chat:write.public"],}); ``` This app is now finished! In the next section, you'll start a local development server and test drive the app before deploying it for your users. ## Start the local development server {#start-the-local-development-server} Start a local development server with `slack run`. Since this is the first time starting the local development server for this project, you'll be prompted to choose a local environment. The local environment is the Slack workspace you will be using to interact with the app you're currently developing. Select the option to install to a new workspace, then select the workspace you want to use for development. Once your local development server has started, you'll see it says `Connected, awaiting events` in your terminal window. At this point, you're ready to build a workflow in Workflow Builder to test your custom function. **Keep your local development server running while building the workflow!** ## Use the workflow step in Workflow Builder {#use-the-workflow-step-in-workflow-builder} In the workspace you just installed your app, open up Workflow Builder and create a new workflow: 1. From your desktop, click your workspace name in the top left. 2. Select **Tools** from the menu, then click **Workflow Builder**. This will open a new window titled "Workflow Builder." 3. In the "Workflow Builder" window that just opened, click the **Create Workflow** button in the top right. You can also find this screen by navigating to the left-side nav menu in Slack and clicking the ellipses **...More** tile, then selecting **Automations**. There are many ways to start a workflow in Workflow Builder. For this tutorial, we will use a link trigger. Under **Start the workflow...**, select **Choose an event**, then select **From a link in Slack**: ![Creating a new workflow in Workflow Builder](/assets/images/1-78428b4e9a7814a393e0e69f7b29626f.png) A modal will appear that show you an example **workflow link** (more on that soon) along with a scrollable container that reveals a **Custom inputs** section. Click the **Add Input** button. Depending on which Slack plan you are on, the latest version of Workflow Builder may still be rolling out and your UI may appear differently. If you do not see an **Add Input** button, we recommend skipping to the code and using our [function sample](https://github.com/slack-samples/deno-function-template) as a template. ![Adding custom inputs to a new workflow in Workflow Builder](/assets/images/2-83fb385b3380ad576c374af83995be25.png) After clicking the **Add Input** button, a form will appear. Use this form to describe the required input parameter for your custom function—i.e., the `color` to interpret. When you're done, click the **Done** button: ![Configuring custom inputs to a new workflow in Workflow Builder](/assets/images/3-c5f941358ab2b40467076455bf217cb5.png) After clicking **Done**, the form will disappear and you'll see the custom input you just defined listed above the **Add Input** button. Since your custom function only has one required input, you can now click the **Continue** button to start building the workflow: ![Finishing creating a new workflow in Workflow Builder](/assets/images/4-76f82b41bf0c524e8785fc41b9e4fee3.png) You'll now be editing your workflow. The only thing configured right now is the trigger that will start your workflow. Recall that this workflow ought to do two things: run the custom function to get the result of the color interpretation, then send an ephemeral message to the user who ran the workflow. To add your custom function as a step in this workflow, search for the name of the function ("Interpret Color") in the right-hand **Steps** sidebar: ![Searching for a custom function in Workflow Builder](/assets/images/5-2ee7e516ff2093d339242f5bdde9b16c.png) In the search results that appear in the right-hand sidebar, you should see the name of your app ("Meaning of Color App") along with "(local)", which indicates that this function is provided via your local development server at this time, followed by your "Interpret Color" function. Click on the **Interpret Color** function to begin adding it to your workflow: ![Configuring inputs for custom function in Workflow Builder](/assets/images/6-22c9b8f196561ea01ac82bac85c80fe8.png) As soon as you click on your function to add it to your workflow, you'll be presented with a modal prompting you for an **Input String**. This is your custom function's required input parameter, so pass in the custom input you configured earlier when first creating this workflow. To do that, click on the curly braces icon to the right of the text box labelled "Input String", then select **Color** from the dropdown. When you're finished, click **Save** to go back to editing your workflow: ![Selecting custom input for function input in Workflow Builder](/assets/images/7-aad86ae39edf39cd221177d4c92cffb5.png) Your function—along with the input configuration you set up—is now the first step in the workflow following the trigger. The last thing this workflow needs to do is send an ephemeral message to the user that started it. In the **Steps** sidebar on the right, click **Messages**: ![Locating Messages functions in Workflow Builder](/assets/images/8-57d245ea6621d3848796cc1a043bd904.png) This brings up built-in functions for common Slack messaging that you can use as steps in your workflow. Select **Send an "only visible to you" message**, which sends an ephemeral message to a user that only they can see: ![Selecting the ephemeral message function in Workflow Builder](/assets/images/9-f7d88fad33470bc0c7016ab5764e22e4.png) After you select **Send an "only visible to you" message**, a modal will appear where you can configure this step. For **Select a channel**, choose the option **Channel where the workflow was used**. For **Select a member of the channel**, choose the option **Person who used this workflow**. For the **Add a message** field, select the **Insert a variable** link below the textbox: ![Configuring ephemeral message step in Workflow Builder](/assets/images/10-bfa5f8fcc7a8108c4e102676fd0a8e48.png) In the **Insert a variable** menu that appears, select the **Result** option located in your custom function's output variable group. ![Selecting custom function output for ephemeral message step in Workflow Builder](/assets/images/11-dea11b9f04487257273e18f1675da36e.png) When you're done, **Save** this step to return to the workflow editor: ![Saving ephemeral message step in Workflow Builder](/assets/images/12-ec44bfff24d6f4e7cb7ff3e8b8edf7aa.png) The workflow is almost ready. The last thing to do before publishing it is configure its name. Click on **Untitled Workflow** at the top to open the **Workflow Details** modal: ![Editing details in Workflow Builder](/assets/images/13-5ae3b495064e9ef9ae9cc884d7aba4b5.png) In the **Workflow Details** modal, edit the **Name** and **Description** of your workflow. These are what the user will see when the workflow is shared with them. When you're done, click the **Save** button: ![Editing name and description in in Workflow Builder](/assets/images/14-4911c70f6a10f80cf395f87cf4f5a86c.png) Begin publishing your workflow by clicking the **Finish Up** button at the top: ![Publishing a workflow in Workflow Builder](/assets/images/15-961c9b5dcf990bd262365d8e775c7f35.png) In the **Finish Up** modal, confirm your workflow's name and description, then scroll down to see **Workflow managers**. If you want to add a workflow collaborator, you can do this here. Scroll down to click **Show more permissions**. This is where you may edit your workflow's permissions: ![Confirming details in Workflow Builder](/assets/images/16-6710fac4a21088d1ed9645270eea13e4.png) Let's leave everything as is, and finally, click **Publish**: ![Final publishing checks in Workflow Builder](/assets/images/17-2f20a38efc7070d53781c030384fca7a.png) **Your workflow has been published!** ## Run your workflow in Slack {#run-your-workflow-in-slack} Your workflow is now ready for you to try out. Since you configured this workflow to start from a link in Slack, you'll need to copy the workflow link and then share it in any channel in your workspace. To copy the workflow link, click the **Copy Link** button: ![Copying workflow link in Workflow Builder](/assets/images/18-94264be02fcac83660527c1d34d09756.png) Where else can I copy the workflow link? When you open up your workflow, hover over the first step, **Starts from a link in Slack**, and click the **Copy Link** icon that appears: ![Copying workflow link in Workflow Builder](/assets/images/21-40b2f1e9b21906a47dff440abdc88a20.png) With the workflow link copied, leave Workflow Builder and go to Slack. In any channel, paste the workflow link and send it in a new message. Once the message is sent, Slack will recognize it as a workflow link and it will unfurl with a button: ![An unfurled workflow link](/assets/images/19-c3c4d9542e14d6f8f86b80ce71f0d180.png) Try out your workflow by clicking the **Start Workflow** button. Enter `purple` for the Color input, then run the workflow by clicking the **Start Workflow** button in the modal. You should see an ephemeral message with the appropriate return value based on how you implemented the custom function: ![Am ephemeral message from a workflow step](/assets/images/20-9b0600d06c43e1b1dcd54151e1dc3842.png) Congratulations! You just successfully built a function, wired it up in Workflow Builder, and executed it inside Slack. Run your workflow a few more times, trying the following inputs to see what happens: * `purple` * `green` * `blue` * `orange` You can also update your function implementation, then re-run the workflow to see your changes propagated to Slack in realtime. ## Deploy your function {#deploy-your-function} Your local development server is great for rapidly testing out your custom functions while building them. When you're ready to deploy your functions in workflows that you intend to be used by people in their day-to-day jobs, you'll need to deploy your function to Slack and then swap out the local function step with the newly-deployed function step. To do this, first go to your terminal where your local development server is running and enter `Ctrl`+`C`. This stops your server. Next, deploy your app by running `slack deploy`. Since this is your first time _deploying_ your app, you'll go through the same steps you did when you first _installed_ your app: select **Install to a new workspace**, then select the workspace you intend to use this function as a part of a workflow in. Once your app has finished deploying, go back into Workflow Builder and select your workflow: ![Selecting your workflow in Workflow Builder](/assets/images/22-6bb145274a2267477e9809275af6fc10.png) To swap out the local version of your app for the deployed version of your function, you'll first create a new step that calls the deployed function, then modify the ephemeral message step to use the deployed function instead of the local one. First, find the deployed version of your function by typing its name in the sidebar's search box. Notice how you have two options now—one for the local version, and one for the newly-deployed version. Click on the deployed version to add it into your workflow: ![Selecting a function from your deployed app in Workflow Builder](/assets/images/23-64e65603068aca4116031980c8ee051f.png) When the modal to configure the **Input String** appears, do the same as before: click on the curly brace icon to the right of the input field, select the **Color** option from the **From a link in Slack** group, then click the **Save** button to return to the editor: ![Configuring function inputs in Workflow Builder](/assets/images/24-d42bd50e6c0c30411dafeeb1ddefa0d0.png) You'll see two separate **Interpret Color** steps now. Delete the top one, which is the old _local_ version, by hovering over the step and clicking the **Delete** icon: ![Deleting the old function in Workflow Builder](/assets/images/25-d00428b49bf925d59bdcdfe010568d72.png) Confirm deletion in the modal that appears: ![Confirming deletion in Workflow Builder](/assets/images/26-297175231a0dce91eb4d57874fcaca41.png) The **Needs attention** warning is expected, since the step that provided input into the ephemeral message step was just removed. Move the deployed **Interpret Color** function so that it is above the ephemeral message step, putting it where the deleted local function was earlier, by selecting the **Move Up** button in step controls: ![Moving the deployed function step up in Workflow Builder](/assets/images/27-7732590460b0b3bfdf51f6265bbd3a32.png) Click on the **Edit** icon in the ephemeral message step to fix the missing data issue: ![Editing the ephemeral message step in Workflow Builder](/assets/images/28-311e35ff504b8c010ff0ae6ebb783338.png) Delete the `Missing Data` element from the **Add a message** field: ![Removing the missing data in Workflow Builder](/assets/images/29-ad85f6c48a692da521a424db419bf9a1.png) Then, click on the **Insert a variable** link underneath the field and select the **Result** option from the **Interpret Color** function group. When finished, click the **Save** button: ![Saving changes in Workflow Builder](/assets/images/30-2636d42a347fa49fac487e1d080f6575.png) **Congratulations! Your workflow is done!** All that's left to do now is **Publish changes**: ![Publishing changes in Workflow Builder](/assets/images/31-540a1f87ab63d5ae53b1e3ae1c96ce24.png) ## Onward {#onward} In this tutorial, you built a custom function, wired it up in Workflow Builder, and fully deployed your app so that it's ready to be used in other workflows. Here are some areas to explore now that you've come this far: * Want to integrate with a third-party? Augment your custom function by leveraging [external authentication](/tools/deno-slack-sdk/guides/integrating-with-services-requiring-external-authentication)! * Instead of using Workflow Builder, add your custom function as steps in [coded workflows](/tools/deno-slack-sdk/guides/creating-workflows)! * Check out our other tutorials for more ideas about what you can do with the workflow automations! --- Source: https://docs.slack.dev/tools/deno-slack-sdk/tutorials/workflows-with-ai-integrations # Design Slack workflows with AI integrations This tutorial teaches you how to harness generative AI to automate customer email responses within Slack. The goal is to improve response efficiency by leveraging AI to draft email responses and enable human review and approval before sending. How the workflow operates: 1. **Receive an email in Slack:** Emails sent to a dedicated Slack channel trigger a workflow, automatically forwarding customer inquiries to Slack. 2. **Notify User & Generate Response:** The workflow posts a notification to Slack, informing the user that the system is generating a response. At the same time, it sends the email’s content to the AI to create a draft response. 3. **AI Drafts Response:** The AI generates a response and posts it back in the same Slack thread. The user can edit and finalize the response directly within Slack before sending it to the customer. ## Project setup {#project-setup} Before diving into code, it’s essential to understand the components at play. We are using the [Slack CLI](/tools/slack-cli) and [Deno Slack SDK](/tools/deno-slack-sdk) to create an app that listens to Slack events and reacts accordingly. The AI integration will use external API calls (e.g., OpenAI) to process customer emails and return responses. At this point you should be familiar with the Slack CLI and how it works. If this is new for you we recommend starting with the [Hello world app](/tools/deno-slack-sdk/tutorials/hello-world-app) first. ### Create a new Slack project {#create-a-new-slack-project} Open your terminal and navigate to a directory where you have permission to create files. Use the following command to scaffold a new Slack project then change into the project directory: ``` slack create my-app --template slack-samples/deno-blank-templatecd my-app ``` Open your project in VS Code. ``` code . ``` ### Set up OpenAI keys {#set-up-openai-keys} Generative AI services (like OpenAI and Anthropic) require an API key to authenticate requests. Let’s get set up with OpenAI. If you haven't already, create an account on OpenAI's platform. After signing in, go to the API settings page and generate an API key. * Go to [https://platform.openai.com/api-keys](https://platform.openai.com/api-keys). * Click **API Keys** * Click **\+ Create Key** Copy the key's value, then go back to VS Code. Create a file in the project called `.env` and put your API key there, assigned to a variable like this: ``` OPENAI_API_KEY= ``` Save the file. Before we add any code to the project, we also need to let the app know where to find the OpenAI dependency. Open the `deno.jsonc` file of the project and add this line to it in the `imports` section: ``` "openai/": "https://deno.land/x/openai@v4.20.1/" ``` ### Prepare your Slack channel {#prepare-your-slack-channel} Slack allows you to forward emails into channels, and we’ll use this to simulate customer inquiries. 1. Create a Slack channel: In your testing workspace, create a new channel specifically for customer emails, e.g., #customer-support. 2. Set up email forwarding: In the Slack sidebar, right-click on your test channel name and go to **View channel details**, then head to the **Integrations** tab. Click on **Send emails to this channel** and generate a new channel email. This is where we'll forward customer inquiries. ## Implement code {#implement-code} In this section, we'll create the necessary project files. ### Configure the event trigger {#configure-the-event-trigger} Back in VS Code, create a folder in your project called `triggers`, then add a file to it named `email_trigger.ts`. Copy and paste this code into the file, then save it. ``` import { Trigger } from "deno-slack-sdk/types.ts";import { TriggerEventTypes, TriggerTypes } from "deno-slack-api/mod.ts";import EmailWorkflow from "../workflows/email_workflow.ts";const emailTrigger: Trigger = { type: TriggerTypes.Event, name: "Email message trigger", description: "A email trigger, responds only to emails being sent via a channel email", workflow: `#/workflows/${EmailWorkflow.definition.callback_id}`, event: { event_type: TriggerEventTypes.MessagePosted, channel_ids: ["C000000000"], // TODO: Must set this to an internal channel filter: { version: 1, root: { statement: "{{data.user_id}} == USLACKBOT", // Messages that come in via a channel e-mail have this as their user }, }, }, inputs: { message_ts: { value: "{{data.message_ts}}", }, channel_id: { value: "{{data.channel_id}}", }, },};export default emailTrigger; ``` Triggers come in a variety of forms in Slack workflows, such as message events, shortcuts, or scheduled events. For this use case, we’ll focus on an event trigger—specifically, a message event trigger. However, we don’t want every message in the channel to trigger our workflow, as that would quickly become overwhelming. Instead, we’ll filter the trigger to respond only to messages from `USLACKBOT`. Here’s what this means in practice: * The trigger will activate only when a message is sent by `USLACKBOT`, ensuring that only email-based messages kick off the workflow. * Inside the trigger event, we will extract some key inputs, including the `message_ts` (timestamp of the message) and `channel_id`. These values will be used in later steps when interacting with Slack messages (e.g., replying in a thread). We’re going to set the channel that we want to listen on by setting the `channel_ids` parameter. Follow these steps to get that channel ID. 1. Right-click on your test channel name in the sidebar. 2. Click on **View channel details**. 3. At the bottom of the modal, notice the channel ID. Copy the channel ID. 4. Paste this value in the `channel_ids` parameter within your `email_trigger.ts` file. ### Create the EmailWorkflow {#create-the-emailworkflow} Once the trigger is tripped, it’ll set a particular workflow into motion. Create another folder in your project, this one called `workflows`, then add a file to it. Name this file `email_workflow.ts`, copy and paste the following code in it, then save it. ``` import { DefineWorkflow, Schema } from "deno-slack-sdk/mod.ts";import { EmailListenerFunction } from "../functions/email_listener_function.ts";const EmailWorkflow = DefineWorkflow({ callback_id: "email_workflow", title: "Email workflow", description: "Workflow listens for emails and creates responses to them", input_parameters: { properties: { message_ts: { type: Schema.types.string, }, channel_id: { type: Schema.types.string, }, }, required: ["message_ts", "channel_id"], },});EmailWorkflow.addStep(EmailListenerFunction, { message_ts: EmailWorkflow.inputs.message_ts, channel_id: EmailWorkflow.inputs.channel_id,});export default EmailWorkflow; ``` You will see that it’s workflow containing only one step. The majority of the functionality will be housed in a function file. Triggers can offer context that we can use in the following steps of our workflow. Note that we are passing in the variables from the trigger to the function (`message_ts` and `channel_id`). ### Create the email listener function {#create-the-email-listener-function} Create a folder in your project, named `functions`, then add a new file to it named it `email_listener_function.ts`. Here we will implement the code for the `EmailListenerFunction` we added as a step in the workflow in the previous section. Copy and paste this code in the new `email_listener_function.ts` file and save it. ``` import { DefineFunction, Schema, SlackFunction } from "deno-slack-sdk/mod.ts";import OpenAI from "openai/mod.ts";import { TriggerEventTypes, TriggerTypes } from "deno-slack-api/mod.ts";import ThreadWorkflow from "../workflows/thread_workflow.ts";export const EmailListenerFunction = DefineFunction({ callback_id: "email_listener_function", title: "Email Listener Function", description: "A function that listens for email on a particular channel and uses AI to generate a response", source_file: "functions/email_listener_function.ts", input_parameters: { properties: { message_ts: { type: Schema.types.string, description: "The timestamp of the email message.", }, channel_id: { type: Schema.types.string, description: "The channel that the email was posted.", }, }, required: ["message_ts", "channel_id"], },});export default SlackFunction( EmailListenerFunction, async ({ client, inputs, env }) => { // 1. Send a message in thread to the e-mail message, // confirming that the AI model is "thinking" const ackResponse = await client.chat.postMessage({ channel: inputs.channel_id, thread_ts: inputs.message_ts, text: "Just a moment while I think of a response :hourglass_flowing_sand:", }); if (!ackResponse.ok) { console.error(ackResponse.error); } // 2. Send email contents to AI model and generate a response for us // Since the event doesn't contain the file itself, must call // `conversations.history` to get that info const historyResponse = await client.conversations.history({ channel: inputs.channel_id, oldest: inputs.message_ts, inclusive: true, limit: 1, }); if (!historyResponse.ok) { console.error(historyResponse.error); } const email_text = historyResponse.messages[0].files[0].plain_text; const openai = new OpenAI({ apiKey: env.OPENAI_API_KEY, }); const chatCompletion = await openai.chat.completions.create({ messages: [ { "role": "system", "content": `You are a helpful assistant. Please write a response to the following email in 100 words:`, }, { "role": "user", "content": `${email_text}` }, ], model: "gpt-3.5-turbo", }); const completionContent = chatCompletion.choices[0].message.content; // 3. Update the "thinking" message to the AI model's response const updateResponse = await client.chat.update({ channel: inputs.channel_id, ts: ackResponse.ts, text: `${completionContent}`, mrkdwn: true, }); if (!updateResponse.ok) { console.log(updateResponse.error); } // 4. Create trigger to listen for new messages on the email message thread const authResponse = await client.auth.test(); const botId = authResponse.user_id; const triggerResponse = await client.workflows.triggers.create({ type: TriggerTypes.Event, name: `Thread Listener response for ts: ${inputs.message_ts}`, description: "Listens on the thread for the message in the name", workflow: `#/workflows/${ThreadWorkflow.definition.callback_id}`, event: { event_type: TriggerEventTypes.MessagePosted, channel_ids: [`${inputs.channel_id}`], filter: { version: 1, root: { operator: "AND", inputs: [{ statement: `{{data.thread_ts}} == ${inputs.message_ts}`, }, { operator: "NOT", inputs: [{ statement: `{{data.user_id}} == ${botId}`, }], }], }, }, }, inputs: { thread_ts: { value: inputs.message_ts, }, channel_id: { value: "{{data.channel_id}}", }, bot_id: { value: botId, }, }, }); if (!triggerResponse.ok) { console.error(triggerResponse.error); } return { outputs: {}, }; },); ``` In this file, we use one of the most popular Slack Web API methods, [`chat.postMessage`](/reference/methods/chat.postMessage), to send a message to a Slack channel. To do this, we utilize the `client` object available within the function definition. In this case, we send a short message to inform the user that our workflow has started and we’re waiting for a response, as seen noted in step 1 in the code comments. ``` // 1. Send a message in thread to the e-mail message, // confirming that the AI model is "thinking" const ackResponse = await client.chat.postMessage({ channel: inputs.channel_id, thread_ts: inputs.message_ts, text: "Just a moment while I think of a response :hourglass_flowing_sand:", }); if (!ackResponse.ok) { console.error(ackResponse.error); } ``` This sends a message letting the user know that something is happening while they wait. Next comes the exciting part: we send the contents of the email to our AI model and wait for its response. There are a few key steps to achieve this. Since the message event itself doesn’t contain file data (as that would be too large), we need to call the [`conversations.history`](/reference/methods/conversations.history) API method to retrieve the message and extract its content. After getting the email content, we send it to our AI model and follow the model’s method to process the information and retrieve the response. ``` // 2. Send email contents to AI model and generate a response for us // Since the event doesn't contain the file itself, must call // `conversations.history` to get that info const historyResponse = await client.conversations.history({ channel: inputs.channel_id, oldest: inputs.message_ts, inclusive: true, limit: 1, }); if (!historyResponse.ok) { console.error(historyResponse.error); } const email_text = historyResponse.messages[0].files[0].plain_text; const openai = new OpenAI({ apiKey: env.OPENAI_API_KEY, }); const chatCompletion = await openai.chat.completions.create({ messages: [ { "role": "system", "content": `You are a helpful assistant. Please write a response to the following email in 100 words:`, }, { "role": "user", "content": `${email_text}` }, ], model: "gpt-3.5-turbo", }); const completionContent = chatCompletion.choices[0].message.content; ``` Next, we post the AI’s response back to Slack so the user can see it! We update the initial “thinking” message by using the [`chat.update`](/reference/methods/chat.update) API method, ensuring we reference the same timestamp (`ts`) as the original message. ``` // 3. Update the "thinking" message to the AI model's response const updateResponse = await client.chat.update({ channel: inputs.channel_id, ts: ackResponse.ts, text: `${completionContent}`, mrkdwn: true, }); if (!updateResponse.ok) { console.log(updateResponse.error); } ``` With that, we’ve completed the first part of our workflow! Next, we set up a listener for replies in the message thread. While this isn’t strictly necessary, it helps make the app feel more cohesive. To achieve this, we’ll recreate the earlier logic but make it work specifically for listening to thread messages posted by users (not by the bot itself). To prevent the bot from triggering its own workflow and causing an endless loop, we use the [`auth.test`](/reference/methods/auth.test) API method to retrieve the bot’s user ID. Using the `triggers.create` method, we can define all the necessary parameters, making sure to filter out the bot’s own messages and only trigger the workflow for user replies within the thread. ``` // 4. Create trigger to listen for new messages on the email message thread const authResponse = await client.auth.test(); const botId = authResponse.user_id; const triggerResponse = await client.workflows.triggers.create({ type: TriggerTypes.Event, name: `Thread Listener response for ts: ${inputs.message_ts}`, description: "Listens on the thread for the message in the name", workflow: `#/workflows/${ThreadWorkflow.definition.callback_id}`, event: { event_type: TriggerEventTypes.MessagePosted, channel_ids: [`${inputs.channel_id}`], filter: { version: 1, root: { operator: "AND", inputs: [{ statement: `{{data.thread_ts}} == ${inputs.message_ts}`, }, { operator: "NOT", inputs: [{ statement: `{{data.user_id}} == ${botId}`, }], }], }, }, }, inputs: { thread_ts: { value: inputs.message_ts, }, channel_id: { value: "{{data.channel_id}}", }, bot_id: { value: botId, }, }, }); if (!triggerResponse.ok) { console.error(triggerResponse.error); } return { outputs: {}, }; },); ``` The trigger we create trips another workflow, the `ThreadWorkflow`, so let's create that now. ### Create the ThreadWorkflow {#create-the-threadworkflow} Create a new file within the `workflows` folder and name it `thread_workflow.ts`. Copy and paste the following code into it and save. ``` import { DefineWorkflow, Schema } from "deno-slack-sdk/mod.ts";import { ListenerDefinition } from "../functions/thread_listener_function.ts";const ThreadWorkflow = DefineWorkflow({ callback_id: "thread_workflow", title: "Thread workflow", description: "A workflow that listens for messages on a thread and responds with AI.", input_parameters: { properties: { thread_ts: { type: Schema.types.string, }, channel_id: { type: Schema.types.string, }, bot_id: { type: Schema.types.string, }, }, required: ["thread_ts", "channel_id", "bot_id"], },});ThreadWorkflow.addStep(ListenerDefinition, { thread_ts: ThreadWorkflow.inputs.thread_ts, channel_id: ThreadWorkflow.inputs.channel_id, bot_id: ThreadWorkflow.inputs.bot_id,});export default ThreadWorkflow; ``` Here we have another short workflow, also with one step. Note that we use the `thread_ts`, `channel_id`, and `bot_id`, passed in from the trigger. Next, we create the `ListenerDefinition`. ### Create the thread listener function {#create-the-thread-listener-function} Finally, let’s take a look at the second function. Create a new file within the `functions` folder of your project and name it `thread_listener_function.ts`. This function is similar to what we just covered, but the methods are adjusted to work with thread messages instead of channel messages. Copy and paste the code below into the new file then save it. ``` import { DefineFunction, Schema, SlackFunction } from "deno-slack-sdk/mod.ts";import OpenAI from "openai/mod.ts";import { ChatCompletionMessageParam } from "openai/resources/mod.ts";export const ListenerDefinition = DefineFunction({ callback_id: "listener_function", title: "listener text using AI", description: "A function that listens on a thread, pulls in the contents and uses AI to respond.", source_file: "functions/thread_listener_function.ts", input_parameters: { properties: { bot_id: { type: Schema.types.string, description: "User ID of the bot", }, thread_ts: { type: Schema.types.string, description: "The thread timestamp", }, channel_id: { type: Schema.types.string, description: "The channel Id", }, }, required: ["thread_ts", "channel_id", "bot_id"], },});export default SlackFunction( ListenerDefinition, async ({ client, inputs, env }) => { // 1. Acknowledge user input and response with "thinking" message const ackResponse = await client.chat.postMessage({ channel: inputs.channel_id, thread_ts: inputs.thread_ts, text: "Just a moment while I think of a response :hourglass_flowing_sand:", }); console.log(ackResponse); if (!ackResponse.ok) { console.error(ackResponse.error); } // 2. Get message contents by pulling in all conversations in the thread // and feed contents to AI model const conversationResponse = await client.conversations.replies({ channel: inputs.channel_id, ts: inputs.thread_ts, }); if (!conversationResponse.ok) { console.error(conversationResponse.error); } const openai = new OpenAI({ apiKey: env.OPENAI_API_KEY, }); let messages: ChatCompletionMessageParam[] = [ { "role": "system", "content": `You are a helpful assistant.`, }, ]; for (let i = 1; i < conversationResponse.messages.length; i++) { // Start at 1, the first message is the file if (conversationResponse.messages[i] != inputs.bot_id) { messages.push({ "role": "user", "content": `${conversationResponse.messages[i].text}`, }); } else { messages.push({ "role": "assistant", "content": `${conversationResponse.messages[i].text}`, }); } } const chatCompletion = await openai.chat.completions.create({ messages: messages, model: "gpt-3.5-turbo", }); // 3. Update "thinking" message with AI model contents const completionContent = chatCompletion.choices[0].message.content; const updateResponse = await client.chat.update({ channel: inputs.channel_id, ts: ackResponse.ts, text: `${completionContent}`, mrkdwn: true, }); if (!updateResponse.ok) { console.log(updateResponse.error); } return { outputs: {}, }; },); ``` ## Update the manifest {#update-the-manifest} Before we can run the app, we have to ensure that all objects we've created for the app are reflected in the app manifest. Open the `manifest.ts` file and replace its contents with the following: ``` import { Manifest } from "deno-slack-sdk/mod.ts";import { EmailListenerFunction } from "./functions/email_listener_function.ts";import { ListenerDefinition } from "./functions/thread_listener.ts";import EmailWorkflow from "./workflows/email_workflow.ts";import ThreadWorkflow from "./workflows/thread_workflow.ts";export default Manifest({ name: "email-response-generator", description: "An app that creates responses to emails automatically within a thread.", icon: "assets/robot-emoji.png", workflows: [EmailWorkflow, ThreadWorkflow], outgoingDomains: ["api.openai.com"], functions: [ EmailListenerFunction, ListenerDefinition ], datastores: [], botScopes: [ "commands", "chat:write", "chat:write.public", "channels:history", "triggers:write", "reactions:read", ],}); ``` ## Run the app locally and test {#run-the-app-locally-and-test} Let’s run the workflow locally. Navigate back to your terminal and run the `slack run` command. Once the app is up and running, important logs will appear to let us know what’s happening with the app, such as which functions were executed and whether they succeeded. A helpful feature of this setup is that whenever you update your project code, the app is automatically reinstalled and updated. This means you can see code changes reflected in your app immediately, without the need to restart any development server. Next, invite your app to your channel. Your app cannot read events from a channel unless it’s actually in the channel, so invite your bot to the testing channel by typing `/invite` into the message composer and then selecting the name of your app. After that, test your setup by sending an email to the channel email address you created earlier. ## Deploy and uninstall {#deploy-and-uninstall} If you want to use your app without needing to run it from your local machine, you’d need to deploy your app. Run the `slack deploy` command in your terminal. This will package your app, and make your functions available in Slack at any time. Once your app is deployed, you can remove the local version. To uninstall, run the `slack delete` command in your project’s root directory. You’ll be prompted to choose whether you want to remove the local version or the deployed version.