Repository: slack-go/slack
Pull request #1350 (2024-11-14 19:37:16)

DEPRECATION!: Legacy Workflows Deprecation
In accordance with the [Legacy Workflow Deprecation Note](https://api.slack.com/changelog/2023-08-workflow-steps-from-apps-step-back), this pull request removes the following:

WebAPI Methods:

- [workflows.StepCompleted](https://api.slack.com/methods/workflows.stepCompleted)
- [workflows.stepFailed](https://api.slack.com/methods/workflows.stepFailed)
- [workflows.updateStep](https://api.slack.com/methods/workflows.stepCompleted)

Events:

- [workflow_step_execute](https://api.slack.com/events/workflow_step_execute)
- [workflow_published](https://api.slack.com/events/workflow_published)
- [workflow_unpublished](https://api.slack.com/events/workflow_unpublished)
- [workflow_deleted](https://api.slack.com/events/workflow_deleted)
- [workflow_step_deleted](https://api.slack.com/events/workflow_step_deleted)


Patch:
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..4f2ea0f
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,8 @@
+# Please see the documentation for all configuration options:
+# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
+version: 2
+updates:
+  - package-ecosystem: "github-actions"
+    directory: "/"
+    schedule:
+      interval: "weekly"
diff --git a/.github/workflows/stale.yaml b/.github/workflows/stale.yaml
index e860f78..8b9867c 100644
--- a/.github/workflows/stale.yaml
+++ b/.github/workflows/stale.yaml
@@ -13,7 +13,7 @@ jobs:
   stale:
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9.0.0
+      - uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9.1.0
         with:
           any-of-labels: 'feedback given'
           days-before-stale: 45
diff --git a/block.go b/block.go
index a3fb1a0..33a7875 100644
--- a/block.go
+++ b/block.go
@@ -1,9 +1,5 @@
 package slack
 
-// @NOTE: Blocks are in beta and subject to change.
-
-// More Information: https://api.slack.com/block-kit
-
 // MessageBlockType defines a named string type to define each block type
 // as a constant for use within the package.
 type MessageBlockType string
@@ -20,12 +16,14 @@ const (
 	MBTRichText MessageBlockType = "rich_text"
 	MBTCall     MessageBlockType = "call"
 	MBTVideo    MessageBlockType = "video"
+	MBTMarkdown MessageBlockType = "markdown"
 )
 
 // Block defines an interface all block types should implement
 // to ensure consistency between blocks.
 type Block interface {
 	BlockType() MessageBlockType
+	ID() string
 }
 
 // Blocks is a convenience struct defined to allow dynamic unmarshalling of
diff --git a/block_action.go b/block_action.go
index c15e4a3..819c0ef 100644
--- a/block_action.go
+++ b/block_action.go
@@ -14,6 +14,11 @@ func (s ActionBlock) BlockType() MessageBlockType {
 	return s.Type
 }
 
+// ID returns the ID of the block
+func (s ActionBlock) ID() string {
+	return s.BlockID
+}
+
 // NewActionBlock returns a new instance of an Action Block
 func NewActionBlock(blockID string, elements ...BlockElement) *ActionBlock {
 	return &ActionBlock{
diff --git a/block_call.go b/block_call.go
index 98f2c02..c81dcb8 100644
--- a/block_call.go
+++ b/block_call.go
@@ -14,7 +14,12 @@ func (s CallBlock) BlockType() MessageBlockType {
 	return s.Type
 }
 
-// NewFileBlock returns a new instance of a file block
+// ID returns the ID of the block
+func (s CallBlock) ID() string {
+	return s.BlockID
+}
+
+// NewCallBlock returns a new instance of a call block
 func NewCallBlock(callID string) *CallBlock {
 	return &CallBlock{
 		Type:   MBTCall,
diff --git a/block_context.go b/block_context.go
index 384fee2..879ee61 100644
--- a/block_context.go
+++ b/block_context.go
@@ -15,6 +15,11 @@ func (s ContextBlock) BlockType() MessageBlockType {
 	return s.Type
 }
 
+// ID returns the ID of the block
+func (s ContextBlock) ID() string {
+	return s.BlockID
+}
+
 type ContextElements struct {
 	Elements []MixedElement
 }
diff --git a/block_conv.go b/block_conv.go
index 26c57bb..7df9b10 100644
--- a/block_conv.go
+++ b/block_conv.go
@@ -63,6 +63,8 @@ func (b *Blocks) UnmarshalJSON(data []byte) error {
 			block = &ImageBlock{}
 		case "input":
 			block = &InputBlock{}
+		case "markdown":
+			block = &MarkdownBlock{}
 		case "rich_text":
 			block = &RichTextBlock{}
 		case "rich_text_input":
diff --git a/block_divider.go b/block_divider.go
index 2d442ba..e10d7b0 100644
--- a/block_divider.go
+++ b/block_divider.go
@@ -13,10 +13,14 @@ func (s DividerBlock) BlockType() MessageBlockType {
 	return s.Type
 }
 
+// ID returns the ID of the block
+func (s DividerBlock) ID() string {
+	return s.BlockID
+}
+
 // NewDividerBlock returns a new instance of a divider block
 func NewDividerBlock() *DividerBlock {
 	return &DividerBlock{
 		Type: MBTDivider,
 	}
-
 }
diff --git a/block_element.go b/block_element.go
index ad3b670..a677e04 100644
--- a/block_element.go
+++ b/block_element.go
@@ -318,6 +318,7 @@ type MultiSelectBlockElement struct {
 	InitialUsers         []string                  `json:"initial_users,omitempty"`
 	InitialConversations []string                  `json:"initial_conversations,omitempty"`
 	InitialChannels      []string                  `json:"initial_channels,omitempty"`
+	Filter               *SelectBlockElementFilter `json:"filter,omitempty"`
 	MinQueryLength       *int                      `json:"min_query_length,omitempty"`
 	MaxSelectedItems     *int                      `json:"max_selected_items,omitempty"`
 	Confirm              *ConfirmationBlockObject  `json:"confirm,omitempty"`
diff --git a/block_file.go b/block_file.go
index ac4453f..2f669b0 100644
--- a/block_file.go
+++ b/block_file.go
@@ -15,6 +15,11 @@ func (s FileBlock) BlockType() MessageBlockType {
 	return s.Type
 }
 
+// ID returns the ID of the block
+func (s FileBlock) ID() string {
+	return s.BlockID
+}
+
 // NewFileBlock returns a new instance of a file block
 func NewFileBlock(blockID string, externalID string, source string) *FileBlock {
 	return &FileBlock{
diff --git a/block_header.go b/block_header.go
index 6dff4b8..3afb4c9 100644
--- a/block_header.go
+++ b/block_header.go
@@ -14,6 +14,11 @@ func (s HeaderBlock) BlockType() MessageBlockType {
 	return s.Type
 }
 
+// ID returns the ID of the block
+func (s HeaderBlock) ID() string {
+	return s.BlockID
+}
+
 // HeaderBlockOption allows configuration of options for a new header block
 type HeaderBlockOption func(*HeaderBlock)
 
diff --git a/block_image.go b/block_image.go
index b3d2cb8..cdee5b4 100644
--- a/block_image.go
+++ b/block_image.go
@@ -12,6 +12,11 @@ type ImageBlock struct {
 	SlackFile *SlackFileObject `json:"slack_file,omitempty"`
 }
 
+// ID returns the ID of the block
+func (s ImageBlock) ID() string {
+	return s.BlockID
+}
+
 // SlackFileObject Defines an object containing Slack file information to be used in an
 // image block or image element.
 //
diff --git a/block_input.go b/block_input.go
index 7c1272a..f74eda6 100644
--- a/block_input.go
+++ b/block_input.go
@@ -18,6 +18,11 @@ func (s InputBlock) BlockType() MessageBlockType {
 	return s.Type
 }
 
+// ID returns the ID of the block
+func (s InputBlock) ID() string {
+	return s.BlockID
+}
+
 // NewInputBlock returns a new instance of an input block
 func NewInputBlock(blockID string, label, hint *TextBlockObject, element BlockElement) *InputBlock {
 	return &InputBlock{
diff --git a/block_markdown.go b/block_markdown.go
new file mode 100644
index 0000000..e22a0d1
--- /dev/null
+++ b/block_markdown.go
@@ -0,0 +1,34 @@
+package slack
+
+// MarkdownBlock defines a block that lets you use markdown to format your text.
+//
+// This block can be used with AI apps when you expect a markdown response from an LLM
+// that can get lost in translation rendering in Slack. Providing it in a markdown block
+// leaves the translating to Slack to ensure your message appears as intended. Note that
+// passing a single block may result in multiple blocks after translation.
+//
+// More Information: https://api.slack.com/reference/block-kit/blocks#markdown
+type MarkdownBlock struct {
+	Type    MessageBlockType `json:"type"`
+	BlockID string           `json:"block_id,omitempty"`
+	Text    string           `json:"text"`
+}
+
+// BlockType returns the type of the block
+func (s MarkdownBlock) BlockType() MessageBlockType {
+	return s.Type
+}
+
+// ID returns the ID of the block
+func (s MarkdownBlock) ID() string {
+	return s.BlockID
+}
+
+// NewMarkdownBlock returns an instance of a new Markdown Block type
+func NewMarkdownBlock(blockID, text string) *MarkdownBlock {
+	return &MarkdownBlock{
+		Type:    MBTMarkdown,
+		BlockID: blockID,
+		Text:    text,
+	}
+}
diff --git a/block_rich_text.go b/block_rich_text.go
index 22020be..18faf8c 100644
--- a/block_rich_text.go
+++ b/block_rich_text.go
@@ -16,6 +16,11 @@ func (b RichTextBlock) BlockType() MessageBlockType {
 	return b.Type
 }
 
+// ID returns the ID of the block
+func (s RichTextBlock) ID() string {
+	return s.BlockID
+}
+
 func (e *RichTextBlock) UnmarshalJSON(b []byte) error {
 	var raw struct {
 		Type        MessageBlockType  `json:"type"`
diff --git a/block_section.go b/block_section.go
index 1ffc17e..0d2352a 100644
--- a/block_section.go
+++ b/block_section.go
@@ -17,6 +17,11 @@ func (s SectionBlock) BlockType() MessageBlockType {
 	return s.Type
 }
 
+// ID returns the ID of the block
+func (s SectionBlock) ID() string {
+	return s.BlockID
+}
+
 // SectionBlockOption allows configuration of options for a new section block
 type SectionBlockOption func(*SectionBlock)
 
diff --git a/block_unknown.go b/block_unknown.go
index 97054c7..7a49a2c 100644
--- a/block_unknown.go
+++ b/block_unknown.go
@@ -11,3 +11,8 @@ type UnknownBlock struct {
 func (b UnknownBlock) BlockType() MessageBlockType {
 	return b.Type
 }
+
+// ID returns the ID of the block
+func (s UnknownBlock) ID() string {
+	return s.BlockID
+}
diff --git a/block_video.go b/block_video.go
index 322c614..4d6739c 100644
--- a/block_video.go
+++ b/block_video.go
@@ -22,6 +22,11 @@ func (s VideoBlock) BlockType() MessageBlockType {
 	return s.Type
 }
 
+// ID returns the ID of the block
+func (s VideoBlock) ID() string {
+	return s.BlockID
+}
+
 // NewVideoBlock returns an instance of a new Video Block type
 func NewVideoBlock(videoURL, thumbnailURL, altText, blockID string, title *TextBlockObject) *VideoBlock {
 	return &VideoBlock{
diff --git a/conversation.go b/conversation.go
index 67d28be..00f7d4e 100644
--- a/conversation.go
+++ b/conversation.go
@@ -36,6 +36,8 @@ type Conversation struct {
 	ConnectedTeamIDs   []string `json:"connected_team_ids,omitempty"`
 	SharedTeamIDs      []string `json:"shared_team_ids,omitempty"`
 	InternalTeamIDs    []string `json:"internal_team_ids,omitempty"`
+	ContextTeamID      string   `json:"context_team_id,omitempty"`
+	ConversationHostID string   `json:"conversation_host_id,omitempty"`
 
 	// TODO support pending_shared
 	// TODO support previous_names
diff --git a/examples/function/function.go b/examples/function/function.go
index d654890..0e189c2 100644
--- a/examples/function/function.go
+++ b/examples/function/function.go
@@ -2,10 +2,11 @@ package main
 
 import (
 	"fmt"
+	"os"
+
 	"github.com/slack-go/slack"
 	"github.com/slack-go/slack/slackevents"
 	"github.com/slack-go/slack/socketmode"
-	"os"
 )
 
 func main() {
@@ -38,7 +39,7 @@ func main() {
 						if callbackID == "sample_function" {
 							userId := ev.Inputs["user_id"]
 							payload := map[string]string{
-								"user_id": userId,
+								"user_id": userId.(string),
 							}
 
 							err := api.FunctionCompleteSuccess(ev.FunctionExecutionID, slack.FunctionCompleteSuccessRequestOptionOutput(payload))
diff --git a/examples/workflow_step/README.md b/examples/workflow_step/README.md
deleted file mode 100644
index da378b8..0000000
--- a/examples/workflow_step/README.md
+++ /dev/null
@@ -1,59 +0,0 @@
-#WorkflowStep
-
-Have you ever wanted to run an app from a Slack workflow? This sample app shows you how it works.
-
-Slack describes some of the basics here:
-https://api.slack.com/workflows/steps
-https://api.slack.com/tutorials/workflow-builder-steps
-
-
-1. Start the example app localy on port 8080
-
-
-2. Use ngrok to expose your app to the internet
-
-```shell
-    ./ngrok http 8080
-```
-Copy the https forwarding URL and paste it into the app manifest down below (event_subscription request_url and interactivity request_url)
-
-
-3. Create a new Slack App at api.slack.com/apps from an app manifest
-
-The manifest of a sample Slack App looks like this:
-```yaml
-display_information:
-  name: Workflowstep-Example
-features:
-  bot_user:
-    display_name: Workflowstep-Example
-    always_online: false
-  workflow_steps:
-    - name: Example Step
-      callback_id: example-step
-oauth_config:
-  scopes:
-    bot:
-      - workflow.steps:execute
-settings:
-  event_subscriptions:
-    request_url: https://*****.ngrok.io/api/v1/example-step
-    bot_events:
-      - workflow_step_execute
-  interactivity:
-    is_enabled: true
-    request_url: https://*****.ngrok.io/api/v1/interaction
-  org_deploy_enabled: false
-  socket_mode_enabled: false
-  token_rotation_enabled: false
-```
-
-("Interactivity" and "Enable Events" should be turned on)
-
-4. Slack Workflow (**paid plan required!**)
-   1. Create a new Workflow at app.slack.com/workflow-builder
-   2. give it a name
-   3. select "Planned date & time"
-   4. add another step and select "Example Step" from App Workflowstep-Example
-   5. configure your app and hit save
-   6. don't forget to publish your workflow
\ No newline at end of file
diff --git a/examples/workflow_step/go.mod b/examples/workflow_step/go.mod
deleted file mode 100644
index 1df243c..0000000
--- a/examples/workflow_step/go.mod
+++ /dev/null
@@ -1,9 +0,0 @@
-module workflowstep-example
-
-go 1.17
-
-require (
-	github.com/gorilla/websocket v1.4.2 // indirect
-	github.com/pkg/errors v0.9.1 // indirect
-	github.com/slack-go/slack v0.10.1 // indirect
-)
diff --git a/examples/workflow_step/go.sum b/examples/workflow_step/go.sum
deleted file mode 100644
index e95148d..0000000
--- a/examples/workflow_step/go.sum
+++ /dev/null
@@ -1,11 +0,0 @@
-github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
-github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
-github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
-github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
-github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
-github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
-github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/slack-go/slack v0.10.1 h1:BGbxa0kMsGEvLOEoZmYs8T1wWfoZXwmQFBb6FgYCXUA=
-github.com/slack-go/slack v0.10.1/go.mod h1:wWL//kk0ho+FcQXcBTmEafUI5dz4qz5f4mMk8oIkioQ=
-github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
diff --git a/examples/workflow_step/handler.go b/examples/workflow_step/handler.go
deleted file mode 100644
index d0eee10..0000000
--- a/examples/workflow_step/handler.go
+++ /dev/null
@@ -1,211 +0,0 @@
-package main
-
-import (
-	"encoding/json"
-	"fmt"
-	"io"
-	"log"
-	"net/http"
-	"net/url"
-	"time"
-
-	"github.com/slack-go/slack"
-	"github.com/slack-go/slack/slackevents"
-)
-
-const (
-	IDSelectOptionBlock  = "select-option-block"
-	IDExampleSelectInput = "example-select-input"
-)
-
-func handleMyWorkflowStep(w http.ResponseWriter, r *http.Request) {
-	if r.Method != http.MethodPost {
-		w.WriteHeader(http.StatusMethodNotAllowed)
-		return
-	}
-
-	// see: https://github.com/slack-go/slack/blob/master/examples/eventsapi/events.go
-	body, err := io.ReadAll(r.Body)
-	if err != nil {
-		w.WriteHeader(http.StatusBadRequest)
-		return
-	}
-
-	eventsAPIEvent, err := slackevents.ParseEvent(json.RawMessage(body), slackevents.OptionNoVerifyToken())
-	if err != nil {
-		log.Printf("[ERROR] Failed on parsing event: %s", err.Error())
-		w.WriteHeader(http.StatusInternalServerError)
-		return
-	}
-
-	// see: https://api.slack.com/apis/connections/events-api#subscriptions
-	if eventsAPIEvent.Type == slackevents.URLVerification {
-		var r *slackevents.ChallengeResponse
-		err := json.Unmarshal([]byte(body), &r)
-		if err != nil {
-			log.Printf("[ERROR] Failed to decode json message on event url_verification: %s", err.Error())
-			w.WriteHeader(http.StatusInternalServerError)
-			return
-		}
-
-		w.Header().Set("Content-Type", "text")
-		w.Write([]byte(r.Challenge))
-		return
-	}
-
-	// see: https://api.slack.com/apis/connections/events-api#receiving_events
-	if eventsAPIEvent.Type == slackevents.CallbackEvent {
-		innerEvent := eventsAPIEvent.InnerEvent
-
-		switch ev := innerEvent.Data.(type) {
-
-		// see: https://api.slack.com/events/workflow_step_execute
-		case *slackevents.WorkflowStepExecuteEvent:
-			if ev.CallbackID == MyExampleWorkflowStepCallbackID {
-				go doHeavyLoad(ev.WorkflowStep)
-
-				w.WriteHeader(http.StatusOK)
-				return
-			}
-			w.WriteHeader(http.StatusBadRequest)
-			log.Printf("[WARN] unknown callbackID: %s", ev.CallbackID)
-			return
-
-		default:
-			w.WriteHeader(http.StatusBadRequest)
-			log.Printf("[WARN] unknown inner event type: %s", eventsAPIEvent.InnerEvent.Type)
-			return
-		}
-	}
-
-	w.WriteHeader(http.StatusBadRequest)
-	log.Printf("[WARN] unknown event type: %s", eventsAPIEvent.Type)
-}
-
-func handleInteraction(w http.ResponseWriter, r *http.Request) {
-	if r.Method != http.MethodPost {
-		w.WriteHeader(http.StatusMethodNotAllowed)
-		return
-	}
-
-	body, err := io.ReadAll(r.Body)
-	if err != nil {
-		w.WriteHeader(http.StatusBadRequest)
-		return
-	}
-
-	jsonStr, err := url.QueryUnescape(string(body)[8:])
-	if err != nil {
-		w.WriteHeader(http.StatusInternalServerError)
-		return
-	}
-
-	var message slack.InteractionCallback
-	if err := json.Unmarshal([]byte(jsonStr), &message); err != nil {
-		log.Printf("[ERROR] Failed to decode json message from slack: %s", jsonStr)
-		w.WriteHeader(http.StatusInternalServerError)
-		return
-	}
-
-	switch message.Type {
-	case slack.InteractionTypeWorkflowStepEdit:
-		// https://api.slack.com/workflows/steps#handle_config_view
-		err := replyWithConfigurationView(message, "", "")
-		if err != nil {
-			log.Printf("[ERROR] Failed to open configuration modal in slack: %s", err.Error())
-		}
-
-	case slack.InteractionTypeViewSubmission:
-		// https://api.slack.com/workflows/steps#handle_view_submission
-
-		// process user inputs
-		// this is just for demonstration, so we print it to console only
-		blockAction := message.View.State.Values
-		selectedOption := blockAction[IDSelectOptionBlock][IDExampleSelectInput].SelectedOption.Value
-		log.Println(fmt.Sprintf("user selected: %s", selectedOption))
-
-		in := &slack.WorkflowStepInputs{
-			IDExampleSelectInput: slack.WorkflowStepInputElement{
-				Value:                   selectedOption,
-				SkipVariableReplacement: false,
-			},
-		}
-
-		err := saveUserSettingsForWorkflowStep(message.WorkflowStep.WorkflowStepEditID, in, nil)
-		if err != nil {
-			log.Printf("[ERROR] Failed on doing a POST request to workflows.updateStep: %s", err.Error())
-			w.WriteHeader(http.StatusInternalServerError)
-		}
-
-	default:
-		log.Printf("[WARN] unknown message type: %s", message.Type)
-		w.WriteHeader(http.StatusInternalServerError)
-	}
-}
-
-func replyWithConfigurationView(message slack.InteractionCallback, privateMetaData string, externalID string) error {
-	headerText := slack.NewTextBlockObject("mrkdwn", "Hello World!\nThis is your workflow step app configuration view", false, false)
-	headerSection := slack.NewSectionBlock(headerText, nil, nil)
-
-	options := []*slack.OptionBlockObject{}
-	options = append(
-		options,
-		slack.NewOptionBlockObject("one", slack.NewTextBlockObject("plain_text", "One", false, false), nil),
-	)
-
-	options = append(
-		options,
-		slack.NewOptionBlockObject("two", slack.NewTextBlockObject("plain_text", "Two", false, false), nil),
-	)
-
-	options = append(
-		options,
-		slack.NewOptionBlockObject("three", slack.NewTextBlockObject("plain_text", "Three", false, false), nil),
-	)
-
-	selection := slack.NewOptionsSelectBlockElement(
-		"static_select",
-		slack.NewTextBlockObject("plain_text", "your choice", false, false),
-		IDExampleSelectInput,
-		options...,
-	)
-
-	// preselect option, if workflow step input is defined
-	initialOption, ok := slack.GetInitialOptionFromWorkflowStepInput(selection, message.WorkflowStep.Inputs, options)
-	if ok {
-		selection.InitialOption = initialOption
-	}
-
-	inputBlock := slack.NewInputBlock(
-		IDSelectOptionBlock,
-		slack.NewTextBlockObject("plain_text", "Select an option", false, false),
-		selection,
-	)
-
-	blocks := slack.Blocks{
-		BlockSet: []slack.Block{
-			headerSection,
-			inputBlock,
-		},
-	}
-
-	cmr := slack.NewConfigurationModalRequest(blocks, privateMetaData, externalID)
-	_, err := appCtx.slack.OpenView(message.TriggerID, cmr.ModalViewRequest)
-	return err
-}
-
-func saveUserSettingsForWorkflowStep(workflowStepEditID string, inputs *slack.WorkflowStepInputs, outputs *[]slack.WorkflowStepOutput) error {
-	return appCtx.slack.SaveWorkflowStepConfiguration(workflowStepEditID, inputs, outputs)
-}
-
-func doHeavyLoad(workflowStep slackevents.EventWorkflowStep) {
-	// process user configuration e.g. inputs
-	log.Printf("Inputs:")
-	for name, input := range *workflowStep.Inputs {
-		log.Printf(fmt.Sprintf("%s: %s", name, input.Value))
-	}
-
-	// do heavy load
-	time.Sleep(10 * time.Second)
-	log.Println("Done")
-}
diff --git a/examples/workflow_step/main.go b/examples/workflow_step/main.go
deleted file mode 100644
index 45494d0..0000000
--- a/examples/workflow_step/main.go
+++ /dev/null
@@ -1,48 +0,0 @@
-package main
-
-import (
-	"fmt"
-	"log"
-	"net/http"
-	"os"
-
-	"github.com/slack-go/slack"
-)
-
-type (
-	appContext struct {
-		slack  *slack.Client
-		config configuration
-	}
-	configuration struct {
-		botToken      string
-		signingSecret string
-	}
-	SecretsVerifierMiddleware struct {
-		handler http.Handler
-	}
-)
-
-const (
-	APIBaseURL = "/api/v1"
-	// MyExampleWorkflowStepCallbackID is configured in slack (api.slack.com/apps).
-	// Select your app or create a new one. Then choose menu "Workflow Steps"...
-	MyExampleWorkflowStepCallbackID = "example-step"
-)
-
-var appCtx appContext
-
-func main() {
-	appCtx.config.botToken = os.Getenv("SLACK_BOT_TOKEN")
-	appCtx.config.signingSecret = os.Getenv("SLACK_SIGNING_SECRET")
-
-	appCtx.slack = slack.New(appCtx.config.botToken)
-
-	mux := http.NewServeMux()
-	mux.HandleFunc(fmt.Sprintf("%s/interaction", APIBaseURL), handleInteraction)
-	mux.HandleFunc(fmt.Sprintf("%s/%s", APIBaseURL, MyExampleWorkflowStepCallbackID), handleMyWorkflowStep)
-	middleware := NewSecretsVerifierMiddleware(mux)
-
-	log.Printf("starting server on :8080")
-	log.Fatal(http.ListenAndServe(":8080", middleware))
-}
diff --git a/examples/workflow_step/middleware.go b/examples/workflow_step/middleware.go
deleted file mode 100644
index 6fe8269..0000000
--- a/examples/workflow_step/middleware.go
+++ /dev/null
@@ -1,41 +0,0 @@
-package main
-
-import (
-	"bytes"
-	"io"
-	"net/http"
-
-	"github.com/slack-go/slack"
-)
-
-func (v *SecretsVerifierMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
-	body, err := io.ReadAll(r.Body)
-	if err != nil {
-		w.WriteHeader(http.StatusBadRequest)
-		return
-	}
-	r.Body.Close()
-	r.Body = io.NopCloser(bytes.NewBuffer(body))
-
-	sv, err := slack.NewSecretsVerifier(r.Header, appCtx.config.signingSecret)
-	if err != nil {
-		w.WriteHeader(http.StatusBadRequest)
-		return
-	}
-
-	if _, err := sv.Write(body); err != nil {
-		w.WriteHeader(http.StatusInternalServerError)
-		return
-	}
-
-	if err := sv.Ensure(); err != nil {
-		w.WriteHeader(http.StatusUnauthorized)
-		return
-	}
-
-	v.handler.ServeHTTP(w, r)
-}
-
-func NewSecretsVerifierMiddleware(h http.Handler) *SecretsVerifierMiddleware {
-	return &SecretsVerifierMiddleware{h}
-}
diff --git a/files.go b/files.go
index b263171..0d6626b 100644
--- a/files.go
+++ b/files.go
@@ -89,6 +89,17 @@ type File struct {
 	NumStars        int      `json:"num_stars"`
 	IsStarred       bool     `json:"is_starred"`
 	Shares          Share    `json:"shares"`
+
+	Subject string              `json:"subject"`
+	To      []EmailFileUserInfo `json:"to"`
+	From    []EmailFileUserInfo `json:"from"`
+	Cc      []EmailFileUserInfo `json:"cc"`
+}
+
+type EmailFileUserInfo struct {
+	Address  string `json:"address"`
+	Name     string `json:"name"`
+	Original string `json:"original"`
 }
 
 type Share struct {
@@ -162,14 +173,14 @@ type UploadFileV2Parameters struct {
 	SnippetText     string
 }
 
-type getUploadURLExternalParameters struct {
-	altText     string
-	fileSize    int
-	fileName    string
-	snippetText string
+type GetUploadURLExternalParameters struct {
+	AltText     string
+	FileSize    int
+	FileName    string
+	SnippetText string
 }
 
-type getUploadURLExternalResponse struct {
+type GetUploadURLExternalResponse struct {
 	UploadURL string `json:"upload_url"`
 	FileID    string `json:"file_id"`
 	SlackResponse
@@ -188,14 +199,14 @@ type FileSummary struct {
 	Title string `json:"title"`
 }
 
-type completeUploadExternalParameters struct {
-	title           string
-	channel         string
-	initialComment  string
-	threadTimestamp string
+type CompleteUploadExternalParameters struct {
+	Files           []FileSummary
+	Channel         string
+	InitialComment  string
+	ThreadTimestamp string
 }
 
-type completeUploadExternalResponse struct {
+type CompleteUploadExternalResponse struct {
 	SlackResponse
 	Files []FileSummary `json:"files"`
 }
@@ -495,20 +506,28 @@ func (api *Client) ShareFilePublicURLContext(ctx context.Context, fileID string)
 	return &response.File, response.Comments, &response.Paging, nil
 }
 
-// getUploadURLExternal gets a URL and fileID from slack which can later be used to upload a file.
-func (api *Client) getUploadURLExternal(ctx context.Context, params getUploadURLExternalParameters) (*getUploadURLExternalResponse, error) {
+// GetUploadURLExternalContext gets a URL and fileID from slack which can later be used to upload a file.
+// Slack API docs: https://api.slack.com/methods/files.getUploadURLExternal
+func (api *Client) GetUploadURLExternalContext(ctx context.Context, params GetUploadURLExternalParameters) (*GetUploadURLExternalResponse, error) {
+	if params.FileName == "" {
+		return nil, fmt.Errorf("FileName cannot be empty")
+	}
+	if params.FileSize == 0 {
+		return nil, fmt.Errorf("FileSize cannot be 0")
+	}
+
 	values := url.Values{
 		"token":    {api.token},
-		"filename": {params.fileName},
-		"length":   {strconv.Itoa(params.fileSize)},
+		"filename": {params.FileName},
+		"length":   {strconv.Itoa(params.FileSize)},
 	}
-	if params.altText != "" {
-		values.Add("initial_comment", params.altText)
+	if params.AltText != "" {
+		values.Add("initial_comment", params.AltText)
 	}
-	if params.snippetText != "" {
-		values.Add("thread_ts", params.snippetText)
+	if params.SnippetText != "" {
+		values.Add("thread_ts", params.SnippetText)
 	}
-	response := &getUploadURLExternalResponse{}
+	response := &GetUploadURLExternalResponse{}
 	err := api.postMethod(ctx, "files.getUploadURLExternal", values, response)
 	if err != nil {
 		return nil, err
@@ -531,28 +550,28 @@ func (api *Client) uploadToURL(ctx context.Context, params uploadToURLParameters
 	return err
 }
 
-// completeUploadExternal once files are uploaded, this completes the upload and shares it to the specified channel
-func (api *Client) completeUploadExternal(ctx context.Context, fileID string, params completeUploadExternalParameters) (file *completeUploadExternalResponse, err error) {
-	request := []FileSummary{{ID: fileID, Title: params.title}}
-	requestBytes, err := json.Marshal(request)
+// CompleteUploadExternalContext once files are uploaded, this completes the upload and shares it to the specified channel
+// Slack API docs: https://api.slack.com/methods/files.completeUploadExternal
+func (api *Client) CompleteUploadExternalContext(ctx context.Context, params CompleteUploadExternalParameters) (file *CompleteUploadExternalResponse, err error) {
+	filesBytes, err := json.Marshal(params.Files)
 	if err != nil {
 		return nil, err
 	}
 	values := url.Values{
 		"token": {api.token},
-		"files": {string(requestBytes)},
+		"files": {string(filesBytes)},
 	}
 
-	if params.channel != "" {
-		values.Add("channel_id", params.channel)
+	if params.Channel != "" {
+		values.Add("channel_id", params.Channel)
 	}
-	if params.initialComment != "" {
-		values.Add("initial_comment", params.initialComment)
+	if params.InitialComment != "" {
+		values.Add("initial_comment", params.InitialComment)
 	}
-	if params.threadTimestamp != "" {
-		values.Add("thread_ts", params.threadTimestamp)
+	if params.ThreadTimestamp != "" {
+		values.Add("thread_ts", params.ThreadTimestamp)
 	}
-	response := &completeUploadExternalResponse{}
+	response := &CompleteUploadExternalResponse{}
 	err = api.postMethod(ctx, "files.completeUploadExternal", values, response)
 	if err != nil {
 		return nil, err
@@ -583,11 +602,11 @@ func (api *Client) UploadFileV2Context(ctx context.Context, params UploadFileV2P
 		return nil, fmt.Errorf("file.upload.v2: file size cannot be 0")
 	}
 
-	u, err := api.getUploadURLExternal(ctx, getUploadURLExternalParameters{
-		altText:     params.AltTxt,
-		fileName:    params.Filename,
-		fileSize:    params.FileSize,
-		snippetText: params.SnippetText,
+	u, err := api.GetUploadURLExternalContext(ctx, GetUploadURLExternalParameters{
+		AltText:     params.AltTxt,
+		FileName:    params.Filename,
+		FileSize:    params.FileSize,
+		SnippetText: params.SnippetText,
 	})
 	if err != nil {
 		return nil, err
@@ -604,11 +623,14 @@ func (api *Client) UploadFileV2Context(ctx context.Context, params UploadFileV2P
 		return nil, err
 	}
 
-	c, err := api.completeUploadExternal(ctx, u.FileID, completeUploadExternalParameters{
-		title:           params.Title,
-		channel:         params.Channel,
-		initialComment:  params.InitialComment,
-		threadTimestamp: params.ThreadTimestamp,
+	c, err := api.CompleteUploadExternalContext(ctx, CompleteUploadExternalParameters{
+		Files: []FileSummary{{
+			ID:    u.FileID,
+			Title: params.Title,
+		}},
+		Channel:         params.Channel,
+		InitialComment:  params.InitialComment,
+		ThreadTimestamp: params.ThreadTimestamp,
 	})
 	if err != nil {
 		return nil, err
diff --git a/interactions.go b/interactions.go
index 8c64147..abd0d55 100644
--- a/interactions.go
+++ b/interactions.go
@@ -33,30 +33,29 @@ const (
 
 // InteractionCallback is sent from slack when a user interactions with a button or dialog.
 type InteractionCallback struct {
-	Type                InteractionType         `json:"type"`
-	Token               string                  `json:"token"`
-	CallbackID          string                  `json:"callback_id"`
-	ResponseURL         string                  `json:"response_url"`
-	TriggerID           string                  `json:"trigger_id"`
-	ActionTs            string                  `json:"action_ts"`
-	Team                Team                    `json:"team"`
-	Channel             Channel                 `json:"channel"`
-	User                User                    `json:"user"`
-	OriginalMessage     Message                 `json:"original_message"`
-	Message             Message                 `json:"message"`
-	Name                string                  `json:"name"`
-	Value               string                  `json:"value"`
-	MessageTs           string                  `json:"message_ts"`
-	AttachmentID        string                  `json:"attachment_id"`
-	ActionCallback      ActionCallbacks         `json:"actions"`
-	View                View                    `json:"view"`
-	ActionID            string                  `json:"action_id"`
-	APIAppID            string                  `json:"api_app_id"`
-	BlockID             string                  `json:"block_id"`
-	Container           Container               `json:"container"`
-	Enterprise          Enterprise              `json:"enterprise"`
-	IsEnterpriseInstall bool                    `json:"is_enterprise_install"`
-	WorkflowStep        InteractionWorkflowStep `json:"workflow_step"`
+	Type                InteractionType `json:"type"`
+	Token               string          `json:"token"`
+	CallbackID          string          `json:"callback_id"`
+	ResponseURL         string          `json:"response_url"`
+	TriggerID           string          `json:"trigger_id"`
+	ActionTs            string          `json:"action_ts"`
+	Team                Team            `json:"team"`
+	Channel             Channel         `json:"channel"`
+	User                User            `json:"user"`
+	OriginalMessage     Message         `json:"original_message"`
+	Message             Message         `json:"message"`
+	Name                string          `json:"name"`
+	Value               string          `json:"value"`
+	MessageTs           string          `json:"message_ts"`
+	AttachmentID        string          `json:"attachment_id"`
+	ActionCallback      ActionCallbacks `json:"actions"`
+	View                View            `json:"view"`
+	ActionID            string          `json:"action_id"`
+	APIAppID            string          `json:"api_app_id"`
+	BlockID             string          `json:"block_id"`
+	Container           Container       `json:"container"`
+	Enterprise          Enterprise      `json:"enterprise"`
+	IsEnterpriseInstall bool            `json:"is_enterprise_install"`
 	DialogSubmissionCallback
 	ViewSubmissionCallback
 	ViewClosedCallback
@@ -137,14 +136,6 @@ type Enterprise struct {
 	Name string `json:"name"`
 }
 
-type InteractionWorkflowStep struct {
-	WorkflowStepEditID string                `json:"workflow_step_edit_id,omitempty"`
-	WorkflowID         string                `json:"workflow_id"`
-	StepID             string                `json:"step_id"`
-	Inputs             *WorkflowStepInputs   `json:"inputs,omitempty"`
-	Outputs            *[]WorkflowStepOutput `json:"outputs,omitempty"`
-}
-
 // ActionCallback is a convenience struct defined to allow dynamic unmarshalling of
 // the "actions" value in Slack's JSON response, which varies depending on block type
 type ActionCallbacks struct {
diff --git a/messages.go b/messages.go
index 13a1ede..90f8fc1 100644
--- a/messages.go
+++ b/messages.go
@@ -49,6 +49,7 @@ const (
 	MsgSubTypeUnpinnedItem              = "unpinned_item"               // [RTM] An item was unpinned from a channel
 	MsgSubTypeEkmAccessDenied           = "ekm_access_denied"           // [Events API, RTM] Message content redacted due to Enterprise Key Management (EKM)
 	MsgSubTypeChannelPostingPermissions = "channel_posting_permissions" // [Events API, RTM] The posting permissions for a channel changed
+	MsgSubTypeAssistantAppThread        = "assistant_app_thread"        // [Events API, RTM] The message is an app assistant thread
 )
 
 // Msg contains information about a slack message
diff --git a/rtm.go b/rtm.go
index ef6ba34..9b30eb3 100644
--- a/rtm.go
+++ b/rtm.go
@@ -25,6 +25,9 @@ const (
 // StartRTM calls the "rtm.start" endpoint and returns the provided URL and the full Info block.
 //
 // To have a fully managed Websocket connection, use `NewRTM`, and call `ManageConnection()` on it.
+//
+// Deprecated: Use [ConnectRTM] instead.
+// For more details, see: https://api.slack.com/changelog/2021-10-rtm-start-to-stop
 func (api *Client) StartRTM() (info *Info, websocketURL string, err error) {
 	ctx, cancel := context.WithTimeout(context.Background(), websocketDefaultTimeout)
 	defer cancel()
@@ -35,6 +38,9 @@ func (api *Client) StartRTM() (info *Info, websocketURL string, err error) {
 // StartRTMContext calls the "rtm.start" endpoint and returns the provided URL and the full Info block with a custom context.
 //
 // To have a fully managed Websocket connection, use `NewRTM`, and call `ManageConnection()` on it.
+//
+// Deprecated: Use [ConnectRTMContext] instead.
+// For more details, see: https://api.slack.com/changelog/2021-10-rtm-start-to-stop
 func (api *Client) StartRTMContext(ctx context.Context) (info *Info, websocketURL string, err error) {
 	response := &infoResponseFull{}
 	err = api.postMethod(ctx, "rtm.start", url.Values{"token": {api.token}}, response)
diff --git a/slackevents/inner_events.go b/slackevents/inner_events.go
index 720640b..1c84e17 100644
--- a/slackevents/inner_events.go
+++ b/slackevents/inner_events.go
@@ -403,14 +403,6 @@ type EmojiChangedEvent struct {
 	Value string `json:"value,omitempty"`
 }
 
-// WorkflowStepExecuteEvent is fired, if a workflow step of your app is invoked
-type WorkflowStepExecuteEvent struct {
-	Type           string            `json:"type"`
-	CallbackID     string            `json:"callback_id"`
-	WorkflowStep   EventWorkflowStep `json:"workflow_step"`
-	EventTimestamp string            `json:"event_ts"`
-}
-
 // MessageMetadataPostedEvent is sent, if a message with metadata is posted
 type MessageMetadataPostedEvent struct {
 	Type             string               `json:"type"`
@@ -452,15 +444,6 @@ type MessageMetadataDeletedEvent struct {
 	DeletedTimestamp string               `json:"deleted_ts"`
 }
 
-type EventWorkflowStep struct {
-	WorkflowStepExecuteID string                      `json:"workflow_step_execute_id"`
-	WorkflowID            string                      `json:"workflow_id"`
-	WorkflowInstanceID    string                      `json:"workflow_instance_id"`
-	StepID                string                      `json:"step_id"`
-	Inputs                *slack.WorkflowStepInputs   `json:"inputs,omitempty"`
-	Outputs               *[]slack.WorkflowStepOutput `json:"outputs,omitempty"`
-}
-
 // JSONTime exists so that we can have a String method converting the date
 type JSONTime int64
 
@@ -986,11 +969,11 @@ type FunctionExecutedEvent struct {
 		DateUpdated int64  `json:"date_updated"`
 		DateDeleted int64  `json:"date_deleted"`
 	} `json:"function"`
-	Inputs              map[string]string `json:"inputs"`
-	FunctionExecutionID string            `json:"function_execution_id"`
-	WorkflowExecutionID string            `json:"workflow_execution_id"`
-	EventTs             string            `json:"event_ts"`
-	BotAccessToken      string            `json:"bot_access_token"`
+	Inputs              map[string]interface{} `json:"inputs"`
+	FunctionExecutionID string                 `json:"function_execution_id"`
+	WorkflowExecutionID string                 `json:"workflow_execution_id"`
+	EventTs             string                 `json:"event_ts"`
+	BotAccessToken      string                 `json:"bot_access_token"`
 }
 
 type InviteRequestedEvent struct {
@@ -1344,7 +1327,6 @@ var EventsAPIInnerEventMapping = map[EventsAPIType]interface{}{
 	TeamJoin:                      TeamJoinEvent{},
 	TokensRevoked:                 TokensRevokedEvent{},
 	EmojiChanged:                  EmojiChangedEvent{},
-	WorkflowStepExecute:           WorkflowStepExecuteEvent{},
 	MessageMetadataPosted:         MessageMetadataPostedEvent{},
 	MessageMetadataUpdated:        MessageMetadataUpdatedEvent{},
 	MessageMetadataDeleted:        MessageMetadataDeletedEvent{},
diff --git a/usergroups.go b/usergroups.go
index 0c2ec4c..256f7bd 100644
--- a/usergroups.go
+++ b/usergroups.go
@@ -3,6 +3,7 @@ package slack
 import (
 	"context"
 	"net/url"
+	"strconv"
 	"strings"
 )
 
@@ -50,20 +51,45 @@ func (api *Client) userGroupRequest(ctx context.Context, path string, values url
 	return response, response.Err()
 }
 
+// createUserGroupParams contains arguments for CreateUserGroup method call
+type createUserGroupParams struct {
+	enableSection bool
+}
+
+// CreateUserGroupOption options for the CreateUserGroup method call.
+type CreateUserGroupOption func(*createUserGroupParams)
+
+// CreateUserGroupOptionEnableSection enable the section for the user group (default: false)
+func CreateUserGroupOptionEnableSection(enableSection bool) CreateUserGroupOption {
+	return func(params *createUserGroupParams) {
+		params.enableSection = enableSection
+	}
+}
+
 // CreateUserGroup creates a new user group.
 // For more information see the CreateUserGroupContext documentation.
-func (api *Client) CreateUserGroup(userGroup UserGroup) (UserGroup, error) {
-	return api.CreateUserGroupContext(context.Background(), userGroup)
+func (api *Client) CreateUserGroup(userGroup UserGroup, options ...CreateUserGroupOption) (UserGroup, error) {
+	return api.CreateUserGroupContext(context.Background(), userGroup, options...)
 }
 
 // CreateUserGroupContext creates a new user group with a custom context.
 // Slack API docs: https://api.slack.com/methods/usergroups.create
-func (api *Client) CreateUserGroupContext(ctx context.Context, userGroup UserGroup) (UserGroup, error) {
+func (api *Client) CreateUserGroupContext(ctx context.Context, userGroup UserGroup, options ...CreateUserGroupOption) (UserGroup, error) {
+	params := createUserGroupParams{}
+
+	for _, opt := range options {
+		opt(&params)
+	}
+
 	values := url.Values{
 		"token": {api.token},
 		"name":  {userGroup.Name},
 	}
 
+	if params.enableSection {
+		values["enable_section"] = []string{strconv.FormatBool(params.enableSection)}
+	}
+
 	if userGroup.TeamID != "" {
 		values["team_id"] = []string{userGroup.TeamID}
 	}
@@ -236,12 +262,20 @@ func UpdateUserGroupsOptionChannels(channels []string) UpdateUserGroupsOption {
 	}
 }
 
+// UpdateUserGroupsOptionEnableSection enable the section for the user group (default: false)
+func UpdateUserGroupsOptionEnableSection(enableSection bool) UpdateUserGroupsOption {
+	return func(params *UpdateUserGroupsParams) {
+		params.EnableSection = enableSection
+	}
+}
+
 // UpdateUserGroupsParams contains arguments for UpdateUserGroup method call
 type UpdateUserGroupsParams struct {
-	Name        string
-	Handle      string
-	Description *string
-	Channels    *[]string
+	Name          string
+	Handle        string
+	Description   *string
+	Channels      *[]string
+	EnableSection bool
 }
 
 // UpdateUserGroup will update an existing user group.
@@ -280,6 +314,10 @@ func (api *Client) UpdateUserGroupContext(ctx context.Context, userGroupID strin
 		values["channels"] = []string{strings.Join(*params.Channels, ",")}
 	}
 
+	if params.EnableSection {
+		values["enable_section"] = []string{strconv.FormatBool(params.EnableSection)}
+	}
+
 	response, err := api.userGroupRequest(ctx, "usergroups.update", values)
 	if err != nil {
 		return UserGroup{}, err
diff --git a/views.go b/views.go
index 0822d2c..5f55b53 100644
--- a/views.go
+++ b/views.go
@@ -110,6 +110,12 @@ type ModalViewRequest struct {
 	ExternalID      string           `json:"external_id,omitempty"`
 }
 
+type PublishViewContextRequest struct {
+	UserID string             `json:"user_id"`
+	View   HomeTabViewRequest `json:"view"`
+	Hash   *string            `json:"hash,omitempty"`
+}
+
 func (v *ModalViewRequest) ViewType() ViewType {
 	return v.Type
 }
@@ -131,12 +137,6 @@ type openViewRequest struct {
 	View      ModalViewRequest `json:"view"`
 }
 
-type publishViewRequest struct {
-	UserID string             `json:"user_id"`
-	View   HomeTabViewRequest `json:"view"`
-	Hash   string             `json:"hash,omitempty"`
-}
-
 type pushViewRequest struct {
 	TriggerID string           `json:"trigger_id"`
 	View      ModalViewRequest `json:"view"`
@@ -212,25 +212,18 @@ func (api *Client) OpenViewContext(
 // PublishView publishes a static view for a user.
 // For more information see the PublishViewContext documentation.
 func (api *Client) PublishView(userID string, view HomeTabViewRequest, hash string) (*ViewResponse, error) {
-	return api.PublishViewContext(context.Background(), userID, view, hash)
+	return api.PublishViewContext(context.Background(), PublishViewContextRequest{UserID: userID, View: view, Hash: &hash})
 }
 
 // PublishViewContext publishes a static view for a user with a custom context.
 // Slack API docs: https://api.slack.com/methods/views.publish
 func (api *Client) PublishViewContext(
 	ctx context.Context,
-	userID string,
-	view HomeTabViewRequest,
-	hash string,
+	req PublishViewContextRequest,
 ) (*ViewResponse, error) {
-	if userID == "" {
+	if req.UserID == "" {
 		return nil, ErrParametersMissing
 	}
-	req := publishViewRequest{
-		UserID: userID,
-		View:   view,
-		Hash:   hash,
-	}
 	encoded, err := json.Marshal(req)
 	if err != nil {
 		return nil, err
diff --git a/workflow_step.go b/workflow_step.go
deleted file mode 100644
index 747a5dc..0000000
--- a/workflow_step.go
+++ /dev/null
@@ -1,101 +0,0 @@
-package slack
-
-import (
-	"context"
-	"encoding/json"
-)
-
-const VTWorkflowStep ViewType = "workflow_step"
-
-type (
-	ConfigurationModalRequest struct {
-		ModalViewRequest
-	}
-
-	WorkflowStepCompleteResponse struct {
-		WorkflowStepEditID string                `json:"workflow_step_edit_id"`
-		Inputs             *WorkflowStepInputs   `json:"inputs,omitempty"`
-		Outputs            *[]WorkflowStepOutput `json:"outputs,omitempty"`
-	}
-
-	WorkflowStepInputElement struct {
-		Value                   string `json:"value"`
-		SkipVariableReplacement bool   `json:"skip_variable_replacement"`
-	}
-
-	WorkflowStepInputs map[string]WorkflowStepInputElement
-
-	WorkflowStepOutput struct {
-		Name  string `json:"name"`
-		Type  string `json:"type"`
-		Label string `json:"label"`
-	}
-)
-
-func NewConfigurationModalRequest(blocks Blocks, privateMetaData string, externalID string) *ConfigurationModalRequest {
-	return &ConfigurationModalRequest{
-		ModalViewRequest{
-			Type:            VTWorkflowStep,
-			Title:           nil, // slack configuration modal must not have a title!
-			Blocks:          blocks,
-			PrivateMetadata: privateMetaData,
-			ExternalID:      externalID,
-		},
-	}
-}
-
-// SaveWorkflowStepConfiguration opens a configuration modal for a workflow step.
-// For more information see the SaveWorkflowStepConfigurationContext documentation.
-func (api *Client) SaveWorkflowStepConfiguration(workflowStepEditID string, inputs *WorkflowStepInputs, outputs *[]WorkflowStepOutput) error {
-	return api.SaveWorkflowStepConfigurationContext(context.Background(), workflowStepEditID, inputs, outputs)
-}
-
-// SaveWorkflowStepConfigurationContext saves the configuration of a workflow step with a custom context.
-// Slack API docs: https://api.slack.com/methods/workflows.updateStep
-func (api *Client) SaveWorkflowStepConfigurationContext(ctx context.Context, workflowStepEditID string, inputs *WorkflowStepInputs, outputs *[]WorkflowStepOutput) error {
-	wscr := WorkflowStepCompleteResponse{
-		WorkflowStepEditID: workflowStepEditID,
-		Inputs:             inputs,
-		Outputs:            outputs,
-	}
-
-	endpoint := api.endpoint + "workflows.updateStep"
-	jsonData, err := json.Marshal(wscr)
-	if err != nil {
-		return err
-	}
-
-	response := &SlackResponse{}
-	if err := postJSON(ctx, api.httpclient, endpoint, api.token, jsonData, response, api); err != nil {
-		return err
-	}
-
-	if !response.Ok {
-		return response.Err()
-	}
-
-	return nil
-}
-
-func GetInitialOptionFromWorkflowStepInput(selection *SelectBlockElement, inputs *WorkflowStepInputs, options []*OptionBlockObject) (*OptionBlockObject, bool) {
-	if len(*inputs) == 0 {
-		return &OptionBlockObject{}, false
-	}
-	if len(options) == 0 {
-		return &OptionBlockObject{}, false
-	}
-
-	if val, ok := (*inputs)[selection.ActionID]; ok {
-		if val.SkipVariableReplacement {
-			return &OptionBlockObject{}, false
-		}
-
-		for _, option := range options {
-			if option.Value == val.Value {
-				return option, true
-			}
-		}
-	}
-
-	return &OptionBlockObject{}, false
-}
diff --git a/workflow_step_execute.go b/workflow_step_execute.go
deleted file mode 100644
index 32db9ba..0000000
--- a/workflow_step_execute.go
+++ /dev/null
@@ -1,85 +0,0 @@
-package slack
-
-import (
-	"context"
-	"encoding/json"
-)
-
-type (
-	WorkflowStepCompletedRequest struct {
-		WorkflowStepExecuteID string            `json:"workflow_step_execute_id"`
-		Outputs               map[string]string `json:"outputs"`
-	}
-
-	WorkflowStepFailedRequest struct {
-		WorkflowStepExecuteID string `json:"workflow_step_execute_id"`
-		Error                 struct {
-			Message string `json:"message"`
-		} `json:"error"`
-	}
-)
-
-type WorkflowStepCompletedRequestOption func(opt *WorkflowStepCompletedRequest) error
-
-func WorkflowStepCompletedRequestOptionOutput(outputs map[string]string) WorkflowStepCompletedRequestOption {
-	return func(opt *WorkflowStepCompletedRequest) error {
-		if len(outputs) > 0 {
-			opt.Outputs = outputs
-		}
-		return nil
-	}
-}
-
-// WorkflowStepCompleted indicates step is completed
-func (api *Client) WorkflowStepCompleted(workflowStepExecuteID string, options ...WorkflowStepCompletedRequestOption) error {
-	// More information: https://api.slack.com/methods/workflows.stepCompleted
-	r := &WorkflowStepCompletedRequest{
-		WorkflowStepExecuteID: workflowStepExecuteID,
-	}
-	for _, option := range options {
-		option(r)
-	}
-
-	endpoint := api.endpoint + "workflows.stepCompleted"
-	jsonData, err := json.Marshal(r)
-	if err != nil {
-		return err
-	}
-
-	response := &SlackResponse{}
-	if err := postJSON(context.Background(), api.httpclient, endpoint, api.token, jsonData, response, api); err != nil {
-		return err
-	}
-
-	if !response.Ok {
-		return response.Err()
-	}
-
-	return nil
-}
-
-// WorkflowStepFailed indicates step is failed
-func (api *Client) WorkflowStepFailed(workflowStepExecuteID string, errorMessage string) error {
-	// More information: https://api.slack.com/methods/workflows.stepFailed
-	r := WorkflowStepFailedRequest{
-		WorkflowStepExecuteID: workflowStepExecuteID,
-	}
-	r.Error.Message = errorMessage
-
-	endpoint := api.endpoint + "workflows.stepFailed"
-	jsonData, err := json.Marshal(r)
-	if err != nil {
-		return err
-	}
-
-	response := &SlackResponse{}
-	if err := postJSON(context.Background(), api.httpclient, endpoint, api.token, jsonData, response, api); err != nil {
-		return err
-	}
-
-	if !response.Ok {
-		return response.Err()
-	}
-
-	return nil
-}


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 61eaa92..219ff31 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -26,8 +26,6 @@ jobs:
           go-version: ${{ matrix.go }}
       - name: run test
         run: go test -v -race ./...
-        env:
-          GO111MODULE: on
   lint:
     runs-on: ubuntu-22.04
     name: lint
@@ -40,6 +38,6 @@ jobs:
         with:
           fetch-depth: 0
       - name: golangci-lint
-        uses: golangci/golangci-lint-action@aaa42aa0628b4ae2578232a66b541047968fac86 # v6.1.0
+        uses: golangci/golangci-lint-action@2226d7cb06a077cd73e56eedd38eecad18e5d837 # v6.5.0
         with:
           version: v1.52.2
diff --git a/block_action_test.go b/block_action_test.go
index 32bd0c2..bb22f45 100644
--- a/block_action_test.go
+++ b/block_action_test.go
@@ -7,13 +7,13 @@ import (
 )
 
 func TestNewActionBlock(t *testing.T) {
-
 	approveBtnTxt := NewTextBlockObject("plain_text", "Approve", false, false)
 	approveBtn := NewButtonBlockElement("", "click_me_123", approveBtnTxt)
-
 	actionBlock := NewActionBlock("test", approveBtn)
+
+	assert.Equal(t, actionBlock.BlockType(), MBTAction)
 	assert.Equal(t, string(actionBlock.Type), "actions")
 	assert.Equal(t, actionBlock.BlockID, "test")
+	assert.Equal(t, actionBlock.ID(), "test")
 	assert.Equal(t, len(actionBlock.Elements.ElementSet), 1)
-
 }
diff --git a/block_call_test.go b/block_call_test.go
index c118542..4966e3d 100644
--- a/block_call_test.go
+++ b/block_call_test.go
@@ -8,6 +8,10 @@ import (
 
 func TestNewCallBlock(t *testing.T) {
 	callBlock := NewCallBlock("ACallID")
+
+	assert.Equal(t, callBlock.BlockType(), MBTCall)
 	assert.Equal(t, string(callBlock.Type), "call")
 	assert.Equal(t, callBlock.CallID, "ACallID")
+	assert.Equal(t, callBlock.BlockID, "")
+	assert.Equal(t, callBlock.ID(), "")
 }
diff --git a/block_context_test.go b/block_context_test.go
index 20fb396..0ecfb80 100644
--- a/block_context_test.go
+++ b/block_context_test.go
@@ -7,15 +7,14 @@ import (
 )
 
 func TestNewContextBlock(t *testing.T) {
-
 	locationPinImage := NewImageBlockElement("https://api.slack.com/img/blocks/bkb_template_images/tripAgentLocationMarker.png", "Location Pin Icon")
 	textExample := NewTextBlockObject("plain_text", "Location: Central Business District", true, false)
-
 	elements := []MixedElement{locationPinImage, textExample}
-
 	contextBlock := NewContextBlock("test", elements...)
+
+	assert.Equal(t, contextBlock.BlockType(), MBTContext)
 	assert.Equal(t, string(contextBlock.Type), "context")
 	assert.Equal(t, contextBlock.BlockID, "test")
+	assert.Equal(t, contextBlock.ID(), "test")
 	assert.Equal(t, len(contextBlock.ContextElements.Elements), 2)
-
 }
diff --git a/block_divider_test.go b/block_divider_test.go
index 35e3d48..27b81aa 100644
--- a/block_divider_test.go
+++ b/block_divider_test.go
@@ -7,8 +7,10 @@ import (
 )
 
 func TestNewDividerBlock(t *testing.T) {
-
 	dividerBlock := NewDividerBlock()
-	assert.Equal(t, string(dividerBlock.Type), "divider")
 
+	assert.Equal(t, dividerBlock.BlockType(), MBTDivider)
+	assert.Equal(t, string(dividerBlock.Type), "divider")
+	assert.Equal(t, dividerBlock.BlockID, "")
+	assert.Equal(t, dividerBlock.ID(), "")
 }
diff --git a/block_element_test.go b/block_element_test.go
index d532fb7..fead400 100644
--- a/block_element_test.go
+++ b/block_element_test.go
@@ -7,17 +7,14 @@ import (
 )
 
 func TestNewImageBlockElement(t *testing.T) {
-
 	imageElement := NewImageBlockElement("https://api.slack.com/img/blocks/bkb_template_images/tripAgentLocationMarker.png", "Location Pin Icon")
 
 	assert.Equal(t, string(imageElement.Type), "image")
 	assert.Contains(t, imageElement.ImageURL, "tripAgentLocationMarker")
 	assert.Equal(t, imageElement.AltText, "Location Pin Icon")
-
 }
 
 func TestNewButtonBlockElement(t *testing.T) {
-
 	btnTxt := NewTextBlockObject("plain_text", "Next 2 Results", false, false)
 	btnElement := NewButtonBlockElement("test", "click_me_123", btnTxt)
 
@@ -25,11 +22,9 @@ func TestNewButtonBlockElement(t *testing.T) {
 	assert.Equal(t, btnElement.ActionID, "test")
 	assert.Equal(t, btnElement.Value, "click_me_123")
 	assert.Equal(t, btnElement.Text.Text, "Next 2 Results")
-
 }
 
 func TestWithStyleForButtonElement(t *testing.T) {
-
 	// these values are irrelevant in this test
 	btnTxt := NewTextBlockObject("plain_text", "Next 2 Results", false, false)
 	btnElement := NewButtonBlockElement("test", "click_me_123", btnTxt)
@@ -40,21 +35,17 @@ func TestWithStyleForButtonElement(t *testing.T) {
 	assert.Equal(t, btnElement.Style, Style("primary"))
 	btnElement.WithStyle(StyleDanger)
 	assert.Equal(t, btnElement.Style, Style("danger"))
-
 }
 
 func TestWithURLForButtonElement(t *testing.T) {
-
 	btnTxt := NewTextBlockObject("plain_text", "Next 2 Results", false, false)
 	btnElement := NewButtonBlockElement("test", "click_me_123", btnTxt)
 
 	btnElement.WithURL("https://foo.bar")
 	assert.Equal(t, btnElement.URL, "https://foo.bar")
-
 }
 
 func TestNewOptionsSelectBlockElement(t *testing.T) {
-
 	testOptionText := NewTextBlockObject("plain_text", "Option One", false, false)
 	testOption := NewOptionBlockObject("test", testOptionText, nil)
 
@@ -62,11 +53,9 @@ func TestNewOptionsSelectBlockElement(t *testing.T) {
 	assert.Equal(t, option.Type, "static_select")
 	assert.Equal(t, len(option.Options), 1)
 	assert.Nil(t, option.OptionGroups)
-
 }
 
 func TestNewOptionsGroupSelectBlockElement(t *testing.T) {
-
 	testOptionText := NewTextBlockObject("plain_text", "Option One", false, false)
 	testOption := NewOptionBlockObject("test", testOptionText, nil)
 	testLabel := NewTextBlockObject("plain_text", "Test Label", false, false)
@@ -77,11 +66,9 @@ func TestNewOptionsGroupSelectBlockElement(t *testing.T) {
 	assert.Equal(t, optGroup.Type, "static_select")
 	assert.Equal(t, optGroup.ActionID, "test")
 	assert.Equal(t, len(optGroup.OptionGroups), 1)
-
 }
 
 func TestNewOptionsMultiSelectBlockElement(t *testing.T) {
-
 	testOptionText := NewTextBlockObject("plain_text", "Option One", false, false)
 	testDescriptionText := NewTextBlockObject("plain_text", "Description One", false, false)
 	testOption := NewOptionBlockObject("test", testOptionText, testDescriptionText)
@@ -90,7 +77,6 @@ func TestNewOptionsMultiSelectBlockElement(t *testing.T) {
 	assert.Equal(t, option.Type, "static_select")
 	assert.Equal(t, len(option.Options), 1)
 	assert.Nil(t, option.OptionGroups)
-
 }
 
 func TestNewOptionsGroupMultiSelectBlockElement(t *testing.T) {
diff --git a/block_file_test.go b/block_file_test.go
index c51a93e..f52270a 100644
--- a/block_file_test.go
+++ b/block_file_test.go
@@ -8,8 +8,11 @@ import (
 
 func TestNewFileBlock(t *testing.T) {
 	fileBlock := NewFileBlock("test", "external_id", "source")
+
+	assert.Equal(t, fileBlock.BlockType(), MBTFile)
 	assert.Equal(t, string(fileBlock.Type), "file")
 	assert.Equal(t, fileBlock.BlockID, "test")
+	assert.Equal(t, fileBlock.ID(), "test")
 	assert.Equal(t, fileBlock.ExternalID, "external_id")
 	assert.Equal(t, fileBlock.Source, "source")
 }
diff --git a/block_header_test.go b/block_header_test.go
index d8ed332..6e6df8a 100644
--- a/block_header_test.go
+++ b/block_header_test.go
@@ -7,11 +7,12 @@ import (
 )
 
 func TestNewHeaderBlock(t *testing.T) {
-
 	textInfo := NewTextBlockObject("plain_text", "This is quite the header", false, false)
-
 	headerBlock := NewHeaderBlock(textInfo, HeaderBlockOptionBlockID("test_block"))
+
+	assert.Equal(t, headerBlock.BlockType(), MBTHeader)
 	assert.Equal(t, string(headerBlock.Type), "header")
+	assert.Equal(t, headerBlock.ID(), "test_block")
 	assert.Equal(t, headerBlock.BlockID, "test_block")
 	assert.Equal(t, headerBlock.Text.Type, "plain_text")
 	assert.Contains(t, headerBlock.Text.Text, "quite the header")
diff --git a/block_image_test.go b/block_image_test.go
index 3cdd203..801a142 100644
--- a/block_image_test.go
+++ b/block_image_test.go
@@ -7,14 +7,14 @@ import (
 )
 
 func TestNewImageBlock(t *testing.T) {
-
 	imageText := NewTextBlockObject("plain_text", "Location", false, false)
 	imageBlock := NewImageBlock("https://api.slack.com/img/blocks/bkb_template_images/tripAgentLocationMarker.png", "Marker", "test", imageText)
 
+	assert.Equal(t, imageBlock.BlockType(), MBTImage)
 	assert.Equal(t, string(imageBlock.Type), "image")
 	assert.Equal(t, imageBlock.Title.Type, "plain_text")
+	assert.Equal(t, imageBlock.ID(), "test")
 	assert.Equal(t, imageBlock.BlockID, "test")
 	assert.Contains(t, imageBlock.Title.Text, "Location")
 	assert.Contains(t, imageBlock.ImageURL, "tripAgentLocationMarker.png")
-
 }
diff --git a/block_input_test.go b/block_input_test.go
index 694e575..fa25f48 100644
--- a/block_input_test.go
+++ b/block_input_test.go
@@ -11,7 +11,10 @@ func TestNewInputBlock(t *testing.T) {
 	element := NewDatePickerBlockElement("action_id")
 	hint := NewTextBlockObject("plain_text", "hint", false, false)
 	inputBlock := NewInputBlock("test", label, hint, element)
+
+	assert.Equal(t, inputBlock.BlockType(), MBTInput)
 	assert.Equal(t, string(inputBlock.Type), "input")
+	assert.Equal(t, inputBlock.ID(), "test")
 	assert.Equal(t, inputBlock.BlockID, "test")
 	assert.Equal(t, inputBlock.Label, label)
 	assert.Equal(t, inputBlock.Element, element)
diff --git a/block_markdown_test.go b/block_markdown_test.go
new file mode 100644
index 0000000..45fe3c5
--- /dev/null
+++ b/block_markdown_test.go
@@ -0,0 +1,17 @@
+package slack
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestNewMarkdownBlock(t *testing.T) {
+	markdownBlock := NewMarkdownBlock("test", "*asfd*")
+
+	assert.Equal(t, markdownBlock.BlockType(), MBTMarkdown)
+	assert.Equal(t, string(markdownBlock.Type), "markdown")
+	assert.Equal(t, markdownBlock.ID(), "test")
+	assert.Equal(t, markdownBlock.BlockID, "test")
+	assert.Equal(t, markdownBlock.Text, "*asfd*")
+}
diff --git a/block_object_test.go b/block_object_test.go
index 1f4874a..b8f55a4 100644
--- a/block_object_test.go
+++ b/block_object_test.go
@@ -9,28 +9,23 @@ import (
 )
 
 func TestNewImageBlockObject(t *testing.T) {
-
 	imageObject := NewImageBlockElement("https://api.slack.com/img/blocks/bkb_template_images/beagle.png", "Beagle")
 
 	assert.Equal(t, string(imageObject.Type), "image")
 	assert.Equal(t, imageObject.AltText, "Beagle")
 	assert.Contains(t, imageObject.ImageURL, "beagle.png")
-
 }
 
 func TestNewTextBlockObject(t *testing.T) {
-
 	textObject := NewTextBlockObject("plain_text", "test", true, false)
 
 	assert.Equal(t, textObject.Type, "plain_text")
 	assert.Equal(t, textObject.Text, "test")
 	assert.True(t, textObject.Emoji, "Emoji property should be true")
 	assert.False(t, textObject.Verbatim, "Verbatim should be false")
-
 }
 
 func TestNewConfirmationBlockObject(t *testing.T) {
-
 	titleObj := NewTextBlockObject("plain_text", "testTitle", false, false)
 	textObj := NewTextBlockObject("plain_text", "testText", false, false)
 	confirmObj := NewTextBlockObject("plain_text", "testConfirm", false, false)
@@ -41,11 +36,9 @@ func TestNewConfirmationBlockObject(t *testing.T) {
 	assert.Equal(t, confirmation.Text.Text, "testText")
 	assert.Equal(t, confirmation.Confirm.Text, "testConfirm")
 	assert.Nil(t, confirmation.Deny, "Deny should be nil")
-
 }
 
 func TestWithStyleForConfirmation(t *testing.T) {
-
 	// these values are irrelevant in this test
 	titleObj := NewTextBlockObject("plain_text", "testTitle", false, false)
 	textObj := NewTextBlockObject("plain_text", "testText", false, false)
@@ -58,11 +51,9 @@ func TestWithStyleForConfirmation(t *testing.T) {
 	assert.Equal(t, confirmation.Style, Style("primary"))
 	confirmation.WithStyle(StyleDanger)
 	assert.Equal(t, confirmation.Style, Style("danger"))
-
 }
 
 func TestNewOptionBlockObject(t *testing.T) {
-
 	valTextObj := NewTextBlockObject("plain_text", "testText", false, false)
 	valDescriptionObj := NewTextBlockObject("plain_text", "testDescription", false, false)
 	optObj := NewOptionBlockObject("testOpt", valTextObj, valDescriptionObj)
@@ -70,11 +61,9 @@ func TestNewOptionBlockObject(t *testing.T) {
 	assert.Equal(t, optObj.Text.Text, "testText")
 	assert.Equal(t, optObj.Description.Text, "testDescription")
 	assert.Equal(t, optObj.Value, "testOpt")
-
 }
 
 func TestNewOptionGroupBlockElement(t *testing.T) {
-
 	labelObj := NewTextBlockObject("plain_text", "testLabel", false, false)
 	valTextObj := NewTextBlockObject("plain_text", "testText", false, false)
 	optObj := NewOptionBlockObject("testOpt", valTextObj, nil)
@@ -83,7 +72,6 @@ func TestNewOptionGroupBlockElement(t *testing.T) {
 
 	assert.Equal(t, optGroup.Label.Text, "testLabel")
 	assert.Len(t, optGroup.Options, 1, "Options should contain one element")
-
 }
 
 func TestValidateTextBlockObject(t *testing.T) {
diff --git a/block_rich_text_test.go b/block_rich_text_test.go
index 903f143..1e26f9e 100644
--- a/block_rich_text_test.go
+++ b/block_rich_text_test.go
@@ -5,6 +5,7 @@ import (
 	"testing"
 
 	"github.com/go-test/deep"
+	"github.com/stretchr/testify/assert"
 )
 
 const (
@@ -83,6 +84,15 @@ const (
 	}`
 )
 
+func TestNewRichTextBlock(t *testing.T) {
+	richTextBlock := NewRichTextBlock("test_block")
+
+	assert.Equal(t, richTextBlock.BlockType(), MBTRichText)
+	assert.Equal(t, string(richTextBlock.Type), "rich_text")
+	assert.Equal(t, richTextBlock.BlockID, "test_block")
+	assert.Equal(t, richTextBlock.ID(), "test_block")
+}
+
 func TestRichTextBlock_UnmarshalJSON(t *testing.T) {
 	cases := []struct {
 		raw      []byte
diff --git a/block_section_test.go b/block_section_test.go
index 6832967..8f8374e 100644
--- a/block_section_test.go
+++ b/block_section_test.go
@@ -7,17 +7,17 @@ import (
 )
 
 func TestNewSectionBlock(t *testing.T) {
-
 	textInfo := NewTextBlockObject("mrkdwn", "*<fakeLink.toHotelPage.com|The Ritz-Carlton New Orleans>*\n★★★★★\n$340 per night\nRated: 9.1 - Excellent", false, false)
-
 	sectionBlock := NewSectionBlock(textInfo, nil, nil, SectionBlockOptionBlockID("test_block"))
+
+	assert.Equal(t, sectionBlock.BlockType(), MBTSection)
 	assert.Equal(t, string(sectionBlock.Type), "section")
 	assert.Equal(t, sectionBlock.BlockID, "test_block")
+	assert.Equal(t, sectionBlock.ID(), "test_block")
 	assert.Equal(t, len(sectionBlock.Fields), 0)
 	assert.Nil(t, sectionBlock.Accessory)
 	assert.Equal(t, sectionBlock.Text.Type, "mrkdwn")
 	assert.Contains(t, sectionBlock.Text.Text, "New Orleans")
-
 }
 
 func TestNewBlockSectionContainsAddedTextBlockAndAccessory(t *testing.T) {
diff --git a/block_video_test.go b/block_video_test.go
index d4b791f..1ccece7 100644
--- a/block_video_test.go
+++ b/block_video_test.go
@@ -7,17 +7,17 @@ import (
 )
 
 func TestNewVideoBlock(t *testing.T) {
-
 	videoTitle := NewTextBlockObject("plain_text", "VideoTitle", false, false)
 	videoBlock := NewVideoBlock(
 		"https://example.com/example.mp4",
 		"https://example.com/thumbnail.png",
 		"alternative text", "blockID", videoTitle)
 
+	assert.Equal(t, videoBlock.Type, MBTVideo)
 	assert.Equal(t, string(videoBlock.Type), "video")
 	assert.Equal(t, videoBlock.Title.Type, "plain_text")
 	assert.Equal(t, videoBlock.BlockID, "blockID")
+	assert.Equal(t, videoBlock.ID(), "blockID")
 	assert.Contains(t, videoBlock.Title.Text, "VideoTitle")
 	assert.Contains(t, videoBlock.VideoURL, "example.mp4")
-
 }
diff --git a/conversation_test.go b/conversation_test.go
index 6b3576c..d49d92d 100644
--- a/conversation_test.go
+++ b/conversation_test.go
@@ -82,6 +82,98 @@ func TestCreateSimpleChannel(t *testing.T) {
 	assertSimpleChannel(t, channel)
 }
 
+// Shared Channel
+var sharedChannel = `{
+	"id": "C024BE91L",
+	"name": "fun",
+	"is_channel": true,
+	"created": 1360782804,
+	"creator": "U024BE7LH",
+	"is_archived": false,
+	"is_general": false,
+	"members": [
+			"U024BE7LH"
+	],
+	"is_shared": true,
+	"context_team_id": "T1ABCD2E12",
+	"is_ext_shared": true,
+	"shared_team_ids": [
+			"T07XY8FPJ5C"
+	],
+	"internal_team_ids": [],
+	"connected_team_ids": [
+			"T07XY8FPJ5C",
+			"T1ABCD2E12"
+	],
+	"connected_limited_team_ids": [],
+	"pending_connected_team_ids": [],
+	"conversation_host_id": "T07XY8FPJ5C",
+	"topic": {
+			"value": "Fun times",
+			"creator": "U024BE7LV",
+			"last_set": 1369677212
+	},
+	"purpose": {
+			"value": "This channel is for fun",
+			"creator": "U024BE7LH",
+			"last_set": 1360782804
+	},
+	"is_member": true,
+	"last_read": "1401383885.000061",
+	"unread_count": 0,
+	"unread_count_display": 0
+}`
+
+func unmarshalSharedChannel(j string) (*Channel, error) {
+	channel := &Channel{}
+	if err := json.Unmarshal([]byte(j), &channel); err != nil {
+		return nil, err
+	}
+	return channel, nil
+}
+
+func TestSharedChannel(t *testing.T) {
+	channel, err := unmarshalSharedChannel(sharedChannel)
+	assert.Nil(t, err)
+	assertSharedChannel(t, channel)
+}
+
+func assertSharedChannel(t *testing.T, channel *Channel) {
+	assertSimpleChannel(t, channel)
+	assert.Equal(t, true, channel.IsShared)
+	assert.Equal(t, true, channel.IsExtShared)
+	assert.Equal(t, "T1ABCD2E12", channel.ContextTeamID)
+	assert.Equal(t, "T07XY8FPJ5C", channel.ConversationHostID)
+	if !reflect.DeepEqual([]string{"T07XY8FPJ5C"}, channel.SharedTeamIDs) {
+		t.Fatal(ErrIncorrectResponse)
+	}
+	if !reflect.DeepEqual([]string{"T07XY8FPJ5C", "T1ABCD2E12"}, channel.ConnectedTeamIDs) {
+		t.Fatal(ErrIncorrectResponse)
+	}
+}
+
+func TestCreateSharedChannel(t *testing.T) {
+	channel := &Channel{}
+	channel.ID = "C024BE91L"
+	channel.Name = "fun"
+	channel.IsChannel = true
+	channel.Created = JSONTime(1360782804)
+	channel.Creator = "U024BE7LH"
+	channel.IsArchived = false
+	channel.IsGeneral = false
+	channel.IsMember = true
+	channel.LastRead = "1401383885.000061"
+	channel.UnreadCount = 0
+	channel.UnreadCountDisplay = 0
+	channel.IsShared = true
+	channel.IsExtShared = true
+	channel.ContextTeamID = "T1ABCD2E12"
+	channel.ConversationHostID = "T07XY8FPJ5C"
+	channel.SharedTeamIDs = []string{"T07XY8FPJ5C"}
+	channel.ConnectedTeamIDs = []string{"T07XY8FPJ5C", "T1ABCD2E12"}
+	assertSharedChannel(t, channel)
+}
+
 // Group
 var simpleGroup = `{
     "id": "G024BE91L",
diff --git a/files_test.go b/files_test.go
index 1df46aa..0fb9b2b 100644
--- a/files_test.go
+++ b/files_test.go
@@ -2,7 +2,9 @@ package slack
 
 import (
 	"bytes"
+	"context"
 	"encoding/json"
+	"fmt"
 	"io"
 	"log"
 	"net/http"
@@ -214,7 +216,7 @@ func TestUploadFileWithoutFilename(t *testing.T) {
 
 func uploadURLHandler(rw http.ResponseWriter, r *http.Request) {
 	rw.Header().Set("Content-Type", "application/json")
-	response, _ := json.Marshal(getUploadURLExternalResponse{
+	response, _ := json.Marshal(GetUploadURLExternalResponse{
 		FileID:        "RandomID",
 		UploadURL:     "http://" + serverAddr + "/abc",
 		SlackResponse: SlackResponse{Ok: true}})
@@ -228,7 +230,7 @@ func urlFileUploadHandler(rw http.ResponseWriter, r *http.Request) {
 
 func completeURLUpload(rw http.ResponseWriter, r *http.Request) {
 	rw.Header().Set("Content-Type", "application/json")
-	response, _ := json.Marshal(completeUploadExternalResponse{
+	response, _ := json.Marshal(CompleteUploadExternalResponse{
 		Files: []FileSummary{
 			{
 				ID:    "RandomID",
@@ -282,3 +284,264 @@ func TestUploadFileV2(t *testing.T) {
 		t.Errorf("Unexpected error: %s", err)
 	}
 }
+
+type mockGetUploadURLExternalHttpClient struct {
+	ResponseStatus int
+	ResponseBody   []byte
+}
+
+func (m *mockGetUploadURLExternalHttpClient) Do(req *http.Request) (*http.Response, error) {
+	if req.URL.Path != "files.getUploadURLExternal" {
+		return nil, fmt.Errorf("invalid path: %s", req.URL.Path)
+	}
+
+	return &http.Response{
+		StatusCode: m.ResponseStatus,
+		Body:       io.NopCloser(bytes.NewBuffer(m.ResponseBody)),
+	}, nil
+}
+
+func TestGetUploadURLExternalContext(t *testing.T) {
+	type testCase struct {
+		title             string
+		params            GetUploadURLExternalParameters
+		wantSlackResponse []byte
+		wantResponse      GetUploadURLExternalResponse
+		wantErr           error
+	}
+	testCases := []testCase{
+		{
+			title: "Testing with required parameters",
+			params: GetUploadURLExternalParameters{
+				FileName: "test.txt",
+				FileSize: 10,
+			},
+			wantSlackResponse: []byte(`{"ok":true,"file_id":"RandomID","upload_url":"http://test-server/abc"}`),
+			wantResponse: GetUploadURLExternalResponse{
+				FileID:    "RandomID",
+				UploadURL: "http://test-server/abc",
+				SlackResponse: SlackResponse{
+					Ok: true,
+				},
+			},
+		},
+		{
+			title: "Testing with optional parameters",
+			params: GetUploadURLExternalParameters{
+				FileSize:    10,
+				FileName:    "test.txt",
+				AltText:     "test-alt-text",
+				SnippetText: "test-snippet-text",
+			},
+			wantSlackResponse: []byte(`{"ok":true,"file_id":"RandomID","upload_url":"http://test-server/abc"}`),
+			wantResponse: GetUploadURLExternalResponse{
+				FileID:    "RandomID",
+				UploadURL: "http://test-server/abc",
+				SlackResponse: SlackResponse{
+					Ok: true,
+				},
+			},
+		},
+		{
+			title: "Testing with request error",
+			params: GetUploadURLExternalParameters{
+				FileName: "test.txt",
+				FileSize: 10,
+			},
+			wantSlackResponse: []byte(`{"ok":false,"error":"errored"}`),
+			wantErr:           fmt.Errorf("errored"),
+		},
+		{
+			title: "Testing with invalid parameters: empty file name",
+			params: GetUploadURLExternalParameters{
+				FileName: "",
+				FileSize: 10,
+			},
+			wantErr: fmt.Errorf("FileName cannot be empty"),
+		},
+		{
+			title: "Testing with invalid parameters: file size 0",
+			params: GetUploadURLExternalParameters{
+				FileName: "test.txt",
+				FileSize: 0,
+			},
+			wantErr: fmt.Errorf("FileSize cannot be 0"),
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.title, func(t *testing.T) {
+			api := &Client{
+				token: validToken,
+				httpclient: &mockGetUploadURLExternalHttpClient{
+					ResponseStatus: 200,
+					ResponseBody:   tc.wantSlackResponse,
+				},
+			}
+
+			gotResponse, err := api.GetUploadURLExternalContext(context.Background(), tc.params)
+
+			if err != nil {
+				if tc.wantErr == nil {
+					t.Fatalf("GetUploadURLExternalContext() error = %v, want nil", err)
+				}
+				if err.Error() != tc.wantErr.Error() {
+					t.Errorf("GetUploadURLExternalContext() error = %v, want %v", err, tc.wantErr)
+				}
+			} else {
+				if tc.wantErr != nil {
+					t.Fatalf("GetUploadURLExternalContext() error = nil, want %v", tc.wantErr)
+				}
+				if !reflect.DeepEqual(gotResponse, &tc.wantResponse) {
+					t.Errorf("GetUploadURLExternalContext() = %v, want %v", gotResponse, tc.wantResponse)
+				}
+			}
+		})
+	}
+}
+
+type mockCompleteUploadExternalHttpClient struct {
+	ResponseStatus int
+	ResponseBody   []byte
+}
+
+func (m *mockCompleteUploadExternalHttpClient) Do(req *http.Request) (*http.Response, error) {
+	if req.URL.Path != "files.completeUploadExternal" {
+		return nil, fmt.Errorf("invalid path: %s", req.URL.Path)
+	}
+
+	return &http.Response{
+		StatusCode: m.ResponseStatus,
+		Body:       io.NopCloser(bytes.NewBuffer(m.ResponseBody)),
+	}, nil
+}
+
+func TestCompleteUploadExternalContext(t *testing.T) {
+	type testCase struct {
+		title        string
+		params       CompleteUploadExternalParameters
+		wantResponse CompleteUploadExternalResponse
+		wantErr      bool
+	}
+	testCases := []testCase{
+		{
+			title: "Testing with required parameters",
+			params: CompleteUploadExternalParameters{
+				Files: []FileSummary{
+					{
+						ID: "ID1",
+					},
+					{
+						ID: "ID2",
+					},
+				},
+			},
+			wantResponse: CompleteUploadExternalResponse{
+				Files: []FileSummary{
+					{
+						ID: "ID1",
+					},
+					{
+						ID: "ID2",
+					},
+				},
+				SlackResponse: SlackResponse{Ok: true},
+			},
+		},
+		{
+			title: "Testing with optional parameters",
+			params: CompleteUploadExternalParameters{
+				Files: []FileSummary{
+					{
+						ID: "ID1",
+					},
+					{
+						ID:    "ID2",
+						Title: "Title2",
+					},
+				},
+				Channel:         "test-channel",
+				InitialComment:  "test-comment",
+				ThreadTimestamp: "1234567890.123456",
+			},
+			wantResponse: CompleteUploadExternalResponse{
+				Files: []FileSummary{
+					{
+						ID: "ID1",
+					},
+					{
+						ID:    "ID2",
+						Title: "Title2",
+					},
+				},
+				SlackResponse: SlackResponse{Ok: true},
+			},
+		},
+		{
+			title: "Testing with error",
+			params: CompleteUploadExternalParameters{
+				Files: []FileSummary{
+					{
+						ID: "ID1",
+					},
+				},
+			},
+			wantErr: true,
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.title, func(t *testing.T) {
+			var resBody map[string]interface{}
+			if !tc.wantErr {
+				resBody = map[string]interface{}{
+					"ok": true,
+				}
+				files := make([]map[string]string, 0)
+				for _, file := range tc.params.Files {
+					m := map[string]string{
+						"id": file.ID,
+					}
+					if file.Title != "" {
+						m["title"] = file.Title
+					}
+					files = append(files, m)
+				}
+				resBody["files"] = files
+			} else {
+				resBody = map[string]interface{}{
+					"ok":    false,
+					"error": "errored",
+				}
+			}
+
+			resBodyBytes, err := json.Marshal(resBody)
+			if err != nil {
+				t.Fatalf("failed to marshal response body: %v", err)
+			}
+
+			api := &Client{
+				token: validToken,
+				httpclient: &mockCompleteUploadExternalHttpClient{
+					ResponseStatus: 200,
+					ResponseBody:   resBodyBytes,
+				},
+			}
+
+			gotResponse, err := api.CompleteUploadExternalContext(context.Background(), tc.params)
+
+			if err != nil {
+				if !tc.wantErr {
+					t.Errorf("CompleteUploadExternalContext() error = %v, want nil", err)
+				}
+			} else {
+				if tc.wantErr {
+					t.Fatalf("CompleteUploadExternalContext() error = nil, want %v", tc.wantErr)
+				}
+				if !reflect.DeepEqual(gotResponse, &tc.wantResponse) {
+					t.Errorf("CompleteUploadExternalContext() = %v, want %v", gotResponse, tc.wantResponse)
+				}
+			}
+		})
+	}
+}
diff --git a/slackevents/inner_events_test.go b/slackevents/inner_events_test.go
index baa4e2c..b2b7094 100644
--- a/slackevents/inner_events_test.go
+++ b/slackevents/inner_events_test.go
@@ -649,67 +649,6 @@ func TestEmojiChanged(t *testing.T) {
 	}
 }
 
-func TestWorkflowStepExecute(t *testing.T) {
-	// see: https://api.slack.com/events/workflow_step_execute
-	rawE := []byte(`
-	{
-		"type":"workflow_step_execute",
-		"callback_id":"open_ticket",
-		"workflow_step":{
-			"workflow_step_execute_id":"1036669284371.19077474947.c94bcf942e047298d21f89faf24f1326",
-			"workflow_id":"123456789012345678",
-			"workflow_instance_id":"987654321098765432",
-			"step_id":"12a345bc-1a23-4567-8b90-1234a567b8c9",
-			"inputs":{
-				"example-select-input":{
-					"value": "value-two",
-					"skip_variable_replacement": false
-				}
-			},
-			"outputs":[
-			]
-		},
-		"event_ts":"1643290847.766536"
-	}
-	`)
-
-	wse := WorkflowStepExecuteEvent{}
-	err := json.Unmarshal(rawE, &wse)
-	if err != nil {
-		t.Error(err)
-	}
-
-	if wse.Type != "workflow_step_execute" {
-		t.Fail()
-	}
-	if wse.CallbackID != "open_ticket" {
-		t.Fail()
-	}
-	if wse.WorkflowStep.WorkflowStepExecuteID != "1036669284371.19077474947.c94bcf942e047298d21f89faf24f1326" {
-		t.Fail()
-	}
-	if wse.WorkflowStep.WorkflowID != "123456789012345678" {
-		t.Fail()
-	}
-	if wse.WorkflowStep.WorkflowInstanceID != "987654321098765432" {
-		t.Fail()
-	}
-	if wse.WorkflowStep.StepID != "12a345bc-1a23-4567-8b90-1234a567b8c9" {
-		t.Fail()
-	}
-	if len(*wse.WorkflowStep.Inputs) == 0 {
-		t.Fail()
-	}
-	if inputElement, ok := (*wse.WorkflowStep.Inputs)["example-select-input"]; ok {
-		if inputElement.Value != "value-two" {
-			t.Fail()
-		}
-		if inputElement.SkipVariableReplacement != false {
-			t.Fail()
-		}
-	}
-}
-
 func TestMessageMetadataPosted(t *testing.T) {
 	rawE := []byte(`
 	{
@@ -2546,12 +2485,33 @@ func TestFunctionExecutedEvent(t *testing.T) {
 			"description": "Runs sample function",
 			"type": "app",
 			"input_parameters": [
+				{
+					"type": "slack#/types/message_context",
+					"name": "message_context",
+					"description": "",
+					"title": "Message Context",
+					"is_required": true
+				},
 				{
 					"type": "slack#/types/user_id",
 					"name": "user_id",
 					"description": "Message recipient",
 					"title": "User",
 					"is_required": true
+				},
+				{
+					"type": "integer",
+					"name": "timestamp",
+					"description": "Timestamp of the event",
+					"title": "Timestamp",
+					"is_required": true
+				},
+				{
+					"type": "boolean",
+					"name": "enabled",
+					"description": "Indicates if the feature is enabled",
+					"title": "Enabled",
+					"is_required": true
 				}
 			],
 			"output_parameters": [
@@ -2568,13 +2528,32 @@ func TestFunctionExecutedEvent(t *testing.T) {
 			"date_updated": 1698947481,
 			"date_deleted": 0
 		},
-		"inputs": { "user_id": "USER12345678" },
+		"inputs": {
+			"user_id": "USER12345678",
+			"timestamp": 1698947481,
+			"enabled": true,
+			"message_context": {
+				"channel_id": "C0123456789",
+				"message_ts": "1733331835.871019"
+			}
+		},
 		"function_execution_id": "Fx1234567O9L",
 		"workflow_execution_id": "WxABC123DEF0",
 		"event_ts": "1698958075.998738",
 		"bot_access_token": "abcd-1325532282098-1322446258629-6123648410839-527a1cab3979cad288c9e20330d212cf"
 	}`
 
+	type MessageContext struct {
+		ChannelId string `json:"channel_id"`
+		MessageTs string `json:"message_ts"`
+	}
+	type TestInputs struct {
+		UserId    string         `json:"user_id"`
+		Timestamp int            `json:"timestamp"`
+		Enabled   bool           `json:"enabled"`
+		Context   MessageContext `json:"message_context"`
+	}
+
 	var event FunctionExecutedEvent
 	if err := json.Unmarshal([]byte(jsonStr), &event); err != nil {
 		t.Errorf("Failed to unmarshal FunctionExecutedEvent: %v", err)
@@ -2591,6 +2570,21 @@ func TestFunctionExecutedEvent(t *testing.T) {
 	if event.FunctionExecutionID != "Fx1234567O9L" {
 		t.Fail()
 	}
+
+	inputStr, err := json.Marshal(event.Inputs)
+	if err != nil {
+		t.Errorf("Failed to marshal Inputs of FunctionExecutedEvent: %v", err)
+	}
+	testInputs := new(TestInputs)
+	err = json.Unmarshal(inputStr, testInputs)
+	if err != nil {
+		t.Errorf("Failed to unmarshal Inputs of FunctionExecutedEvent: %v", err)
+	}
+	assert.Equal(t, "USER12345678", testInputs.UserId)
+	assert.Equal(t, 1698947481, testInputs.Timestamp)
+	assert.True(t, testInputs.Enabled)
+	assert.Equal(t, "C0123456789", testInputs.Context.ChannelId)
+	assert.Equal(t, "1733331835.871019", testInputs.Context.MessageTs)
 }
 
 func TestInviteRequestedEvent(t *testing.T) {
diff --git a/slacktest/funcs.go b/slacktest/funcs.go
index d083af2..7be54fe 100644
--- a/slacktest/funcs.go
+++ b/slacktest/funcs.go
@@ -15,11 +15,9 @@ func (sts *Server) queueForWebsocket(s, hubname string) {
 	channel, err := getHubForServer(hubname)
 	if err != nil {
 		log.Printf("Unable to get server's channels: %s", err.Error())
+	} else {
+		channel.sent <- s
 	}
-	sts.seenOutboundMessages.Lock()
-	sts.seenOutboundMessages.messages = append(sts.seenOutboundMessages.messages, s)
-	sts.seenOutboundMessages.Unlock()
-	channel.sent <- s
 }
 
 func handlePendingMessages(c *websocket.Conn, hubname string) {
@@ -43,9 +41,7 @@ func (sts *Server) postProcessMessage(m, hubname string) {
 		log.Printf("Unable to get server's channels: %s", err.Error())
 		return
 	}
-	sts.seenInboundMessages.Lock()
-	sts.seenInboundMessages.messages = append(sts.seenInboundMessages.messages, m)
-	sts.seenInboundMessages.Unlock()
+	sts.seenInboundMessages.observe(m)
 	// send to firehose
 	channel.seen <- m
 }
diff --git a/slacktest/server.go b/slacktest/server.go
index 6d98494..ba01939 100644
--- a/slacktest/server.go
+++ b/slacktest/server.go
@@ -106,28 +106,20 @@ func (sts *Server) GetGroups() []slack.Group {
 
 // GetSeenInboundMessages returns all messages seen via websocket excluding pings
 func (sts *Server) GetSeenInboundMessages() []string {
-	sts.seenInboundMessages.RLock()
-	m := sts.seenInboundMessages.messages
-	sts.seenInboundMessages.RUnlock()
-	return m
+	return sts.seenInboundMessages.get()
 }
 
 // GetSeenOutboundMessages returns all messages seen via websocket excluding pings
 func (sts *Server) GetSeenOutboundMessages() []string {
-	sts.seenOutboundMessages.RLock()
-	m := sts.seenOutboundMessages.messages
-	sts.seenOutboundMessages.RUnlock()
-	return m
+	return sts.seenOutboundMessages.get()
 }
 
 // SawOutgoingMessage checks if a message was sent to connected websocket clients
 func (sts *Server) SawOutgoingMessage(msg string) bool {
-	sts.seenOutboundMessages.RLock()
-	defer sts.seenOutboundMessages.RUnlock()
-	for _, m := range sts.seenOutboundMessages.messages {
+	for _, m := range sts.seenOutboundMessages.get() {
 		evt := &slack.MessageEvent{}
-		jErr := json.Unmarshal([]byte(m), evt)
-		if jErr != nil {
+		err := json.Unmarshal([]byte(m), evt)
+		if err != nil {
 			continue
 		}
 
@@ -135,17 +127,16 @@ func (sts *Server) SawOutgoingMessage(msg string) bool {
 			return true
 		}
 	}
+
 	return false
 }
 
 // SawMessage checks if an incoming message was seen
 func (sts *Server) SawMessage(msg string) bool {
-	sts.seenInboundMessages.RLock()
-	defer sts.seenInboundMessages.RUnlock()
-	for _, m := range sts.seenInboundMessages.messages {
+	for _, m := range sts.seenInboundMessages.get() {
 		evt := &slack.MessageEvent{}
-		jErr := json.Unmarshal([]byte(m), evt)
-		if jErr != nil {
+		err := json.Unmarshal([]byte(m), evt)
+		if err != nil {
 			// This event isn't a message event so we'll skip it
 			continue
 		}
@@ -153,6 +144,7 @@ func (sts *Server) SawMessage(msg string) bool {
 			return true
 		}
 	}
+
 	return false
 }
 
@@ -184,11 +176,14 @@ func (sts *Server) SendMessageToBot(channel, msg string) {
 	m.User = defaultNonBotUserID
 	m.Text = fmt.Sprintf("<@%s> %s", sts.BotID, msg)
 	m.Timestamp = fmt.Sprintf("%d", time.Now().Unix())
-	j, jErr := json.Marshal(m)
-	if jErr != nil {
-		log.Printf("Unable to marshal message for bot: %s", jErr.Error())
+
+	j, err := json.Marshal(m)
+	if err != nil {
+		log.Printf("Unable to marshal message for bot: %s", err.Error())
 		return
 	}
+
+	sts.seenOutboundMessages.observe(string(j))
 	go sts.queueForWebsocket(string(j), sts.ServerAddr)
 }
 
@@ -200,11 +195,14 @@ func (sts *Server) SendDirectMessageToBot(msg string) {
 	m.User = defaultNonBotUserID
 	m.Text = msg
 	m.Timestamp = fmt.Sprintf("%d", time.Now().Unix())
-	j, jErr := json.Marshal(m)
-	if jErr != nil {
-		log.Printf("Unable to marshal private message for bot: %s", jErr.Error())
+
+	j, err := json.Marshal(m)
+	if err != nil {
+		log.Printf("Unable to marshal private message for bot: %s", err.Error())
 		return
 	}
+
+	sts.seenOutboundMessages.observe(string(j))
 	go sts.queueForWebsocket(string(j), sts.ServerAddr)
 }
 
@@ -216,18 +214,21 @@ func (sts *Server) SendMessageToChannel(channel, msg string) {
 	m.Text = msg
 	m.User = defaultNonBotUserID
 	m.Timestamp = fmt.Sprintf("%d", time.Now().Unix())
+
 	j, jErr := json.Marshal(m)
 	if jErr != nil {
 		log.Printf("Unable to marshal message for channel: %s", jErr.Error())
 		return
 	}
-	stringMsg := string(j)
-	go sts.queueForWebsocket(stringMsg, sts.ServerAddr)
+
+	sts.seenOutboundMessages.observe(string(j))
+	go sts.queueForWebsocket(string(j), sts.ServerAddr)
 }
 
 // SendToWebsocket send `s` as is to connected clients.
 // This is useful for sending your own custom json to the websocket
 func (sts *Server) SendToWebsocket(s string) {
+	sts.seenOutboundMessages.observe(s)
 	go sts.queueForWebsocket(s, sts.ServerAddr)
 }
 
diff --git a/slacktest/server_test.go b/slacktest/server_test.go
index 45f6208..01a603a 100644
--- a/slacktest/server_test.go
+++ b/slacktest/server_test.go
@@ -27,7 +27,7 @@ func TestCustomNewServer(t *testing.T) {
 
 func TestServerSendMessageToChannel(t *testing.T) {
 	s := NewTestServer()
-	go s.Start()
+	s.Start()
 	s.SendMessageToChannel("C123456789", "some text")
 	time.Sleep(2 * time.Second)
 	assert.True(t, s.SawOutgoingMessage("some text"))
@@ -36,7 +36,7 @@ func TestServerSendMessageToChannel(t *testing.T) {
 
 func TestServerSendMessageToBot(t *testing.T) {
 	s := NewTestServer()
-	go s.Start()
+	s.Start()
 	s.SendMessageToBot("C123456789", "some text")
 	expectedMsg := fmt.Sprintf("<@%s> %s", s.BotID, "some text")
 	time.Sleep(2 * time.Second)
@@ -46,7 +46,7 @@ func TestServerSendMessageToBot(t *testing.T) {
 
 func TestBotDirectMessageBotHandler(t *testing.T) {
 	s := NewTestServer()
-	go s.Start()
+	s.Start()
 	s.SendDirectMessageToBot("some text")
 	expectedMsg := "some text"
 	time.Sleep(2 * time.Second)
@@ -55,14 +55,14 @@ func TestBotDirectMessageBotHandler(t *testing.T) {
 }
 
 func TestGetSeenOutboundMessages(t *testing.T) {
-	maxWait := 5 * time.Second
 	s := NewTestServer()
-	go s.Start()
+	s.Start()
 
 	s.SendMessageToChannel("foo", "should see this message")
-	time.Sleep(maxWait)
+
 	seenOutbound := s.GetSeenOutboundMessages()
-	assert.True(t, len(seenOutbound) > 0)
+	assert.Len(t, seenOutbound, 1)
+
 	hadMessage := false
 	for _, msg := range seenOutbound {
 		var m = slack.Message{}
@@ -79,7 +79,7 @@ func TestGetSeenOutboundMessages(t *testing.T) {
 func TestGetSeenInboundMessages(t *testing.T) {
 	maxWait := 5 * time.Second
 	s := NewTestServer()
-	go s.Start()
+	s.Start()
 
 	api := slack.New("ABCDEFG", slack.OptionAPIURL(s.GetAPIURL()))
 	rtm := api.NewRTM()
@@ -108,7 +108,7 @@ func TestGetSeenInboundMessages(t *testing.T) {
 func TestSendChannelInvite(t *testing.T) {
 	maxWait := 5 * time.Second
 	s := NewTestServer()
-	go s.Start()
+	s.Start()
 	rtm := s.GetTestRTMInstance()
 	go rtm.ManageConnection()
 	evChan := make(chan (slack.Channel), 1)
@@ -137,7 +137,7 @@ func TestSendChannelInvite(t *testing.T) {
 func TestSendGroupInvite(t *testing.T) {
 	maxWait := 5 * time.Second
 	s := NewTestServer()
-	go s.Start()
+	s.Start()
 	rtm := s.GetTestRTMInstance()
 	go rtm.ManageConnection()
 	evChan := make(chan (slack.Channel), 1)
@@ -165,12 +165,12 @@ func TestSendGroupInvite(t *testing.T) {
 
 func TestServerSawMessage(t *testing.T) {
 	s := NewTestServer()
-	go s.Start()
+	s.Start()
 	assert.False(t, s.SawMessage("foo"), "should not have seen any message")
 }
 
 func TestServerSawOutgoingMessage(t *testing.T) {
 	s := NewTestServer()
-	go s.Start()
+	s.Start()
 	assert.False(t, s.SawOutgoingMessage("foo"), "should not have seen any message")
 }
diff --git a/slacktest/types.go b/slacktest/types.go
index 4c90ed9..c6e776f 100644
--- a/slacktest/types.go
+++ b/slacktest/types.go
@@ -40,15 +40,29 @@ type hub struct {
 }
 
 type messageChannels struct {
-	seen   chan (string)
-	sent   chan (string)
-	posted chan (slack.Message)
+	seen   chan string
+	sent   chan string
+	posted chan slack.Message
 }
 type messageCollection struct {
 	sync.RWMutex
 	messages []string
 }
 
+func (mc *messageCollection) observe(msg string) {
+	mc.Lock()
+	defer mc.Unlock()
+	mc.messages = append(mc.messages, msg)
+}
+
+func (mc *messageCollection) get() []string {
+	mc.RLock()
+	defer mc.RUnlock()
+
+	m := mc.messages
+	return m
+}
+
 type serverChannels struct {
 	sync.RWMutex
 	channels []slack.Channel
@@ -68,7 +82,7 @@ type Server struct {
 	BotName              string
 	BotID                string
 	ServerAddr           string
-	SeenFeed             chan (string)
+	SeenFeed             chan string
 	channels             *serverChannels
 	groups               *serverGroups
 	seenInboundMessages  *messageCollection
diff --git a/workflow_step_execute_test.go b/workflow_step_execute_test.go
deleted file mode 100644
index 60c21c8..0000000
--- a/workflow_step_execute_test.go
+++ /dev/null
@@ -1,35 +0,0 @@
-package slack
-
-import (
-	"encoding/json"
-	"net/http"
-	"testing"
-)
-
-func workflowStepHandler(rw http.ResponseWriter, r *http.Request) {
-	rw.Header().Set("Content-Type", "application/json")
-	response, _ := json.Marshal(SlackResponse{
-		Ok: true,
-	})
-	rw.Write(response)
-}
-
-func TestWorkflowStepCompleted(t *testing.T) {
-	http.HandleFunc("/workflows.stepCompleted", workflowStepHandler)
-	once.Do(startServer)
-	api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/"))
-
-	if err := api.WorkflowStepCompleted("executeID"); err != nil {
-		t.Errorf("Unexpected error: %s", err)
-	}
-}
-
-func TestWorkflowStepFailed(t *testing.T) {
-	http.HandleFunc("/workflows.stepFailed", workflowStepHandler)
-	once.Do(startServer)
-	api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/"))
-
-	if err := api.WorkflowStepFailed("executeID", "error message"); err != nil {
-		t.Errorf("Unexpected error: %s", err)
-	}
-}
diff --git a/workflow_step_test.go b/workflow_step_test.go
deleted file mode 100644
index f2d2eb7..0000000
--- a/workflow_step_test.go
+++ /dev/null
@@ -1,190 +0,0 @@
-package slack
-
-import (
-	"testing"
-
-	"github.com/google/go-cmp/cmp"
-)
-
-const (
-	IDExampleSelectInput = "ae9642ae-a9ef-4394-904b-a5c7a83bf4a6"
-	IDSelectOptionBlock  = "832bf7af-22ea-4acb-82e3-a0cc3722052b"
-)
-
-func TestNewConfigurationModalRequest(t *testing.T) {
-	blocks := configModalBlocks()
-	privateMetaData := "An optional string that will be sent to your app in view_submission and block_actions events. Max length of 3000 characters."
-	externalID := "c4baf441-fbc1-4131-b349-7c8df0ae7df6"
-
-	result := NewConfigurationModalRequest(blocks, privateMetaData, externalID)
-
-	if result.ModalViewRequest.Title != nil {
-		t.Fail()
-	}
-	if result.PrivateMetadata != privateMetaData {
-		t.Fail()
-	}
-	if result.ExternalID != externalID {
-		t.Fail()
-	}
-}
-
-func TestGetInitialOptionFromWorkflowStepInput(t *testing.T) {
-	options, testOption := createOptionBlockObjects()
-	selection := createSelection(options)
-
-	scenarios := []struct {
-		options        []*OptionBlockObject
-		inputs         *WorkflowStepInputs
-		expectedResult *OptionBlockObject
-		expectedFlag   bool
-	}{
-		{
-			options:        options,
-			inputs:         createWorkflowStepInputs1(),
-			expectedResult: &OptionBlockObject{},
-			expectedFlag:   false,
-		},
-		{
-			options:        []*OptionBlockObject{},
-			inputs:         createWorkflowStepInputs4(testOption.Value),
-			expectedResult: &OptionBlockObject{},
-			expectedFlag:   false,
-		},
-		{
-			options:        options,
-			inputs:         createWorkflowStepInputs2(),
-			expectedResult: &OptionBlockObject{},
-			expectedFlag:   false,
-		},
-		{
-			options:        options,
-			inputs:         createWorkflowStepInputs3(),
-			expectedResult: &OptionBlockObject{},
-			expectedFlag:   false,
-		},
-		{
-			options:        options,
-			inputs:         createWorkflowStepInputs4(testOption.Value),
-			expectedResult: testOption,
-			expectedFlag:   true,
-		},
-	}
-
-	for _, scenario := range scenarios {
-		result, ok := GetInitialOptionFromWorkflowStepInput(selection, scenario.inputs, scenario.options)
-		if ok != scenario.expectedFlag {
-			t.Fail()
-		}
-
-		if !cmp.Equal(result, scenario.expectedResult) {
-			t.Fail()
-		}
-	}
-}
-
-func createOptionBlockObjects() ([]*OptionBlockObject, *OptionBlockObject) {
-	var options []*OptionBlockObject
-	options = append(
-		options,
-		NewOptionBlockObject("one", NewTextBlockObject("plain_text", "One", false, false), nil),
-	)
-
-	option2 := NewOptionBlockObject("two", NewTextBlockObject("plain_text", "Two", false, false), nil)
-	options = append(
-		options,
-		option2,
-	)
-
-	options = append(
-		options,
-		NewOptionBlockObject("three", NewTextBlockObject("plain_text", "Three", false, false), nil),
-	)
-
-	return options, option2
-}
-
-func createSelection(options []*OptionBlockObject) *SelectBlockElement {
-	return NewOptionsSelectBlockElement(
-		"static_select",
-		NewTextBlockObject("plain_text", "your choice", false, false),
-		IDExampleSelectInput,
-		options...,
-	)
-}
-
-func configModalBlocks() Blocks {
-	headerText := NewTextBlockObject("mrkdwn", "Hello World!\nThis is your workflow step app configuration view", false, false)
-	headerSection := NewSectionBlock(headerText, nil, nil)
-
-	options, _ := createOptionBlockObjects()
-
-	selection := createSelection(options)
-
-	inputBlock := NewInputBlock(
-		IDSelectOptionBlock,
-		NewTextBlockObject("plain_text", "Select an option", false, false),
-		NewTextBlockObject("plain_text", "Hint", false, false),
-		selection,
-	)
-
-	blocks := Blocks{
-		BlockSet: []Block{
-			headerSection,
-			inputBlock,
-		},
-	}
-
-	return blocks
-}
-
-func createWorkflowStepInputs1() *WorkflowStepInputs {
-	return &WorkflowStepInputs{}
-}
-
-func createWorkflowStepInputs2() *WorkflowStepInputs {
-	return &WorkflowStepInputs{
-		"test": WorkflowStepInputElement{
-			Value:                   "random-string",
-			SkipVariableReplacement: false,
-		},
-		"123-test": WorkflowStepInputElement{
-			Value:                   "another-string",
-			SkipVariableReplacement: false,
-		},
-	}
-}
-
-func createWorkflowStepInputs3() *WorkflowStepInputs {
-	return &WorkflowStepInputs{
-		"test": WorkflowStepInputElement{
-			Value:                   "random-string",
-			SkipVariableReplacement: false,
-		},
-		"123-test": WorkflowStepInputElement{
-			Value:                   "another-string",
-			SkipVariableReplacement: false,
-		},
-		IDExampleSelectInput: WorkflowStepInputElement{
-			Value:                   "lorem-ipsum",
-			SkipVariableReplacement: true,
-		},
-	}
-}
-
-func createWorkflowStepInputs4(optionValue string) *WorkflowStepInputs {
-	return &WorkflowStepInputs{
-		"test": WorkflowStepInputElement{
-			Value:                   "random-string",
-			SkipVariableReplacement: false,
-		},
-		"123-test": WorkflowStepInputElement{
-			Value:                   "another-string",
-			SkipVariableReplacement: false,
-		},
-		IDExampleSelectInput: WorkflowStepInputElement{
-			Value:                   optionValue,
-			SkipVariableReplacement: false,
-		},
-	}
-}