In this article
May 15, 2025
May 15, 2025

How to sync users from Entra ID to your app using Node and WorkOS

Step-by-step tutorial that walks you through the necessary steps to add automated user provisioning to your app using SCIM, Entra ID, Node, and WorkOS, with just a few lines of code.

If you're building for the enterprise, SSO is just half the story—automated user provisioning is the other half. Without it, you're stuck manually managing accounts, opening the door to delays, mistakes, and security gaps. With provisioning in place, users get instant access when they need it—and lose it the moment they leave. That’s where SCIM comes in. It’s the industry-standard protocol for syncing users between identity providers and applications—securely, reliably, and automatically.

In this tutorial, we’ll walk through how to sync users from Microsoft Entra ID (formerly Azure AD) into your app using SCIM, Node.js, and WorkOS. You’ll learn how to provision, update, and deprovision users in your app automatically—without having to build a SCIM server from scratch.

While going through this tutorial, remember that some things might get outdated as products evolve. Dashboards change, and new SDK versions are released every week. If, while you follow this tutorial, something is not working for you, please refer to these docs for the most up-to-date guidance:

!!If you want to integrate with a different directory provider or use a different SDK see our Directory Sync quickstart.!!

Prerequisites

To follow this tutorial, you will need the following:

  • Access to an Entra ID (Azure AD) directory.
  • A WorkOS account.
  • A Node.js app (Node 16 or higher).

Step 1: Install the SDK

Install the WorkOS Node SDK to your app.

  • Using npm: npm install @workos-inc/node
  • Using yarn: yarn add @workos-inc/node
  • Using pnpm: pnpm add @workos-inc/node

Step 2: Set secrets

To make calls to WorkOS, you must authenticate using the WorkOS API key and client ID. Copy these values from the WorkOS dashboard.

Store the values as managed secrets and pass them to the SDK as environment variables:

Environment variables example:

	
WORKOS_API_KEY='sk_example_123456789'
WORKOS_CLIENT_ID='client_123456789'
	

!!For more information on safely handling secrets, see Best practices for secrets management.!!

Step 3: Configure the Entra ID connection

The first step to connecting with a directory is creating an organization in the WorkOS dashboard. You will then be able to create a new connection to the organization’s directory.

!!An organization is a collection of users that also acts as a container for enterprise features (like SSO). By enabling an SSO connection for a specific organization, you enable the feature for all users who are members of this organization. This way, you can enable features like forcing all users that use a specific email domain to use a specific SSO connection. For more information on organizations and how to use them, see Model your B2B SaaS with organizations.!!

First, we will configure Entra ID SCIM at the WorkOS dashboard, and then we will move on to the Entra ID admin dashboard to finish the configuration.

WorkOS configuration

  1. Go to the WorkOS dashboard and select Organizations from the left-hand navigation bar.
  2. Pick (or create) the organization for which you’d like to configure an Entra ID SCIM connection.
  3. Select “Configure manually” under the “Directory Sync” section and enter a name for the directory.
  4. Click “Create Directory”.

Your Entra ID directory sync has now been created successfully with an Endpoint and Bearer Token.

Entra ID configuration

!!Instead of doing this yourself or sending the instructions to the admin responsible for the connection, use the WorkOS Admin Portal, an out-of-the-box UI for IT admins to configure SSO and Directory Sync connections. You can integrate it into your app or get a link from the WorkOS dashboard and send it to the admin. For more, see the docs.!!

