Deferred and scheduled webhooks

Defer a webhook or schedule a webhook to trigger at a specified time.

You can defer or schedule webhooks so that they return information at different times.

Deferred webhooks

You can set your webhooks to only respond after a certain time and/or when an object matches the specific criteria that you want to appear in your webhook.

The following example demonstrates how to create a deferred webhook that fires 10 seconds after the job has been created. It will not fire if the job Description field is cancel or changes to cancel within 10 seconds of the job being created.

  1. Modify your webhook.js file to add the following configuration:

    const url = "https://b14b2804.ngrok.io"
    
    const jsonDeferred = {
      name: "test_deferred",
      url: url,
      type: "graphql_deferred",
      field: "CreatedDate",
      offset: 10000,
      filter: "Description != 'cancel'",
      query: `
        subscription {
          jobs {
            UID
            Name
            Description
            Duration
            Start
            End
            CreatedDate
            JobAllocations {
              UID
            }
          }
        }
      `
    }
    
    console.log(JSON.stringify(jsonDeferred, null, 2))
    
    • The field must be a field with an INSTANT value, such as Start, End, ActualStart, EstimatedEnd, and so on. In the case above, the webhook is being deferred on the basis of the CreatedDate value.

    • The offset is the number of milliseconds being deferred before the webhook fires. In this case, it is 10000 milliseconds. The minimum deferred time is 5000 milliseconds. The offset value can also be negative.

    • The filter field is used to filter jobs so that only relevant results are returned by the webhook after the offset period. In this case, jobs with cancel in the Description will not be returned by the webhook.

  2. Create a webhook request body:

    node webhook.js > temp.json
  3. Check that the temp.json file now contains the jsonDeferred GraphQL query. The file should include the following configuration:

    {
      "name": "test_deferred",
      "url": "https://b14b2804.ngrok.io",
      "type": "graphql_deferred",
      "field": "CreatedDate",
      "offset": 10000,
      "filter": "Description != 'cancel'",
      "query": "\n      subscription {\n        jobs {\n          UID\n          Name\n          Description\n          Duration\n          Start\n          End\n          CreatedDate\n          JobAllocations {\n            UID\n          }\n        }\n      }\n    "
    }
  4. Create the deferred webhook using the cURL command:

    curl -s -X POST -H "Authorization: Bearer $AUTH_TOKEN"  -H "Content-Type: application/json" -d @temp.json 'https://api.skedulo.com/webhooks' | jq

    This returns the following response confirming that the webhook is established, including the webhook id:

    {
      "result": {
        "id": "6b6d32e0-ad1f-45fa-9f03-b5264ca02ecb",
        "name": "test_deferred",
        "url": "https://b14b2804.ngrok.io",
        "headers": {},
        "schemaName": "Jobs",
        "fieldName": "CreatedDate",
        "offset": 10000,
        "filter": "Description != 'cancel'",
        "query": "\n      subscription {\n        jobs {\n          UID\n          Name\n          Description\n          Duration\n          Start\n          End\n          CreatedDate\n          JobAllocations {\n            UID\n          }\n        }\n      }\n    ",
        "customFields": {},
        "type": "graphql_deferred"
      }
    }
  5. Create a new job using GraphQL to confirm that the webhook has been created with a deferred offset of 10000 milliseconds (10 seconds).

    If you still have the test webhook running, you will receive two responses for this job in the terminal that is listening on port 8080.

    • The first response is from the test webhook, which fires the response immediately when the job is created.

    • The second response is sent 10 seconds later from the test_deferred webhook. Both responses are identical except for the skedulo-webhook-id and skedulo-request-id numbers that the webhook that sent the request.

  6. Confirm that the webhook filters jobs with cancel in the Description field:

    a. Create a new job or update an existing job to have the value cancel in the subscription field.

    b. The deferred webhook will not show a response as jobs with cancel in the Description field are being ignored.

If your test webhook is still active, you will still receive an immediate response from that webhook.

Dynamically deferred webhooks

Configure deferred webhooks to respond to offset values assigned to custom fields on objects.

This provides object-level control over when the webhook fires.

For example, you can set a custom field on a job with an offset that triggers an SMS to the customer one day before the job to remind them of their appointment. The offset allows the “one day” to differ between jobs and is configurable and hard coded at an organizational level.

