Easily handle long-running operations using middleware in the Microsoft Graph JavaScript SDK

If you use the Microsoft Graph JavaScript SDK and need to handle long-running operations, build it as a middleware. Here’s how.

Why you should consider to use a Microsoft Graph SDK

Microsoft Graph JavaScript SDK offers you a convenient way to connect to Microsoft Graph - the API to data and insights on Microsoft 365. The SDK takes care of authentication and other request-related plumbing helping you to focus on building your app.

Long-running operations on Microsoft Graph

While most operations on Microsoft Graph are instantaneous, there are some exceptions. One example is creating a Microsoft Graph connector schema which can take several minutes. After you submit the schema, you get back a 202 Accepted response, with a Location header with the URL that you can call to check the status of the schema creation operation.

TIP: If you’re building code to handle the long-running operation of creating the Microsoft Graph connector schema, use the Microsoft 365 Developer Proxy mock to simulate the API behavior. Using the mock you’ll be able to easily test your code, without having to wait for the schema to be provisioned each time. It’ll save you a lot of time!

POST https://graph.microsoft.com/v1.0/external/connections/contosohr/schema
content-type: application/json

  "baseType": "microsoft.graph.externalItem",
  "properties": [
      "name": "ticketTitle",
      "type": "String",
      "isSearchable": "true",
      "isRetrievable": "true",
      "labels": [
      "name": "priority",
      "type": "String",
      "isQueryable": "true",
      "isRetrievable": "true",
      "isSearchable": "false"
      "name": "assignee",
      "type": "String",
      "isRetrievable": "true"

202 Accepted
Location: https://graph.microsoft.com/v1.0/external/connections/contosohr/operations/616bfeed-666f-4ce0-8cd9-058939010bfc

Calling the URL gives you the current status of the creation operation:

GET https://graph.microsoft.com/v1.0/external/connections/contosohr/operations/616bfeed-666f-4ce0-8cd9-058939010bfc

200 OK
content-type: application/json

  "id": "1.neu.0278337E599FC8DBF5607ED12CF463E4.6410CCF8F6DB8758539FB58EB56BF8DC",
  "status": "inprogress",
  "error": null

If you want to wait until the operation completes, you need to poll the URL at regular intervals, until the status changes from inprogress to either completed or failed.

TIP: For checking the status of creating Microsoft Graph connector schema, Microsoft recommends polling the status every 1 minute.

As you can imagine, handling this flow adds a lot of code. A lot of code that makes it harder to understand what your app is doing exactly, but which you need nonetheless. Luckily, you don’t need to put it in the main flow of your app. By implementing it as a middleware, you can make it readily available to any piece of your code that needs it without having to invoke it explicitly.

What’s middleware

Microsoft Graph SDKs come with the concept of middleware. Middleware are pieces of code that handle outgoing requests and incoming responses. They’re applied in a predefined order and run on every request you issue using the Microsoft Graph SDK.

Microsoft Graph SDKs ship with several middleware handlers and you can add your own to the pipeline too. What’s neat about middleware, is that it’s automatically applied to every request and doesn’t clutter your main code, making it easier to follow.

Handle long-running operations using middleware

The following is code that you can use to handle the long-running operation of creating Microsoft Graph connector schema:

export function CompleteJobWithDelayMiddleware(delayMs) {
  this.nextMiddleware = undefined;
  this.execute = async (context) => {
    // wait for response
    await this.nextMiddleware.execute(context);

    const location = context.response.headers.get('location');
    if (location) {
      console.debug(`Location: ${location}`);

      if (location.indexOf('/operations/') < 0) {
        // not a job URL we should follow

      console.log(`Waiting ${delayMs}ms before following location ${location}...`);
      await new Promise(resolve => setTimeout(resolve, delayMs));

      context.request = location;
      context.options.method = 'GET';
      context.options.body = undefined;
      await this.execute(context);

    if (context.request.indexOf('/operations/') < 0) {
      // not a job

    const res = context.response.clone();
    if (!res.ok) {
      console.debug('Response is not OK');
    const body = await res.json();
    console.debug(`Status: ${body.status}`);
    if (body.status === 'inprogress') {
      console.debug(`Waiting ${delayMs}ms before trying again...`);
      await new Promise(resolve => setTimeout(resolve, delayMs));
      await this.execute(context);

  return {
    execute: async (context) => {
      return await this.execute(context);
    setNext: (next) => {
      this.nextMiddleware = next;

The middleware starts with executing the request and capturing the response. Then, it checks if the response contains the Location header.

If it does, it checks if the URL in the Location header contains the /operations/ segment. Remember, the middleware runs on every request so before processing it, you need to be sure that you’re looking at the right request/response.

If the URL in the Location header doesn’t contain the /operations/ segment, the middleware stops its execution.

If it does contain the /operations/ segment, the middleware will wait for the specified amount of time, and then call the URL from the Location header, by passing it to the middleware chain.

Going back to the main flow, if the response didn’t have a Location header, and the request didn’t have the /operations/ segment, it’s some other request that’s outside of the responsibility of this middleware, so the middleware skips its further processing.

If the request URL does contain the /operations/ segment, then the middleware reads the body to check the status of the operation. If the operation is in progress, the middleware will wait and call the status URL again. If the status is different, it will return the response to the caller.

It’s a lot of code, isn’t it? What’s really cool about it though, is that because it’s implemented in the middleware, it’s invisible in your main code flow, where the only thing you’ll see is:

const res = await client
  .header('content-type', 'application/json')
    baseType: 'microsoft.graph.externalItem',
    properties: schema

const status = res.status;
if (status === 'completed') {
  console.log('Schema created');
else {
  console.error(`Schema creation failed: ${res.error.message}`);

See how simple to understand this code is? It’s only possible, because of the complexity of handling the long-running operation has been put in a middleware.

To use this middleware, add it to the code where you instantiate GraphClient:

const credential = new ClientSecretCredential(

const authProvider = new TokenCredentialAuthenticationProvider(credential, {
  scopes: ['https://graph.microsoft.com/.default'],

const middleware = MiddlewareFactory.getDefaultMiddlewareChain(authProvider);
// add as a second middleware to get access to the access token
middleware.splice(1, 0, new CompleteJobWithDelayMiddleware(60000));

export const client = Client.initWithMiddleware({ middleware });

Because this middleware executes API calls, it’s necessary that you add it after the auth middleware (so at least second in the chain) so that it gets access to the access token, or the API requests from the middleware will fail.


Using the Microsoft Graph JavaScript SDK is an easy way for you to get the data and insights from Microsoft 365 in your app. The SDK simplifies authentication and handles complex API scenarios for you. If you need to handle long-running operations, like creating Microsoft Graph connector schema, implement it as middleware to keep your main code flow clean and make the logic available throughout your app without repeating the code.

Others found also helpful: