Use case: Sending a survey on job completion

A worked example of creating a public page survey that is sent when a job is completed.

Example scenario

When a job is complete, we wish to send the customer a survey response form and then receive and store the responses.

The workflow we want to acheive is:

  • When a job is completed, a triggered action fires and calls a webhook URL with a secret header.
  • The webhook generates a form ID (which is stored against the job record), and sends an SMS to the customer with a URL containing the job ID and form ID.
  • The customer navigates to the URL in a web browser and fills in the form.
  • The response is sent to a form handler, which is another public function.
  • The form handler validates that the form ID matches the ID stored with the job, and if it does, then the results are stored against the job.

How to use public content to achieve the desired workflow

To create a publicly-available form that is linked to a specific job and for from which the responses can be received and stored, the following items must be created:

Create custom fields on the Job object for the form ID and survey responses

Run sked artifacts custom-field upsert -f Jobs-formId.custom-field.json where Jobs-formId.custom-field.json contains the following:

Jobs-formId.custom-field.json
{
  "metadata": {
    "type": "CustomField"
  },
  "objectName": "Jobs",
  "name": "formId",
  "field": {
    "type": "String",
    "description": "a hard-to-guess id to ensure form is only usable by recipient",
    "display": {
      "label": "FormResponseID",
      "order": 0,
      "isAlert": false,
      "showIf": null,
      "showOnDesktop": false,
      "showOnMobile": false,
      "editableOnMobile": false,
      "requiredOnMobile": false
    },
    "constraints": {
      "required": false,
      "accessMode": "ReadWrite",
      "maxLength": 64
    }
  }
}

Repeat for Jobs-q1.custom-field.json, Jobs-q2.custom-field.json, and Jobs-q3.custom-field.json, as detailed below.

Jobs-q1.custom-field.json
{
  "metadata": {
    "type": "CustomField"
  },
  "objectName": "Jobs",
  "name": "q1",
  "field": {
    "type": "String",
    "description": "question 1",
    "display": {
      "label": "Q1",
      "order": 0,
      "isAlert": false,
      "showIf": "it.q1 !== null",
      "showOnDesktop": true,
      "showOnMobile": false,
      "editableOnMobile": false,
      "requiredOnMobile": false
    },
    "constraints": {
      "required": false,
      "accessMode": "ReadWrite",
      "maxLength": 255
    }
  }
}
Jobs-q2.custom-field.json
{
  "metadata": {
    "type": "CustomField"
  },
  "objectName": "Jobs",
  "name": "q2",
  "field": {
    "type": "Picklist",
    "description": "",
    "display": {
      "label": "Q2",
      "order": 0,
      "isAlert": false,
      "showIf": "it.q2 !== null",
      "showOnDesktop": true,
      "showOnMobile": false,
      "editableOnMobile": false,
      "requiredOnMobile": false
    },
    "constraints": {
      "required": false,
      "accessMode": "ReadWrite"
    },
    "multipleAllowed": false,
    "allowedValues": [
      {
        "value": "Good",
        "label": "Good",
        "active": true,
        "default": false
      },
      {
        "value": "Bad",
        "label": "Bad",
        "active": true,
        "default": false
      },
      {
        "value": "Indifferent",
        "label": "Indifferent",
        "active": true,
        "default": false
      }
    ]
  }
}
Jobs-q3.custom-field.json
{
  "metadata": {
    "type": "CustomField"
  },
  "objectName": "Jobs",
  "name": "q3",
  "field": {
    "type": "Picklist",
    "description": "",
    "display": {
      "label": "Q3",
      "order": 0,
      "isAlert": false,
      "showIf": "it.q3 !== \"\"",
      "showOnDesktop": true,
      "showOnMobile": false,
      "editableOnMobile": false,
      "requiredOnMobile": false
    },
    "constraints": {
      "required": false,
      "accessMode": "ReadWrite"
    },
    "multipleAllowed": false,
    "allowedValues": [
      {
        "value": "X1",
        "label": "1",
        "active": true,
        "default": false
      },
      {
        "value": "X2",
        "label": "2",
        "active": true,
        "default": false
      },
      {
        "value": "X3",
        "label": "3",
        "active": true,
        "default": false
      },
      {
        "value": "X4",
        "label": "4",
        "active": true,
        "default": false
      },
      {
        "value": "X5",
        "label": "5",
        "active": true,
        "default": false
      }
    ]
  }
}

