Bulk Launch a Workflow

This is an example outlining how to bulk launch workflows using our REST API.

Goal

Technical audiences will learn how to use Ironclad’s API to launch multiple workflows based on a template. Some examples of where this use case/flow could be used:

  1. A new contract amendment that multiple counterparties must sign, each in their own workflow.
  2. An updated terms of agreement that all or a subset of existing customers must read and approve.
  3. In an HR scenario, a new or modified employment agreement, or confirmation of a completed compliance task that employees must sign.

Overview

This guide provides an example of how to launch instances of the same workflow for multiple counterparties in a bulk fashion using the Ironclad public APIs. The workflows are all based on the same workflow template and are launched one at a time, each to a different counterparty (set of workflow metadata). The Create a Workflow Asynchronously endpoint will be used to launch each workflow and we will show how to get the status of each workflow after being launched.

Prerequisites

  1. OAuth Client App: An OAuth client app that has been granted the createWorkflows scope. Learn more about generating an OAuth token and expected headers in the Authentication article.
  2. Published workflow design and Workflow Designer access: See your internal company admin for access to Workflow Designer.
  3. Beginner understanding of Workflow Designer: For a refresher, please see Ironclad Academy and Ironclad's Help Center.
  4. Counterparty Information: A list of counterparties in a delimited format that can be programmatically read one row at a time, to launch a workflow for each. Typically, this will be in CSV format extracted from an XLSX file. For complex field structures in an XLSX file follow these guidelines:
    4.1 Addresses: Each part of the address is stored in its own column and they are pulled together into the correct JSON structure before sending the request (i.e. Street 1, Street 2, City, State, Postcode, Country).
    4.2 Monetary Value: The amount and currency are stored in separate columns and then put together in the correct JSON structure before sending the request.
    4.3 Multi-Select: Selections should be listed in a delimited fashion in a single column. Values can then be parsed and put in the correct JSON structure.
    4.4 Table: Name/value pairs for each of the rows in the table can be stored in a column in a delimited format. Values can then be parsed and put in the correct JSON structure.

For more information on constructing complex values from an XLSX file, please see the Supported Field Types for sample values located in the Launch a Workflow Guide

While our example uses this kind of static data extract, you could also read the counterparty details in a live fashion from some sort of datastore.

👍

Tip: End-to-End Testing

Test your API requests in sandbox mode or in your separate sandbox instance (if you have purchased one) before running them in a live production environment. Testing should include launching a subset of workflows and running them end-to-end to completion (e.g., dispatching signature requests and archiving).

Note: This guide and any software contained in it should be used at your own risk by individuals qualified to evaluate its effectiveness. IT IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL IRONCLAD BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH YOUR USE OF THIS GUIDE OR ANY SOFTWARE IT CONTAINS.

