In this article
March 4, 2026
March 4, 2026

Send Slack notifications from your app without building OAuth

How to post Slack messages to your users' workspaces in minutes, without writing a single line of OAuth plumbing, using WorkOS Pipes.

Many SaaS applications need to send Slack notifications to their users: deployment alerts, task assignments, billing reminders. The feature itself is simple. The OAuth setup to make it work for every user's workspace is not.

To support multi-tenant Slack messaging (where each of your users connects their own Slack workspace, not a shared bot), you normally have to:

  • Register and configure a Slack App.
  • Implement the full OAuth 2.0 authorization code flow.
  • Build a callback endpoint to exchange codes for tokens.
  • Store and encrypt user tokens in your database.
  • Write token refresh logic and handle expiry.
  • Handle errors when tokens are revoked.

That's days of work before you send a single notification.

WorkOS Pipes removes all of it. Pipes handles OAuth flows, token storage, and refresh logic so your app just asks for a valid Slack access token when it needs one. You call the Slack API. Done.

In this tutorial, we’ll build a Node.js app that lets users connect their Slack workspace via the Pipes widget, then posts a formatted notification to a channel they choose, without writing OAuth code yourself.

What we’ll build:

  • A Pipes widget that lets users connect and manage their Slack account.
  • A backend endpoint that fetches a fresh Slack access token from WorkOS.
  • A chat.postMessage call that posts a notification to the user's chosen channel.

!!The same pattern works for any provider. WorkOS Pipes supports GitHub, Linear, Google, Salesforce, and more.!!

Prerequisites

  • A WorkOS account (free to sign up + up to 1 million active users / month)
  • Node.js 18+ installed
  • Basic familiarity with Express and React

What is WorkOS Pipes?

WorkOS Pipes lets your users securely connect their third-party accounts (Slack, GitHub, Linear, Google, Salesforce) to your application without implementing the underlying OAuth flow.

Instead of building and maintaining OAuth infrastructure for each provider, you configure the provider once in the WorkOS dashboard, drop the Pipes widget in your app, and call the third-party account you want using the access token WorkOS gives you.

At a high level, the flow works like this:

  1. You configure Slack as a provider in the WorkOS dashboard.
  2. Add the Pipes widget to your app. Your frontend renders the Pipes widget so users can connect their Slack account.
  3. When your app needs to send a notification, your backend calls WorkOS to fetch a fresh Slack access token.
  4. WorkOS returns a valid token, refreshing it in the background if needed.
  5. You use that token to call chat.postMessage on the Slack API.
Example screenshot of the Pipes Widget in an app

Step 1: Configure Slack in the WorkOS dashboard

Go to the WorkOS dashboard > Pipes.

Click Connect provider and select Slack from the list.

Screenshot of the Pipes page in the WorkOS dashboard

Choose your OAuth scopes

For sending notifications to a channel, you need two scopes:

  • chat:write Lets your app post messages to channels the user has authorized.
  • channels:read Lets your app list the user's public channels so they can choose a destination.

Select both scopes in the configuration dialog. Add an optional description, like "Used to send you deployment alerts and task notifications in Slack." This description appears in the Pipes widget and helps your users understand why they're connecting their account.

Shared credentials vs. your own OAuth app

For the fastest local development, use the shared credentials option. This uses WorkOS-managed OAuth credentials so users can connect immediately, with no Slack App setup required. Shared credentials are available in sandbox environments only.

For production, you'll configure the provider with your own OAuth credentials. Create a Slack App at api.slack.com/apps, copy the Redirect URI from the WorkOS Dashboard into your Slack App's OAuth & Permissions settings, then paste the Client ID and Client Secret back into WorkOS.

Screenshot of the Slack configuration dialog box in the WorkOS dashboard

Step 2: Add the Pipes widget to your frontend

The Pipes widget is a pre-built React component that shows users which providers are available, lets them connect or disconnect accounts, and handles the OAuth flow entirely on their behalf. You embed it once and it handles everything.

Install dependencies

WorkOS Widgets uses Radix Themes for its UI components and TanStack Query for data fetching. Install all three:

  
npm install @workos-inc/widgets @radix-ui/themes @tanstack/react-query
  