To configure the connection on Entra ID’s side, follow these steps:

  1. Log in: Log in to the Entra ID Active Directory Admin dashboard.
  2. Select “Enterprise Applications” from the list of Azure services.
  3. If your application is already created, select it from the list of applications and continue to step 5.
  4. If you haven’t created a SCIM application in Azure, select “New Application”. Give your application a descriptive name, and select the “Integrate any other application you don’t find in the gallery (Non-gallery)” option, then click “Create”.
  5. Select “Provisioning” from the “Manage” section found in the navigation menu.
  6. Click the “Get Started” button.
  7. Select the “Automatic” Provisioning Mode from the dropdown menu.
  8. In the “Admin Credentials” section, copy and paste the Endpoint from your WorkOS Dashboard in the “Tenant URL” field.
  9. Copy and paste the Bearer Token from your WorkOS Dashboard into the Secret Token field.
  10. Click “Test Connection” to receive confirmation that your connection has been set up correctly. Then, select “Save” to persist the credentials.
  11. Expand the “Mappings” section.
  12. Make sure the group and user attribute mappings are enabled, and are mapping the correct fields. The default mapping should work, but your specific Azure setup may require you to add a custom mapping.
  13. Make sure that you are mapping objectId to externalId within the Attribute Mapping section.
  14. In order for your users and groups to be synced, you will need to assign them to your Entra ID SCIM Application. Select “Users and groups” from the “Manage” section of the navigation menu.
  15. Select “Add user/group” from the top menu.
  16. Select “None selected” under the “Users and Groups”. In the menu, select the users and groups that you want to add to the SCIM application, and click “Select”.
  17. Select “Assign” to add the selected users and groups to your SCIM application.
  18. In the Provisioning menu, confirm the “Provisioning Status” is set to “On” and that the “Scope” is set to “Sync only assigned users and groups”.
  19. Begin provisioning users and groups and witness realtime changes in the Events page of your WorkOS dashboard.

!!For more details and screenshots, see the Entra ID SCIM integration guide.!!

Step 4: Sync users and groups to your app

Once you've connected Entra ID to WorkOS, any time a directory-related resource—like a user or group—is created, updated, or deleted in Entra ID, WorkOS will generate a corresponding event. These events allow your app to stay in sync with the identity provider in real time, ensuring user data is always up to date.

About events

Each WorkOS event has the following structure.

Attribute Description
event A string that distinguishes the event type.
id Unique identifier for the event.
data Event payload. Payloads match the corresponding API objects.
created_at Timestamp of when the event occurred.

For example, this is an event about the creation of a new user:

	
{
  "event": "dsync.user.created",
  "id": "event_07FKJ843CVE8F7BXQSPFH0M53V",
  "data": {
    "id": "directory_user_01E1X1B89NH8Z3SDFJR4H7RGX7",
    "directory_id": "directory_01ECAZ4NV9QMV47GW873HDCX74",
    "organization_id": "org_01EZTR6WYX1A0DSE2CYMGXQ24Y",
    "idp_id": "8931",
    "emails": [
      {
        "primary": true,
        "type": "work",
        "value": "lela.block@example.com"
      }
    ],
    "first_name": "Lela",
    "last_name": "Block",
    "username": "lela.block@example.com",
    "state": "active",
    "created_at": "2021-06-25T19:07:33.155Z",
    "updated_at": "2021-06-25T19:07:33.155Z",
    "custom_attributes": {
      "department": "Engineering",
      "job_title": "Software Engineer"
    },
    "role": { "slug": "member" },
    "raw_attributes": {}
  },
  "created_at": "2021-06-25T19:07:33.155Z"
}
	

The different directory events are:

Event Description
dsync.activated Triggered when a directory is activated.
dsync.deleted Triggered when a directory is deleted. The state attribute indicates directory state before deletion.
dsync.group.created Triggered when a directory group is created.
dsync.group.deleted Triggered when a directory group is deleted.
dsync.group.updated Triggered when a directory group is updated.
dsync.group.user_added Triggered when a directory group user is added.
dsync.group.user_removed Triggered when a directory group user is removed.
dsync.user.created Triggered when a directory user is created.
dsync.user.deleted Triggered when a directory user is deleted. The state attribute indicates directory user state at time of deletion.
dsync.user.updated Triggered when a directory user is updated.

!!For details and sample payloads on each event, see Understanding the events lifecycle.!!

Sync events to your app

After WorkOS generates these events, your app needs to consume them to stay in sync with the latest changes. You can do this in one of two ways: by polling the Events API for new events, or by setting up webhooks to receive them automatically in real time. Either approach ensures that your app reflects the current state of the directory.

