Using the Workflow Service to approve new or updated data

In this tutorial we'll see how to use the Workflow REST API to model a workflow for a simple approvals process.

Let's imagine we want to model the process of approving some data. To do this, we first create a task definition that models the possible flows. We can then create a tasks each time we want to actually approve some data.

Note we'll only transition between states manually in this tutorial. To learn how to automate workflows, follow this tutorial on setting up a data quality control to check new pricing data for outliers.

Note: To complete this tutorial, you must have suitable access control permissions. This can most easily be achieved by assigning your LUSID user the built-in lusid-administrator role, which should already be the case if you are the domain owner. If you are informed you do not have a license to perform a certain operation, contact support.

Step 1: Creating a task definition to model the workflow

We must first create a task definition using the CreateTaskDefinition API, passing in:

  • An API access token as a Bearer token in the Authorization HTTP header. See how to obtain a token.

  • A scope and code that together uniquely identify the task definition.

  • A displayName and description for the task definition.

  • A set of possible states; in our example we need Pending, Approved, Denied and NotRequired.

  • A fieldSchema defining a set of fields, for our example we need assignee, dataToApprove and reasonForDecision as fields.

  • An initialState of Pending, and a dataToApprove field that is required to enter this state.

  • A set of triggers to prompt state transitions. Each trigger should contain the following:

    • A name for the trigger.

    • The type of the trigger - for now only External triggers are available.

  • A set of transitions to control movement from the initial state to all the other states. Each transition should contain the following:

    • A fromState specifying the state the transition begins in.

    • A toState specifying the state the transition ends in.

    • The name of the trigger prompting the transition to occur.

    • Optionally, a guard condition which must be met for the transition to succeed. Read more on using guards in workflows.

Note: To avoid infinite loops, there must always be at least one terminal state within the task definition; that is, a state from which no onward transitions are possible.

curl -X POST 'https://<your-domain>.lusid.com/workflow/api/taskdefinitions' 
  -H 'Authorization: Bearer <your-api-access-token>'
  -H 'Content-Type: application/json-patch+json'
  -d '{
  "id": {
    "scope": "approvals",
    "code": "dataApproval"
  },
  "displayName": "Data Approval",
  "description": "A task definition for a simple data approval task",
  "states": [
    { "name": "Pending" },
    { "name": "Approved" },
    { "name": "Denied" },
    { "name": "NotRequired" }
  ],
  "fieldSchema": [
    {
      "name": "assignee",
      "type": "String"
    },
    {
      "name": "dataToApprove",
      "type": "String"
    },
    {
      "name": "reasonForDecision",
      "type": "String"
    }
  ],
  "initialState": {
    "name": "Pending",
    "requiredFields": [
      "dataToApprove"
    ],
  },
  "triggers": [
    {
      "name": "grant",
      "trigger": {
        "type": "External"
      }
    },
    {
      "name": "reject",
      "trigger": {
        "type": "External"
      }
    },
    {
      "name": "cancel",
      "trigger": {
        "type": "External"
      }
    }
  ],
  "transitions": [
    {
      "fromState": "Pending",
      "toState": "Approved",
      "trigger": "grant",
      "guard": "fields['assignee'] exists"
    },
    {
      "fromState": "Pending",
      "toState": "Denied",
      "trigger": "reject",
      "guard": "fields['reasonForDecision'] exists and fields['assignee'] exists"
    },
    {
      "fromState": "Pending",
      "toState": "NotRequired",
      "trigger": "cancel"
    }
  ]
}'

Part of a response is shown below. Note the asAtVersionNumber of the task definition, which increments if you update the task definition:

 {
  "id": {
    "scope": "approvals",
    "code": "dataApproval"
  },
  "version": {
    "asAtCreated": "2023-05-15T11:08:15.1522320+00:00",
    "userIdCreated": "00ujk6twb4jDcHGjN2p8",
    "asAtModified": "2023-05-15T11:08:15.1522320+00:00",
    "userIdModified": "00ujk6twb4jDcHGjN2p8",
    "asAtVersionNumber": 1
  },
  "displayName": "Data Approval",
  "description": "A task definition for a simple data approval task",
...

Step 2: Creating a task to approve some data

Now that we have our task definition, we can test our workflow using a new approval. To do so, we create a task from our task definition and include the required fields for the task to enter its initial state and set the workflow in motion.

To create a task we call the CreateTask API, passing in:

  • The scope and code of the task definition.

  • Optionally, any correlationIds to track and group API calls.

  • The name and an appropriate value for the required dataToApprove field:

curl -X POST "https://<your-domain>.lusid.com/workflow/api/tasks"
  -H "Authorization: Bearer <your-API-access-token>"
  -H "Content-Type: application/json"
  -d '{
  "taskDefinitionId": {
    "scope": "approvals",
    "code": "dataApproval"
  },
  "correlationIds": ["track-this-in-the-logs"],
  "fields": [
    {
      "name": "dataToApprove",
      "value": "Data x for y client"
    }
  ]
}'

Part of a response is shown below. Note the following:

  • The asAtVersionNumber of the task, which increments if you update the task (see step 3).

  • The task is in its initial state of Pending.

  • LUSID automatically creates a unique id for the task, which you can use to retrieve or update the task at any time.

{
  "id": "79315001-b4bf-40e6-aae1-24850119aa9f",
  "correlationIds": [
    "track-this-in-the-logs"
  ],
  "taskDefinitionId": {
    "scope": "approvals",
    "code": "dataApproval"
  },
  "taskDefinitionVersion": {
    "asAtModified": "2023-05-15T11:08:15.1522320+00:00"
  },
  "version": {
    "asAtCreated": "2023-05-15T12:39:12.8015030+00:00",
    "userIdCreated": "00uji4twb4jDcHGjN2p7",
    "asAtModified": "2023-05-15T12:39:12.8015030+00:00",
    "userIdModified": "00uji4twb4jDcHGjN2p7",
    "asAtVersionNumber": 1
  },
  "state": "Pending",
  "terminalState": false,
  ...

Step 3: Updating the task to grant approval manually

Our task now exists in the Pending state. To transition it to another state, such as Approved, we can call the UpdateTask API, passing in the following:

  • Within the request URL:

    • The unique id for the task we want to update.

    • A trigger to prompt a state transition. For our example, we'll use the grant trigger to move to the Approved state.

  • The name and an appropriate value for the assignee field, which is required to meet the guard on the grant trigger.

  • Names and appropriate values for any other fields we want to propagate, since this is a PUT operation and resources not specified are set to null (and therefore lost). In our example, we'll include the dataToApprove field in order to pass the data to the Approved state.

curl -X POST "https://<your-domain>.lusid.com/workflow/api/tasks/79315001-b4bf-40e6-aae1-24850119aa9f?trigger=grant"
  -H "Authorization: Bearer <your-API-access-token>"
  -H "Content-Type: application/json"
  -d '{
  "fields": [
    {
      "name": "assignee",
      "value": "Joe Bloggs"
    },
    {
      "name": "dataToApprove",
      "value": "Data x for this y client"
    }
  ]
}'

Part of a response is shown below. Note the following:

  • The asAtVersionNumber of the task has been incremented.

  • The task is in its terminal state of Approved.

{
  ...
  "taskDefinitionVersion": {
    "asAtModified": "2023-05-16T08:12:42.2904480+00:00"
  },
  "version": {
    "asAtCreated": "2023-05-16T13:57:37.9615180+00:00",
    "userIdCreated": "00uji4twb4jDcHGjN2p7",
    "asAtModified": "2023-05-16T13:58:08.3013150+00:00",
    "userIdModified": "00uji4twb4jDcHGjN2p7",
    "asAtVersionNumber": 2
  },
  "state": "Approved",
  "terminalState": true,
  ...

Since the task is now in its terminal state, it is not possible to trigger more state transitions for this particular task. To do anything else with this workflow, we need to create a new task from our task definition using the CreateTask API.