When creating a webhook, the offset field on an object can also be an object with two fields:

  • field: is the name of a number or duration field on the schema.
  • default: is an optional value used as the offset if the field has no value.

In the following example, we create a custom number field on the Shifts object called CustomNumber, then create a webhook with a default offset value of 15 seconds (15000).

The webhook will send a notification for the shift after 15 seconds unless a different CustomNumber value is provided on the shift.

Example: Deferred webhook on the Shifts object using a dynamic offset

  1. Create a custom number field on the Shifts object called CustomNumber. Use the following payload in a POST request to the /custom/standalone/fields endpoint:

      {
          "name": "CustomNumber",
          "schemaName": "Shifts",
          "column": {
            "type": "int"
          }
      }
  2. Create a file called httpserver.js with the following configuration:

    const http = require("http");
          
    http.createServer((req, res) => {
      const body = [];
      req
        .on('data', (chunk) => {
          body.push(chunk);
        })
        .on('end', () => {
          const bodyStr = Buffer.concat(body).toString();
          const obj = {
            headers: req.headers,
            body: JSON.parse(bodyStr)
          }
          console.log(JSON.stringify(obj, null, 2))
          res.write(JSON.stringify(obj, null, 2))
          res.end()
        });
        
      res.writeHead(200, {"Content-Type": "application/json"})
    }).listen(8080)
    
  3. Start the HTTP server in one terminal: node httpserver.js

  4. Start ngrok in another terminal:

    ./ngrok http 8080

    Take note of the HTTPS address of your ngrok server.

    Use this address as the "url" in the webhook file in the webhook configuration file in Step 5.

  5. Create a webhook with an offset field value of 15 seconds (15000):

      {
        "name": "graphql_deferred_offset_field_shift",
        "url": "https://b64dd386.ngrok.io",
        "type": "graphql_deferred",
        "field": "Start",
        "offset": {
          "field": "CustomNumber",
              "default": 15000
         },
        "filter": "DisplayName != 'cancel'",
        "query": "\nsubscription {\n  shifts {\n    UID\n    DisplayName\n    Duration\n    Start\n    End\n   CustomNumber\n   CreatedDate\n    }\n}\n"
      }

    The following response shows the webhook has been created successfully with the CustomNumber offset field name on the Shifts object and the offset value of 15 seconds (15000):

      {
        "result": {
          "id": "b345c228-9c19-48c4-a27c-d9cb489c30d7",
          "name": "graphql_deferred_offset_field_shift",
          "url": "https://b64dd386.ngrok.io",
          "headers": {},
          "schemaName": "Shifts",
          "fieldName": "Start",
          "offset": {
            "fieldName": "CustomNumber",
            "default": 15000
          },
          "filter": "DisplayName != 'cancel'",
          "query": "\nsubscription {\n  shifts {\n    UID\n    DisplayName\n    Duration\n    Start\n    End\n   CustomNumber\n   CreatedDate\n    }\n}\n",
          "customFields": {
            "Shifts": [
              "CustomNumber"
            ]
          },
          "type": "graphql_deferred"
        }
      }

    The default on the offset object is optional, and is used if the field has no value. If the field has no value and no default is provided then the webhook is not triggered for that object.

  6. Create two shifts using GraphQL, one with a custom number offset and the other without:

        mutation {
          schema {
            shift1: insertShifts(input: {
              RegionId: "00036206-7555-4280-b1b7-86d566437391"
              DisplayName: "Shift no custom number"
              CustomNumber: null
              Start: "2020-01-16T20:22:00Z"
              Duration: 60
            })
            shift2: insertShifts(input: {
              RegionId: "00036206-7555-4280-b1b7-86d566437391"
              DisplayName: "Shift custom number"
              CustomNumber: 10000
              Start: "2020-01-16T20:22:00Z"
              Duration: 60
            })
          }
        }
  7. Check the terminal that is listening on port 8080 to see the response:

    {
      "result": [
        {
          "timestamp": "2020-01-16T20:22:15.825909Z",
          "level": "WARN",
          "message": "Webhook request returned status code 404",
          "data": {
            "webhookId": "42170bdd-2c09-47ca-b484-8a02e705b9ce",
            "webhookRequestId": "d563e37c-e468-4b66-a9bd-36ff7c1ddad1"
          }
        },
        {
          "timestamp": "2020-01-16T20:22:11.131707Z",
          "level": "WARN",
          "message": "Webhook request returned status code 404",
          "data": {
            "webhookId": "42170bdd-2c09-47ca-b484-8a02e705b9ce",
            "webhookRequestId": "6d8aad32-2306-47ef-a491-5ca5b33ac8e3"
          }
        },
      ]
    }