With the events API, your application retrieves events from WorkOS. It provides a consistent, ordered stream of immutable events via a paginated endpoint, allowing your application to pull and process changes at its own pace. This architecture not only ensures data integrity and easier error recovery, but also simplifies debugging and audit logging.

With webhooks, WorkOS automatically notifies your app when an event occurs by invoking an endpoint hosted within your application. Although webhooks are popular, they are also prone to issues like missed deliveries, out-of-order events, and scalability bottlenecks (for more, see Why you should rethink your webhook strategy). We recommend using the events API instead.

Aspect Events API Webhooks
Timing Controlled by your app. Your server can process events at its own pace. Real-time. Webhooks trigger as soon as an event occurs.
Order A consistent order is guaranteed. No guarantee of order on receipt. Events contain timestamps to determine order.
Reconciliation Replayable. Can go back to a specific point in time and reprocess events. Failed requests are retried with exponential back-off for up to 3 days.
Security Authentication, confidentiality, and integrity protection by default. You must expose a public endpoint and validate webhook signatures.

Sync data using the events API

To start consuming events, you first need to pick a starting place in the data set. For that, you need a cursor.

A cursor is a bookmark to track your app’s position in the events list. The very first call to the events API won’t have a cursor. Subsequent requests to WorkOS should include the updated cursor using the after parameter. You will need to update and store your cursor after processing an event.

Determine the event types you want to pull, and call the listEvents method. In this example, we are pulling the first 100 events:

	
import { WorkOS } from '@workos-inc/node';

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

const listOfEvents = await workos.events.listEvents({
  events: [
    'dsync.activated',
    'dsync.deleted',
    'dsync.user.created',
    'dsync.user.updated',
    'dsync.user.deleted',
  ],
  limit: 100
});

let events = listOfEvents.data;
let after = listOfEvents.listMetadata.after;
	

This is an example response (one user created, and one updated):

	
{
  "data": [
    {
      "event": "dsync.user.created",
      "id": "event_07FKJ843CVE8F7BXQSPFH0M53V",
      "data": {
        "id": "directory_user_01E1X1B89NH8Z3SDFJR4H7RGX7",
        "directory_id": "directory_01ECAZ4NV9QMV47GW873HDCX74",
        "organization_id": "org_01EZTR6WYX1A0DSE2CYMGXQ24Y",
        "idp_id": "8931",
        "emails": [
          {
            "primary": true,
            "type": "work",
            "value": "lela.block@example.com"
          }
        ],
        "first_name": "Lela",
        "last_name": "Block",
        "username": "lela.block@example.com",
        "state": "active",
        "created_at": "2021-06-25T19:07:33.155Z",
        "updated_at": "2021-06-25T19:07:33.155Z",
        "custom_attributes": {
          "department": "Engineering",
          "job_title": "Software Engineer"
        },
        "role": { "slug": "member" },
        "raw_attributes": {}
      },
      "created_at": "2021-06-25T19:07:33.155Z"
    },
    {
      "event": "dsync.user.updated",
      "id": "event_08GHJ944DVE9G8CXRTPGI1N64W",
      "data": {
        "id": "directory_user_01E2Y2C90OI9H4TGFSK5H9TYU8",
        "directory_id": "directory_01ECBZ5PW0RNX58HY984IDCZ85",
        "organization_id": "org_01EZTR6WYX1A0DSE2CYMGXQ24Y",
        "idp_id": "8931",
        "emails": [
          {
            "primary": true,
            "type": "work",
            "value": "jacob.morris@example.com"
          }
        ],
        "first_name": "Jacob",
        "last_name": "Morris",
        "username": "jacob.morris@example.com",
        "state": "active",
        "created_at": "2021-05-15T13:00:00.000Z",
        "updated_at": "2021-07-01T10:45:00.000Z",
        "custom_attributes": {
          "department": "Marketing",
          "job_title": "Marketing Manager"
        },
        "role": { "slug": "admin" },
        "raw_attributes": {}
      },
      "created_at": "2021-07-01T10:45:00.000Z"
    }
  ],
  "list_metadata": {
    "after": "event_08GHJ944DVE9G8CXRTPGI1N64W"
  }
}
	

