Replacement with Automation Rules

Migration instruction from Webhooks to Automation Rules

It’s time to completely deprecate the Webhook plugin. Therefore we’ve created this article in order to help you with the migration process from Webhooks to Automation Rules.

❗️

Webhooks will be removed the 24 of November, 2021

All the Webhook profiles have to be migrated before this date to Automation Rules

Let’s go through the migration process together with you and start with comparison of blocks in Automation rules and Webhook.

Trigger block

When in Automation rules correspond to the Trigger on events and Trigger on entities blocks in Webhooks plugin profile. You can choose the entity to be affected from the dropdown in ‘When’ same as how you can check the box of Trigger on Entities.

Filter block

Filters in Automation rules go directly after the triggers and they correspond to the Filter block in webhooks.

You can do far more in filtering with Automation rules than what you used to be able to do in Webhooks plugin:

Action block

Then in Automation Rules corresponds to the combination of template block and Webhook URL in Webhooks plugin:

Webhook plugin URL

Webhook plugin Template

Automation Rule action

Corresponding example

External link

JSON payload

Send HTTP request action,
Webhook URL as URL
JSON payload as Request body

External integration

Targetprocess API link /api/v1/
pointing to the same entity as in the trigger

fields of the entity, including {{ID}} of the entity

Update entity action,
with all the corresponding fields set

Modify an entity on its creation

Targetprocess API link /api/v1/
pointing to a different entity from what we have in the trigger

fields of the entity

Create entity action,
with all the corresponding fields set

Add another entity

External link

empty template

Javasciprt action, where we enumerate all the fields that used to be send by default in Webhook plugin

External integration with empty template

Example: External integration (sending Slack notifications)

Slack notification using Webhooks plugin shows the procedure to create a webhook for slack notification.

Slack notifications using Automation Rules shows the procedure to create an Automation rule for sending slack notification.

The below picture depicts the complete comparison of how it looks when all trigger, filter and action parts are put all together in both Webhooks and Automation rules.

You can modify any block as you wish with UI if feasible or some complex features with Javascript.
Feel free to have a look at our Dev guide that contains more real time examples of Automation Rules: Automation Rules overview

Example: modify an entity on its creation

This example sets the default description on creation of Userstory. This is how it looks in webhooks and Automation rules.

And the result for both would look like this.

Example: add another entity (follower) when a work item is created or updated

Now when users create an entity (e.g. User Story, Feature, Bug, etc.), they are automatically subscribed as followers to this entity and nested ones.

The result in both the cases would look like this.

Example: External integration with empty template

📘

How to apply JSON of an Automation Rule?

For User Story

Webhook setup:

Webhook for US Create, Update, DeleteWebhook for US Create, Update, Delete

Webhook for US Create, Update, Delete

Automation rule setup:

{
  "pipeline": [
    {
      "type": "source:targetprocess:EntityChanged",
      "entityTypes": [
        "UserStory"
      ],
      "modifications": {
        "created": true,
        "deleted": true,
        "updated": [
          "Feature",
          "CustomFields",
          "Description",
          "EntityState",
          "EntityType",
          "Iteration",
          "Name",
          "Priority",
          "Project",
          "Release",
          "AssignedTeams",
          "TeamIteration",
          "Owner",
          "InitialEstimate",
          "EffortToDo",
          "Effort",
          "EffortCompleted",
          "LastCommentedUser",
          "LastEditor",
          "NumericPriority",
          "Progress",
          "TimeRemain",
          "TimeSpent",
          "CreateDate",
          "EndDate",
          "LastCommentDate",
          "ModifyDate",
          "PlannedEndDate",
          "PlannedStartDate",
          "StartDate",
          "Id"
        ]
      }
    },
    {
      "type": "action:JavaScript",
      "script": "const webhookUrl = \"https://webhook.site/b146b650-9dbb-4131-83f3-774084af8353\";\n\nconst api = context.getService(\"targetprocess/api/v2\");\nconst [author, customFields, newOwner] = await Promise.all([\n  api.getByIdAsync(\"GeneralUser\", args.Author.Id, {\n    select: \"{id,email,modifydate,firstname,lastname,isadministrator}\"\n  }),\n  api.queryAsync(\"CustomField\", {\n    select: \"{name,entityFieldName,value,isSystem,required,fieldType,config,isdynamicColumn,entityFieldNameV2}\",\n    orderBy: \"entityFieldName\",\n    where: `entitytype.id=${args.Current.EntityType.Id} and Process.Projects.Count(Id=${args.Current.Project.Id})>0`\n  }),\n  api.getByIdAsync(\"GeneralUser\", args.Current.Owner.Id, {\n    select: \"fullName\"\n  })\n]);\nconst oldOwner = args.Previous && args.Previous.Owner.Id != args.Current.Owner.Id\n  ? await api.getByIdAsync(\"GeneralUser\", args.Previous.Owner.Id, {\n    select: \"fullName\"\n  })\n  : newOwner;\n\nfunction getProcessedCfValue(field, cfValue, valueArray) {\n  let value = cfValue || undefined;\n  if (value || field.fieldType == \"Number\") {\n    switch (field.fieldType) {\n      case \"Date\":\n        if (value) {\n          let date = new Date(value);\n          let offset = value.substring(19, 22);\n          let ticks = ((date.getTime() * 10000) + 621355968000000000) + (parseInt(offset) * 60 * 600000000);\n          value = value.substring(0, 10);\n          valueArray.push({ cfColumn: field.entityFieldName, cfValue: ticks });\n        }\n        break;\n      case \"Number\":\n        if (!value) { value = 0; }\n        valueArray.push({ cfColumn: field.entityFieldName, cfValue: value });\n        break;\n      case \"Entity\":\n        value = value.Id;\n        valueArray.push({ cfColumn: field.entityFieldName, cfValue: value });\n        break;\n      case \"URL\":\n        value = `${value.label}\\r\\n${value.url}`;\n        valueArray.push({ cfColumn: field.entityFieldName, cfValue: value });\n        break;\n      default:\n        valueArray.push({ cfColumn: field.entityFieldName, cfValue: value });\n        break;\n    }\n  }\n  return value;\n}\n\nfunction buildMeta(field, value) {\n  return {\n    \"Name\": field.name,\n    \"Items\": field.value ? field.value.split(\"\\r\\n\") : [],\n    \"IsSystem\": field.isSystem,\n    \"EntityFieldName\": field.entityFieldName,\n    \"FieldType\": fieldTypes.indexOf(field.fieldType),\n    \"Required\": field.required,\n    \"Value\": value,\n    \"DefaultValue\": field.config.defaultValue || undefined,\n    \"IsDynamicColumn\": field.isdynamicColumn,\n    \"EntityFieldNameV2\": field.entityFieldNameV2,\n    \"CalculationModel\": field.config.calculationModel || undefined\n  }\n}\n\nlet newFieldValues = [];\nlet newFieldMeta = [];\nlet oldFieldValues = [];\nlet oldFieldMeta = [];\nconst fieldTypes = [\"Text\", \"DropDown\", \"CheckBox\", \"URL\", \"Date\", \"RichText\", \"Number\", \"Money\", \"Entity\",\n  \"MultipleSelectionList\", \"TemplatedURL\", \"MultipleEntities\", \"Money\"];\nfor (let field of customFields) {\n  let newValue = getProcessedCfValue(field, args.Current[field.name], newFieldValues);\n  newFieldMeta.push(buildMeta(field, newValue));\n  if (args.Previous) {\n    let oldValue = getProcessedCfValue(field, args.Previous[field.name], oldFieldValues);\n    oldFieldMeta.push(buildMeta(field, oldValue));\n  }\n}\n\nfunction buildEntity(enity, fieldValues, fieldMeta, owner) {\n  if (!enity) { return {} }\n  let formattedEntity = {\n    \n    \"ID\": args.ResourceId,\n    \"UserStoryID\": args.ResourceId,\n    \"Name\": enity.Name,\n    \"Description\": enity.Description || undefined,\n    \"StartDate\": enity.StartDate ? enity.StartDate.substr(0, 19) : undefined,\n    \"CreateDate\": enity.CreateDate.substring(0, 19),\n    \"ModifyDate\": enity.ModifyDate.substring(0, 19),\n    \"PlannedStartDate\": enity.PlannedStartDate ? enity.PlannedStartDate.substr(0, 19) : undefined,\n    \"PlannedEndDate\": enity.PlannedEndDate ? enity.PlannedEndDate.substr(0, 19) : undefined,\n    \"LastCommentDate\": enity.LastCommentDate ? enity.LastCommentDate.substr(0, 19) : undefined,\n    \"NumericPriority\": enity.NumericPriority,\n    \"Effort\": enity.Effort,\n    \"EffortCompleted\": enity.EffortCompleted,\n    \"EffortToDo\": enity.EffortToDo,\n    \"TimeSpent\": enity.TimeSpent,\n    \"TimeRemain\": enity.TimeRemain,\n    \"InitialEstimate\": enity.InitialEstimate,\n    \"LastCommentUserID\": enity.LastCommentedUser ? enity.LastCommentedUser.Id : undefined,\n    \"OwnerID\": enity.Creator.Id,\n    \"LastEditorID\": enity.LastEditor.Id,\n    \"EntityStateID\": enity.EntityState.Id,\n    \"PriorityID\": enity.Priority.Id,\n    \"ProjectID\": enity.Project.Id,\n    \"ParentID\": enity.Feature ? enity.Feature.Id : undefined,\n    \"SquadIterationID\": enity.SquadIterationID? enity.SquadIterationID.Id :undefined,\n    \"ReleaseID\": enity.Release ? enity.Release.Id : undefined,\n\n    \"FeatureID\": enity.Feature ? enity.Feature.Id : undefined,\n    \"EntityTypeName\": \"Tp.BusinessObjects.\" + args.ResourceType,\n    \"EntityStateName\": enity.EntityState.Name,\n    \"PriorityName\": enity.Priority.Name,\n    \"ProjectName\": enity.Project.Name,\n    \"ParentName\": enity.Feature ? enity.Feature.Name : undefined,\n    \"FeatureName\": enity.Feature ? enity.Feature.Name : undefined,\n    \"ReleaseName\": enity.Release ? enity.Release.Name : \"Backlog\",\n    \"EntityTypeID\": enity.EntityType.Id,\n    \"CustomFieldsMetaInfo\": fieldMeta,\n    \"EndDate\": enity.EndDate ? enity.EndDate.substring(0, 19) : undefined,\n    \"Progress\": enity.Progress,\n    \"LastStateChangeDate\": enity.LastStateChangeDate ? enity.LastStateChangeDate.substring(0, 19) : undefined,\n    \"OwnerName\": owner,\n    \"SquadID\": enity.Team ? enity.Team.Id : undefined,\n    \"SquadName\": enity.Team ? enity.Team.Name : undefined,\n    \"ResponsibleSquadID\": enity.ResponsibleTeam ? enity.ResponsibleTeam.Id : undefined\n  };\n  for (let cf of fieldValues) {\n    formattedEntity[cf.cfColumn] = cf.cfValue;\n  }\n  return formattedEntity;\n}\n\nconst http = require('http');\nawait http.postAsync(webhookUrl, {\n  body: {\n    \"Entity\": buildEntity(args.Current, newFieldValues, newFieldMeta, newOwner),\n    \"OldEntity\": buildEntity(args.Previous, oldFieldValues, oldFieldMeta, oldOwner),\n    \"ChangedFields\": args.Modification == \"Updated\" ? args.ChangedFields : [],\n    \"Author\": {\n      \"ID\": author.id,\n      \"UserID\": author.id,\n      \"FirstName\": author.firstname,\n      \"LastName\": author.lastname,\n      \"Email\": author.email,\n      \"ModifyDate\": author.modifydate? author.modifydate.substring(0, 19):undefined,\n      \"IsAdministrator\": author.isadministrator\n    },\n    \"Modification\": args.Modification\n  },\n  headers: { 'content-type': 'application/json' }\n})"
    }
  ]
}

