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.

1219

Filter block

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

1129

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

1150

Action block

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

Webhook plugin URLWebhook plugin TemplateAutomation Rule actionCorresponding example
External linkJSON payloadSend 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 entityUpdate 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 entityCreate entity action,
with all the corresponding fields set
Add another entity
External linkempty templateJavasciprt action, where we enumerate all the fields that used to be send by default in Webhook pluginExternal integration with empty template
1123

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.

1222

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.

1181

And the result for both would look like this.

494

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.

1167

The result in both the cases would look like this.

1112

Example: External integration with empty template

📘

How to apply JSON of an Automation Rule?

For User Story

Webhook setup:

586

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 :

545

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:

485

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