In all subsequent calls, you have to use list_metadata.after for pagination. You'd pass that value as a after parameter to get the next page of results, and then update it to the new value contained in the API's response.

For more info, see the Sync data using the Events API quickstart.

Step 5 (optional): Get specific users and groups

The API offers some more endpoints you can use for specific use cases.

Get a user

To get the details of an existing directory user:

	
const { WorkOS } = require('@workos-inc/node');

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

const directoryUserID = 'directory_user_123';
const user = await workos.directorySync.getUser(directoryUserID);
	

The response looks like this:

	
{
  "id": "directory_user_01E1JG7J09H96KYP8HM9B0G5SJ",
  "idp_id": "2836",
  "directory_id": "directory_01ECAZ4NV9QMV47GW873HDCX74",
  "organization_id": "org_01EZTR6WYX1A0DSE2CYMGXQ24Y",
  "first_name": "Marcelina",
  "last_name": "Davis",
  "email": "marcelina@example.com",
  "groups": [
    {
      "id": "directory_group_01E64QTDNS0EGJ0FMCVY9BWGZT",
      "name": "Engineering",
      "created_at": "2021-06-25T19:07:33.155Z",
      "updated_at": "2021-06-25T19:07:33.155Z",
      "raw_attributes": {}
    }
  ],
  "state": "active",
  "created_at": "2021-06-25T19:07:33.155Z",
  "updated_at": "2021-06-25T19:07:33.155Z",
  "custom_attributes": {
    "department": "Engineering",
    "job_title": "Software Engineer"
  },
  "raw_attributes": {},
  "role": { "slug": "member" }
}
	

List users

You can get directory users for a given directory or directory group:

	
const { WorkOS } = require('@workos-inc/node');

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

// Fetch all Directory Users in a Directory
const usersFromDirectory = await workos.directorySync.listUsers({
  directory: 'directory_123',
});

// Fetch all Directory Users in a Directory Group
const usersByGroup = await workos.directorySync.listUsers({
  group: 'directory_group_123',
});
	

The response is an array of users. You can use this to build an onboarding experience that allows an admin to select who to invite and create accounts for.

Get a group

To get the details of an existing directory group:

	
const { WorkOS } = require('@workos-inc/node');

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

const directoryGroupId = 'directory_group_123';
const group = await workos.directorySync.getGroup(directoryGroupId);
	

List groups

To get directory groups for a given directory or directory user:

	
const { WorkOS } = require('@workos-inc/node');

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

// Fetch all Directory Groups in a Directory
const groupsFromDirectory = await workos.directorySync.listGroups({
  // The ID of the Directory to fetch Directory Groups for
  directory: 'directory_123',
});

// Fetch all Directory Groups for a Directory User
const groupsByUser = await workos.directorySync.listGroups({
  // The ID of the Directory User to fetch Directory Groups for
  user: 'directory_user_123',
});
	

!!Use the optional limit, before, and after parameters to paginate through results. See the API Reference for details.!!

Step 6 (optional): Stream events to Datadog

WorkOS supports real-time streaming of events to Datadog. By analyzing WorkOS activity directly in Datadog, you are able to:

  • View trends in user sign-ins, user growth, new SSO connections and more.
  • Debug customer issues related to sign-in, email verification, password resets and more.
  • Generate reports of user activity per customer organization.
  • Set alerts for unexpected activity, such as sudden spike in failed password attempts.

To set up real-time streaming of WorkOS events to Datadog, follow these steps:

  1. Create a new Datadog API key to give WorkOS permission to send event activity as logs to your Datadog account.
  2. Configure event streaming in the WorkOS dashboard using the Datadog API key.
  3. Add the WorkOS Datadog dashboard to your Datadog account.

For detailed instructions and screenshots, see the docs: Stream events to Datadog.

Next steps

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.