For Role Efforts

Webhook Setup :

Webhook for RoleEfforts Create, Update, DeleteWebhook for RoleEfforts Create, Update, Delete

Webhook for RoleEfforts Create, Update, Delete

Automation Rule setup:

{
  "pipeline": [
    {
      "type": "source:targetprocess:EntityChanged",
      "entityTypes": [
        "roleeffort"
      ],
      "modifications": {
        "created": true,
        "deleted": true,
        "updated": [
          "Effort"
        ]
      }
    },
    {
      "type": "action:JavaScript",
      "script": "const webhookUrl = \"https://webhook.site/b146b650-9dbb-4131-83f3-774084af8353\";\nconst api = context.getService(\"targetprocess/api/v2\");\n\nconst author = await api.getByIdAsync(\"GeneralUser\", args.Author.Id, {\n  select: \"{id,email,modifydate,firstname,lastname,isadministrator}\"\n});\nconst http = require('http');\n\nawait http.postAsync(webhookUrl, {\n  body: {\n    \"Entity\": {\n     \"ID\": args.ResourceId,\n      \"RoleEffortID\": args.ResourceId,\n      \"InitialEstimate\": args.Current.InitialEstimate,\n      \"Effort\": args.Current.Effort,\n      \"EffortCompleted\": args.Current.EffortCompleted,\n      \"EffortToDo\": args.Current.EffortToDo,\n      \"TimeSpent\": args.Current.TimeSpent,\n      \"TimeRemain\": args.Current.TimeRemain,\n      \"AssignableID\": args.Current.Assignable.Id,\n      \"RoleID\": args.Current.Role.Id,\n      \"AssignableName\": args.Current.Assignable.Name,\n      \"RoleName\": args.Current.Role.Name\n  \n    \n    },\n    \"OldEntity\": args.Previous ? {\n      \"ID\": args.ResourceId,\n      \"RoleEffortID\": args.ResourceId,\n      \"InitialEstimate\": args.Previous.InitialEstimate,\n      \"Effort\": args.Previous.Effort,\n      \"EffortCompleted\": args.Previous.EffortCompleted,\n      \"EffortToDo\": args.Previous.EffortToDo,\n      \"TimeSpent\": args.Previous.TimeSpent,\n      \"TimeRemain\": args.Previous.TimeRemain,\n      \"AssignableID\": args.Previous.Assignable.Id,\n      \"RoleID\": args.Previous.Role.Id,\n      \"AssignableName\": args.Previous.Assignable.Name,\n      \"RoleName\": args.Previous.Role.Name\n\n    } : {},\n    \"ChangedFields\": args.Modification==\"Updated\"?args.ChangedFields:[],\n    \"Author\": {\n      \"ID\": author.id,\n      \"UserID\": author.id,\n      \"FirstName\": author.firstname,\n      \"LastName\": author.lastname,\n      \"Email\": author.email,\n      \"ModifyDate\": author.modifydate? author.modifydate.substring(0, 19):undefined,\n      \"IsAdministrator\": author.isadministrator\n    },\n    \"Modification\": args.Modification\n  },\n  headers: { 'content-type': 'application/json' }\n})"
    }
  ]
}

