Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Workflow Steps from Apps #597

Merged
merged 11 commits into from
Sep 3, 2020
Merged
66 changes: 66 additions & 0 deletions docs/_steps/adding_editing_workflow_step.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
---
title: Adding or editing workflow steps
lang: en
slug: adding-editing-steps
order: 3
beta: true
---

<div class='section-content'>

When a builder adds (or later edits) your step in their workflow, your app will receive a `workflow_step_edit` action. The callback assigned to the `edit` property of the `WorkflowStep` configuration object passed in during instantiation will run when this action occurs.

Whether a builder is adding or editing a step, you need to provide them with a special `workflow_step` modal — a workflow step configuration modal — where step-specific settings are chosen. Since the purpose of this modal is tied to a workflow step's configuration, it has more restrictions than typical modals—most notably, you cannot include `title​`, `submit​`, or `close`​ properties in the payload. By default, the `callback_id` used for this modal will be the same as that of the workflow step.

Within the `edit` callback, the `configure()` utility can be used to easily open your step's configuration modal by passing in an object with your view's `blocks`. To disable configuration save before certain conditions are met, pass in `submit_disabled` with a value of `true`.

To learn more about workflow step configuration modals, [read the documentation](https://api.slack.com/reference/workflows/configuration-view).

</div>

```javascript
const ws = new WorkflowStep('add_task', {
edit: async ({ ack, step, configure }) => {
await ack();

const blocks = [
{
'type': 'input',
'block_id': 'task_name_input',
'element': {
'type': 'plain_text_input',
'action_id': 'name',
'placeholder': {
'type': 'plain_text',
'text': 'Add a task name'
}
},
'label': {
'type': 'plain_text',
'text': 'Task name'
}
},
{
'type': 'input',
'block_id': 'task_description_input',
'element': {
'type': 'plain_text_input',
'action_id': 'description',
'placeholder': {
'type': 'plain_text',
'text': 'Add a task description'
}
},
'label': {
'type': 'plain_text',
'text': 'Task description'
}
},
];

await configure({ blocks });
},
save: async ({ ack, step, update }) => {},
execute: async ({ step, complete, fail }) => {},
});
```
113 changes: 0 additions & 113 deletions docs/_steps/configuring_workflow_steps.md

This file was deleted.

31 changes: 31 additions & 0 deletions docs/_steps/creating_workflow_step.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
title: Creating a workflow step
lang: en
slug: creating-steps
order: 2
beta: true
---

<div class='section-content'>

To create a new workflow step, Bolt provides the `WorkflowStep` class.

When instantiating a new `WorkflowStep`, pass in the step's `callback_id`, which is defined in your app configuration, and a step configuration object.

The configuration object for a `WorkflowStep` contains three properties: `edit`, `save`, and `execute`. Each of these properties must either hold a value of a single callback or an array of callbacks. All callbacks have access to a `step` object that contains information about the workflow step event, as well as one or more utility functions.

After instantiating your workflow step, pass it the instance into `app.step()`. Behind the scenes, your app will listen and respond to the workflow step’s events using the callbacks provided in the configuration object.

</div>

```javascript
const { WorkflowStep } = require('@slack/bolt');

const ws = new WorkflowStep('add_task', {
edit: async ({ ack, step, configure }) => {},
save: async ({ ack, step, update }) => {},
execute: async ({ step, complete, fail }) => {},
});

app.step(ws);
```
41 changes: 24 additions & 17 deletions docs/_steps/executing_workflow_steps.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,37 @@
title: Executing workflow steps
lang: en
slug: executing-steps
order: 3
order: 5
beta: true
---

<div class="section-content">

When your workflow is executed by an end user, your app will receive a `workflow_step_execute` event. This event includes the user's `inputs` and a unique workflow execution ID. Your app must either call [`workflows.stepCompleted`](https://api.slack.com/methods/workflows.stepCompleted) with the `outputs` you specified in `workflows.updateStep`, or [`workflows.stepFailed`](https://api.slack.com/methods/workflows.stepFailed) to indicate the step failed.
When your workflow step is executed by an end user, your app will receive a `workflow_step_execute` event. The method assigned to the `execute` property of the `WorkflowStep` configuration object passed in during instantiation will run when this event occurs.

Using the `inputs` from the configuration modal submission in the `save` callback, this is where we make third-party API calls, save things to a database, update the end user's Home Tab, and/or decide what outputs will be available to subsequent workflow steps by mapping values to the `outputs` object.

Within the `execute` callback, your app must either call `complete()` to indicate that the step's execution was successful, or `fail()` to indicate that the step's execution failed.

</div>

```javascript
app.event('workflow_step_execute', async ({ event, client }) => {
// Unique workflow edit ID
let workflowExecuteId = event.workflow_step.workflow_step_execute_id;
let inputs = event.workflow_step.inputs;

await client.workflows.stepCompleted({
workflow_step_execute_id: workflowExecuteId,
outputs: {
taskName: inputs.taskName.value,
taskDescription: inputs.taskDescription.value
}
});

// You can do anything else you want here. Some ideas:
// Display results on the user's home tab, update your database, or send a message into a channel
const ws = new WorkflowStep('add_task', {
edit: async ({ ack, step, configure }) => {},
save: async ({ ack, step, update }) => {},
execute: async ({ step, complete, fail }) => {
const { inputs } = step;

const outputs = {
taskName: inputs.taskName.value.value,
taskDescription: inputs.taskDescription.value.value,
};

// if everything was successful
await complete({ outputs });

// if something went wrong
// fail({ error: { message: "Just testing step failure!" } });
},
});
```
59 changes: 59 additions & 0 deletions docs/_steps/saving_workflow_step.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
---
title: Saving the step configuration
lang: en
slug: saving-steps
order: 4
beta: true
---

<div class='section-content'>

When the workflow step's configuration has been saved (using the step configuration modal from the `edit` callback), your app will listen for the `view_submission` event. The method assigned to the `save` property of the `WorkflowStep` configuration object passed in during instantiation will run when this event occurs.

Once the configuration for the workflow step has been determined, builders often use that configuration to craft the custom outputs and behavior that occurs when the end user executes the step.

Within the `save` callback, the `update()` method can be used to save the builder's step configuration by passing in the following arguments: `inputs`, `outputs`, `step_name` and `step_image_url`.

`inputs` is an object representing the data your app expects to receive from the user upon workflow step execution. To use variables that were collected earlier in the workflow, you can include handlebar-style syntax (`{{ variable }}`). During the workflow step's execution, those variables will be replaced with their actual runtime value.

`outputs` is an array of objects containing data that your app will provide upon the workflow step's completion. Outputs can then be used in subsequent steps of the workflow.

`step_name` and `step_image_url` are available for a more customized look and feel of your workflow step.

To learn more about how to structure these parameters, [read the documentation](https://api.slack.com/reference/workflows/workflow_step).

</div>

```javascript
const ws = new WorkflowStep('add_task', {
edit: async ({ ack, step, configure }) => {},
save: async ({ ack, step, update }) => {
await ack();

const { values } = view.state;
const taskName = values.task_name_input.name;
const taskDescription = values.task_description_input.description;

const inputs = {
taskName: { value: taskName },
taskDescription: { value: taskDescription }
};

const outputs = [
{
type: 'text',
name: 'taskName',
label: 'Task name',
},
{
type: 'text',
name: 'taskDescription',
label: 'Task description',
}
];

await update({ inputs, outputs });
},
execute: async ({ step, complete, fail }) => {},
});
```
6 changes: 4 additions & 2 deletions docs/_steps/workflow_steps_beta.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ beta: true
---

<div class="section-content">
⚠️ Workflow [steps from apps](https://api.slack.com/workflows/steps) is a beta feature. As the feature is developed, **Bolt for JavaScript's API will change to add better native support.** To develop with workflow steps in Bolt, use the `@slack/bolt@feat-workflow-steps` version of the package rather than the standard `@slack/bolt`.
Workflow Steps from apps allow your app to create and process custom workflow steps that users can add using [Workflow Builder](https://api.slack.com/workflows).

The [API documentation](https://api.slack.com/workflows/steps) includes more information on setting up a beta app.
A workflow step is made up of three distinct user events: workflow builders adding or editing the step, saving or updating the step's configuration, and the end user's execution of the step. All three events must be handled for a workflow step to function.

The [API documentation](https://api.slack.com/workflows/steps) includes more information on setting up your app.
</div>
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@slack/bolt",
"version": "2.3.0",
"version": "2.3.0-workflowStepsBeta.1",
"description": "A framework for building Slack apps, fast.",
"author": "Slack Technologies, Inc.",
"license": "MIT",
Expand Down Expand Up @@ -44,8 +44,8 @@
"dependencies": {
"@slack/logger": "^2.0.0",
"@slack/oauth": "^1.2.0",
"@slack/types": "^1.6.0",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reminder :: @slack/types and @slack/web-api need to be updated to appropriate versions prior to merge

"@slack/web-api": "^5.9.0",
"@slack/types": "feat-workflow-steps",
"@slack/web-api": "feat-workflow-steps",
"@types/express": "^4.16.1",
"@types/node": ">=10",
"@types/promise.allsettled": "^1.0.3",
Expand Down
12 changes: 12 additions & 0 deletions src/App.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
} from './middleware/builtin';
import { processMiddleware } from './middleware/process';
import { ConversationStore, conversationContext, MemoryStore } from './conversation-store';
import { WorkflowStep } from './WorkflowStep';
import {
Middleware,
AnyMiddlewareArgs,
Expand Down Expand Up @@ -311,6 +312,17 @@ export default class App {
return this;
}

/**
* Register WorkflowStep middleware
*
* @param workflowStep global workflow step middleware function
*/
public step(workflowStep: WorkflowStep): this {
const m = workflowStep.getMiddleware();
this.middleware.push(m);
return this;
}

/**
* Convenience method to call start on the receiver
*
Expand Down
Loading