Create custom user roles for the survey webhook and survey form endpoint

It is best practice to minimise the permissions that public functions run with. The webhook only needs to store the form ID against the job and send an SMS. The form handler needs to be able to read the form ID and store the survey responses.

To create the webhook user role, do the following:

  • Run the following command in the CLI:
sked artifacts user-role upsert -f user-role/survey-webhook.user-role.json

where user-role/survey-webhook.user-role.json contains the following:

{
  "metadata": {
    "type": "UserRole"
  },
  "description": "Role for survey-webhook",
  "name": "SurveyWebhook",
  "custom":true,
  "permissionPatterns": [
    "skedulo.tenant.data.modify",
    "skedulo.tenant.notifications.sms.send",
    "skedulo.tenant.extension.packages.view"
  ]
}

Then run:

sked artifacts user-role upsert -f user-role/survey-handler.user-role.json

where user-role/survey-handler.user-role.json is as follows:

{
  "metadata": {
    "type": "UserRole"
  },
  "description": "Role for survey-handler",
  "name": "SurveyHandler",
  "custom":true,
  "permissionPatterns": [
    "skedulo.tenant.data.view",
    "skedulo.tenant.data.modify",
    "skedulo.tenant.extension.packages.view"
  ]
}

Create public functions for the webhook

sked function generate --name=survey-webhook --outputdir=functions/survey-webhook

Update the state.json in functions/survey-webhook to add the public function configuration:

{
  "metadata": {
    "type": "Function"
  },
  "name": "survey-webhook",
  "source": "./",
  "unauthenticated": {
    "executionRole": "survey-webhook"
  }
}

Create public functions for the form handler

sked function generate --name=survey-handler --outputdir=functions/survey-handler

Update the state.json in functions/survey-handler to add the public function configuration:

{
  "metadata": {
    "type": "Function"
  },
  "name": "survey-handler",
  "source": "./",
  "unauthenticated": {
    "executionRole": "survey-handler"
  }
}

Create a public page for the survey form

sked artifacts public-content upsert -f state.json

where state.json looks like:

{
  "metadata": {
    "type": "PublicContent"
  },
  "source": "./",
  "name": "survey",
  "compilationCommand": "yarn build"
}

Create configuration variables in Skedulo for the webhook to work

In the Skedulo app, create configuration variables for:

  • SKEDULO_WEB_APP_URL - this will look something like https://my-team.my.skedulo.com
  • WEBHOOK_HASH - this should be a hard to guess secret hash - e.g. the result of openssl rand -base64 40

Create a triggered action that calls the webhook

Run sked artifacts triggered-action upsert -f survey-questions.triggered-action.json where survey-questions.triggered-action.json is as follows:

{
  "metadata": {
    "type": "TriggeredAction"
  },
  "name": "survey-questions",
  "enabled": true,
  "trigger": {
    "type": "object_modified",
    "filter": "Operation == 'UPDATE' AND Current.JobStatus != Previous.JobStatus AND Current.JobStatus == 'Complete'",
    "schemaName": "Jobs"
  },
  "action": {
    "type": "call_url",
    "url": "https://{{ TENANT_DOMAIN_NAME }}/function/survey-webhook/webhook",
    "query": "{ UID Name Contact { Phone MobilePhone }}",
    "headers": {
      "sked-webhook-hash": "{{ WEBHOOK_HASH }}"
    }
  }
}