Custom Workflow Actions

HubSpot's workflows are used to automate business processes and to allow HubSpot's customers to be more efficient. You can create custom workflow actions to integrate your service with HubSpot's workflows.

First, you'll define your custom action, including the inputs which must be filled out by the user creating a workflow, and the URL that will be requested when the custom action is executed. Then, when customers install your application, they can add your custom action to their workflows. When those workflows execute, HTTPS requests will be sent to the configured URL with the payload that you set up. In this article, learn how to:

Before you get started, do take note of the following:

https://api.hubspot.com/automation/v4/actions/{appId}?hapikey={API_key}

Define your custom action

To create a custom workflow action, you'll need to define the action using the following fields. This definition also specifies the request format for requests coming from HubSpot, as well as the handling of responses from your service.

  • actionUrl: the URL where an HTTPS request is sent when the action is executed. The request body will contain information about which user the action is executing on behalf of, and what values were entered for the input fields.
  • inputFields: these define the set of valid values for the action's inputs, either a static list or a webhook URL can be provided. If a webhook URL is provided, the options will be fetched from that URL whenever the action is edited by a customer in the Workflows tool. These are optional for each field.
  • outputFields: the values that the action will output that can be used by later actions in the workflow. A custom action can have zero, one, or many outputs.
  • executionRules: a list of definitions you can specify to surface errors from your service to the user creating the workflow.
  • labels: copy that describes to the user what the action's fields represent and what the action does. English labels are required, but labels can be specified in any of the following supported languages as well: French (fr), German (de), Japanese (ja), Spanish (es), Brazilian Portuguese (pt-br), and Dutch (nl). The specification for each language includes the following fields:
    • actionName: the action's name shown in the Choose an action panel in the workflow editor.
    • actionDescription: a detailed description for the action shown when the user is configuring the custom action.
    • actionCardContent: a summarized description shown in the action's card.
    • inputFieldLabels: an object that maps the definitions from inputFields to the corresponding labels the user will see when configuring the action.
    • outputFieldLabels: an object that maps the definitions from outputFields to the corresponding labels shown in the workflows tool.
    • inputFieldDescriptions: an object that maps the definitions from inputFields to the descriptions below the corresponding labels.
    • executionRules: an object that maps the definitions from your executionRules to error messages that will be shown to the user if they misconfigure a custom action. These allow you to specify custom messages for action execution results on the workflow history.

An example of a basic action definition is specified below:

JSON
//
{
  "actionUrl": "https://example.com/hubspot",
  "inputFields": [
    {
      "typeDefinition": {
        "name": "widgetName",
        "type": "string",
        "fieldType": "text"
      },
      "supportedValueTypes": ["STATIC_VALUE"],
      "isRequired": true
    },
    {
      "typeDefinition": {
        "name": "widgetOwner",
        "type": "enumeration",
        "referencedObjectType": "OWNER"
      },
      "supportedValueTypes": ["STATIC_VALUE"]
    }
  ],
  "labels": {
    "en": {
      "actionName": "Create Widget",
      "actionDescription": "This action will create a new widget in our system. So cool!",
      "actionCardContent": "Create widget {{widgetName}}",
      "inputFieldLabels": {
        "widgetName": "Widget Name",
        "widgetOwner": "Widget Owner"
      },
      "inputFieldDescriptions": {
        "widgetName": "Enter the full widget name. I support <a href=\"https://hubspot.com\">links</a> too."
      },
      "executionRules": {
        "alreadyExists": "The widget with name {{ widgetName }} already exists"
      }
    }
  },
  "executionRules": [
    {
      "labelName": "alreadyExists",
      "conditions": {
        "errorCode": "ALREADY_EXISTS"
      }
    }
  ],
  "objectTypes": ["CONTACT", "DEAL"]
}

Validate the request source

Requests made for your custom action will use the v2 version of the X-HubSpot-Signature. Learn more about validating requests from HubSpot.