Your use of the Ironclad API must comply with Ironclad’s API Terms of Use (available at https://legal.ironcladapp.com/api-terms-of-use) and the terms of the Enterprise Services Agreement (or equivalent) entered into between you and Ironclad.

Details

Each asynchronous workflow launch request has 2 required parameters in the request body: (1) template ID and (2) attributes. The remainder of this guide will cover how to discover and construct these parameters.

Copy the template below into your preferred editor and populate it as you walk through the steps in this guide.

{
  "template": "[WORKFLOW_TEMPLATE_ID]",
  "attributes": {
    "[FIELD_ID_1]": "[FIELD_VALUE_1]",
    "[FIELD_ID_2]": "[FIELD_VALUE_2]",
    "[FIELD_ID_3]": "[FIELD_VALUE_3]",
  }
}

Note: Your data will be transmitted to Ironclad in the request body. The request body will be either (1) a raw JSON with the format above or (2) a multipart/form-data request if you plan to include binary files directly in the body. The JSON payload (template and attributes) will be substantially the same in both scenarios, but it will be included as one request body part in the multipart/form-data scenario.

1. Request Body - Template ID

The template parameter identifies which workflow design to launch. Workflow designs receive a template ID when they are published.

  1. In Workflow Designer, publish the workflow you wish to launch if it has not been published already.
  2. See Getting Started for an overview of retrieving template IDs in Ironclad. Copy it into your request body as the value for the template parameter. See the example response below where the template ID (id) is returned via the List All Workflows Schema endpoint:
{
           "id": "663aa05ae7f86c48b1dce81d",
           "name": "Standard Agreement",
           "schema": {
               "counterpartyName": {
                   "type": "string",
                   "displayName": "Counterparty Name"
               }, ......

Note: If you are testing in a production Ironclad environment instead of a development environment, we recommend using sandbox mode. Sandbox mode isolates your tests to that environment and avoids creating properties in your full production environment until you are ready to publish your final workflow design.

2. Request Body - Attributes

The attributes parameter specifies the values you want to pass into your workflow's fields. Constructing the attributes parameter involves two steps: (1) inventorying the fields that can/must be populated and (2) creating the request body.

2.1. Retrieve the Workflow's Launch Form Schema

Each workflow design has a "launch form" schema that describes the field ID, type, and display name of fields that must be provided in order to launch the workflow.

Retrieve this schema by sending a GET request to the Retrieve a Workflow Schema endpoint using (1) the workflow design's template ID from Section 1 and (2) the ?form=launch URL query parameter.

If the API request is successful, you should receive a response body that conforms to the following format:

{
  "id": "[TEMPLATE_ID]",
  "name": "[NAME_OF_WORKFLOW_DESIGN]",
  "schema": {
    "[FIELD_ID_1]": {
      "type": "[FIELD_TYPE]", // string, number, boolean, date, duration, monetaryAmount, email, address, or array
      "displayName": "[FIELD_DISPLAY_NAME]"
    },
    "[FIELD_ID_2]": {
      "type": "[FIELD_TYPE]",
      "displayName": "[FIELD_DISPLAY_NAME]"
    },
    "[FIELD_ID_3]": {
      "type": "[FIELD_TYPE]",
      "displayName": "[FIELD_DISPLAY_NAME]"
    }
  }
}

The schema property holds objects that describe each field. The objects describing each field will have the following properties:

- **Field API Name**: The unique ID automatically generated by Ironclad to identify each field. You will use this property when creating the payload to specify which field you are passing data into. An object is nested within this property and contains the following properties:
- **Field Type**: The type of data the field can be populated with. Field types can be edited in the Data Manager.
- **Field Display Name**: The question customized by workflow designers when building workflows and seen by end users when submitting the Launch Form. Field display names can be changed in Workflow Designer.

The following example illustrates data types corresponding to different fields created in an Ironclad launch form. For an overview of fields that can be used in Workflow Designer, please see the following article, open your design in Workflow Designer, or use the Data Manager. For more information on the Data Manager please see the following article

Schema Object Model (Example)

{
  "id": "60f83d3ecdd9d05a78472166",
  "name": "My Workflow Design Name",
  "schema": {
    "counterpartyName": {
      "type": "string",
      "displayName": "Counterparty Name"
    },
    "role4e49a9bbd20c4101b1e475294e534076": {
      "type": "email",
      "displayName": "Counterparty Signer Email"
    },
    "rolec326ea18494d41b9af47303412e30c0b": {
      "type": "string",
      "displayName": "Counterparty Signer Name"
    },
    "someTextField": {
      "type": "string",
      "displayName": "Some Text Field"
    },
    "someDropdownField": {
      "type": "string",
      "displayName": "Some Dropdown Field"
    },
    "someSingleSelectMultipleChoiceField": {
      "type": "string",
      "displayName": "Some Single-Select Multiple Choice Field"
    },
    "someMultiSelectMultipleChoiceField": {
      "type": "array",
      "displayName": "Some Multi-Select Multiple Choice Field",
      "elementType": {
        "type": "string"
      }
    },
    "someNumberField": {
      "type": "number",
      "displayName": "Some Number Field"
    },
    "someBooleanField": {
      "type": "boolean",
      "displayName": "Some Boolean Field (Yes/No)"
    },
    "someDateField": {
      "type": "date",
      "displayName": "Some Date Field"
    },
    "someMonetaryField": {
      "type": "monetaryAmount",
      "displayName": "Some Monetary Field"
    },
    "someAddressField": {
      "type": "address",
      "displayName": "Some Address Field"
    },
    "someDurationField": {
      "type": "duration",
      "displayName": "Some Duration Field"
    },
    "someTable": {
      "type": "array",
      "displayName": "Some Table",
      "elementType": {
        "type": "object",
        "schema": {
          "someTextFieldInTable": {
            "type": "string",
            "displayName": "Some Text Field In Table"
          },
          "someNumberFieldInTable": {
            "type": "number",
            "displayName": "Some Number Field In Table"
          },
          "someMultiSelectFieldInTable": {
            "type": "array",
            "displayName": "Some Multi-Select Field In Table",
            "elementType": {
              "type": "string"
            }
          }
        }
      }
    },
    "someAdditionalFileUploadField": {
      "type": "array",
      "displayName": "Some Additional File Upload Field",
      "elementType": {
        "type": "document"
      }
    }
  }
}

2.2. Create Request Body (Attributes)

Construct your attributes parameter using the field IDs from Section 2.1 above. Each field has a data type that dictates how values must be formatted. To learn how to format values for the different field types, please see the Create Request Body (Attributes) section in Launch a Workflow article.

3. Bulk Launching the Workflow(s)

Launching an individual workflow using the launch API endpoint is documented in our Launch a Workflow article. For this bulk launch example use the asynchronous launch details in that article.

To bulk launch multiple workflows, you’ll need to iterate over a data set containing the field values needed for each workflow launch and call the asynchronous launch API each time.

3.1. Asynchronous Route

The following example outlines how to bulk launch workflows asynchronously and then poll for changes to the status.

Some general recommendations around polling:

  • Please try to handle polling so that the API is not being queried for longer than what would be naturally expected (i.e. it should not take more than a few minutes to be completed, so cancel polling if it does).
  • While we have aimed to make checking the workflow job status responsive and a lightweight process on our end, we recommend ensuring there’s a time gap when you are checking the status of the workflow launch. Please do not poll any quicker than 2 seconds between requests.

3.2. Rate Limits

The Ironclad public API utilizes rate limits to ensure the best performance and operability. To learn more please visit our Rate Limits article.

Example Code

The following example code uses Node.js along with the dotenv, axios, and simple-oauth2 packages. This is for demonstration purposes only as to how your code may look at the start.

require('dotenv').config();
const axios = require('axios').default;
const fs = require("fs");
const simpleOauth2 = require('simple-oauth2');

const oauth2 = simpleOauth2.create({
	client: {
   		id: 'YOUR_CLIENT_ID',
		  secret: 'YOUR_CLIENT_SECRET',
	},
	auth: {
		tokenHost: 'https://ironcladapp.com',  // For demo instances use 'https://demo.ironcladapp.com'
		tokenPath: '/oauth/token',
		authorizePath: '/oauth/authorize',
	},
});

const authorizationUri = oauth2.authorizationCode.authorizeURL({
	redirect_uri: 'http://yourapp.com/callback',
	scope: 'public.workflows.readSchemas public.workflows.createWorkflows',
});
	

const tokenParms = {
	code: 'CODE_FROM_CALLBACK',
	redirect_uri: 'https://yourapp.com/callback',
};


const result = oauth2.authorizationCode.getToken(tokenParms);
const accessToken = oauth2.accessToken.create(result);
		


// Your host URL may vary based on the implementation.
const hostUrl = (process.env.HOST_URL ? process.env.HOST_URL : 'ironcladapp');  // For demo instances use 'https://demo.ironcladapp.com'
const apiUrl = `https://${hostUrl}.com/public/api/v1`;

// The ID of the workflow template to be launched.
const workflowTemplateId = '60f83d3ecdd9d05a78472166';


const checkWorkflowProgress = async (workflowId) => {
  const asyncStatusUrl = `${apiUrl}/workflows/async/${workflowId}`;
  return await axios.get(asyncStatusUrl, {
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${accessToken}`,  // oAuth2 Access Token retrieved 
      },
  });
};

const pollForWorkflowStatusChanges = async (workflowId) => {
  let pollingCount = 0;
  const pollingLimit = 10;
  return new Promise((resolve, reject) => {
    const interval = setInterval(async () => {
      pollingCount += 1;
      console.log(`Polling count: ${pollingCount}.`);
      const response = await checkWorkflowProgress(workflowId);

      if (response.data.status === 'success') {
        console.log('Successfully launched the workflow!');
        resolve(response.data);
        clearInterval(interval);
      } else if (response.data.status === 'in_progress') {
          console.log('Workflow launch still in progress.');
      } else if (response.data.status === 'failed') {
        console.log('Workflow launch failed');
        reject(new Error(
          `Workflow launch failed with error: ${JSON.stringify(response.data.error,)}`),
          );
          clearInterval(interval);
      } else {
        console.log('Unknown job status. This should not happen!');
      }

      if (pollingCount >= pollingLimit) {
        clearInterval(interval);
        reject(new Error('Did not retrieve a success status after 10 tries.'));
      }
    }, 5000);
  });
};
  
const launchWorkflowAsynchronously = async () => {
  const asyncUrl = `${apiUrl}/workflows/async`;
  console.log(`Starting launch of workflow with template ${workflowTemplateId}.`,);
  const launchResponse = await axios.post(asyncUrl, ironcladRequestBody, {
    headers: {
      'Content-Type': 'application/json',
      'x-as-user-email': '[SOME_AUTHORIZED_USER@YOUR_COMPANY.COM]',  // Email address of the Ironclad user that is launching the workflow
      'Authorization': `Bearer ${apiToken}`,  // oAuth2 Access Token retrieved 
    },
  });
  console.log(`Now polling for changes to the async status.`);
  return await pollForWorkflowStatusChanges(launchResponse.data.asyncJobId);
};
 
//  In this example the counterparty data will be read from a CSV file.

// specify path to csv
const path = "counterpartydata.csv";

// Read the CSV file
fs.readFile(path, "utf8", (err,data) => {
  if (err){
    console.error("Error while reading:", err);
    return;
  }
 
})

// Split the data into lines
const lines = data.split("\n");

// Initialize the output counterParty array
const counterpartyInfo = [];

//Loop through each line and split it into fields
lines.forEach((line) => {
  const fields = line.split(",");
  counterpartyInfo.push(fields);
});

// looping through the array to launch a workflow for each row of data

for (let i = 0; i < counterpartyInfo.length; i++) {

// The Ironclad Request body needed to launch the workflow.  Setting values from current row in array
const ironcladRequestBody = {
  template: workflowTemplateId,
  attributes: {
    //example values below that would be read from counterPartyData object row
    counterpartyName: counterpartyInfo[i].counterpartyName, // 'Example Company'  
    role4e49a9bbd20c4101b1e475294e534076: counterpartyInfo[i].signer, // '[email protected]'
    rolec326ea18494d41b9af47303412e30c0b: counterpartyInfo[i].signerName, // 'Jane Doe'
    someTextField: counterpartyInfo[i].someTextField, // 'A random text value.'
    someDateField: counterpartyInfo[i].someDateField, // '2021-05-11T17:16:53Z'
    someNumberField: counterpartyInfo[i].someNumberField, // 12345
    someBooleanField: counterpartyInfo[i].someBooleanField, // true
    someMultiSelectField: counterpartyInfo[i].someMultiSelectField, //['Option A', 'Option C']
    someMonetaryField : {
      currency: 'USD',
      amount: counterpartyInfo[i].someAmount // 15.75,
    },
    
    someAddressField: {
      lines:  [counterpartyInfo[i].addressLine1, counterpartyInfo[i].addressLine2],
      locality: counterpartyInfo[i].city,
      region: counterpartyInfo[i].state,
      postcode: counterpartyInfo[i].zip,
      country: counterpartyInfo[i].country,
    },
  },
};

// launching the workflow
setTimeout(function() {launchWorkflowAsynchronously()
  .then((response) => console.log(response))
  .catch((err) => console.log(err));}, 1500);  // adding in delay for rate limiting

}

When each workflow is launched through the asynchronous endpoint, a JobId will be returned as well as a URL to check the status. This asyncJobId or asynchJobStatusUrl can then be used to check the status on the launch of the workflow. The return values returned when each workflow is launched will look similar to this:

{
   "asyncJobId": "PYWC2wl1zDak9X8jXb_qs",
   "asyncJobStatusUrl": "https://demo.ironcladapp.com/public/api/v1/workflows/async/PYWC2wl1zDak9X8jXb_qs"
}

3.3. Checking on status of workflows

This endpoint will check the status of an asynchronous workflow job initiated through the POST /async endpoint. It returns a JSON payload indicating the status of the JobId submitted. This can be done for each workflow initiated with the bulk method example above.

GET /workflows/asynch/{asyncJobId}