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.

12191219

Filter block

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

11291129

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

11501150

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

11231123

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.

12221222

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.

11811181

And the result for both would look like this.

494494

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.

11671167

The result in both the cases would look like this.

11121112

Example: External integration with empty template

📘

How to apply JSON of an Automation Rule?

For User Story

Webhook setup:

586586

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 :

545545

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:

485485

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})"
    }
  ]
}