Default payloads

There are two types of calls made for custom workflow actions:

  • Field option fetches: populate a list of valid options when a user is configuring a field.
  • Action execution requests: made when an action is being executed by a workflow that includes your custom action. 

Field option fetches

Requests to fetch options are made when a user is configuring your custom action in their workflow.

Request format:

JSON
{
  "origin": {
    // The customer's portal ID
    "portalId": 1,

    // Your custom action definition ID
    "actionDefinitionId": 2,

    // Your custom action definition version
    "actionDefinitionVersion": 3
  },

   // The values for the fields that have already been filled out by the workflow user
  "inputFields": {
    "widgetName": {
      "type": "OBJECT_PROPERTY",
      "propertyName": "widget_name"
    },
    "widgetColor": {
      "type": "STATIC_VALUE",
      "value": "blue"
    }
  },

  "fetchOptions": {
    // The search query provided by the user. This should be used to filter the returned
    // options. This will only be included if the previous option fetch returned
    // `searchable: true` and the user has entered a search query.
    "q": "option label",

    // The pagination cursor. This will be the same pagination cursor that was returned by
    // the previous option fetch; it can be used to keep track of which options have already
    // been fetched.
    "after": "1234="
  }
}

Expected response format:

JSON
//
{
  "options": [
    {
      "label": "Big Widget",
      "description": "Big Widget",
      "value": "10"
    },
    {
      "label": "Small Widget",
     "description": "Small Widget",
      "value": "1"
    }
  ],

  // Optional. The pagination cursor. If this is provided, the Workflows app will render
  // a button to load more results at the bottom of the list of options when a user is
  // selecting an option, and when the next page is loaded this value will be included in
  // the request payload under `fetchOptions.after`.
  "after": "1234=",

  // Optional. Default is false. If this is true, the Workflows app will render a search
  // field to allow a user to filter the available options by a search query, and when
  // a search query is entered by the user, options will be re-fetched with that search
  // term in the request payload under `fetchOptions.q`.
  "searchable": true
}

To limit the number of options that are returned by an option fetch, you can set a pagination cursor, which will tell workflows that more options can be loaded. If you'd like the list of options to be searchable, you can return "searchable": true to allow the results to be filtered by a search query.

Action execution requests

Execution requests are made when a workflow is executing your custom action against an enrolled object.

Request format: 

JSON
// 
{
  "origin": {
    // The customer's portal ID
    "portalId": 1,

    // Your custom action definition ID
    "actionDefinitionId": 2,

    // Your custom action definition version
    "actionDefinitionVersionId": 3
  },
  "object": {
    // The type of CRM object that is enrolled in the workflow
    "objectType": "CONTACT",

    // The ID of the CRM object that is enrolled in the workflow
    "objectId": 4,
  
    // The values of the properties of the CRM object that were specified
    // on the objectRequestOptions of the definition
    "properties": {
      "firstname": "Test"
    }
  },

  // The field values specified by the workflow user
  "inputFields": {
    "widgetName": "My test widget",
    "widgetColor": "blue",
    "widgetSize": "10"
  },

  // A unique ID for this execution. This can be used for idempotency to
  // deduplicate potential duplicate deliveries from workflows, and it
  // should also be used as the callbackId if your custom action will be
  // blocking execution.
  "callbackId": "ap-123-456-7-8"
}

Customize the payload with functions

You can customize the requests that are made for your custom action by configuring serverless functions for your custom action.

Customize field option fetches

There are two hooks to customize the field option fetch lifecycle:

  • PRE_FETCH_OPTIONS: this allows you to configure the request sent from HubSpot.
  • POST_FETCH_OPTIONS: this allows you to transform the response from your service into a format that's understood by workflows.

PRE_FETCH_OPTIONS

Function input argument format:

JSON
{
  "origin": {
    // The customer's portal ID
    "portalId": 1,

    // Your custom action definition ID
    "actionDefinitionId": 2,

    // Your custom action definition version
    "actionDefinitionVersion": 3
  },

  // Your configured external data field webhook URL
  "webhookUrl": "https://myapi.com/hubspot/widget-sizes",

   // The values for the fields that have already been filled out by the workflow user
  "inputFields": {
    "widgetName": {
      "type": "OBJECT_PROPERTY",
      "propertyName": "widget_name"
    },
    "widgetColor": {
      "type": "STATIC_VALUE",
      "value": "blue"
    }
  }
}

Expected Function output format:

JSON
{
  // The webhook URL for HubSpot to call
  "webhookUrl": "https://myapi.com/hubspot/widget-sizes",
  
  // Optional. The request body.
  "body": "{\"widgetName\": \"My new widget\"}",

  // Optional. A map of custom request headers to add.  
  "httpHeaders": {
    "My-Custom-Header": "header value"
  },

  // Optional. The Content-Type of the request. Default is application/json.
  "contentType": "application/json",

  // Optional. The Accept type of the request. Default is application/json.
  "accept": "application/json",
 
  // Optional. The HTTP method with which to make the request.
  // Valid values are GET, POST, PUT, PATCH, and DELETE.
  // Default is POST.
  "httpMethod": "POST"
}

POST_FETCH_OPTIONS

Function input argument format:

JSON
//
{
  // The requested field key
  "fieldKey": "widgetSize",

  // The webhook response body from your service
  "responseBody": "{\"widgetSizes\": [10, 1]}"
}

Expected function output format:

JSON
//
{
  "options": [
    {
      "label": "Big Widget",
      "description": "Big Widget",
      "value": "10"
    },
    {
      "label": "Small Widget",
      "description": "Small Widget",
      "value": "1"
    }
  ]
}

Customize execution requests

There is one hook into the action execution lifecycle, a PRE_ACTION_EXECUTION function. This function allows you to configure the request that is sent from HubSpot.

Function input argument format:

JSON
//
{
  "origin": {
    // The customer's portal ID
    "portalId": 1,

    // Your custom action definition ID
    "actionDefinitionId": 2,

    // Your custom action definition version
    "actionDefinitionVersionId": 3
  },
  "object": {
    // The type of CRM object that the custom action is executing against
    "objectType": "CONTACT",

    // The ID of the HubSpot CRM object that the custom action is executing against
    "objectId": 4,
  
    // The values of the properties of the CRM object that were specified
    // on the objectRequestOptions of the definition
    "properties": {
      "firstname":  "Test"
    },
  },

  // The field values specified by the workflow user
  "inputFields": {
    "widgetName": "My test widget",
    "widgetColor": "blue",
    "widgetSize": "10"
  },

  // A unique ID for this execution. This can be used for idempotency to
  // deduplicate potential duplicate deliveries from workflows, and it
  // should also be used as the callbackId if your custom action will be
  // blocking execution.
  "callbackId": "ap-123-456-7-8",

  // The configured action URL from your definition  
  "webhookUrl": "https://myapi.com/hubspot"
}

Expected function output format:

JSON
//
{
  // The webhook URL for HubSpot to call
  "webhookUrl": "https://myapi.com/hubspot",
  
  // Optional. The request body.
  "body": "{\"widgetName\": \"My new widget\", \"widgetColor\": \"blue\"}",

  // Optional. A map of custom request headers to add.  
  "httpHeaders": {
    "My-Custom-Header": "header value"
  },

  // Optional. The Content-Type of the request. Default is application/json.
  "contentType": "application/json",

  // Optional. The Accept type of the request. Default is application/json.
  "accept": "application/json",
 
  // Optional. The HTTP method with which to make the request.
  // Valid values are GET, POST, PUT, PATCH, and DELETE.
  // Default is POST.
  "httpMethod": "POST"
}

Publish your custom action