Configure your WorkOS API key and allow CORS

Because the Pipes widget makes client-side requests to WorkOS, you need to add your app's origin to the allowed origins list in the WorkOS dashboard. Go to Authentication → Sessions and add your local URL (for example, http://localhost:3000). This prevents CORS errors when the widget initializes.

Screenshot of the dashboard's Authentication → Sessions page

Next, set your WorkOS credentials in your environment:

  
# .env
WORKOS_API_KEY=sk_...
WORKOS_CLIENT_ID=client_...
  

You can find your credentials at the WorkOS dashboard landing page.

Screenshot of the WorkOS dashboard landing page

Generate a widget token on your backend

The Pipes widget needs an authorization token tied to the current user. Add an endpoint to your Express backend to generate one:

  
// server.js
import express from 'express';
import { WorkOS } from '@workos-inc/node';

const app = express();
const workos = new WorkOS(process.env.WORKOS_API_KEY);

// Returns a short-lived token the Pipes widget uses to authenticate
app.get('/api/widget-token', async (req, res) => {
  // In a real app, get the authenticated user ID from your session
  const userId = req.session.userId;

  const { token } = await workos.userManagement.getAccessToken({
    userId,
    clientId: process.env.WORKOS_CLIENT_ID,
  });

  res.json({ token });
});
  

Render the Pipes widget

Add the widget to the settings or integrations page of your app:

  
// src/pages/Integrations.jsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { Theme } from '@radix-ui/themes';
import { PipesWidget } from '@workos-inc/widgets';
import '@radix-ui/themes/styles.css';

const queryClient = new QueryClient();

export default function IntegrationsPage() {
  async function fetchToken() {
    const res = await fetch('/api/widget-token');
    const { token } = await res.json();
    return token;
  }

  return (
    <QueryClientProvider client={queryClient}>
      <Theme>
        <div className="integrations-page">
          <h2>Connected Accounts</h2>
          <p>
            Connect your Slack workspace to receive notifications directly
            in your chosen channel.
          </p>
          <PipesWidget
            clientId={import.meta.env.VITE_WORKOS_CLIENT_ID}
            getAccessToken={fetchToken}
          />
        </div>
      </Theme>
    </QueryClientProvider>
  );
}
  

When a user visits this page, they'll see a Slack connection card. Clicking it launches the Slack OAuth flow, and after authorizing, the widget shows their account as connected. If the token ever expires or is revoked, the widget automatically prompts the user to reconnect.

Step 3: Fetch a fresh Slack token on your backend

When your app needs to send a notification, it calls WorkOS to get a valid Slack access token for that user. WorkOS handles token refresh transparently; you always get a usable token.

Add a helper function to your backend:

  
// lib/slack.js
import { WorkOS } from '@workos-inc/node';

const workos = new WorkOS(process.env.WORKOS_API_KEY);

export async function getSlackTokenForUser(userId) {
  const { accessToken } = await workos.pipes.getAccessToken({
    userId,
    provider: 'slack',
  });

  return accessToken;
}
  

If the user hasn't connected their Slack account yet, this call returns a descriptive error you can use to redirect them to the integrations page. If the token has expired, WorkOS refreshes it automatically before returning.

Step 4: Post a Slack notification with chat.postMessage

Now put it together. When something happens in your app (a deployment completes, a task is assigned, a subscription renews) call the Slack API using the token you just fetched:

  
// lib/notifications.js
import { getSlackTokenForUser } from './slack.js';