Scheduled webhooks

You can also schedule a webhook to fire at certain intervals using cron.

Cron is a time-based scheduling tool that allows you to configure events to run at fixed times or intervals so that you can automate certain operations.

The "cron" field in a scheduled webhook request expects a cron expression where * represents a unit of time field. Units of time are represented in the following way:

# ┌───────────── minute (0 - 59)
# │ ┌───────────── hour (0 - 23)
# │ │ ┌───────────── day of the month (1 - 31)
# │ │ │ ┌───────────── month (1 - 12)
# │ │ │ │ ┌───────────── day of the week (0 - 6) (Sunday to Saturday.
# │ │ │ │ │                                       Sunday can also be 7.)
# │ │ │ │ │
# │ │ │ │ │
# * * * * * 

For example, to run a job every Friday at 11.45AM, the cron expression would look like this:

45 11 * * 5 

To create a scheduled webhook, you can include the "cron" field in the JSON request body with the time or interval you want to schedule the request included using the above format.

For example, the following webhook requests a response from the tenant organization every minute:

Method: POST

Endpoint: /webhooks

Request body:

{
  "name": "scheduled_every_minute",
  "url": "https://45d2e3b7.ngrok.io",
  "headers": {},
  "cron": "* * * * *",
  "type": "scheduled"
}

The webhook returns a response from the server every minute until the webhook is deleted:

{
  "headers": {
    "user-agent": "Skedulo-Webhook",
    "skedulo-webhook-id": "2c3d8594-3d66-4e9e-ada1-fde48f37fc88",
    "skedulo-request-id": "a201143a-e36c-4420-ad8a-1787c1e5f7bb",
    "content-length": "2",
    "content-type": "application/json; charset=UTF-8",
    "accept-encoding": "gzip, deflate",
    "host": "45d2e3b7.ngrok.io",
    "accept": "*/*",
    "x-forwarded-proto": "https",
    "x-forwarded-for": "52.33.155.238"
  },
  "body": {}
}
{
  "headers": {
    "user-agent": "Skedulo-Webhook",
    "skedulo-webhook-id": "2c3d8594-3d66-4e9e-ada1-fde48f37fc88",
    "skedulo-request-id": "891a3b8e-67e8-4831-a316-9bdfa83b2e12",
    "content-length": "2",
    "content-type": "application/json; charset=UTF-8",
    "accept-encoding": "gzip, deflate",
    "host": "45d2e3b7.ngrok.io",
    "accept": "*/*",
    "x-forwarded-proto": "https",
    "x-forwarded-for": "52.33.155.238"
  },
  "body": {}
}
{
  "headers": {
    "user-agent": "Skedulo-Webhook",
    "skedulo-webhook-id": "2c3d8594-3d66-4e9e-ada1-fde48f37fc88",
    "skedulo-request-id": "dfb3d5b4-d2f7-4582-9edd-fe4455961658",
    "content-length": "2",
    "content-type": "application/json; charset=UTF-8",
    "accept-encoding": "gzip, deflate",
    "host": "45d2e3b7.ngrok.io",
    "accept": "*/*",
    "x-forwarded-proto": "https",
    "x-forwarded-for": "52.33.155.238"
  },
  "body": {}
}
{
  "headers": {
    "user-agent": "Skedulo-Webhook",
    "skedulo-webhook-id": "2c3d8594-3d66-4e9e-ada1-fde48f37fc88",
    "skedulo-request-id": "7e04be33-2b69-4110-a807-b45dccbde19c",
    "content-length": "2",
    "content-type": "application/json; charset=UTF-8",
    "accept-encoding": "gzip, deflate",
    "host": "45d2e3b7.ngrok.io",
    "accept": "*/*",
    "x-forwarded-proto": "https",
    "x-forwarded-for": "52.33.155.238"
  },
  "body": {}
}