Application Tracking with HubDB, Customer Objects, and Coded Actions | HubSpot.Extend() 2022

Greta, a lead developer at Bright Digital, shows how to build a job application tracking system in HubSpot using custom objects, HubDB, coded actions, and Node.js. She walks through syncing data with workflows, creating dynamic pages, and using GraphQL to associate records and display information on membership pages.
Hi, I'm 10 Greta and I am very excited to join you all during HubSpot Extend. I'm also very honored to be able to host a session where we're going to talk about custom objects in the cms. So let's take a look at what we're talking about today. I'm the lead developer at Bright Digital, which is a Diamond HubSpot partner located in the Netherlands. I absolutely love working with the HubSpot API, building integrations using coded actions or anything else API related, but I also love working with the HubSpot CMS. We going to start this session by taking a look at how you can create a custom object using the ui, which is currently in public beta and available through the Self Enrollment tool. This is however only available for enterprise clients. After that, I'll show you how you can build an application that reads data from an external API and store that data as custom object records in HubSpot. Next, I'll show you the functionality you can use to create or update a Hub DBRO in each time a custom object record is created or updated by using coded actions in a workflow. After that we're going to see what code we need to create an overview of the HubDB rows available in HubDB and creating dynamic detail pages by using HubDB as well. On those dynamic detail pages we'll have a form available which we will use to associate a contact to a custom object record by using a coded action in the workflow as well. And and at the end we'll take a look at using serverless functions and GraphQL to create additional functionality in your overview. So user can save a custom object record and later view those saved custom object records through a membership page by using GraphQL. So let's start with the first part of the session. The beta I'm going to demonstrate right now is the beta called Manage and visualize your data model. And this beta allows you to not only visualize all your objects and the associations between them in your HubSpot UI, but it also gives you the option to create a custom object through your ui. If I navigate to my objects, you see that we have the option to go to our custom objects and the next part of this beta, which is my favorite part of the beta, is the option to create a custom object through the ui. If I were to click on this button, you you see that I get a form where I can start creating my custom object by giving it a name singular, a name for plural, and defining a primary display property and the primary display property is a required field and will also be used to act as the title of your custom object records. We can also define secondary display properties, but you can also start adding new properties after you created the custom object. In my case, I already created a few custom objects, but for the sake of this demonstration, let's try and create a new custom object. I already have available a application object and a job object, which are associated with each other. So in this case, I'm going to create an object called Recruiter. So I'm going to give it a singular name and a plural name, and I'm going to give it a proper primary display property name, which is going to be a single line text. If I click on this create button, you see the success message that our custom object has been created. And right now I'm in the default screen for our recruiter object. I have the option to manage the properties for my new recruiter object. I can customize the recruiter form, but it also can define an association between my other objects. So let's say that my recruiter object needs to be associated with my job object, which would make sense. Then we simply select the job object, click on create, and right now my recruiter object is associated to my job object. I can also define a association label through this ui. Some other cool stuff we have available right here is defining a pipeline where I can create my own custom stages for my custom object and the option to customize my record. And by customizing our record, we mean that we can customize the UI and which data we want to show when you're viewing a custom object record. So right now I have two custom objects available in this portal. I have my job object, and I have my application object. Imagine your company is hosting and managing all their job posts in an external platform, and you somehow want to retrieve those job posts and store them in HubSpot. That will mean we would have to build an application that reads the data from the external platform, formats the data, and uses the HubSpot API to store them as custom object records. Let's take a look at the code you might need to create such an integration. So imagine that the external platform that we're currently using to host and manage our job post has webhooks available, and we can use those webhooks to send data to an endpoint each time a job post is created, updated, or deleted. That would mean we would have to create an application that exposes those endpoints. So what I did in this case is that I Use Node JS and Express to create different endpoints that we can use to receive data from an external application. Every time I'm building an application that is going to Communicate with the HubSpot API or any other API that has API limits, I'm also including an NPM package called Rate Limiter Flexible. And Rate Limiter Flexible allows me to define API limits and determine how many requests I can make during a period of time. If we scroll down, you see that I created a few different endpoints using Node JS Express. I have an endpoint available where data can be posted to each time a job post is created, an endpoint available where data can be posted to each time a job post is deleted. And the same goes for an endpoint where data can be posted to each time a job post is updated. Now let's take a look at the data an external platform might post to these endpoints, and we'll start with the endpoint where we create a job. In Postman, I created few requests that contain data that might be coming from an external platform. In my example, I have a JSON body that always contains an object id, and the object ID is the ID of the record from the external application. I also have an object with all the data of the record. All this data we want to store in HubSpot. So if this data is posted to our job endpoint, we want to do something with the data, but we also want to make sure that we're not creating duplicates. And this is where the object ID of the external application is really important. So if we go back to our code, you see that the first thing we do is we're going to retrieve a job, which might seem odd when we're sending data to an endpoint that is used to create a job. But what I'm doing is in this function is I'm using the HubSpot API, in this case the CRM Search API, to check if we already have a job post available with an external ID, similar to the ID that is being posted to our endpoint. The next part is an if statement based on the result of our search. So if we have a job id, that means that we already have a record available in HubSpot for this specific job post from the external platform. And in this case I'm going to update the job. If there's no job ID available, I can safely assume that there is no record available in HubSpot yet, and I can start creating a job. So imagine that the current job post isn't available in HubSpot yet. Then we're going to use a create job function to call the HubSpot API and create a new record. If we take a look at that specific function, you see that I am giving the complete body of our request to this function. I'm also passing the limiter and I'm also passing a private app token which is used to authenticate with the HubSpot API. I'm going to remove one of my tokens from my limiter. So I'm telling my limiter that I'm currently making request with the HubSpot API. This prevents my application from running into the burst limits. And next I'm going to format the data that is being posted to my endpoint because I have a lot of properties available in HubSpot already where it can store this information but might not be formatted in the right way. So my format job data function is one of the key functionalities in my application. Right now we're passing all the data from Postman directly to this function and I'm building up a data object that we can pass to the HubSpot API to create or update a record. I'm using the internal property names from HubSpot and passing in a value from the JSON and depending on the property type, I might have to do something with that value. If it is a single line text, I can simply check if it has a value and if not I'm going to pass it an empty string. But if I have a more complex property, I might have to add some additional functionality to format the data correctly or check if all the options are available in HubSpot. So what you see right here is that I'm having an expiry date and I'm converting it to a number. So it works with the HubSpot API, but I also have a few properties that are a select or multi select and if that's the case, I need to check in HubSpot if I currently have an option available for the value that is being passed from the external application. So this format data function together with the get property options function is allowing me to format the data in such a way that I can always use it to create new object or update an object. If we go back to my Create job function, right now we have a formatted object that we can pass to the HubSpot API and specifically the CRM HubSpot API for my custom object. And by posting the Object to the HubSpot API we can create a new custom object record if we Go back to my index. You see that this is the base of the functionality to create a new record. But we also have an endpoint available where we can update a record. And it's very similar to the Create functionality, because we're first going to check if we have a record available based on the external ID from the application, and if not, we're going to create an object. But if we do have a record available for the external id, we're going to update a record. And if we take a look at that specific function, it's very similar to the Create job function. We're going to format our data with the same function we use in the Create job function, and we're going to send the date to HubSpot, but instead of passing it directly to the job endpoint, we're also passing it a job ID in the URL. And we're not using the post method, but we're using the patch method. And this is telling HubSpot that we want to update a record instead of creating a new one. The last endpoint we have available is the delete endpoint, and Postman is sending the complete job object to this endpoint. So we're first going to retrieve the specific job, and we're going to retrieve the job ID from HubSpot, and then we're going to call our Delete job function. And if we take a look at the delete job function, it's quite simple. We're using a delete method, and we're again using the CRM Objects API and we're passing the job ID in the URL. And by using the Delete method method and passing the job ID in the URL, we can delete a record. So these three endpoints with the simple functions of retrieving a job, updating a job, creating a job, or deleting a job allows you to create some basic CRUD functionality and receiving data from an external platform on endpoints we created using Node JS and Express. So this is a very basic implementation of an integration between an external platform and and HubSpot, using the HubSpot API and creating custom object records, updating custom object records, or deleting custom object records. So what we've built so far are two custom objects, a custom object for jobs and a custom object for applications. We have built an integration that reads data or can receive data from an external platform and create job records, update job records, or delete job records. And now we're going to see how you can use that data in your CMS if you use custom objects, you have the option to create dynamic pages based on those custom objects. So you can create an overview of your custom objects and you can create a detail page of your custom objects. HubDB has the same functionality, but the HubDB API requires no authentication. And based on that, I'm going to create an Overview that uses HubDB to show all available job posts. However, we need to get the data that is currently stored in our custom object record in HubDB. So right now we have around 20 job posts that we created based on the data available from the external platform. I also have an appdb available that has the same information as the job records, and these rows are added through a workflow. If I update a job post, my workflow will also update one of the rows. And the functionality of this is quite basic. What we're going to do is we first have a job enrollment trigger that contains all of the properties that we want to sync. And based on this trigger, we know for sure that every time a change is made on the job record, it will also be enrolled in this workflow. We're going to check if our custom object record already has a value for the property of the broken. Because what this workflow does, if it's creating a new hubdbro, it will retrieve the ID of the hubdbro and store it on the custom object record. Because we're doing that, I can check in my workflow if we have hubdbrow property value available, and if so, we can update a row instead of creating a new one. So this if then branch allows us to decide if we need to update a hubdbro or if we need to add a hub DB row. Our custom coded actions are quite similar. So I'll open the one that we use for adding a hubdbro. I'm going to make it full screen and you see that we retrieve all the properties of our custom object record that we want to store in HubDB and we're going to use all that data to format a new object and use the HubDB API to add a new row with that data. So we already have a good integration available where we can send data to. So what I did is I'm created a new job post that could be coming from the external API and I'm going to send it to my integration which is currently running. And if we take a look at the terminal, you can see that we're going to check if all the options are available for the select and multi select properties. And if not, I'm going to create a new row. And you see that the last message is that we successfully created a new job. So if we go back to HubSpot and we're going to refresh the page, you see that we do not have 20 records, but we have 21 records available. And my new job record is right here. If we go to the workflow and we're going to check the details and the action logs, we will see that my new job record is currently enrolled in the workflow and has completed the workflow. And if everything works correctly, we do not have 20 rows available in the Hub DB, but we should have 21 rows available in HubDB. Let's check if it's set already. Yeah, and there it is. So right now I have created a new custom object record by using my integration and my workflow is adding a new row to HubDB. So right now we have a HubDB with around 21 rows based on our custom object records. And we want to create an overview of all these rows available. But we also want to create a detail page for each of our jobs available. This is where we can use a really cool functionality called Dynamic Pages. If you set this option, enable creation of dynamic pages using row data to on, you get some additional options where you can select which of the columns you want to use for specific information for those pages. But we also get two new columns available called Page Title and Page Path. And this is used for the page title. Makes sense, right? And this is going to be the slug of your website page. Based on this, HubSpot will try to create a new dynamic page for each row that gets added. If we navigate to our site pages and ignore the crazy styling on my HubSpot button, you can see that I created a page called Jobs. And if I navigate to the settings of this page and you go to the advanced settings, you can set a data source for this page. And you can see that I can select hotdb, but I can also select CRM data. So I could create a detail page, but also an overview page for each job record that gets added. In this case, I decided to use HouDDB. This also requires some specific code, so let's take a quick look at how that would work. I have a custom coded template available called Job Overview, and I'm going to use the row data that is available in HDB in this template. So what you see right here is that I can retrieve a Dynamic PageHubDB table ID and I can retrieve a Dynamic Page HubDB row. In our template, we can check if we're currently on a dynamic detail page or if we are on the overview page. And this allows us to determine which modules we want to use in our pages. So if we're on a detail page, we do not want to use the same modules as on the overview page. So you see that if I'm on a detail page, I'm going to load a text block, an image I'm going to use, load another text block, and another text block, I'm going to load a form, and on the overview, I'm going to load my job overview. In this template, we can pass information from our hub DB row to our modules. So if we take a look at the first module, you can see that I'm passing it a row name. If you take a look at the second module, I'm passing it the featured image that is currently stored for this row. The third module contains our description, which is all the text about our job. And I'm also passing it the row ID to our form. And I'll show you later on what we're using that for. And if we take a look at the page itself, you can see that the information is displayed through our module. So we have our name, we have location information, we have our description, and we have a form that contains the job id. So by setting our row to this variable right here, we can use all the columns available in our modules. The overview itself is a bit more complex. If we take a quick look at the page, you can see that we have some additional functionality. We have the option to filter on experience, on department and on education. We can search for a specific job. We have the option to load more jobs instead of showing them all at once. And this is the power of the HotDB API combined with React. So what we did for this specific module is we loaded a React app in a job overview module. So right here we have our job overview module, which only contains a few divs with some data attributes to use in our React app. And what we're doing in our React app is we are calling the HubDB API to retrieve all the rows available when we first load the page. We're going to retrieve six posts based on the count. And if we take a look at the API, you can see that I'm passing it a limit based on the count set in the state of our React app. I'm also generating selects or checkboxes, and we're also updating state each time someone is clicking on one of those filters. The information stored in our state is then used again in the hotdb API to create URL parameters. So right here you see that if I'm going to click on the department development, we're passing it to our HDB API as a URL parameter. So we're passing it department in and then the department we selected development in this case. So by using the HotDB API and combining it with React, we get a really nice overview which can be used by the user to search for the specific record they are looking for. So this is some really cool functionality from HubDB itself. And the HotDB API is really powerful for filtering on specific rows. So we have our detail pages for our job posts. If someone is going to apply for our specific job, we might want to do something in the CRM. In the beginning I showed you that we currently have two custom objects, one for jobs and one for applications. So what if we wanted to create a new application when someone is going to submit this form and we need to associate it to the contact but also associated to our job? This is a perfect scenario for using custom coded actions as well. We have a workflow right here with a contact enrollment trigger where we set it to has filled out job form on any page. So each time our application form is going to be used and the job ID property is available, then a contact will be enrolled in this specific workflow. We then have a custom coded action where we first will create a new custom object record. So we're going to use the data available in our form. We're then building up a object with properties where we add the internal name for each of those, the internal name of the properties, and we're going to call our CRM Objects API. In the previous parts of the session I used the job endpoint because we were creating job records. But right now I'm going to create a application record and I'm passing it all the information that I want to store in this record and calling this API. I'm using the post method again and we're giving the application ID back to the workflow so we can use that in a second custom coded action, as you can see right here, and use that to associate new the created record to the contact that has submitted the form. I'm going to use the objects API again and the application endpoint to be specific. But in this case I'm using the associations endpoint. I'm telling it I want to associate an application to a contact and I'm passing it the application ID which I got from the first coded action and I'M passing it contact id. After that I got a third custom coded action that is going to associate our created application to a job which works exactly the same as the contact association, but I'm simply telling it to use the job association and passing it the application ID and the job id. So if I were to submit this form, and I'm doing that really quick right now, this is my amazing motivation for this job and I'm passing it a portfolio website and I submit this form, you will see that this workflow will be triggered and my contact will be enrolled. And right here you see that the first custom coded action has successfully executed, which means we should have a new application record available. The second coded action is also executed successfully, and if we refresh we might see that the third custom coded action is also successfully executed, which should mean that we have an application record right now based on the Project Manager job I submitted to and it should be associated to the Project Manager job record and also to my contact. So if I go to my application overview and I shouldn't have refresh the page because it popped up directly, you see that I have a new application record for Project Manager. You can see that it has a bunch of records based on the information that I set in the form, but you can also see that it is associated to my contact and it is associated to the project manage job. So this is really nice. We have a workflow that allows us to create a record and also associate it to a custom object called job, but also to my contact. And as a user of the HubSpot UI, this gives me a quick overview of all the applications, but also which jobs the applications were submitted to and which context submitted application. So now we're at the part I'm most excited about. I'm going to show you how you can use serverless functions to create an association between your custom object records and a context. And I'm also going to show you how you can display those associated records on a membership page using GraphQL in a module or using GraphQL in the API. So what we see right here is the job overview I displayed before. And you also see that I have a small heart symbol available on each of my job cards. If I were to click on one of these hearts, my serverless function will be called and it will create an association between my contact and this specific job record. I also have an account page available and if I refresh the page, you should see the jobs I saved a second ago where I will display all the associated jobs but also the jobs I applied to, if you remember correctly, I had a custom coded action where I will create an application record, associate that to a contact, but also associate that to a specific job. And by using GraphQL I can display all those jobs. So the jobs I apply to on a membership page. So let's start with serverless functions and how those work. What we see right here is my serverless JSON and in my serverless JSON I'm going to tell HubSpot which endpoint I want to create, and I'm also going to tell HubSpot which method I want to use in those endpoints. So right here I have a Jobs Watch endpoint and I will be expecting post requests. I also have a Jobs unwatch endpoint, I have a Jobs retrieve endpoint and there I'm going to retrieve all the associated jobs of a contact, which is a get method and I'm also going to create a GraphQL endpoint. I'm going to show that a minute later. If we take a look at the functions that are connected to these endpoints, you will see that we're calling the HubSpot API again to create an association exactly like we did in the custom coded action. When we get the post request, we're going to expect a job ID in the body, but we also get all information available of the current contact. I then call the HubSpot API to create an association, but if we get an error we're going to display that instead. If the job ID or the contact ID isn't available, we are going to send that as a response. You can see that if the contact isn't logged in, I'm passing it a message that the contact currently isn't logged in. If I were to open this page in a screen where I'm not logged in and I'll click on the heart, you see that I get a message that I have to register for an account. The functionality we use in the job overview itself is quite simple. In my job cards I I have a function available that will trigger each time the hardest clicked called Handle save. The Handle save function then calls the Watch API. I need this one to either create an association by calling my serverless endpoint called Watch or to delete an association by calling my serverless endpoint called Unwatch. If we go back to my serverless functions, you can see that I also have an endpoint available to unwatch a specific document job and this is going to delete the association that currently is available between the contact and the job. If I go back to my UI and you see that currently I'm associated to this specific job record, you can also see it right here and I click the heart again, I'm going to call my unwatch endpoint and remove the association. If I go back to my overview, you'll see that currently that specific job has been removed. So this is some really nice functionality to create a association between your contact and a object record by using serverless functions and adding that functionality to your website ui. We also need some functionality to display these jobs and also display the jobs we apply to. Now, displaying the jobs we saved normally requires two API calls, right? So we're going to use the contact that currently is logged in and we're going to call all associated jobs and that will give us the job IDs of the associated job records. However, we will need a second API call to retrieve the job information, the job record itself. If we move to the jobs we apply to, we need a bunch more API calls. We need an API call to first retrieve the associated applications. Then we need a call to retrieve the associated jobs that are associated to the applications, and then we need a call to retrieve the job records itself to display some kind of information. And this is where GraphQL comes in quite handy and it's going to allow us to write our own query and to make a single API call to retrieve all the information we need. Let's start by taking a look at how you can use a GraphQL request in a module. What I did right here is I created a new folder called Queries and I added a new file with the extension of GraphQL, I can start writing my query directly in this file. I'm going to give it a label so I can connect it to my module later on a description. So Fereal knows what it's going to do. But I can also give it a parameter that I expect to use in my query. And in this case I expect to use the contact ID because I want to retrieve all associated records of a specific contact. When we start building our query, you can see that we first going to call the CRM and after that we're going to access the contact object and I'm going to pass it in a unique identifier called id. I'm also going to pass it a unique identifier value and that's going to be the contact ID of the currently logged in contact. So I'm going to call the associations and in this case I'm going to call the Associated applications. By accessing the application collection, I'm then accessing the items. And by accessing the items, I can access the properties of each application associated to my contact. I need another association of my application, and to be precise, I need the associated job that is associated to the application I'm currently looking at. So again, I'm accessing my associations, and in this case I'm going to access the job collection and then I can access the items. So right now I have all jobs that are associated to the applications that are associated to the contact. And right here I'm going to pass properties I want to use in my module. So right now I have this query, but I somehow need to access this query. If I go to the meta JSON of my module, I can tell it I want to use a data query by giving it a data query path. And you can see right here that I'm passing it the applications graphql file that is stored in my queries folder. And this needs to be a relative path based on the design tools. By doing this, I then have access to the module dataquery values and I'm setting a query to Module dataQuery data CRM contact. And based on that value, I'm going to set my applications with query associations papplication collection application to contact.items if you look closely, you can see that it's very similar to how we wrote the query. But by doing this, I suddenly have the option to loop through all my associated applications. I do not want to display any information of the application, but I want to display the associated job. So again, I'm going to set an attribute called jobs and I'm going to set the associated jobs. And now I can loop through each job that is associated to an application that is associated to a contact and display those on my membership page. And this is some basic HTML where I'm going to use the properties, and this is the internal property name that I wrote down in my query. So you see, I have a name, a page like job type, department location, summary, and by doing this I get a really nice overview of all the jobs I apply to. So this is really cool. Now we also have the jobs I saved, but I'm not going to use the module in this case because I also want to be able to call my serverless function and remove a card from the UI when the association is removed, I created those surfless functions. So by clicking this heart, I want to be able to remove an associated job. If I navigate back to my code you can see that I have a second React app which is a bit more simple than the job overview I showed before. But I'm going to call a GraphQL endpoint, and if you remember correctly, I showed you that in my serverless JSON I created a GraphQL endpoint with the method get. Now, if we take a look at the function that is connected to that endpoint, you can see that we have a query which is going to be a bit more simple than the query we used in the module. Because in this case, I'm simply retrieving all the associated jobs of the contact that is currently logged in and retrieving the information I currently need, and I'm then posting my query. So what I'm doing right here is I'm building up an object, a query object. I'm going to pass an operation name called Jobs. I'm going to write my query in this object, I'm going to tell it which variable I want to use. And in this case, I want to use the contact ID that is available in my serverless function by default. I'm then using the Axios API to call the collector GraphQL endpoint, and I'm going to retrieve all the information I need. If I get back to my React app, you can see that I'm storing the jobs in the state and passing it to my posts. So right here we have all the jobs that are returned through my GraphQL API call. And if we take a quick look at the post itself, which are the job cards, you can see that I can access the data quite similar to how I did that in the module HTML. I have the page lock available, so if I'm going to click on this heart symbol, it's going to call my serverless function to unwatch the job, but it's also going to call the GraphQL endpoint again to retrieve all associated jobs. And by doing this, I can simply update the UI and removing an association without having to refresh the page each time an association is created. So if I click right here and here and associate a bunch of new jobs and refresh the page again, you can see that my associated jobs will be displayed again. If I want to remove one again, I can simply click on the heart. So this is some amazing functionality using serverless functions and using GraphQL to simplify your API calls and preventing you from having to make 2, 3, 4, or even more API calls to display this information. So we started the session by creating an custom object using the UI we showed an integration that can retrieve data from an external platform and based on that create a custom object record, update a record, or delete a record. We had a workflow that we used to create or update new hotdb rows based on custom object records and we displayed those rows in an overview using React and using the HDB API, but also creating dynamic pages. We had a form that if submitted, triggered a workflow where we would create a new custom object record and associate that to a job and a context. And we used all that information together with serverless function GraphQL to display in a membership page. So this really shows all the cool stuff you can do with custom object records with surface functions. With GraphQL I really think that the sky is the limit based on everything you can do right now. So I would like to thank everyone for watching and I would like to thank HubSpot to give me this chance to demonstrate some of the cool stuff you can do with HubSpot. Feel free to reach out on LinkedIn or follow us for more videos and make sure to visit our website to view some of the more cool cases we build for our clients. Have a great day.