For Bug

Webhook setup:

Webhook for Bug create, updateWebhook for Bug create, update

Webhook for Bug create, update

Automation Rule setup:

{
  "pipeline": [
    {
      "type": "source:targetprocess:EntityChanged",
      "entityTypes": [
        "bug"
      ],
      "modifications": {
        "created": true,
        "deleted": false,
        "updated": [
          "EntityState"
        ]
      }
    },
    {
      "or": [
        {
          "and": [
            {
              "value": {
                "type": "constant",
                "value": "Testing Rejected"
              },
              "target": {
                "name": "Name",
                "type": "field",
                "target": {
                  "name": "EntityState",
                  "type": "field",
                  "target": {
                    "type": "pipelineBlockOutput"
                  }
                }
              },
              "operator": {
                "type": "is"
              }
            }
          ]
        }
      ],
      "type": "filter:Relational"
    },
    {
      "type": "action:JavaScript",
      "script": "const webhookUrl = \"https://webhook.site/b64de715-1b5c-4c77-bd0a-6d3b4beffa40\";\n\nconst api = context.getService(\"targetprocess/api/v2\");\nconst [author, customFields, newOwner] = await Promise.all([\n  api.getByIdAsync(\"GeneralUser\", args.Author.Id, {\n    select: \"{id,email,modifydate,firstname,lastname,isadministrator}\"\n  }),\n  api.queryAsync(\"CustomField\", {\n    select: \"{name,entityFieldName,value,isSystem,required,fieldType,config,isdynamicColumn,entityFieldNameV2}\",\n    orderBy: \"entityFieldName\",\n    where: `entitytype.id=${args.Current.EntityType.Id} and Process.Projects.Count(Id=${args.Current.Project.Id})>0`\n  }),\n  api.getByIdAsync(\"GeneralUser\", args.Current.Owner.Id, {\n    select: \"fullName\"\n  })\n]);\nconst oldOwner = args.Previous && args.Previous.Owner.Id != args.Current.Owner.Id\n  ? await api.getByIdAsync(\"GeneralUser\", args.Previous.Owner.Id, {\n    select: \"fullName\"\n  })\n  : newOwner;\n\nfunction getProcessedCfValue(field, cfValue, valueArray) {\n  let value = cfValue || undefined;\n  if (value || field.fieldType == \"Number\") {\n    switch (field.fieldType) {\n      case \"Date\":\n        if (value) {\n          let date = new Date(value);\n          let offset = value.substring(19, 22);\n          let ticks = ((date.getTime() * 10000) + 621355968000000000) + (parseInt(offset) * 60 * 600000000);\n          value = value.substring(0, 10);\n          valueArray.push({ cfColumn: field.entityFieldName, cfValue: ticks });\n        }\n        break;\n      case \"Number\":\n        if (!value) { value = 0; }\n        valueArray.push({ cfColumn: field.entityFieldName, cfValue: value });\n        break;\n      case \"Entity\":\n        value = value.Id;\n        valueArray.push({ cfColumn: field.entityFieldName, cfValue: value });\n        break;\n      case \"URL\":\n        value = `${value.label}\\r\\n${value.url}`;\n        valueArray.push({ cfColumn: field.entityFieldName, cfValue: value });\n        break;\n      default:\n        valueArray.push({ cfColumn: field.entityFieldName, cfValue: value });\n        break;\n    }\n  }\n  return value;\n}\n\nfunction buildMeta(field, value) {\n  return {\n    \"Name\": field.name,\n    \"Items\": field.value ? field.value.split(\"\\r\\n\") : [],\n    \"IsSystem\": field.isSystem,\n    \"EntityFieldName\": field.entityFieldName,\n    \"FieldType\": fieldTypes.indexOf(field.fieldType),\n    \"Required\": field.required,\n    \"Value\": value,\n    \"DefaultValue\": field.config.defaultValue || undefined,\n    \"IsDynamicColumn\": field.isdynamicColumn,\n    \"EntityFieldNameV2\": field.entityFieldNameV2,\n    \"CalculationModel\": field.config.calculationModel || undefined\n  }\n}\n\nlet newFieldValues = [];\nlet newFieldMeta = [];\nlet oldFieldValues = [];\nlet oldFieldMeta = [];\nconst fieldTypes = [\"Text\", \"DropDown\", \"CheckBox\", \"URL\", \"Date\", \"RichText\", \"Number\", \"Money\", \"Entity\",\n  \"MultipleSelectionList\", \"TemplatedURL\", \"MultipleEntities\", \"Money\"];\nfor (let field of customFields) {\n  let newValue = getProcessedCfValue(field, args.Current[field.name], newFieldValues);\n  newFieldMeta.push(buildMeta(field, newValue));\n  if (args.Previous) {\n    let oldValue = getProcessedCfValue(field, args.Previous[field.name], oldFieldValues);\n    oldFieldMeta.push(buildMeta(field, oldValue));\n  }\n}\n\nfunction buildEntity(enity, fieldValues, fieldMeta, owner) {\n  if (!enity) { return {} }\n  let formattedEntity = {\n\n    \"ID\": args.ResourceId,\n    \"BugID\": args.ResourceId,\n    \"Name\": enity.Name,\n    \"CreateDate\": enity.CreateDate.substring(0, 19),\n    \"ModifyDate\": enity.ModifyDate.substring(0, 19),\n    \"NumericPriority\": enity.NumericPriority,\n    \"Effort\": enity.Effort,\n    \"EffortCompleted\": enity.EffortCompleted,\n    \"EffortToDo\": enity.EffortToDo,\n    \"TimeSpent\": enity.TimeSpent,\n    \"TimeRemain\": enity.TimeRemain,\n    \"OwnerID\": enity.Creator.Id,\n    \"LastEditorID\": enity.LastEditor.Id,\n    \"EntityStateID\": enity.EntityState.Id,\n    \"PriorityID\": enity.Priority.Id,\n    \"ProjectID\": enity.Project.Id,\n\"SeverityID\": enity.Severity.Id,\n    \"EntityTypeName\": \"Tp.BusinessObjects.\" + args.ResourceType,\n    \"EntityStateName\": enity.EntityState.Name,\n    \"PriorityName\": enity.Priority.Name,\n    \"ProjectName\": enity.Project.Name,\n    \"ReleaseName\": enity.Release ? enity.Release.Name : \"Backlog\",\n    \"SeverityName\": enity.Severity.Name,\n    \"EntityTypeID\": enity.EntityType.Id,\n    \"CustomFieldsMetaInfo\": fieldMeta,\n\n    \"Description\": enity.Description || undefined,\n    \"StartDate\": enity.StartDate ? enity.StartDate.substr(0, 19) : undefined,\n    \n    \"PlannedStartDate\": enity.PlannedStartDate ? enity.PlannedStartDate.substr(0, 19) : undefined,\n    \"PlannedEndDate\": enity.PlannedEndDate ? enity.PlannedEndDate.substr(0, 19) : undefined,\n    \"LastCommentDate\": enity.LastCommentDate ? enity.LastCommentDate.substr(0, 19) : undefined,\n    \n   \n    \"InitialEstimate\": enity.InitialEstimate,\n    \"LastCommentUserID\": enity.LastCommentedUser ? enity.LastCommentedUser.Id : undefined,\n   \n    \"ParentID\": enity.Feature ? enity.Feature.Id : undefined,\n    \"SquadIterationID\": enity.SquadIterationID ? enity.SquadIterationID.Id : undefined,\n    \"SquadIterationName\": enity.SquadIterationID ? enity.SquadIterationID.Name : undefined,\n    \"ReleaseID\": enity.Release ? enity.Release.Id : undefined,\n\n    \"FeatureID\": enity.Feature ? enity.Feature.Id : undefined,\n    \"UserStoryName\": enity.UserStory ? enity.UserStory.Name : undefined,\n    \"UserStoryID\": enity.UserStory ? enity.UserStory.Id : undefined,\n    \"ParentName\": enity.Feature ? enity.Feature.Name : undefined,\n    \"FeatureName\": enity.Feature ? enity.Feature.Name : undefined,\n   \n  \n    \"EndDate\": enity.EndDate ? enity.EndDate.substring(0, 19) : undefined,\n    \"Progress\": enity.Progress,\n    \"LastStateChangeDate\": enity.LastStateChangeDate ? enity.LastStateChangeDate.substring(0, 19) : undefined,\n    \"OwnerName\": owner,\n    \"SquadID\": enity.Team ? enity.Team.Id : undefined,\n    \"SquadName\": enity.Team ? enity.Team.Name : undefined,\n    \"ResponsibleSquadID\": enity.ResponsibleTeam ? enity.ResponsibleTeam.Id : undefined\n  };\n  for (let cf of fieldValues) {\n    formattedEntity[cf.cfColumn] = cf.cfValue;\n  }\n  return formattedEntity;\n}\n\nconst http = require('http');\nawait http.postAsync(webhookUrl, {\n  body: {\n    \n    \"Entity\": buildEntity(args.Current, newFieldValues, newFieldMeta, newOwner),\n    \"OldEntity\": buildEntity(args.Previous, oldFieldValues, oldFieldMeta, oldOwner),\n    \"ChangedFields\": args.Modification == \"Updated\" ? args.ChangedFields : [],\n    \"Author\": {\n      \"ID\": author.id,\n      \"UserID\": author.id,\n      \"FirstName\": author.firstname,\n      \"LastName\": author.lastname,\n      \"Email\": author.email,\n      \"ModifyDate\": author.modifydate ? author.modifydate.substring(0, 19) : undefined,\n      \"IsAdministrator\": author.isadministrator\n    },\n    \"Modification\": args.Modification\n  },\n  headers: { 'content-type': 'application/json' }\n})"
    }
  ]
}