Scheduled tasks on HubSpot Free (using Python script and cron)

In this blog post, I explain how to create scheduled tasks in HubSpot using the HubSpot CRM API, a Python script and a cron schedule.

Scheduled tasks on  HubSpot Free (using Python script and cron)
This article is a translation of the original version, in Spanish.

I've recently started using HubSpot's CRM to get organized. While it is designed for business and collaborative use, it is very adaptable for personal use.

The free version is really amazing. But, in my opinion, it has the big disadvantage of not being able to set tasks to repeat. Many of the tasks we perform in our day to day life are of this type:

  • Check the evolution of sales.
  • Daily/weekly/monthly reports, or with any other periodicity.
  • See if there are blocked tasks or tickets that need our attention.
  • And an endless number of other examples, which will depend on our specific casuistry.

In order to create these repetitive tasks, without ( for now ) upgrading the HubSpot CRM to a paid version, I have created a Python script that in conjunction with the "cron" tasks in Linux, performs the same task. It uses the HubSpot API via HTTP requests.

This development has been useful to me, for my situation. But it may not be for all situations; it may not be secure and/or too complex depending on what situations. There are other options, such as Zapier, that can serve the same purpose in a simpler way.

Script configuration

The script (in Python) is at the end of the article and available at this link. It is recommended to familiarize yourself with it to better understand how it works.

All variables to be changed are indicated as "XXXXXXXXXXXXXX" in the code. The HubSpot API reference for creating tasks can be found on the official website.

The HTTP request is sent using "request" package via POST, combining the task details, the authorization Bearer Token and additional headers.

The different tasks configured, since the same script can be used to create different tasks, are stored in the "task" object. More information on this section can be found in the Adding tasks section of the article.

Depending on whether or not the task has an association configured, the request will vary. The script uses the first parameter passed when executed to decide which task needs to be created.

For personal needs, the script is configured to associate tasks only to objects of type Person. In case you need to assign tasks to other objects, you would have to modify the code. Among other changes, it may be necessary to update associationCategory and associationTypeId, as well as logics, and/or store other necessary details in the "tasks" object as well.

API Key (PAT, Personal Access Token)

To authenticate the script, a private application must be created. Doing so, we will be able to access our CRM data. The official article on how to create them is available at this link.

It is enough to follow the instructions in the previous article. The scope needed by this private application is crm.objects.contacts.write.

You will see the Personal Access Token on the screen. It will start with pat-. It is also worth noting that from the logs/logs section you will be able to see all the requests made; this is really useful, for example, when making changes to the script, and to see that no illicit or unwanted actions are performed.

It is recommended to rotate this access token periodically.

Adding tasks

The script stores in the "task" object the different variables for each configured task.
For example, task1 has a title starting with "Report" and the details for the association with another object (a person, in this case). Task task2 has no value for the association.
To add more tasks, simply edit the "task" object, specifying a name (which will be used as a parameter when calling the script) and the details of the task, following the example structure.

"report" : {"task_title":"Preparar informe semanal ",
"task_body":"For the weekly weekly scheduled for every Thursday at 10:00 a.m.",

"report" will be the key that we will use as the first parameter when calling the script to create the task with these details.

Cron configuration

Once we have configured the task(s) we want to create on a scheduled basis, created the private application and copied the token into the script, we can start configuring the cron.

In my case, I created a new user on the linux machine that will run the cron, but it is not strictly necessary. You can even take this script to a Google Cloud Function, an AWS Lambda or a Cloudflare Worker.

The script will be saved in the desired path (it will be used later). And we will execute "crontab -e" with the user that we want to execute the script, to edit the cron schedule. Here, under the existing entries (if any), we will add an entry for each schedule we want to create.

As an example, if we would like that every Wednesday at 8 o'clock in the morning we create the task with the key "report" we would add:

0 8 * * WED /usr/bin/python /var/ report > /var/log/hs-scheduler.log

The end, "> /var/log/hs-scheduler.log", is used to have a log of the executions and possible errors that could arise. It is optional, but highly recommended.

To finish the example, if we would like, in addition to the previously scheduled task, to add one at 10 am every working day to feed the canary (having configured in the object "tasks" the details), we will add (keeping the rest of the entries):

0 10 * * MON-FRI /usr/bin/python /var/ canario > /var/log/hs-scheduler.log

Script code

# This script, executed as a cron job, enables the generation of scheduled tasks in HubSpot CRM (even using the free version, on which it is not natively supported).
# All the variables that need to be changed are indicated as "XXXXXXXXXX" in the code below. Essentially, the script uses the HubSpot HTTP API to perform task creation requests (engagements).
#   All the API documentation is available here:
#   For in-deep task creation via API documentation, check:
# The HTTP request is sent using “requests” via POST, combining the details of the tasks and the authorization and content-type headers.
# The script has saved into the ‘task’ object different variables for each configured task. For instance, task1 has a title starting with ‘informe’ and an association with another object (a person, in this case). task2 has no association configured. 
#   To add more tasks, just edit the “task” object, with a name and a object value for that entry following the structure shown as example.
# Depending on whether or not it has an association, the payload varies. The script uses the first param when called to decide which task needs to be created.
#   If it is the task "task" that has to be created:
#    >> task2  <<
#   Or add the following entry on the crontab, changing the schedule:
#    >>  0 8 * * THU /usr/bin/python /home/hubspot-api/ task2 > /var/log/hs-scheduler.log  <<

import requests, time, sys

url = ""
task = {"task1" : {"task_title":"Informe XXXXXXXXXX - ","task_body":"", "association":"XXXXXXXXXX"}, "task2":{"task_title":"Revisar tickets y deals - ","task_body":"", "association":""}}

objetivo = str(sys.argv[1])
fecha_bonita = time.asctime()[4:10] + time.asctime()[19:]

if task.get(objetivo)["association"] == "":
    payload = '{"properties": {"hs_timestamp": '+ str(int((time.time()* 1000))) +',"hs_task_body": "'+ task.get(objetivo)["task_body"]+'", "hubspot_owner_id": "XXXXXXXXXX","hs_task_subject": "'+ task.get(objetivo)["task_title"]+ fecha_bonita +'", "hs_task_status": "WAITING"},"associations": []}'
    payload = '{"properties": {"hs_timestamp": '+ str(int((time.time()* 1000))) +',"hs_task_body": "'+ task.get(objetivo)["task_body"]+'", "hubspot_owner_id": "XXXXXXXXXX","hs_task_subject": "'+ task.get(objetivo)["task_title"]+ fecha_bonita +'", "hs_task_status": "WAITING"},"associations": [{ "to": {"id": "'+ task.get(objetivo)["association"]+'"},"types": [{"associationCategory": "HUBSPOT_DEFINED","associationTypeId": 10}]}]}'

headers = {"authorization": "Bearer pat-XXXXXXXXXX", "content-type":"application/json"}
res =, data=payload, headers=headers)


Also available on this GitHub Gist.

Any doubts?

Do not hesitate to contact me with any questions, suggestions, complaints or clarifications you may have, I'll be happy to talk to you!