diff --git a/docs/content/tutorial/ai-chatbot.md b/docs/content/tutorial/ai-chatbot.md index 9f4038f36..fb0498752 100644 --- a/docs/content/tutorial/ai-chatbot.md +++ b/docs/content/tutorial/ai-chatbot.md @@ -100,7 +100,7 @@ Navigate to the Bolty **App Home** and select a provider from the drop-down menu If you don't see Bolty listed under **Apps** in your workspace right away, never fear! You can mention **@Bolty** in a public channel to add the app, then navigate to your **App Home**. -![Choose your AI provider](/img/ai-chatbot/6.png) +![Choose your AI provider](/img/tutorials/ai-chatbot/6.png) ## Setting up your workflow {#workflow} @@ -108,11 +108,11 @@ Within your development workspace, open Workflow Builder by clicking on your wor Click **Untitled Workflow** at the top to rename your workflow. For this tutorial, we'll call the workflow **Welcome to the channel**. Enter a description, such as _Summarizes channels for new members_, and click **Save**. -![Setting up a new workflow](/img/ai-chatbot/1.png) +![Setting up a new workflow](/img/tutorials/ai-chatbot/1.png) Select **Choose an event** under **Start the workflow...**, and then choose **When a person joins a channel**. Select the channel name from the drop-down menu and click **Save**. -![Start the workflow](/img/ai-chatbot/2.png) +![Start the workflow](/img/tutorials/ai-chatbot/2.png) Under **Then, do these things**, click **Add steps** and complete the following: @@ -121,20 +121,20 @@ Under **Then, do these things**, click **Add steps** and complete the following: 3. Under **Add a message**, enter a short message, such as _Hi! Welcome to `{}The channel that the user joined`. Would you like a summary of the recent conversation?_ Note that the _`{}The channel that the user joined`_ is a variable; you can insert it by selecting **{}Insert a variable** at the bottom of the message text box. 4. Select the **Add Button** button, and name the button _Yes, give me a summary_. Click **Done**. -![Send a message](/img/ai-chatbot/3.png) +![Send a message](/img/tutorials/ai-chatbot/3.png) We'll add two more steps under the **Then, do these things** section. First, scroll to the bottom of the list of steps and choose **Custom**, then choose **Bolty** and **Bolty Custom Function**. In the **Channel** drop-down menu, select **Channel that the user joined**. Click **Save**. -![Bolty custom function](/img/ai-chatbot/4.png) +![Bolty custom function](/img/tutorials/ai-chatbot/4.png) For the final step, complete the following: 1. Choose **Messages** and then **Send a message to a person**. Under **Select a member**, choose **Person who clicked the button** from the drop-down menu. 2. Under **Add a message**, click **Insert a variable** and choose **`{}Summary`** under the **Bolty Custom Function** section in the list that appears. Click **Save**. -![Summary](/img/ai-chatbot/5.png) +![Summary](/img/tutorials/ai-chatbot/5.png) When finished, click **Finish Up**, then click **Publish** to make the workflow available in your workspace. @@ -149,7 +149,7 @@ In order for Bolty to provide summaries of recent conversation in a channel, Bol To test this, leave the channel you just invited Bolty to and rejoin it. This will kick off your workflow and you'll receive a direct message from **Welcome to the channel**. Click the **Yes, give me a summary** button, and Bolty will summarize the recent conversations in the channel you joined. -![Channel summary](/img/ai-chatbot/7.png) +![Channel summary](/img/tutorials/ai-chatbot/7.png) The central part of this functionality is shown in the following code snippet. Note the use of the [`user_context`](https://api.slack.com/automation/types#usercontext) object, a Slack type that represents the user who is interacting with our workflow, as well as the `history` of the channel that will be summarized, which includes the ten most recent messages. @@ -191,7 +191,7 @@ To ask Bolty a question, you can chat with Bolty in any channel the app is in. U You can also navigate to **Bolty** in your **Apps** list and select the **Messages** tab to chat with Bolty directly. -![Ask Bolty](/img/ai-chatbot/8.png) +![Ask Bolty](/img/tutorials/ai-chatbot/8.png) ## Next steps {#next-steps} diff --git a/docs/content/tutorial/custom-steps-for-jira.md b/docs/content/tutorial/custom-steps-for-jira.md index 5da362a83..c87328208 100644 --- a/docs/content/tutorial/custom-steps-for-jira.md +++ b/docs/content/tutorial/custom-steps-for-jira.md @@ -127,21 +127,21 @@ If your app is up and running, you'll see a message noting that the app is start 2. Select **New Workflow** > **Build Workflow**. 3. Click **Untitled Workflow** at the top of the pane to rename your workflow. We'll call it **Create Issue**. For the description, enter _Creates a new issue_, then click **Save**. -![Workflow details](/img/custom-steps-jira/1.png) +![Workflow details](/img/tutorials/custom-steps-jira/1.png) 4. Select **Choose an event** under **Start the workflow...**, and then select **From a link in Slack**. Click **Continue**. -![Start the workflow](/img/custom-steps-jira/2.png) +![Start the workflow](/img/tutorials/custom-steps-jira/2.png) 5. Under **Then, do these things** click **Add steps** to add the custom step. Your custom step will be the function defined in the [`create_issue.py`](https://github.com/slack-samples/bolt-python-jira-functions/blob/main/listeners/functions/create_issue.py) file. Scroll down to the bottom of the list on the right-hand pane and select **Custom**, then **BoltPy Jira Functions** > **Create an issue**. Enter the project details, issue type (optional), summary (optional), and description (optional). Click **Save**. -![Custom function](/img/custom-steps-jira/3.png) +![Custom function](/img/tutorials/custom-steps-jira/3.png) 6. Add another step and select **Messages** > **Send a message to a channel**. Select **Channel where the workflow was used** from the drop-down list and then select **Insert a variable** and **Issue url**. Click **Save**. -![Insert variable for issue URL](/img/custom-steps-jira/4.png) +![Insert variable for issue URL](/img/tutorials/custom-steps-jira/4.png) 7. Click **Publish** to make the workflow available to your workspace. @@ -150,16 +150,16 @@ If your app is up and running, you'll see a message noting that the app is start 1. Copy your workflow link. 2. Navigate to your app's home tab and click **Connect an Account** to connect your JIRA account to the app. -![Connect account](/img/custom-steps-jira/5.png) +![Connect account](/img/tutorials/custom-steps-jira/5.png) 3. Click **Allow** on the screen that appears. -![Allow connection](/img/custom-steps-jira/6.png) +![Allow connection](/img/tutorials/custom-steps-jira/6.png) 4. In any channel, post the workflow link you copied. 5. Click **Start Workflow** and observe as the link to a new JIRA ticket is posted in the channel. Click the link to be directed to the newly-created issue within your JIRA project. -![JIRA issue](/img/custom-steps-jira/7.png) +![JIRA issue](/img/tutorials/custom-steps-jira/7.png) When finished, you can click the **Disconnect Account** button in the home tab to disconnect your app from your JIRA account. diff --git a/docs/content/tutorial/custom-steps-workflow-builder-existing.md b/docs/content/tutorial/custom-steps-workflow-builder-existing.md new file mode 100644 index 000000000..e5c584a4c --- /dev/null +++ b/docs/content/tutorial/custom-steps-workflow-builder-existing.md @@ -0,0 +1,283 @@ +--- +title: Custom Steps for Workflow Builder (existing app) +--- + +:::info[This feature requires a paid plan] +If you don't have a paid workspace for development, you can join the [Developer Program](https://api.slack.com/developer-program) and provision a sandbox with access to all Slack features for free. +::: + +If you followed along with our [create a custom step for Workflow Builder: new app](/tutorial/custom-steps-workflow-builder-new) tutorial, you have seen how to add custom steps to a brand new app. But what if you have an app up and running currently to which you'd like to add custom steps? You've come to the right place! + +In this tutorial we will: +- Start with an existing Bolt app +- Add a custom **workflow step** in the [app settings](https://api.slack.com/apps) +- Wire up the new step to a **function listener** in our project, using the [Bolt for Python](https://slack.dev/bolt-python/) framework +- See the step as a custom workflow step in Workflow Builder + +## Prerequisites {#prereqs} + +The custom steps feature is compatible with Bolt version 1.20.0 and above. First, update your `package.json` file to reflect version 1.20.0 of Bolt, then run the following command in your terminal: + +```sh +python3 -m venv .venv +source .venv/bin/activate +pip install -r requirements.txt +``` + +In order to add custom workflow steps to an app, the app also needs to be org-ready. To do this, navigate to your [app settings page](https://api.slack.com/apps) and select your Bolt app. + +Navigate to **Org Level Apps** in the left nav and click **Opt-In**, then confirm **Yes, Opt-In**. + +![Make your app org-ready](/img/tutorials/custom-steps-wfb-existing/org-ready.png) + +## Adding a new workflow step {#add-step} + +Before we can add the new workflow step, we first need to ensure the workflow step is listening for the `function_executed` event so that our app knows when the workflow step is executed. + +### Adding the `function_executed` event subscription {#event-subscription} + +Navigate to **App Manifest** in the left nav and add the `function_executed` event subscription, then click **Save Changes**: + +```json +... + "settings": { + "event_subscriptions": { + "bot_events": [ + ... + "function_executed" + ] + }, + } +``` + +### Adding the workflow step {#add-step} + +Navigate to **Workflow Steps** in the left nav and click **Add Step**. This is where we'll configure our step's inputs, outputs, name, and description. + +![Add step](/img/tutorials/custom-steps-wfb-existing/add-step.png) + +For illustration purposes in this tutorial, we're going to write a custom step called Request Time Off. When the step is invoked, a message will be sent to the provided manager with an option to approve or deny the time-off request. When the manager takes an action (approves or denies the request), a message is posted with the decision and the manager who made the decision. The step will take two user IDs as inputs, representing the requesting user and their manager, and it will output both of those user IDs as well as the decision made. + +Add the pertinent details to the step: + +![Define step](/img/tutorials/custom-steps-wfb-existing/define-step.png) + +Remember this `callback_id`. We will use this later when implementing a function listener. Then add the input and output parameters: + +![Add inputs](/img/tutorials/custom-steps-wfb-existing/inputs.png) + +![Add outputs](/img/tutorials/custom-steps-wfb-existing/outputs.png) + +Save your changes. + +### Viewing our updates in the App Manifest {#view-updates} + +Navigate to **App Manifest** and notice your new step reflected in the `functions` property! Exciting. It should look like this: + +```json +"functions": { + "request_time_off": { + "title": "Request time off", + "description": "Submit a request to take time off", + "input_parameters": { + "manager_id": { + "type": "slack#/types/user_id", + "title": "Manager", + "description": "Approving manager", + "is_required": true, + "hint": "Select a user in the workspace", + "name": "manager_id" + }, + "submitter_id": { + "type": "slack#/types/user_id", + "title": "Submitting user", + "description": "User that submitted the request", + "is_required": true, + "name": "submitter_id" + } + }, + "output_parameters": { + "manager_id": { + "type": "slack#/types/user_id", + "title": "Manager", + "description": "Approving manager", + "is_required": true, + "name": "manager_id" + }, + "request_decision": { + "type": "boolean", + "title": "Request decision", + "description": "Decision to the request for time off", + "is_required": true, + "name": "request_decision" + }, + "submitter_id": { + "type": "slack#/types/user_id", + "title": "Submitting user", + "description": "User that submitted the request", + "is_required": true, + "name": "submitter_id" + } + } + } + } +``` + +Next, we'll define a function listener to handle what happens when the workflow step is used. + +## Adding function and action listeners {#adding-listeners} + +### Implementing the function listener {#function-listener} + +Direct your attention back to your app project in VSCode or your preferred code editor. Here we'll add logic that your app will execute when the custom step is executed. + +Open your `app.py` file and add the following function listener code for the `request_time_off` step. + +```py +@app.function("request_time_off") +def handle_request_time_off(inputs: dict, fail: Fail, logger: logging.Logger, say: Say): + + submitter_id = inputs["submitter_id"] + manager_id = inputs["manager_id"] + + try: + say( + channel=manager_id, + text=f"<@{submitter_id}> requested time off! What say you?", + blocks=[ + { + "type": 'section', + "text": { + "type": 'mrkdwn', + "text": f"<@{submitter_id}> requested time off! What say you?", + }, + }, + { + 'type': 'actions', + 'elements': [ + { + 'type': 'button', + 'text': { + 'type': 'plain_text', + 'text': 'Approve', + 'emoji': True, + }, + 'value': 'approve', + 'action_id': 'approve_button', + }, + { + 'type': 'button', + 'text': { + 'type': 'plain_text', + 'text': 'Deny', + 'emoji': True, + }, + 'value': 'deny', + 'action_id': 'deny_button', + }, + ], + }, + ], + ) + except Exception as e: + logger.exception(e) + fail(f"Failed to handle a function request (error: {e})") +``` + +#### Anatomy of a `.function()` listener {#function-listener-anatomy} + +The function decorator (`function()`) accepts an argument of type `str` and is the unique callback ID of the step. For our custom step, we’re using `request_time_off`. Every custom step you implement in an app needs to have a unique callback ID. + +The callback function is where we define the logic that will run when Slack tells the app that a user in the Slack client started a workflow that contains the `request_time_off` custom step. + +The callback function offers various utilities that can be used to take action when a function execution event is received. The ones we’ll be using here are: + +* `inputs` provides access to the workflow variables passed into the step when the workflow was started +* `fail` indicates when the step invoked for the current workflow step has an error +* `logger` provides a Python standard logger instance +* `say` calls the `chat.Postmessage` API method + +### Implementing the action listener {#action-listener} + +This custom step also requires an action listener to respond to the action of a user clicking a button. + +In that same `app.py` file, add the following action listener: + +```py +@app.action(re.compile("(approve_button|deny_button)")) +def manager_resp_handler(ack: Ack, action, body: dict, client: WebClient, complete: Complete, fail: Fail, logger: logging.Logger): + + ack() + + try: + inputs = body['function_data']['inputs'] + manager_id = inputs['manager_id'] + submitter_id = inputs['submitter_id'] + request_decision = action['value'] + + client.chat_update( + channel=body['channel']['id'], + message=body['message'], + ts=body["message"]["ts"], + text=f'Request {"approved" if request_decision == 'approve' else "denied"}!' + ) + + complete({ + 'manager_id': manager_id, + 'submitter_id': submitter_id, + 'request_decision': request_decision == 'approve' + }) + + except Exception as e: + logger.exception(e) + fail(f"Failed to handle a function request (error: {e})") +``` + +#### Anatomy of an `.action()` listener {#action-listener-anatomy} + +Similar to a function listener, the action listener registration method (`.action()`) takes two arguments: + +- The first argument is the unique callback ID of the action that your app will respond to. In our case, because we want to execute the same logic for both buttons, we’re using a little bit of RegEx magic to listen for two callback IDs at the same time — `approve_button` and `deny_button`. +- The second argument is an asynchronous callback function, where we define the logic that will run when Slack tells our app that the manager has clicked or tapped the Approve button or the Deny button. + +Just like the function listener’s callback function, the action listener’s callback function offers various utilities that can be used to take action when an action event is received. The ones we’ll be using here are: +- `client`, which provides access to Slack API methods +- `action`, which provides the action’s event payload +- `complete`, which is a utility method indicating to Slack that the step behind the workflow step that was just invoked has completed successfully +- `fail`, which is a utility method for indicating that the step invoked for the current workflow step had an error + +Slack will send an action event payload to your app when one of the buttons is clicked or tapped. In the action listener, we’ll extract all the information we can use, and if all goes well, let Slack know the step was successful by invoking complete. We’ll also handle cases where something goes wrong and produces an error. + +Now that the custom step has been added to the app and we've defined step and action listeners for it, we're ready to see the step in action in Workflow Builder. Go ahead and run your app to pick up the changes. + +### Creating a workflow with the new step {#add-new-step} + +Turn your attention to the Slack client where your app is installed. + +Open Workflow Builder by clicking on the workspace name, then **Tools**, then **Workflow Builder**. + +Click the button to create a **New Workflow**, then **Build Workflow**. Choose to start your workflow **from a link in Slack**. + +In the **Steps** pane to the right, search for your app name and locate the **Request time off** step we created. + +![Find step](/img/tutorials/custom-steps-wfb-existing/find-step.png) + +Select the step and choose the desired inputs and click **Save**. + +![Step inputs](/img/tutorials/custom-steps-wfb-existing/step-inputs.png) + +Next, click **Finish Up**, give your workflow a name and description, then click **Publish**. Copy the link for your workflow on the next screen, then click **Done**. + +### Running the workflow {#run-workflow} + +In any channel where your app is installed, paste the link you copied and send it as a message. The link will unfurl into a button to start the workflow. Click the button to start the workflow. If you set yourself up as the manager, you will then see a message from your app. Pressing either button will return a confirmation or denial of your time off request. + +![Message](/img/tutorials/custom-steps-wfb-existing/app-message.png) + +## Next steps {#next-steps} + +Nice work! Now that you've added a workflow step to your Bolt app, a world of possibilities is open to you! Create and share workflow steps across your organization to optimize Slack users' time and make their working lives more productive. + +If you're looking to create a brand new Bolt app with custom workflow steps, check out [the tutorial here](/tutorial/custom-steps-workflow-builder-new). + +If you're interested in exploring how to create custom steps to use in Workflow Builder as steps with our Deno Slack SDK, too, that tutorial can be found [here](https://tools.slack.dev/deno-slack-sdk/tutorials/workflow-builder-custom-step/). diff --git a/docs/content/tutorial/custom-steps-workflow-builder-new.md b/docs/content/tutorial/custom-steps-workflow-builder-new.md new file mode 100644 index 000000000..9d01b8676 --- /dev/null +++ b/docs/content/tutorial/custom-steps-workflow-builder-new.md @@ -0,0 +1,359 @@ +--- +title: Custom Steps for Workflow Builder (new app) +--- + +:::info[This feature requires a paid plan] +If you don't have a paid workspace for development, you can join the [Developer Program](https://api.slack.com/developer-program) and provision a sandbox with access to all Slack features for free. +::: + +Adding a workflow step to your app and implementing a corresponding function listener is how you define a custom Workflow Builder step. In this tutorial, you'll use [Bolt for Python](/bolt-python/) to add your workflow step, then wire it up in [Workflow Builder](https://slack.com/help/articles/360035692513-Guide-to-Workflow-Builder). + +When finished, you'll be ready to build scalable and innovative workflow steps for anyone using Workflow Builder in your workspace. + +## What are we building? {#what-are-we-building} + +In this tutorial, you'll be wiring up a sample app with a sample step and corresponding function listener to be used as a workflow step in Workflow Builder. Here's how it works: + +* When someone starts the workflow, Slack will notify your app that your custom step was invoked as part of a workflow. +* Your app will send a message to the requestor, along with a button to complete the step. +* When the user clicks or taps the button, Slack will let your app know, and your app will respond by changing the message. + +:::info[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 [Bolt Python custom step sample](https://github.com/slack-samples/bolt-python-custom-step-template) as a template. The sample custom step provided in the template will be a good place to start exploring! +::: + +## Prerequisites {#prereqs} + +Before we begin, let's make sure you're set up for success. Ensure you have a development workspace where you have permission to install apps. We recommend setting up your own space used for exploration and testing in a [developer sandbox](https://api.slack.com/developer-program). + +## Cloning the sample project {#clone} + +For this tutorial, We'll use `boltstep` as the app name. For your app, be sure to use a unique name that will be easy for you to find: then, use that name wherever you see `boltstep` in this tutorial. The app will be named "Bolt Custom Step", as that is defined in the `manifest.json` file of the sample app code. + +Let's start by opening a terminal and cloning the starter template repository: + +```sh +git clone https://github.com/slack-samples/bolt-python-custom-step-template boltstep +``` + +Once the terminal is finished cloning the template, change directories into your newly prepared app project: + +```sh +cd boltstep +``` + +If you're using VSCode (highly recommended), you can enter `code .` from your project's directory and VSCode will open your new project. + +You can also open a terminal window from inside VSCode like this: `Ctrl` + `~` + +Once in VSCode, open the terminal. Let's install our package dependencies: run the following command(s) in the terminal inside VSCode: + +```sh +npm install +``` + +We now have a Bolt app ready for development! Open the `manifest.json` file and copy its contents; you'll need this in the next step. + +## Creating your app from a manifest {#create-app} + +Open a browser and navigate to [your apps page](https://api.slack.com/apps). This is where we will create a new app with our previously copied manifest details. Click the **Create New App** button, then select **From an app manifest** when prompted to choose how you'd like to configure your app's settings. + +![Create app from manifest](/img/tutorials/custom-steps-wfb-new/manifest.png) + +Next, select a workspace where you have permissions to install apps, and click **Next**. Select the **JSON** tab and clear the existing contents. Paste the contents of the `manifest.json` file you previously copied. + +Click **Next** again. You will be shown a brief overview of the features your app includes. You'll see we are creating an app with a `chat:write` bot scope, an App Home and Bot User, as well as Socket Mode, Interactivity, an Event Subscription, and Org Deploy. We'll get into these details later. Click **Create**. + +### App settings {#app-settings} + +All of your app's settings can be configured within these screens. By creating an app from an existing manifest, you will notice many settings have already been configured. Navigate to **Org Level Apps** and notice that we've already opted in. This is a requirement for adding workflow steps to an app. + +Navigate to **Event Subscriptions** and expand **Subscribe to bot events** to see that we have subscribed to the `function_executed` event. This is also a requirement for adding workflow steps to our app, as it lets our app know when a step has been triggered, allowing our app to respond to it. + +Another configuration setting to note is **Socket Mode**. We have turned this on for our local development, but socket mode is not intended for use in a production environment. When you are satisfied with your app and ready to deploy it to a production environment, you should switch to using public HTTP request URLs. Read more about getting started with HTTP in [Bolt for Python here](/bolt-python/getting-started). + +Clicking on **Workflow Steps** in the left nav will show you that one workflow step has been added! This reflects the `function` defined in our manifest: functions are workflow steps. We will get to this step's implementation later. + +![Workflow step](/img/tutorials/custom-steps-wfb-new/workflow-step.png) + +### Tokens {#tokens} + +In order to connect our app here with the logic of our sample code set up locally, we need to obtain two tokens, a bot token and an app token. + +* **Bot tokens** are associated with bot users, and are only granted once in a workspace where someone installs the app. The bot token your app uses will be the same no matter which user performed the installation. Bot tokens are the token type that most apps use. +* **App-level** tokens represent your app across organizations, including installations by all individual users on all workspaces in a given organization and are commonly used for creating websocket connections to your app. + +To generate an app token, navigate to **Basic Information** and scroll down to **App-Level Token**. + +![App token](/img/tutorials/custom-steps-wfb-new/app-token.png) + +Click **Generate Token and Scopes**, then **Add Scope** and choose `connections:write`. Choose a name for your token and click **Generate**. Copy that value, save it somewhere accessible, and click **Done** to close out of the modal. + +Next up is the bot token. We can only get this token by installing the app into the workspace. Navigate to **Install App** and click the button to install, choosing **Allow** at the next screen. + +![Install app](/img/tutorials/custom-steps-wfb-new/install.png) + +You will then have a bot token. Again, copy that value and save it somewhere accessible. + +![Bot token](/img/tutorials/custom-steps-wfb-new/bot-token.png) + +💡 Treat your tokens like passwords and keep them safe. Your app uses them to post and retrieve information from Slack workspaces. Minimally, do NOT commit them to version control. + +## Starting your local development server {#local} + +While building your app, you can see your changes appear in your workspace in real-time with `npm start`. Soon we'll start our local development server and see what our sample code is all about! But first, we need to store those tokens we gathered as environment variables. + +Navigate back to VSCode. Rename the `.env.sample` file to `.env`. Open this file and update `SLACK_APP_TOKEN` and `SLACK_BOT_TOKEN` with the values you previously saved. It will look like this, with your actual token values where you see `` and ``: + +```sh +SLACK_APP_TOKEN= +SLACK_BOT_TOKEN= +``` + +Now save the file and try starting your app: + +```sh +npm start +``` + +You'll know the local development server is up and running successfully when it emits a bunch of `[DEBUG]` statements to your terminal, the last one containing `connected:ready`. + +With your development server running, continue to the next step. + +:::info +If you need to stop running the local development server, press `` + `c` to end the process. +::: + +## Wiring up the sample step in Workflow Builder {#wfb} + +The starter project you cloned contains a sample custom step lovingly titled “Sample step". Let’s see how a custom step defined in Bolt appears in Workflow Builder. + +In the Slack Client of your development workspace, open Workflow Builder by clicking on the workspace name, **Tools**, then **Workflow Builder**. Create a new workflow, then select **Build Workflow**: + +![Creating a new workflow](/img/tutorials/custom-steps-wfb-new/wfb-1.png) + +Select **Choose an event** under **Start the workflow...**, then **From a link in Slack** to configure this workflow to start when someone clicks its shortcut link: + +![Starting a new workflow from a shortcut link](/img/tutorials/custom-steps-wfb-new/wfb-2.png) + +Click the **Continue** button to confirm that this is workflow should start with a shortcut link: + +![Confirming a new shortcut workflow setup](/img/tutorials/custom-steps-wfb-new/wfb-3.png) + +Find the sample step provided in the template by either searching for the name of your app (e.g., `Bolt Custom Step`) or the name of your step (e.g. `Sample step`) in the Steps search bar. + +If you search by app name, any custom step that your app has defined will be listed. + +Add the “Sample step" in the search results to the workflow: + +![Adding the sample step to the workflow](/img/tutorials/custom-steps-wfb-new/wfb-4.png) + +As soon as you add the “Sample step" to the workflow, a modal will appear to configure the step's input—in this case, a user variable: + +![Configuring the sample step's inputs](/img/tutorials/custom-steps-wfb-new/wfb-5.png) + +Configure the user input to be “Person who used this workflow”, then click the **Save** button: + +![Saving the sample step after configuring the user input](/img/tutorials/custom-steps-wfb-new/wfb-6.png) + +Click the **Finish Up** button, then provide a name and description for your workflow. + +Finally, click the **Publish** button: + +![Publishing a workflow](/img/tutorials/custom-steps-wfb-new/wfb-7.png) + +Copy the shortcut link, then exit Workflow Builder and paste the link to a message in any channel you’re in: + +![Copying a workflow link](/img/tutorials/custom-steps-wfb-new/wfb-8.png) + +After you send a message containing the shortcut link, the link will unfurl and you’ll see a **Start Workflow** button. + +Click the **Start Workflow** button: + +![Starting your new workflow](/img/tutorials/custom-steps-wfb-new/wfb-9.png) + +You should see a new direct message from your app: + +![A new direct message from your app](/img/tutorials/custom-steps-wfb-new/wfb-10.png) + +The message from your app asks you to click the **Complete step** button: + +![A new direct message from your app](/img/tutorials/custom-steps-wfb-new/wfb-11.png) + +Once you click the button, the direct message to you will be updated to let you know that the step interaction was successfully completed: + +![Sample step finished successfully](/img/tutorials/custom-steps-wfb-new/wfb-12.png) + +Now that we’ve gotten a feel for how we will use the custom step, let’s learn more about how function listeners work. + +## Discovering listeners {#listeners} + +Now that we’ve seen how custom steps are used in Workflow Builder, let’s understand how the function listener code works to respond to an event when the step is triggered. + +We’ll first review the step definition in the `manifest.json`, then we’ll look at the two listener functions in our app code: one to let us know when the step starts, and one to let us know when someone clicks or taps one of the buttons we sent over. + +### Defining the custom step {#define-custom-step} + +Opening the `manifest.json` file included in the sample app shows a `functions` property that includes a definition for our `sample_step`: + +```json +// manifest.json +... + "functions": { + "sample_step": { + "title": "Sample step", + "description": "Runs sample step", + "input_parameters": { + "user_id": { + "type": "slack#/types/user_id", + "title": "User", + "description": "Message recipient", + "is_required": true, + "hint": "Select a user in the workspace", + "name": "user_id" + } + }, + "output_parameters": { + "user_id": { + "type": "slack#/types/user_id", + "title": "User", + "description": "User that completed the step", + "is_required": true, + "name": "user_id" + } + } + } + } +``` + +From the step definition, we can see an input parameter and an output parameter defined. + +### Inputs and outputs {#inputs-outputs} + +The custom step will take the following input: Message recipient (as a Slack User ID). + +The custom step will produce the following output: The user that completed the step. + +* When the step is invoked, a message will be sent to the user who invoked the workflow with a button to complete the step. +* When the button is clicked, a message is posted indicating the step's completion. + +### Implementing the function listener {#function-listener} + +The first thing we’ll do when adding a custom workflow step to our Bolt app is register a new **function listener**. In Bolt, a function listener allows developers to execute custom code in response to specific Slack events or actions by registering a method that handles predefined requests or commands. We register a function listener via the `function` method provided by our app instance. + +1. Open your project’s `app.py` file in your code editor. +2. Between the initialization code for the app instance and the `sample_step` registration, you'll see a listener defined for our custom step: + +```py +# app.py +... +@app.function("sample_step") +def handle_sample_step_event(inputs: dict, say: Say, fail: Fail, logger: logging.Logger): + user_id = inputs["user_id"] + + try: + say( + channel=user_id, # sending a DM to this user + text="Click the button to signal the step has completed", + blocks=[ + { + "type": "section", + "text": {"type": "mrkdwn", "text": "Click the button to signal the step has completed"}, + "accessory": { + "type": "button", + "text": {"type": "plain_text", "text": "Complete step"}, + "action_id": "sample_click", + }, + } + ], + ) + except Exception as e: + logger.exception(e) + fail(f"Failed to handle a step request (error: {e})") + + +``` + +#### Anatomy of a `.function()` listener {#function-listener-anatomy} + +The function decorator (`function()`) accepts an argument of type `str` and is the unique callback ID of the step. For our custom step, we’re using `sample_step`. Every custom step you implement in an app needs to have a unique callback ID. + +The callback function is where we define the logic that will run when Slack tells the app that a user in the Slack client started a workflow that contains the `sample_step` custom step. + +The callback function offers various utilities that can be used to take action when a step execution event is received. The ones we’ll be using here are: + +* `inputs` provides access to the workflow variables passed into the step when the workflow was started +* `fail` indicates when the step invoked for the current workflow step has an error +* `logger` provides a Python standard logger instance +* `say` calls the `chat.Postmessage` API method + +#### Understanding the function listener's callback logic {#function-listener-callback-logic} + +When our step is executed, we want a message to be sent to the invoking user. That message should include a button that prompts the user to complete the step. + +When Slack tells your Bolt app that the `sample_step` step was invoked, this step uses `chat.postMessage` to send a message to the `user_id` channel (which means this will be sent as a DM to the Slack user whose ID == `user_id`) with some text and blocks. The Block Kit element being sent as part of the message is a button, labeled 'Complete step' (which sends the `sample_click` action ID). + +Once the message is sent, your Bolt app will wait until the user has clicked the button. As soon as they click or tap the button, Slack will send back the action ID associated with the button to your Bolt app. + +In order for your Bolt app to listen for these actions, we’ll now define an action listener. + +### Implementing the action listener {#action-listener} + +The message we send to the user will include the button prompting them to complete the step. + +To listen for and respond to this button click, you'll see an `.action()` listener to `app.py`, right after the function listener definition: + +```py +# app.py +... +@app.action("sample_click") +def handle_sample_click( + ack: Ack, body: dict, context: BoltContext, client: WebClient, complete: Complete, fail: Fail, logger: logging.Logger +): + ack() + + try: + # Since the button no longer works, we should remove it + client.chat_update( + channel=context.channel_id, + ts=body["message"]["ts"], + text="Congrats! You clicked the button", + ) + + # Signal that the step completed successfully + complete({"user_id": context.actor_user_id}) + except Exception as e: + logger.exception(e) + fail(f"Failed to handle a step request (error: {e})") + +``` + +#### Anatomy of an `.action()` listener {#action-listener-anatomy} + +Similar to a function listener, the action listener registration method (`.action()`) takes two arguments: + +* The first argument is the unique callback ID of the action that your app will respond to. +* The second argument is an asynchronous callback function, where we define the logic that will run when Slack tells our app that the user has clicked or tapped the button. + +Just like the function listener’s callback function, the action listener’s callback function offers various utilities that can be used to take action when an action event is received. The ones we’ll be using here are: + +* `client`, which provides access to Slack API methods +* `action`, which provides the action’s event payload +* `complete`, which is a utility method indicating to Slack that the step behind the workflow step that was just invoked has completed successfully +* `fail`, which is a utility method for indicating that the step invoked for the current workflow step had an error + +#### Understanding the action listener's callback logic {#action-listener-callback-logic} + +Recall that we sent over a message with the button back in the function listener. + +When the button is pressed, we want to complete the step, update the message, and define `outputs` that can be used for subsequent steps in Workflow Builder. + +Slack will send an action event payload to your app when the button is clicked or tapped. In the action listener, we extract all the information we can use, and if all goes well, let Slack know the step was successful by invoking `complete`. We also handle cases where something goes wrong and produces an error. + +## Next steps {#next-steps} + +That's it — we hope you learned a lot! + +In this tutorial, we added custom steps via the manifest, but if you'd like to see how to add custom steps in the [app settings](https://api.slack.com/apps) to an existing app, follow along with the [Create a custom step for Workflow Builder: existing Bolt app](/tutorials/custom-steps-workflow-builder-existing) tutorial. + +If you're interested in exploring how to create custom steps to use in Workflow Builder as steps with our Deno Slack SDK, too, that tutorial can be found [here](https://tools.slack.dev/deno-slack-sdk/tutorials/workflow-builder-custom-step/). diff --git a/docs/content/tutorial/custom-steps.md b/docs/content/tutorial/custom-steps.md new file mode 100644 index 000000000..79b02089b --- /dev/null +++ b/docs/content/tutorial/custom-steps.md @@ -0,0 +1,272 @@ +--- +title: Custom Steps +--- + +:::info[This feature requires a paid plan] +If you don't have a paid workspace for development, you can join the [Developer Program](https://api.slack.com/developer-program) and provision a sandbox with access to all Slack features for free. +::: + + +With custom steps for Bolt apps, your app can create and process workflow steps that users later add in Workflow Builder. This guide goes through how to build a custom step for your app using the [app settings](https://api.slack.com/apps). + +If you're looking to build a custom step using the Deno Slack SDK, check out our guide on [creating a custom step for Workflow Builder with the Deno Slack SDK](https://tools.slack.dev/deno-slack-sdk/tutorials/workflow-builder-custom-step/). + +You can also take a look at the template for the [Bolt for Python custom workflow step](https://github.com/slack-samples/bolt-python-custom-step-template) on GitHub. + +There are two components of a custom step: the step definition in the app manifest, and a listener to handle the `function_executed` event in your project code. + +## Opt in to org-ready apps {#org-ready-apps} + +Before we create the step definition, we first need to opt in to organization-ready apps. The app must opt-in to org-ready apps to be able to add the custom step to its manifest. This can be done in one of two ways: + +- Set the manifest `settings.org_deploy_enabled` property to `true`. +- Alternatively, navigate to your [apps](https://api.slack.com/apps), select your app, then under the **Features** section in the navigation, select **Org Level Apps** and then **Opt-In**. + +Whichever method you use, the following will be reflected in the app manifest as such: + +```json + "settings": { + "org_deploy_enabled": true, + ... + } +``` + +Next, the app must be installed at the organization level. While it is possible to install the app at a workspace level, doing so means that the custom steps will not appear in Workflow Builder. To remedy this, install the app at the organization level. + +If you are a developer who is not an admin of their organization, you will need to request an Org Admin to perform this installation at the organization level. To do this: + +- Navigate to your [apps](https://api.slack.com/apps) page and select the app you'd like to install. +- Under **Settings**, select **Collaborators**. +- Add an Org Admin as a collaborator. + +The Org Admin can then install your app directly at the org level from the [app settings](https://api.slack.com/apps) page. + +## Defining the custom step {#define-step} + +A workflow step's definition contains information about the step, including its `input_parameters`, `output_parameters`, as well as display information. + +Each step is defined in the `functions` object of the manifest. Each entry in the `functions` object is a key-value pair representing each step. The key is the step's `callback_id`, which is any string you wish to use to identify the step (max 100 characters), and the value contains the details listed in the table below for each separate custom step. We recommend using the step's name, like `sample_step` in the code example below for the step's `callback_id`. + +Field | Type | Description | Required? +---- | ----- | ------------|---------- +`title` | String | A string to identify the step. Max 255 characters. | Yes +`description` | String | A succinct summary of what your step does. | No +`input_parameters` | Object | An object which describes one or more [input parameters](#inputs-outputs) that will be available to your step. Each top-level property of this object defines the name of one input parameter available to your step.| No +`output_parameters` | Object | An object which describes one or more [output parameters](#inputs-outputs) that will be returned by your step. Each top-level property of this object defines the name of one output parameter your step makes available. | No + +Once you are in your [app settings](https://api.slack.com/apps), navigate to **Workflow Steps** in the left nav. Click **Add Step** and fill out your step details, including callback ID, name, description, input parameters, and output parameters. + +### Defining input and output parameters {#inputs-outputs} + +Step inputs and outputs (`input_parameters` and `output_parameters`) define what information goes into a step before it runs and what comes out of a step after it completes, respectively. + +Both inputs and outputs adhere to the same schema and consist of a unique identifier and an object that describes the input or output. + +Each input or output that belongs to `input_parameters` or `output_parameters` must have a unique key. + +Field | Type | Description +------|------|------------- +`type` | String | Defines the data type and can fall into one of two categories: primitives or Slack-specific. +`title` | String | The label that appears in Workflow Builder when a user sets up this step in their workflow. +`description` | String | The description that accompanies the input when a user sets up this step in their workflow. +`dynamic_options` | Object | For custom steps dynamic options in Workflow Builder, define this property and point to a custom step designed to return the set of dynamic elements once the step is added to a workflow within Workflow Builder. Dynamic options in Workflow Builder can be rendered in one of two ways: as a drop-down menu (single-select or multi-select), or as a set of fields. Refer to [custom steps dynamic options in Workflow Builder](/automation/runonslack/custom-steps-dynamic-options) for more details. +`is_required` | Boolean | Indicates whether or not the input is required by the step in order to run. If it’s required and not provided, the user will not be able to save the configuration nor use the step in their workflow. This property is available only in v1 of the manifest. We recommend v2, using the `required` array as noted in the example above. +`hint` | String | Helper text that appears below the input when a user sets up this step in their workflow. + +In addition, the `dynamic_options` field has two required properties: + +Property | Type | Description +------|------|------------- +`function` | String | A reference to the custom step that should be used as a dynamic option. +`inputs` | Object | Maps the inputs from the custom step consuming the dynamic option to the inputs required by the step used as a dynamic option. + +For example: + +``` +"inputs": { + "category": { + "value": "{{input_parameters.category}}" + } +} +``` + +Once you've added your step details, save your changes, then navigate to **App Manifest**. Notice your new step configuration reflected in the `function` property! + +#### Sample manifest {#sample-manifest} + +Here is a sample app manifest laying out a step definition. This definition tells Slack that the step in our workspace with the callback ID of `sample_step` belongs to our app, and that when it runs, we want to receive information about its execution event. + +```json +"functions": { + "sample_step": { + "title": "Sample step", + "description": "Runs sample step", + "input_parameters": { + "properties": { + "user_id": { + "type": "slack#/types/user_id", + "title": "User", + "description": "Message recipient", + "hint": "Select a user in the workspace", + "name": "user_id" + } + }, + "required": { + "user_id" + } + }, + "output_parameters": { + "properties": { + "user_id": { + "type": "slack#/types/user_id", + "title": "User", + "description": "User that received the message", + "name": "user_id" + } + }, + "required": { + "user_id" + } + }, + } +} +``` + +### Adding steps for existing apps {#existing-apps} + +If you are adding custom steps to an existing app directly to the app manifest, you will also need to add the `function_runtime` property to the app manifest. Do this in the `settings` section as such: + +```json +"settings": { + ... + "function_runtime": "remote" +} +``` + +If you are adding custom steps in the **Workflow Steps** section of the [App Config](https://api.slack.com/apps) as shown above, then this will be added automatically. + +## Listening to function executions {#listener} + +When your custom step is executed in a workflow, your app will receive a `function_executed` event. The callback provided to the `function()` method will be run when this event is received. + +The callback is where you can access `inputs`, make third-party API calls, save information to a database, update the user’s Home tab, or set the output values that will be available to subsequent workflow steps by mapping values to the `outputs` object. + +Your app must call `complete()` to indicate that the step’s execution was successful, or `fail()` to signal that the step failed to complete. + +Notice in the example code here that the name of the step, `sample_step`, is the same as it is listed in the manifest above. This is required. + +```py +@app.function("sample_step") +def handle_sample_step_event(inputs: dict, fail: Fail, complete: Complete,logger: logging.Logger): + user_id = inputs["user_id"] + try: + client.chat_postMessage( + channel=user_id, + text=f"Greetings <@{user_id}>!" + ) + complete({"user_id": user_id}) + except Exception as e: + logger.exception(e) + fail(f"Failed to complete the step: {e}") + +``` + +Here's another example. Note in this snippet, the name of the step, `create_issue`, must be listed the same as it is listed in the manifest file. + +```py +@app.function("create_issue") +def create_issue_callback(ack: Ack, inputs: dict, fail: Fail, complete: Complete, logger: logging.Logger): + ack() + JIRA_BASE_URL = os.getenv("JIRA_BASE_URL") + + headers = { + "Authorization": f'Bearer {os.getenv("JIRA_SERVICE_TOKEN")}', + "Accept": "application/json", + "Content-Type": "application/json", + } + + try: + project: str = inputs["project"] + issue_type: str = inputs["issuetype"] + + url = f"{JIRA_BASE_URL}/rest/api/latest/issue" + + payload = json.dumps( + { + "fields": { + "description": inputs["description"], + "issuetype": {"id" if issue_type.isdigit() else "name": issue_type}, + "project": {"id" if project.isdigit() else "key": project}, + "summary": inputs["summary"], + }, + } + ) + + response = requests.post(url, data=payload, headers=headers) + + response.raise_for_status() + json_data = json.loads(response.text) + complete(outputs={ + "issue_id": json_data["id"], + "issue_key": json_data["key"], + "issue_url": f'https://{JIRA_BASE_URL}/browse/{json_data["key"]}' + }) + except Exception as e: + logger.exception(e) + fail(f"Failed to handle a step request (error: {e})") + +``` + +### Anatomy of a function listener {#anatomy} + +The first argument (in our case above, `sample_step`) is the unique callback ID of the step. After receiving an event from Slack, this identifier is how your app knows which custom step handler to invoke. This `callback_id` also corresponds to the step definition provided in your manifest file. + +The second argument is the callback function, or the logic that will run when your app receives notice from Slack that `sample_step` was run by a user—in the Slack client—as part of a workflow. + +Field | Description +------|------------ +`client` | A `WebClient` instance used to make things happen in Slack. From sending messages to opening modals, `client` makes it all happen. For a full list of available methods, refer to the [Web API methods](/methods). Read more about the `WebClient` for Bolt Python [here](https://tools.slack.dev/bolt-python/concepts/web-api/). +`complete` | A utility method that invokes `functions.completeSuccess`. This method indicates to Slack that a step has completed successfully without issue. When called, `complete` requires you include an `outputs` object that matches your step definition in [`output_parameters`](#inputs-outputs). +`fail` | A utility method that invokes `functions.completeError`. True to its name, this method signals to Slack that a step has failed to complete. The `fail` method requires an argument of `error` to be sent along with it, which is used to help users understand what went wrong. +`inputs` | An alias for the `input_parameters` that were provided to the step upon execution. + +## Responding to interactivity {#interactivity} + +Interactive elements provided to the user from within the `function()` method’s callback are associated with that unique `function_executed` event. This association allows for the completion of steps at a later time, like once the user has clicked a button. + +Incoming actions that are associated with a step have the same `inputs`, `complete`, and `fail` utilities as offered by the `function()` method. + +```py +# If associated with a step, step-specific utilities are made available +@app.action("sample_click") +def handle_sample_click(context: BoltContext, complete: Complete, fail: Fail, logger: logging.Logger): + try: + # Signal the step has completed once the button is clicked + complete({"user_id": context.actor_user_id}) + except Exception as e: + logger.exception(e) + fail(f"Failed to handle a step request (error: {e})") + +``` + +## Deploying a custom step {#deploy} + +When you're ready to deploy your steps for wider use, you'll need to decide *where* to deploy, since Bolt apps are not hosted on the Slack infrastructure. + +### Control step access {#access} + +You can choose who has access to your custom steps. To define this, refer to the [custom function access](/automation/functions/access) page. + +### Distribution {#distribution} + +Distribution works differently for Slack apps that contain custom steps when the app is within a standalone (non-Enterprise Grid) workspace versus within an Enterprise Grid organization. + +* **Within a standalone workspace**: Slack apps that contain custom steps can be installed on the same workspace and used within that workspace. We do not support distribution to other standalone workspaces (also known as public distribution). +* **Within an organization**: Slack apps that contain custom steps should be org-ready (enabled for private distribution) and installed on the organization level. They must also be granted access to at least one workspace in the organization for the steps to appear in Workflow Builder. + +Apps containing custom steps cannot be distributed publicly or submitted to the Slack Marketplace. We recommend sharing your code as a public repository in order to share custom steps in Bolt apps. + +## Related tutorials {#tutorials} + +* [Custom steps for Workflow Builder (new app)](/tutorial/custom-steps-WB-new) +* [Custom steps for Workflow Builder (existing app)](/tutorial/custom-steps-WB-existing) diff --git a/docs/content/tutorial/modals.md b/docs/content/tutorial/modals.md index 68875f97b..07a9d394b 100644 --- a/docs/content/tutorial/modals.md +++ b/docs/content/tutorial/modals.md @@ -14,7 +14,7 @@ Let's take a look at the technologies we'll use in this tutorial: ## Final product overview {#final_product} If you follow through with the extra credit tasks, your final app will look like this: -![Final product](/img/modals/final_product.gif) +![Final product](/img/tutorials/modals/final_product.gif) --- @@ -65,21 +65,21 @@ Here's a copy of what the modal payload looks like — this is what powers t 3. Find the base path to your server by clicking **Share**, then copy the Live site link. - ![Get the base link](/img/modals/base_link.gif) + ![Get the base link](/img/tutorials/modals/base_link.gif) 4. On your app page, navigate to **Interactivity & Shortcuts**. Append "/slack/events" to your base path URL and enter it into the **Request URL** e.g., `https://festive-harmonious-march.glitch.me/slack/events`. This allows your server to retrieve information from the modal. You can see the code for this within the Glitch project. - ![Interactivity URL](/img/modals/interactivity_url.png) + ![Interactivity URL](/img/tutorials/modals/interactivity_url.png) 5. Create the slash command so you can access it within Slack. Navigate to the **Slash Commands** section and create a new command. Note the **Request URL** is the same link as above, e.g. `https://festive-harmonious-march.glitch.me/slack/events` . The code that powers the slash command and opens a modal can be found within the Glitch project. - ![Slash command details](/img/modals/slash_command.png) + ![Slash command details](/img/tutorials/modals/slash_command.png) 6. Select **Install App**. After you've done this, you'll see a **Bot User OAuth Access Token**, copy this. 7. Navigate to your Glitch project and click the `.env` file where the credentials are stored, and paste your bot token where the `SLACK_BOT_TOKEN` variable is shown. This allows your server to send authenticated requests to the Slack API. You'll also need to head to your app's settings page under **Basic Information** and copy the _Signing secret_ to place into the `SLACK_SIGNING_SECRET` variable. - ![Environment variables](/img/modals/heart_icon.gif) + ![Environment variables](/img/tutorials/modals/heart_icon.gif) 8. Test by heading to Slack and typing `/thankyou`. diff --git a/docs/sidebars.js b/docs/sidebars.js index c0e4f936d..cf35d83d5 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -86,7 +86,7 @@ const sidebars = { { type: "category", label: "Tutorials", - items: ["tutorial/ai-chatbot", "tutorial/custom-steps-for-jira", "tutorial/modals"], + items: ["tutorial/ai-chatbot", "tutorial/custom-steps", "tutorial/custom-steps-for-jira", "tutorial/custom-steps-workflow-builder-new", "tutorial/custom-steps-workflow-builder-existing", "tutorial/modals"], }, { type: "html", value: "
" }, { diff --git a/docs/static/img/ai-chatbot/1.png b/docs/static/img/tutorials/ai-chatbot/1.png similarity index 100% rename from docs/static/img/ai-chatbot/1.png rename to docs/static/img/tutorials/ai-chatbot/1.png diff --git a/docs/static/img/ai-chatbot/2.png b/docs/static/img/tutorials/ai-chatbot/2.png similarity index 100% rename from docs/static/img/ai-chatbot/2.png rename to docs/static/img/tutorials/ai-chatbot/2.png diff --git a/docs/static/img/ai-chatbot/3.png b/docs/static/img/tutorials/ai-chatbot/3.png similarity index 100% rename from docs/static/img/ai-chatbot/3.png rename to docs/static/img/tutorials/ai-chatbot/3.png diff --git a/docs/static/img/ai-chatbot/4.png b/docs/static/img/tutorials/ai-chatbot/4.png similarity index 100% rename from docs/static/img/ai-chatbot/4.png rename to docs/static/img/tutorials/ai-chatbot/4.png diff --git a/docs/static/img/ai-chatbot/5.png b/docs/static/img/tutorials/ai-chatbot/5.png similarity index 100% rename from docs/static/img/ai-chatbot/5.png rename to docs/static/img/tutorials/ai-chatbot/5.png diff --git a/docs/static/img/ai-chatbot/6.png b/docs/static/img/tutorials/ai-chatbot/6.png similarity index 100% rename from docs/static/img/ai-chatbot/6.png rename to docs/static/img/tutorials/ai-chatbot/6.png diff --git a/docs/static/img/ai-chatbot/7.png b/docs/static/img/tutorials/ai-chatbot/7.png similarity index 100% rename from docs/static/img/ai-chatbot/7.png rename to docs/static/img/tutorials/ai-chatbot/7.png diff --git a/docs/static/img/ai-chatbot/8.png b/docs/static/img/tutorials/ai-chatbot/8.png similarity index 100% rename from docs/static/img/ai-chatbot/8.png rename to docs/static/img/tutorials/ai-chatbot/8.png diff --git a/docs/static/img/custom-steps-jira/1.png b/docs/static/img/tutorials/custom-steps-jira/1.png similarity index 100% rename from docs/static/img/custom-steps-jira/1.png rename to docs/static/img/tutorials/custom-steps-jira/1.png diff --git a/docs/static/img/custom-steps-jira/2.png b/docs/static/img/tutorials/custom-steps-jira/2.png similarity index 100% rename from docs/static/img/custom-steps-jira/2.png rename to docs/static/img/tutorials/custom-steps-jira/2.png diff --git a/docs/static/img/custom-steps-jira/3.png b/docs/static/img/tutorials/custom-steps-jira/3.png similarity index 100% rename from docs/static/img/custom-steps-jira/3.png rename to docs/static/img/tutorials/custom-steps-jira/3.png diff --git a/docs/static/img/custom-steps-jira/4.png b/docs/static/img/tutorials/custom-steps-jira/4.png similarity index 100% rename from docs/static/img/custom-steps-jira/4.png rename to docs/static/img/tutorials/custom-steps-jira/4.png diff --git a/docs/static/img/custom-steps-jira/5.png b/docs/static/img/tutorials/custom-steps-jira/5.png similarity index 100% rename from docs/static/img/custom-steps-jira/5.png rename to docs/static/img/tutorials/custom-steps-jira/5.png diff --git a/docs/static/img/custom-steps-jira/6.png b/docs/static/img/tutorials/custom-steps-jira/6.png similarity index 100% rename from docs/static/img/custom-steps-jira/6.png rename to docs/static/img/tutorials/custom-steps-jira/6.png diff --git a/docs/static/img/custom-steps-jira/7.png b/docs/static/img/tutorials/custom-steps-jira/7.png similarity index 100% rename from docs/static/img/custom-steps-jira/7.png rename to docs/static/img/tutorials/custom-steps-jira/7.png diff --git a/docs/static/img/tutorials/custom-steps-wfb-existing/add-step.png b/docs/static/img/tutorials/custom-steps-wfb-existing/add-step.png new file mode 100644 index 000000000..81b32d5e0 Binary files /dev/null and b/docs/static/img/tutorials/custom-steps-wfb-existing/add-step.png differ diff --git a/docs/static/img/tutorials/custom-steps-wfb-existing/app-message.png b/docs/static/img/tutorials/custom-steps-wfb-existing/app-message.png new file mode 100644 index 000000000..a8420a6b5 Binary files /dev/null and b/docs/static/img/tutorials/custom-steps-wfb-existing/app-message.png differ diff --git a/docs/static/img/tutorials/custom-steps-wfb-existing/define-step.png b/docs/static/img/tutorials/custom-steps-wfb-existing/define-step.png new file mode 100644 index 000000000..32b578576 Binary files /dev/null and b/docs/static/img/tutorials/custom-steps-wfb-existing/define-step.png differ diff --git a/docs/static/img/tutorials/custom-steps-wfb-existing/find-step.png b/docs/static/img/tutorials/custom-steps-wfb-existing/find-step.png new file mode 100644 index 000000000..54e2741c6 Binary files /dev/null and b/docs/static/img/tutorials/custom-steps-wfb-existing/find-step.png differ diff --git a/docs/static/img/tutorials/custom-steps-wfb-existing/inputs.png b/docs/static/img/tutorials/custom-steps-wfb-existing/inputs.png new file mode 100644 index 000000000..77434e44d Binary files /dev/null and b/docs/static/img/tutorials/custom-steps-wfb-existing/inputs.png differ diff --git a/docs/static/img/tutorials/custom-steps-wfb-existing/org-ready.png b/docs/static/img/tutorials/custom-steps-wfb-existing/org-ready.png new file mode 100644 index 000000000..e3abd5c7f Binary files /dev/null and b/docs/static/img/tutorials/custom-steps-wfb-existing/org-ready.png differ diff --git a/docs/static/img/tutorials/custom-steps-wfb-existing/outputs.png b/docs/static/img/tutorials/custom-steps-wfb-existing/outputs.png new file mode 100644 index 000000000..3b7d326c5 Binary files /dev/null and b/docs/static/img/tutorials/custom-steps-wfb-existing/outputs.png differ diff --git a/docs/static/img/tutorials/custom-steps-wfb-existing/step-inputs.png b/docs/static/img/tutorials/custom-steps-wfb-existing/step-inputs.png new file mode 100644 index 000000000..bf8fc7871 Binary files /dev/null and b/docs/static/img/tutorials/custom-steps-wfb-existing/step-inputs.png differ diff --git a/docs/static/img/tutorials/custom-steps-wfb-new/app-token.png b/docs/static/img/tutorials/custom-steps-wfb-new/app-token.png new file mode 100644 index 000000000..c500bb003 Binary files /dev/null and b/docs/static/img/tutorials/custom-steps-wfb-new/app-token.png differ diff --git a/docs/static/img/tutorials/custom-steps-wfb-new/bot-token.png b/docs/static/img/tutorials/custom-steps-wfb-new/bot-token.png new file mode 100644 index 000000000..2d624117d Binary files /dev/null and b/docs/static/img/tutorials/custom-steps-wfb-new/bot-token.png differ diff --git a/docs/static/img/tutorials/custom-steps-wfb-new/install.png b/docs/static/img/tutorials/custom-steps-wfb-new/install.png new file mode 100644 index 000000000..bbfc83c13 Binary files /dev/null and b/docs/static/img/tutorials/custom-steps-wfb-new/install.png differ diff --git a/docs/static/img/tutorials/custom-steps-wfb-new/manifest.png b/docs/static/img/tutorials/custom-steps-wfb-new/manifest.png new file mode 100644 index 000000000..013c0f7be Binary files /dev/null and b/docs/static/img/tutorials/custom-steps-wfb-new/manifest.png differ diff --git a/docs/static/img/tutorials/custom-steps-wfb-new/wfb-1.png b/docs/static/img/tutorials/custom-steps-wfb-new/wfb-1.png new file mode 100644 index 000000000..566a11224 Binary files /dev/null and b/docs/static/img/tutorials/custom-steps-wfb-new/wfb-1.png differ diff --git a/docs/static/img/tutorials/custom-steps-wfb-new/wfb-10.png b/docs/static/img/tutorials/custom-steps-wfb-new/wfb-10.png new file mode 100644 index 000000000..859c4bf1a Binary files /dev/null and b/docs/static/img/tutorials/custom-steps-wfb-new/wfb-10.png differ diff --git a/docs/static/img/tutorials/custom-steps-wfb-new/wfb-11.png b/docs/static/img/tutorials/custom-steps-wfb-new/wfb-11.png new file mode 100644 index 000000000..be2159a59 Binary files /dev/null and b/docs/static/img/tutorials/custom-steps-wfb-new/wfb-11.png differ diff --git a/docs/static/img/tutorials/custom-steps-wfb-new/wfb-12.png b/docs/static/img/tutorials/custom-steps-wfb-new/wfb-12.png new file mode 100644 index 000000000..0e59ed5a5 Binary files /dev/null and b/docs/static/img/tutorials/custom-steps-wfb-new/wfb-12.png differ diff --git a/docs/static/img/tutorials/custom-steps-wfb-new/wfb-2.png b/docs/static/img/tutorials/custom-steps-wfb-new/wfb-2.png new file mode 100644 index 000000000..8744bee39 Binary files /dev/null and b/docs/static/img/tutorials/custom-steps-wfb-new/wfb-2.png differ diff --git a/docs/static/img/tutorials/custom-steps-wfb-new/wfb-3.png b/docs/static/img/tutorials/custom-steps-wfb-new/wfb-3.png new file mode 100644 index 000000000..17601ffda Binary files /dev/null and b/docs/static/img/tutorials/custom-steps-wfb-new/wfb-3.png differ diff --git a/docs/static/img/tutorials/custom-steps-wfb-new/wfb-4.png b/docs/static/img/tutorials/custom-steps-wfb-new/wfb-4.png new file mode 100644 index 000000000..79f06ac7b Binary files /dev/null and b/docs/static/img/tutorials/custom-steps-wfb-new/wfb-4.png differ diff --git a/docs/static/img/tutorials/custom-steps-wfb-new/wfb-5.png b/docs/static/img/tutorials/custom-steps-wfb-new/wfb-5.png new file mode 100644 index 000000000..3a304316c Binary files /dev/null and b/docs/static/img/tutorials/custom-steps-wfb-new/wfb-5.png differ diff --git a/docs/static/img/tutorials/custom-steps-wfb-new/wfb-6.png b/docs/static/img/tutorials/custom-steps-wfb-new/wfb-6.png new file mode 100644 index 000000000..b5e85e95e Binary files /dev/null and b/docs/static/img/tutorials/custom-steps-wfb-new/wfb-6.png differ diff --git a/docs/static/img/tutorials/custom-steps-wfb-new/wfb-7.png b/docs/static/img/tutorials/custom-steps-wfb-new/wfb-7.png new file mode 100644 index 000000000..a8992b84e Binary files /dev/null and b/docs/static/img/tutorials/custom-steps-wfb-new/wfb-7.png differ diff --git a/docs/static/img/tutorials/custom-steps-wfb-new/wfb-8.png b/docs/static/img/tutorials/custom-steps-wfb-new/wfb-8.png new file mode 100644 index 000000000..1a3d636cb Binary files /dev/null and b/docs/static/img/tutorials/custom-steps-wfb-new/wfb-8.png differ diff --git a/docs/static/img/tutorials/custom-steps-wfb-new/wfb-9.png b/docs/static/img/tutorials/custom-steps-wfb-new/wfb-9.png new file mode 100644 index 000000000..1347c463f Binary files /dev/null and b/docs/static/img/tutorials/custom-steps-wfb-new/wfb-9.png differ diff --git a/docs/static/img/tutorials/custom-steps-wfb-new/workflow-step.png b/docs/static/img/tutorials/custom-steps-wfb-new/workflow-step.png new file mode 100644 index 000000000..3ae8c26fa Binary files /dev/null and b/docs/static/img/tutorials/custom-steps-wfb-new/workflow-step.png differ diff --git a/docs/static/img/modals/base_link.gif b/docs/static/img/tutorials/modals/base_link.gif similarity index 100% rename from docs/static/img/modals/base_link.gif rename to docs/static/img/tutorials/modals/base_link.gif diff --git a/docs/static/img/modals/final_product.gif b/docs/static/img/tutorials/modals/final_product.gif similarity index 100% rename from docs/static/img/modals/final_product.gif rename to docs/static/img/tutorials/modals/final_product.gif diff --git a/docs/static/img/modals/heart_icon.gif b/docs/static/img/tutorials/modals/heart_icon.gif similarity index 100% rename from docs/static/img/modals/heart_icon.gif rename to docs/static/img/tutorials/modals/heart_icon.gif diff --git a/docs/static/img/modals/interactivity_url.png b/docs/static/img/tutorials/modals/interactivity_url.png similarity index 100% rename from docs/static/img/modals/interactivity_url.png rename to docs/static/img/tutorials/modals/interactivity_url.png diff --git a/docs/static/img/modals/slash_command.png b/docs/static/img/tutorials/modals/slash_command.png similarity index 100% rename from docs/static/img/modals/slash_command.png rename to docs/static/img/tutorials/modals/slash_command.png