By default, your custom action is created in an unpublished state, and will only be visible in the developer portal associated with your HubSpot application. To make your custom action visible to customers, update the published flag on your action definition to true.

Test your custom action

You can test your custom action by creating a workflow in the workflows tool and adding your custom action.

Execute your custom action

An action's success is determined by examining the status code returned by your service:

  • 2xx status codes: this indicates the action has completed successfully.
  • 4xx status codes: this indicates the action failed.
    • The exception here is 429 Rate Limited status codes; those are re-treated as retries, and the Retry-After header is respected.
  • 5xx status codes: this indicates there was a temporary problem with your service, and your action will be retried at a later time.

Block custom action execution

Custom actions can block workflow execution. Instead of executing the next action in the workflow after your custom action after receiving a "completed" (2xx or 4xx status code) response from your service, the workflow will stop executing ("block") that enrollment until you tell the workflow to continue.

To block a custom action asynchronously, your action execution response must have the following format:

JSON
//
{
  "outputFields": {
    // Required. Must be BLOCK for your custom action to block execution.
    "hs_execution_state": "BLOCK",

    // Optional. If not provided, a default expiration of 1 week is used.
    // Must be specified in ISO 8601 Duration format.
    // See https://en.wikipedia.org/wiki/ISO_8601#Durations
    "hs_expiration_duration": "P1WT1H"
  }
}

You can optionally specify a value for the hs_default_expiration field, after which your custom action will be considered expired. The execution of the workflow will then resume and the action following your custom action will be executed, even though you have not told the workflow to continue.

Complete a blocked execution

To complete a blocked custom action execution, use the following endpoint: /callbacks/{appId}/{callbackId}/complete

The request body format is:

JSON
//
{
  "outputFields": {
    // Required. The final execution state. Valid values are SUCCESS
    // (to indicate that your custom action completed successfully) or
    // FAIL_CONTINUE (to indicate that there was a problem with your
    // custom action execution)
    "hs_execution_state": "SUCCESS"
  }
}

Add custom execution messages

Specify rules on your action that determine which message displays on the workflow's history page when the action executes. The rules will be matched against the output values from your action.  These output values should be provided in the webhook's response body, in the following format:

JSON
//
{
  "outputFields": {
    "errorCode": "ALREADY_EXISTS",
    "widgetName": "Test widget"
  }
}

The executionRules will be tested in the order provided. If there are multiple matches, only the message from the first rule that matches is displayed to the user.

The rule matches when the execution output corresponds to a specified value in the rule. For example, consider this set of executionRules :

JSON
//
[
  {
    // This matches the key of a label on the action's `labels.LANGUAGE.executionRules` map
    "labelName": "alreadyExists",
    "conditions": {
      "errorCode": "ALREADY_EXISTS"
    }
  },
  {
    "labelName": "widgetWrongSize",
    "conditions": {
      "errorCode": "WIDGET_SIZE",
      "sizeError": ["TOO_SMALL", "TOO_BIG"]
    }
  },
  {
    "labelName": "widgetInvalidSize",
    "conditions": {
      "errorCode": "WIDGET_SIZE"
    }
  }
]

The following matches would occur:

  • {"errorCode": "ALREADY_EXISTS", "widgetName": "Test widget"}: This would match the first rule, since errorCode is equal to ALREADY_EXISTS. In this instance, even though there is a widgetName output, it isn't used in the rule definition so any value is allowed.
  • {"errorCode": "WIDGET_SIZE", "sizeError": "TOO_SMALL"}: This would match the second rule, since TOO_SMALL is one of the matching sizeErrors, and errorCode is WIDGET_SIZE.
  • {"errorCode": "WIDGET_SIZE", "sizeError": "NOT_A_NUMBER"}: This would match the third rule, since even though the errorCode is WIDGET_SIZE, the sizeError does not match any of the values specified by the second rule (TOO_SMALL or TOO_BIG).

