n8n is used for automations and integrating external platforms with our service. We use it to process every server event.
CRUD
CRUD stands for Create, Read, Update, Delete and represents all four ways you can interact with a data object.
I have configured our server to send all CUD updates (no reads) to the global n8n entrypoint - The API router workflow. This means for all “items” in our service (think row in a table, like a member or appointment), n8n will receive a trigger when anything is created, updated, or deleted.
This covers basically all notification cases I can think of (which are usually tied to creates), as well as giving us the ability to do deeper processing if we need to based on any event.
Structure
To keep things organized, there are 3 types of n8n workflows for dealing with reacting to server events. These aren’t actually different types in n8n or anything, just a way for me to stay organized.
1. API Router
To make thing simple, there is a global API router workflow that receives every event. This has a webhook trigger. I’ve added code to our server that watches for any CUD event, does a small amount of processing, and sends it over to this single endpoint.
The API router’s main job is to detect which “collection” the event belongs to. This is the table, like “members” or “appointments”. And it sorts the event to the relevant sub-workflow.
2. Collection Sub-Workflows
Because each “collection” has a different data structure, it makes the most sense to break them out into their own sub-workflows.
These sub-workflow share one step in common - they sort the event into three paths based on if it was created, updated, or deleted.
For there, we can implement our custom actions for what we actually want to do. For example, when an appointment is created, we can send an email.
3. Specific Task Workflows
Specific self contained tasks that we will use in many different places should also be broken out into their own sub-workflows for re-use. Anything that requires some amount of configuration or formatting should be made into a sub-workflow.
For example, to send an email from our server, I made a sub-workflow that just expects a specific json input. Then from anywhere you can send an email from our server by calling that workflow and passing in the formatted json.
If n8n has a prebuilt module for what you are trying to do, just use that. These are for emulating that experience with custom actions.
Data
Let’s look closer at what is actually being sent over to n8n.
We will walk through a specific example: when an appointment has been created.
Now it’s just a matter of pulling out the desired fields for whatever we want to do with them.
Underscores
Any field that has an underscore in front of the name is added by the server as part of some custom processing before sending the event over to n8n. For example, “_event” is an added field for easily knowing which CUD event has occured.
The following shows how the CUD switch works, to separate events based on if they were created, updated, or deleted. It simply checks {{ $json.body._event }}.
Event Switch
Populater
Since we are running a relational database, it is often necessary to have data from related fields in order to do anything useful.
Example: You can’t send an update email for an appointment if you don’t also have the related member’s email address.
There two schools of thought here.
You look up the data when you need it. This is what I would do if these events were being processed entirely on our server.
You prepare all the data you might need beforehand. This is more secure for us since we are using a third party service that we don’t want to give read access to our database.
To do this I’ve made a relation saturator helper in the server that runs before sending to n8n on every event. it is located at pocketbase/pb_hooks/utils/populate.ts.
This works by storing a list of fields where we want to instead see the object, and fetching that data and replacing it in the event. It runs on all events, so any time it finds the field “member”, for example, it’ll populate it with the related object.
So when something like an appointment event comes into n8n, you will also have access to the related member data and can use it for any processing.
Considerations for Updates
_diff
Update operations require a bit of extra processing and data because you need to know specifically what was updated.
I added functionality for the server side to automatically perform this diff check and send it as part of the body to n8n.
The most useful output is found at “_diff” within the json body, which will tell you exactly what field was updated, and what is was updated to. The structure is as follows:
The “_diff” field will show all updated fields as new objects with the key being the name of the field and containing “old” and “new”. It can have multiple depending on how many fields were updated at once.
This allows us to decide between very specific update events in n8n, for example like sending a “appointment was completed” email.
_old
In addition to “_diff”, the “_old” object is also added and is just the previous values of the object before it was updated.
I haven’t really found a use for this yet, but there it is. I decided to include it anyways since it may be useful at some point.
Building in n8n
n8n’s interface is more raw than some competitors. This could make it a bit more intimidating, but also makes it a lot easier to work with when it’s learned. It doesn’t have many of the limitations that platforms like zapier have.
Once your comfortable working with json objects, n8n is a breeze. You just drag and drop the json fields where ever you need them to be within the different modules.
Example data
One of the biggest benefits to splitting our workflows by collection type, besides the obvious organization benefit, is that I’m able to save an example payload of that type.
This makes testing and building much easier, as you don’t need to actually send real server events. You already have an object to work with.
Let’s look at the appointments workflow again. If you click on the first node, you’ll see it’s already got an appointment json object there.
You can edit the fields in this object to test various flows. Try changing the “_event” to “created”, and you’ll see it now gets routed to the created path. You can use this to mock any event easily, without needing to send them.
Use whats there
There are lots of routes and switches already set up in the various workflows. Try looking through them to see how they work. You can go through a workflow step by step by pressing the small play button above each node in the flow.
Note on sub-workflows
You may notice that only one workflow in our account is active. This is misleading.
Any workflow that uses the “trigger from another workflow” as it’s entrypoint is turned of by design.
Also, any flow that goes to any sub-workflow only counts as 1 execution in n8n. You can go to as many sub-workflows as you want, and it’ll still only count the one run.
So our API router workflow is the only active one, because its the only workflow that can be triggered by anything external. All the others are sub-workflows.