export async function sendSlackNotification({ userId, channelId, message }) {
  let accessToken;

  try {
    accessToken = await getSlackTokenForUser(userId);
  } catch (err) {
    // User hasn't connected Slack yet — handle gracefully
    console.warn(`No Slack connection for user ${userId}:`, err.message);
    return { sent: false, reason: 'not_connected' };
  }

  const response = await fetch('https://slack.com/api/chat.postMessage', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${accessToken}`,
    },
    body: JSON.stringify({
      channel: channelId,
      text: message,
      // Optional: use Block Kit for richer formatting
      blocks: [
        {
          type: 'section',
          text: {
            type: 'mrkdwn',
            text: message,
          },
        },
      ],
    }),
  });

  const data = await response.json();

  if (!data.ok) {
    throw new Error(`Slack API error: ${data.error}`);
  }

  return { sent: true, ts: data.ts };
}
  

Call this from wherever the triggering event happens in your app:

  
// Example: notify on deployment success
app.post('/api/deployments/:id/complete', async (req, res) => {
  const { userId, projectName } = await getDeployment(req.params.id);

  await sendSlackNotification({
    userId,
    channelId: await getUserSlackChannel(userId), // stored from user preferences
    message: `*${projectName}* deployed successfully to production.`,
  });

  res.json({ status: 'ok' });
});
  

Step 5: Let users choose their notification channel

You'll want to give users a way to pick which Slack channel receives notifications. Use the same Slack token to fetch their available channels:

  
// server.js
app.get('/api/slack/channels', async (req, res) => {
  const userId = req.session.userId;
  const accessToken = await getSlackTokenForUser(userId);

  const response = await fetch(
    'https://slack.com/api/conversations.list?types=public_channel,private_channel&exclude_archived=true&limit=200',
    {
      headers: { Authorization: `Bearer ${accessToken}` },
    }
  );

  const { channels } = await response.json();

  res.json({
    channels: channels.map((c) => ({ id: c.id, name: c.name })),
  });
});
  

Render this as a dropdown in your notification preferences UI so users can select their destination channel once and save it to your database. From then on, your backend uses their saved channelId each time it sends a notification.

Putting it all together

Here's the complete flow, end to end:

  1. User visits your integrations page → sees the Pipes widget → clicks Connect Slack → completes the OAuth flow in Slack → returns to your app with their account connected.
  2. User opens notification preferences → your app calls conversations.list using their Slack token → user selects a channel → your app saves that channel ID.
  3. A deployment completes (or any event fires) → your backend calls workos.pipes.getAccessToken() → gets a fresh Slack token → calls chat.postMessage → notification appears in the user's chosen channel.

No tokens in your database. No refresh cron jobs. No OAuth callback routes. WorkOS handles all of it.

What to build next

Once you have the Slack integration in place, the same Pipes pattern extends naturally:

  • Add more providers. The Pipes widget automatically shows any providers you've configured in the WorkOS Dashboard. Add GitHub to notify when a PR is opened, or Linear to sync issue status; your backend uses the same token-fetching pattern for every provider.
  • Send richer notifications with Block Kit. Slack's Block Kit lets you build interactive message layouts with buttons, dropdowns, and images. Swap the text field in your chat.postMessage call for a blocks array to make your notifications more actionable.
  • Let users configure notification rules. Build a preferences UI where users can choose which events trigger notifications and which channels receive them. Your backend already has everything it needs. Just store the user's preferences and reference them when sending.
  • Combine providers. WorkOS Pipes is designed to grow with your product. If you need to integrate with a data source not currently listed, reach out. We actively add new providers based on customer needs.

Frequently Asked Questions

Do I need to build and maintain a Slack App? For sandbox environments, no; you can use WorkOS shared credentials and start sending notifications immediately. For production, you'll need to create a Slack App to get your own Client ID and Secret, but WorkOS handles everything after that.

What happens if a user's Slack token expires or is revoked? WorkOS automatically refreshes tokens in the background. If a token is revoked (the user uninstalled your Slack App from their workspace), getAccessToken returns an error with a clear reason, and you can direct the user back to the Pipes widget to reconnect.

Can I send Slack messages to multiple workspaces from the same app? Yes. Each user who connects Slack via Pipes gets their own access token, scoped to their workspace. Your backend simply calls getAccessToken with the relevant user ID and WorkOS handles the per-user credential isolation.

What Slack scopes does this tutorial require?chat:write to post messages and channels:read to list channels. If you need to send direct messages to users, add im:write and use conversations.open to open a DM before calling chat.postMessage.

Can I post as the user instead of as a bot? WorkOS Pipes uses user-scoped tokens, so with the right scopes (specifically chat:write:user), you can post messages that appear to come from the individual user rather than from a bot. Check Slack's scope documentation for the specifics.

This site uses cookies to improve your experience. Please accept the use of cookies on this site. You can review our cookie policy here and our privacy policy here. If you choose to refuse, functionality of this site will be limited.