This matching mechanism allows you to specify fallback errors, so that you can have specific errors for important error cases, but fall back to more generic error messages for less common errors.

Example 1

A basic example with the following input fields, created for contact and deal workflows:

  • a static input field
  • a dropdown field with options
  • a field whose value is a HubSpot owner
  • a field whose value is pulled from a property (that the user creating the workflow selects) on the enrolled object.
JSON
//
{
  "actionUrl": "https://example.com/hubspot",
  "inputFields": [
    {
      "typeDefinition": {
        "name": "widgetName",
        "type": "string",
        "fieldType": "text"
      },
      "supportedValueTypes": ["STATIC_VALUE"],
      "isRequired": true
    },
    {
      "typeDefinition": {
        "name": "widgetColor",
        "type": "enumeration",
        "fieldType": "select",
        "options": [
          { "value": "red", "label": "Red" },
          { "value": "blue", "label": "Blue" },
          { "value": "green", "label": "Green" }
        ]
      },
      "supportedValueTypes": ["STATIC_VALUE"]
    },
    {
      "typeDefinition": {
        "name": "widgetOwner",
        "type": "enumeration",
        "referencedObjectType": "OWNER"
      },
      "supportedValueTypes": ["STATIC_VALUE"]
    },
    {
      "typeDefinition": {
        "name": "widgetQuantity",
        "type": "number"
      },
      "supportedValueTypes": ["OBJECT_PROPERTY"]
    }
  ],
  "labels": {
    "en": {
      "actionName": "Create Widget - Example 1",
      "actionDescription": "This action will create a new widget in our system. So cool!",
      "actionCardContent": "Create widget {{widgetName}}",
      "inputFieldLabels": {
        "widgetName": "Widget Name",
        "widgetColor": "Widget Color",
        "widgetOwner": "Widget Owner",
        "widgetQuantity": "Widget Quantity"
      },
      "inputFieldDescriptions": {
        "widgetName": "Enter the full widget name. I support <a href=\"https://hubspot.com\">links</a> too.",
        "widgetColor": "This is the color that will be used to paint the widget."
      }
    }
  },
  "objectTypes": ["CONTACT", "DEAL"]
}

Example 2

The following custom action uses a serverless function to transform the payload that is sent to the configured actionUrl. Since no objectTypes are specified, this action will be available in all workflows types.

JSON
//
{
  "actionUrl": "https://api.example.com/v1/widgets",
  "inputFields": [
    {
      "typeDefinition": {
        "name": "widgetName",
        "type": "string",
        "fieldType": "text"
      },
      "supportedValueTypes": ["STATIC_VALUE"],
      "isRequired": true
    }
  ],
  "labels": {
    "en": {
      "actionName": "Create Widget - Example 2",
      "actionCardContent": "Create widget {{widgetName}}",
      "inputFieldLabels": {
        "widgetName": "Widget Name"
      }
    }
  },
  "functions": [
    {
      "functionType": "PRE_ACTION_EXECUTION",
      "functionSource": "exports.main = function(event, callback) { return callback(transformRequest(event)); }\nfunction transformRequest(request) { return { webhookUrl: request.webhookUrl, body: JSON.stringify(request.fields), contentType: 'application/x-www-form-urlencoded', accept: 'application/json', httpMethod: 'POST' }; }"
    }
  ]
}

Example 3

The following custom action has field dependencies and options that are fetched from an API. Because the widget size depends on the widget color, the user won't be able to input a value for the widget size until a widget color is chosen.

The widget cost is also dependent on the widget color, but it is conditional on the value that the user selects for the widget color; the user won't be able to input a value for the widget cost unless Red is selected as the widget color.

