Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/drop-node-18.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@slack/bolt": major
---

Drop Node.js 18 support. The minimum required runtime is now Node.js 20 (npm >=9.6.4).
5 changes: 5 additions & 0 deletions .changeset/drop-workflow-steps.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@slack/bolt": major
---

Remove deprecated `WorkflowStep` class and all associated types, middleware, and utilities. Use `CustomFunction` and `app.function()` instead.
5 changes: 5 additions & 0 deletions .changeset/improve-error-handling.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@slack/bolt": minor
---

Improve error handling by leveraging `@slack/web-api` v8 error classes. Authorization errors are now properly wrapped (preserving the original error's class identity). Default error handlers log richer details for web-api errors (API error codes, rate limit durations, HTTP status codes). Re-export `SlackError`, `WebAPIPlatformError`, `WebAPIRequestError`, `WebAPIHTTPError`, and `WebAPIRateLimitedError` from the package entry point.
7 changes: 7 additions & 0 deletions .changeset/use-native-fetch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@slack/bolt": major
---

Replace axios with native fetch for response_url calls. Remove `agent` and `clientTls` options from `AppOptions` — use `clientOptions.fetch` to provide a custom fetch implementation for proxy/TLS needs. Add a `dispatcher` option to `SocketModeReceiver` for proxy/TLS configuration in socket mode.

`respond()` now throws a `RespondError` when the `response_url` request returns a non-2xx status (restoring the throw-on-failure behavior that axios provided) and resolves to a `Response` on success rather than an axios response object.
1 change: 0 additions & 1 deletion .github/workflows/ci-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ jobs:
fail-fast: false
matrix:
node-version:
- 18.x
- 20.x
- 22.x
- 24.x
Expand Down
2 changes: 0 additions & 2 deletions .github/workflows/samples.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ jobs:
fail-fast: false
matrix:
node-version:
- 18.x
- 20.x
- 22.x
- 24.x
Expand Down Expand Up @@ -48,7 +47,6 @@ jobs:
fail-fast: false
matrix:
node-version:
- 18.x
- 20.x
- 22.x
- 24.x
Expand Down
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ Listeners receive a single object with these properties (availability depends on

## Code Conventions

- **TypeScript** throughout. Compiler options in `tsconfig.json` (extends `@tsconfig/node18`, CommonJS output).
- **TypeScript** throughout. Compiler options in `tsconfig.json` (extends `@tsconfig/node20`, CommonJS output).
- **Biome** for formatting and linting. Configuration in `biome.json`.
- **Testing:** See the Testing section below for test frameworks and conventions.

Expand All @@ -125,7 +125,7 @@ Listeners receive a single object with these properties (availability depends on
4. **Don't duplicate `package.json` values** -- reference it for versions, engines, and dependency lists.
5. **Don't add `WorkflowStep` code** -- it is deprecated. Use `CustomFunction` and `app.function()` instead.
6. **Build before running unit tests directly** -- `npm test` handles this automatically, but `npm run test:unit` requires a build to exist first.
7. **Keep the Receiver abstraction clean** -- receivers should only handle transport concerns (ingesting events, sending ack responses). Business logic belongs in middleware and listeners.
7. **Keep the Receiver abstraction clean** -- receivers should only handle transport concerns (ingesting events, sending ack responses). Business logic belongs in middleware and listeners. Do **not** add receiver-specific options to `AppOptions`: configuration that only a particular receiver understands (e.g. proxy/TLS via an undici `dispatcher` for Socket Mode) must be set on the receiver instance, which is then passed to `App` via the `receiver` option. The long-term goal is for `App` to be fully agnostic of which receiver is in use, so avoid growing the App's surface with transport concerns.
8. **Prefer middleware for cross-cutting concerns** -- authorization, logging, validation, and feature-level request handling (like `Assistant`) all use the middleware pattern.
9. **TypeScript types are part of the API** -- changes to exported types are breaking changes. Add type tests for new public types.
10. **Every listener type needs four things:** type definitions, built-in middleware matchers, an App method, and tests.
Expand Down
296 changes: 296 additions & 0 deletions docs/english/migration/migration-v5.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,296 @@
---
sidebar_label: Migrating to v5
---

# Migrating @slack/bolt from v4 to v5

_Minimum Node.js version: 20_

Bolt for JS v5 follows the Node Slack SDK's shift from axios to the native [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API). It also removes the deprecated Workflow Steps feature (retired by Slack in September 2024) and raises the minimum Node.js version to 20.

All internal `@slack/*` Node Slack SDK dependencies have been bumped to their next major versions. See the section below on [Upgrading the Node Slack SDK dependencies]({#node-slack-sdk-dependencies}) for information and migration instructions.

If your app doesn't use proxy/TLS configuration or inspect `respond()` utility function return values, this upgrade is likely a version bump and done.

---

## Breaking Changes {#breaking-changes}

### We've raised the minimum Node.js version to 20 {#minimum-node-version}

We've dropped support for Node.js 18. Node.js 20 or later is required.

---

### TypeScript consumers target ES2022 or later {#typescript-es2022-target}

Bolt v5 depends on v8 of the `@slack/web-api` Node Slack SDK package, whose error classes use the ES2022 [`Error(message, { cause })`](https://developer.mozilla.org/en-US/docs/Web/API/Error/Error) constructor. Its type definitions reference the global `ErrorOptions` type.

If your project's `tsconfig.json` targets an older ECMAScript library and does not set `skipLibCheck`, your build may fail after upgrading with an error like:

```text
error TS2304: Cannot find name 'ErrorOptions'.
```

To fix it we recommend raising your compilation target to `es2022` or later, as it aligns with the Node.js 20 baseline.

Alternatively, set `"skipLibCheck": true` to skip type-checking of dependency declaration files. Note that Bolt's own build and its sample apps already extend [`@tsconfig/node20`](https://www.npmjs.com/package/@tsconfig/node20), which targets `es2022`, so projects based on those templates are unaffected.

---

### We've removed the `agent` and `clientTls` options from the `AppOptions` interface {#removed-agent-clienttls}

You should configure transport via the `clientOptions.fetch` option or use the Node.js built-in proxy support.

**Before (v4):**

```typescript
import { App } from '@slack/bolt';
import { HttpsProxyAgent } from 'https-proxy-agent';
import fs from 'node:fs';

const app = new App({
token: process.env.SLACK_BOT_TOKEN,
signingSecret: process.env.SLACK_SIGNING_SECRET,
agent: new HttpsProxyAgent('http://corporate.proxy:8080'),
clientTls: {
cert: fs.readFileSync('/path/to/client-cert.pem'),
key: fs.readFileSync('/path/to/client-key.pem'),
},
});
```

#### Preferred: Built-in proxy support {#built-in-proxy-support}

Node.js can read proxy environment variables natively via [`http.setGlobalProxyFromEnv()`](https://nodejs.org/docs/latest/api/http.html#httpsetglobalproxyfromenvproxyenv). Call it once at startup and `globalThis.fetch` routes through your proxy automatically, no extra packages needed.

##### Option A: programmatically call once at startup {#programmatically-call-startup}

```typescript
import http from 'node:http';
import { App } from '@slack/bolt';

http.setGlobalProxyFromEnv();

const app = new App({
token: process.env.SLACK_BOT_TOKEN,
signingSecret: process.env.SLACK_SIGNING_SECRET,
});
```

##### Option B: use an environment variable {#use-an-environment-variable}

```bash
NODE_USE_ENV_PROXY=1 HTTPS_PROXY=http://corporate.proxy:8080 node app.js
```

```typescript
import { App } from '@slack/bolt';

// No proxy configuration needed — globalThis.fetch respects the environment
const app = new App({
token: process.env.SLACK_BOT_TOKEN,
signingSecret: process.env.SLACK_SIGNING_SECRET,
});
```

#### Alternative: use an undici `Dispatcher` instance for proxy and TLS {#undici-dispatcher-proxy-tls}

If you need per-client configuration, use the `clientOptions.fetch` option with an [undici](https://undici.nodejs.org/) `Dispatcher` instance:

```typescript
import { App } from '@slack/bolt';
import { fetch, ProxyAgent, Agent } from 'undici';
import fs from 'node:fs';

// Proxy only
const proxyDispatcher = new ProxyAgent('http://corporate.proxy:8080');

// TLS only
const tlsDispatcher = new Agent({
connect: {
cert: fs.readFileSync('/path/to/client-cert.pem'),
key: fs.readFileSync('/path/to/client-key.pem'),
ca: fs.readFileSync('/path/to/ca-cert.pem'),
},
});

const app = new App({
token: process.env.SLACK_BOT_TOKEN,
signingSecret: process.env.SLACK_SIGNING_SECRET,
clientOptions: {
fetch: (url, init) => fetch(url, { ...init, dispatcher: tlsDispatcher }),
},
});
```

---

### We've updated the `SocketModeReceiver` class to accept a `dispatcher` option instead of proxy agents {#socketmodereceiver-dispatcher}

The `SocketModeReceiver` class now accepts a `dispatcher` option for unified proxy and TLS configuration of both the WebSocket connection and HTTP API calls. The `dispatcher` option accepts any undici-compatible `Dispatcher` instance.

**Before (v4):**

```typescript
import { App } from '@slack/bolt';
import { HttpsProxyAgent } from 'https-proxy-agent';

const app = new App({
token: process.env.SLACK_BOT_TOKEN,
appToken: process.env.SLACK_APP_TOKEN,
socketMode: true,
agent: new HttpsProxyAgent('http://corporate.proxy:8080'),
});
```

#### Preferred: Use built-in proxy support {#socketmode-built-in-proxy-support}

Node.js can read proxy environment variables natively via [`http.setGlobalProxyFromEnv()`](https://nodejs.org/docs/latest/api/http.html#httpsetglobalproxyfromenvproxyenv). Call it once at startup and both the WebSocket connection and API calls route through your proxy automatically.

```typescript
import http from 'node:http';
import { App } from '@slack/bolt';

http.setGlobalProxyFromEnv();

const app = new App({
token: process.env.SLACK_BOT_TOKEN,
appToken: process.env.SLACK_APP_TOKEN,
socketMode: true,
});
```

#### Alternative: Use an undici `Dispatcher` instance for proxy and TLS {#socketmode-undici-dispatcher-proxy-tls}

If you need per-client configuration, pass a `dispatcher` option to both the `SocketModeReceiver` class (for the WebSocket connection) and the `clientOptions.fetch` option (for the app's internal `WebClient` class API calls):

```typescript
import { App, SocketModeReceiver } from '@slack/bolt';
import { fetch, ProxyAgent } from 'undici';

const dispatcher = new ProxyAgent('http://corporate.proxy:8080');

const receiver = new SocketModeReceiver({
appToken: process.env.SLACK_APP_TOKEN,
dispatcher,
});

const app = new App({
token: process.env.SLACK_BOT_TOKEN,
receiver,
clientOptions: {
fetch: (url, init) => fetch(url, { ...init, dispatcher }),
},
});
```

---

### We've removed Workflow Steps from Apps {#removed-workflow-steps}

The Workflow Steps from Apps feature [was retired in September 2024](/changelog/2023-08-workflow-steps-from-apps-step-back/). The `WorkflowStep` class, the `app.step()` method, and all related types have been deleted from Bolt for JS. You should remove any imports of the `WorkflowStep` class or the `WorkflowStepEdit` type.

Use the `app.function()` method with custom functions instead.

---

### We've updated the `respond()` utility function to return a native `Response` object {#respond-returns-fetch-response}

The `respond()` utility function now uses native fetch internally. If you inspect the return value, the shape has changed from an `AxiosResponse` object to a standard Fetch [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response) object.

**Before (v4):**

```typescript
app.command('/ticket', async ({ command, ack, respond }) => {
await ack();
const result = await respond(`Ticket created: ${command.text}`);
// result was an AxiosResponse
console.log(result.status); // 200
console.log(result.data); // response body (pre-parsed)
console.log(result.headers); // AxiosHeaders object
});
```

**After (v5):**

```typescript
app.command('/ticket', async ({ command, ack, respond }) => {
await ack();
const result = await respond(`Ticket created: ${command.text}`);
// result is a native Fetch Response
console.log(result.status); // 200
console.log(await result.text()); // response body (call .text() or .json())
console.log(result.headers); // Headers object
});
```

If you're only calling `await respond(...)` without using the return value (the common case), no changes are needed.

---

### We've upgraded the Node Slack SDK dependencies {#node-slack-sdk-dependencies}

All Node Slack SDK packages have been bumped to their next major versions.

* The `logger` package has been updated to v5.
* The `oauth` package has been updated to v4.
* The `types` package has been updated to v3.

Three packages have more substantial breaking changes:

* The `socket-mode` package has been updated to v3. See the guide on [migrating @slack/socket-mode from v2 to v3](/tools/node-slack-sdk/migration/socket-mode/migrating-socket-mode-package-to-v3) for handling breaking changes.
* The `web-api` package has been updated to v8. See the guide on [migrating @slack/web-api from v7 to v8](/tools/node-slack-sdk/migration/web-api/migrating-web-api-package-to-v8) for handling breaking changes.

---

### We've improved error handling throughout {#error-handling}

This version of Bolt leverages the new error classes from v8 of the `@slack/web-api` Node Slack SDK package. Errors thrown by the internal `WebClient` class are now proper `Error` subclasses.

**Before (v4):**

```typescript
import { App } from '@slack/bolt';

const app = new App({ /* ... */ });

app.error(async ({ error }) => {
if ('code' in error && error.code === 'slack_webapi_platform_error') {
console.log((error as any).data?.error);
}
});
```

**After (v5):**

```typescript
import { App } from '@slack/bolt';
import { WebAPIPlatformError, WebAPIRequestError } from '@slack/web-api';

const app = new App({ /* ... */ });

app.error(async ({ error }) => {
if (error instanceof WebAPIPlatformError) {
console.log(error.data.error); // e.g. 'channel_not_found'
} else if (error instanceof WebAPIRequestError) {
console.log(error.cause); // the underlying fetch/network error
}
});
```

---

## New Features {#new-features}

### We've added a `dispatcher` option to the `SocketModeReceiver` class {#new-dispatcher-option}

Unified proxy and TLS configuration for both WebSocket connections and HTTP API calls. Pass any undici-compatible `Dispatcher` instance. See the section above on [the updated `SocketModeReceiver` class](#socketmodereceiver-dispatcher).

### We've added `instanceof` operator support to error classes {#instanceof-errors}

Both Bolt's own error classes (e.g., `AppInitializationError`, `AuthorizationError`) and the underlying `@slack/web-api` Node Slack SDK package error classes (e.g., `WebAPIPlatformError`, `WebAPIRequestError`) now properly extend the `Error` class. Use the `instanceof` operator for type-safe error handling instead of string comparisons on the `error.code` property.

### We've made the `app.function()` method the sole custom step mechanism {#app-function}

While the `app.function()` method already existed in Bolt v4, it is now the only way to handle custom function executions (replacing the removed `app.step()` method and `WorkflowStep` class). It provides `complete()` and `fail()` callback functions for signaling outcomes, and an `inputs` property for accessing function parameters.
3 changes: 1 addition & 2 deletions docs/english/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,6 @@ App options are passed into the `App` constructor. When the `receiver` argument
| Option | Description |
| :--- | :--- |
| `receiver` | An instance of `Receiver` that parses and handles incoming events. Must conform to the [`Receiver` interface](/tools/bolt-js/concepts/receiver), which includes `init(app)`, `start()`, and `stop()`. More information about receivers is [in the documentation](/tools/bolt-js/concepts/receiver). |
| `agent` | Optional HTTP `Agent` used to set up proxy support. Read more about custom agents in the [Node Slack SDK documentation](/tools/node-slack-sdk/web-api#proxy-requests-with-a-custom-agent). |
| `clientTls` | Optional `string` to set a custom TLS configuration for HTTP client requests. Must be one of: `"pfx"`, `"key"`, `"passphrase"`, `"cert"`, or `"ca"`. |
| `convoStore` | A store to set and retrieve state-related conversation information. `set()` sets conversation state and `get()` fetches it. By default, apps have access to an in-memory store. More information and an example can be found [in the documentation](/tools/bolt-js/legacy/conversation-store). |
| `token` | A `string` from your app's configuration (under "Settings" > "Install App") required for calling the Web API. May not be passed when using `authorize`, `orgAuthorize`, or OAuth. |
| `botId` | Can only be used when `authorize` is not defined. The optional `botId` is the ID for your bot token (ex: `B12345`) which can be used to ignore messages sent by your app. If a `xoxb-` token is passed to your app, this value will automatically be retrieved by your app calling the [`auth.test` method](/reference/methods/auth.test). |
Expand All @@ -141,6 +139,7 @@ App options are passed into the `App` constructor. When the `receiver` argument
| `extendedErrorHandler` | Option that accepts a `boolean` value. When set to `true`, the global error handler is passed an object with additional request context. Available from version 3.8.0, defaults to `false`. More information on advanced error handling can be found [in the documentation](/tools/bolt-js/concepts/error-handling). |
| `ignoreSelf` | `boolean` to enable a middleware function that ignores any messages coming from your app. Requires a `botId`. Defaults to `true`. |
| `clientOptions.slackApiUrl` | Allows setting a custom endpoint for the Slack API. Used most often for testing. |
| `clientOptions.fetch` | A custom `fetch` implementation (conforming to the WHATWG Fetch standard) used for all HTTP requests to Slack Web API calls, OAuth, and `respond()`. Use this to configure proxy or custom TLS behavior (for example, by wrapping a request with an undici `Dispatcher`). A global proxy can alternatively be configured with `http.setGlobalProxyFromEnv()`. |
| `socketMode` | Option that accepts a `boolean` value. When set to `true` the app is started in [Socket Mode](/tools/bolt-js/concepts/socket-mode), i.e. it allows your app to connect and receive data from Slack via a WebSocket connection. Defaults to `false`.
| `developerMode` | `boolean` to activate the developer mode. When set to `true` the `logLevel` is automatically set to `DEBUG` and `socketMode` is set to `true`. However, explicitly setting these two properties takes precedence over implicitly setting them via `developerMode`. Furthermore, a custom OAuth failure handler is provided to help debugging. Finally, the body of all incoming requests are logged and thus sensitive information like tokens might be contained in the logs. Defaults to `false`. |
| `deferInitialization` | `boolean` to defer initialization of the app and places responsibility for manually calling the `async` `App#init()` method on the developer. `init()` must be called before `App#start()`. Defaults to `false`. |
Expand Down
Loading