JSON
//
{
  "actionUrl": "https://example.com/hubspot",
  "inputFields": [
    {
      "typeDefinition": {
        "name": "widgetName",
        "type": "string",
        "fieldType": "text"
      },
      "supportedValueTypes": ["STATIC_VALUE"],
      "isRequired": true
    },
    {
      "typeDefinition": {
        "name": "widgetColor",
        "type": "enumeration",
        "fieldType": "select",
        "options": [
          { "value": "red", "description": "red", "label": "Red" },
          { "value": "blue", "description": "blue", "label": "Blue" },
          { "value": "green", "description": "green", "label": "Green" }
        ]
      },
      "supportedValueTypes": ["STATIC_VALUE"],
    },
    {
      "typeDefinition": {
        "name": "widgetSize",
        "type": "enumeration",
        "fieldType": "select",
        "optionsUrl": "https://api.example.com/v1/widget-sizes"
      },
      "supportedValueTypes": ["STATIC_VALUE"]
    },
    {
      "typeDefinition": {
        "name": "widgetCost",
        "type": "number",
        "fieldType": "number"
      },
      "supportedValueTypes": ["OBJECT_PROPERTY"]
    }
  ],
  "inputFieldDependencies": [
    {
      "dependencyType": "SINGLE_FIELD",
      "controllingFieldName": "widgetColor",
      "dependentFieldNames": ["widgetSize"]
    },
    {
      "dependencyType": "CONDITIONAL_SINGLE_FIELD",
      "controllingFieldName": "widgetColor",
      "controllingFieldValue": "red",
      "dependentFieldNames": ["widgetCost"]
    }
  ],
  "labels": {
    "en": {
      "actionName": "Create Widget - Example 3",
      "actionCardContent": "Create widget {{widgetName}}",
      "inputFieldLabels": {
        "widgetName": "Widget Name",
        "widgetColor": "Widget Color",
        "widgetSize": "Widget Size",
        "widgetCost": "Widget Cost"
      }
    }
  },
  "objectTypes": ["CONTACT", "DEAL"],
  "functions": [
    {
      "functionType": "PRE_FETCH_OPTIONS",
      "id": "widgetSize",
      "functionSource": "exports.main = function(event, callback) { return callback(transformRequest(event)); }\nfunction transformRequest(request) { return { webhookUrl: request.webhookUrl + '?color=' + request.fields.widgetColor.value, body: JSON.stringify(request.fields), httpMethod: 'GET' }; }"
    }
  ]
}

Example 4

The following example is a blocking custom action. The callbacks API can be used to tell HubSpot to complete the action and have the enrolled object continue to the next action in the workflow.

You will not need to specify that the action blocks at the time that you create the action; that will be determined by the response from your configured actionUrl.

JSON
//
{
  "actionUrl": "https://example.com/hubspot",
  "inputFields": [
    {
      "typeDefinition": {
        "name": "moonPhase",
        "type": "enumeration",
        "fieldType": "select",
        "options": [
          { "value": "full_moon", "description": "Full Moon", "label": "Full" },
          { "value": "half_moon", "description": "Half Moon", "label": "Half" },
          { "value": "quarter_moon", "description": "Quarter Moon", "label": "Quarter" },
          { "value": "new_moon", "description": "New Moon", "label": "New" }
        ]
      },
      "supportedValueTypes": ["STATIC_VALUE"]
    }
  ],
  "objectRequestOptions": {
    "properties": ["email"]
  },
  "labels": {
    "en": {
      "actionName": "Wait For Moon Phase",
      "actionCardContent": "Wait until a {{moonPhase}} moon",
      "inputFieldLabels": {
        "widgetName": "Moon Phase"
      }
    },
    "fr": {
      "actionName": "Attendez la phase lunaire",
      "actionCardContent": "Attendez la lune {{moonPhase}}",
      "inputFieldLabels": {
        "widgetName": "Phase de lune"
      }
    }
  },
  "objectTypes": ["CONTACT"]
}

Was this page helpful? *
This form is for feedback on our developer docs. If you have feedback on the HubSpot product, please share it in our Idea Forum instead.