Now that we have a functional toolbar, let's go ahead and enable the run button which is going to trigger the actual workflow execution. First let's go back into our schema. In here, we can find the workflows table. What I wanna do now is assign the graph property a proper type. When we first developed the schema, we didn't yet have the node registry file.
But now that we do, we also have the step node type, which we can now reuse inside of this schema and make it more type safe. So first let's import the type edge from XYflow React. Then let's import the type step node type from our node registry. Then let's export a new type called WorkflowGraph which is an object with nodes and edges. And make sure that for the nodes type you use the StepNodeType in an array.
And for the edges we can just use the default ones from XYflow React. And now besides exporting this type we can also assign it to the graph field. Now let's add a new dependency to our project called toposort. Toposort is going to help us validate the graphs in our workflow as well as correctly order the connections, nodes, and edges. Also make sure to add a dev dependency called types toposort.
After the installation go inside of your package.json to verify that you now have Toposort inside of your dependency and Types Toposort inside of your dev dependencies. Now let's create a new util inside of features workflows lib folder and let's call it validateGraph.ts. First, let's add our new dependency Toposort. Then, let's import our new type WorkflowGraph from libDatabaseSchema. Then, let's create a new function validateGraph, which destructures the nodes and the edges from the prop workflow graph and it returns an array of strings, which are essentially all the problems that this function found.
So let's first define that constant, problems, which is an array of strings, and by default, it's an empty array. Make sure that you return it so you no longer have the type error in the function. Then let's load all the trigger nodes we have from our workflow graph. If there are more than one trigger nodes, let's push a problem. A workflow needs exactly one start trigger.
And then let's write how many of them we found and why this graph isn't validated. Then Let's check how many edges we have. If we have no edges, let's make sure to throw another problem, connect your nodes before running. And in the else block, let's open a try and catch where we call Toposort and we map over its edges and if this Toposort fails, meaning if it goes into the catch block, let's push a new problem, workflow has a cycle, remove the loop before running. So by initializing Toposort and adding this specific iteration of edges with the source and the target inside, Toposort can detect if there is a cyclic workflow, meaning that one node ends up being connected to the previous node, which breaks our workflow.
So this is our validate graph function which we can use to check whether our workflow has any structural problems or not. Now let's go inside of our data.ts file where we have functions like list workflows, get workflows, create workflow, etc. And let's develop a new function which we're going to use to save the workflows graph information. So first of all, let's import workflow graph type from lib database schema. Then let's import validate graph function from our lib which we've just created.
Then let's open a new function save workflow graph which accepts organization id, id and graph. Since we have three parameters here, I like to have a practice of putting them inside of an object instead of just in a specific order. Then let's use our validate graph util and pass along the graph prop which is equivalent to the workflow graph type and let's extract problems from that lib. If problems.length is larger than zero let's simply throw a new error joining all of those problems together since they are strings we can display them in that manner. Other than that let's do await database update the workflows table, set the graph property and updated add property to whatever is the current new date result.
And then let's write a simple where query both the workflows ID and the workflows organization ID match. And that is our function save workflow graph. Now let's use this new data function inside of our actions. So first things first, import save workflow graph from the data, then import the workflow graph type from lib database schema, and then let's go down until we find our run workflow action. And first let's extend its props.
So let's make it accept an object id and graph. Id is a type of string and graph is a type of workflow graph. Make sure that you validated the organization ID if you haven't so far, and then after that validation simply await save workflow graph and pass along the organization ID, the ID of the workflow, and the graph field. So now we have an action which starts a long-running task and validates our workflow, but we're also going to need a function which will be used to stop a long-running task in case the user changed their mind. So let's import runs from trigger.dev SDK and then at the bottom after run workflow action, let's export cancel workflow action.
The only thing it needs to accept is the run ID. Validate the organization ID and then go ahead and cancel the run. This is a pretty good spot for a checkpoint so go ahead and stage your files and then do a commit. Now let's enable the run button inside of our right sidebar component so that it runs the server action which we just modify, the run workflow action. So inside of the right sidebar, first let's import run workflow action from features workflows actions, then let's import our validate graph function, then scroll down until you find the run button function and first make it accept a new prop workflow ID then using the use react flow hook extract the nodes and the edges using get nodes and get edges and make sure you type it with step node type.
Then add the use transition hook from react and extract is pending and start transition. Then attach the is pending to the disabled prop of the button. Then construct the graph using nodes which invokes the get nodes and edges, which invokes get edges. Then let's find if there are any problems using our validate graph function. If there are problems, let's go ahead and do toast.error and let's always render the first in the array.
Then let's call start transition and let's execute an asynchronous function which then awaits run workflow action and passes along the id which is the workflow ID from the prop, which we've added, and the graph so we can validate it on the backend as well. And then at the bottom, in the right sidebar function, where we actually render the run button, make sure you pass along the workflow ID. So ensure that you actually have useReactFlow imported and useTransition. You should have both of those because we actually use useTransitions in the actions menu for our delete workflow action. But just in case your agent didn't do that, go ahead and manually add this.
And you should absolutely have used React flow because that's what we use to update the node data and to add new nodes. And now to construct the graph using the get nodes and get edges. Now make sure that your local trigger server is running and go ahead and press the run button. As you can see we have an error connect your nodes before running So I have this error because I didn't connect my nodes, meaning that our validate graph function is working. So it noticed that I have zero edges, which meant that I didn't connect any of my nodes.
And now that my node is connected, I can click on run and as you can see, I have no errors. And inside of my development server here, I can see that Hello World was triggered and then hello world was successfully initialized. So if you play a bit more and for example, remove all nodes and then click run again, you will find another error. Our workflow needs exactly one start trigger, found zero. So you need to have at least one node added.
I mean one start node and your nodes need to be connected. Otherwise you will get hit with an error. Perfect. So we have now completely wired up our data fetcher, our server action and our component. But what's actually interesting is does the actual graph now update in the database?
Because so far We never really updated this graph field. We only defined it as this JSON field. All the data persistence was done using Liveblocks Room because Liveblocks is a database in itself. So let's quickly check NEON to see what's up in our database. Go inside of your Neon project and find the tables.
And in here you will find the workflows. As you can see, so far I didn't have any graph field populated except this one, which is the first graph in which I hit the run button. So now let me go ahead and pick any random workflow like this one, joint baboon which currently has nothing in the graph and I'm going to go ahead and maybe add a couple of nodes here. And I'm going to connect them and I might even add some data here. And then I'm going to click run.
Since there are no errors, I know that this had to have been saved successfully. And there we go. Once I refresh, you can see that now a joint baboon has this data in here. Beautiful. And you can even see the exact nodes which are inside and if I'm correct we should also see the values stored.
So this data persistence inside of our graph field is used for a different purpose in comparison to LiveBlocks. So LiveBlocks data persistence came as a great help to us because it's what renders the UI. So every single time we move our nodes around Liveblocks updates its database. We don't have to synchronize that, they do it for us. But whenever we want to execute the workflow, this is where it's important for us to validate whether this graph is correctly connected, whether it has any missing edges, and whether we can actually execute it.
But right now, the task which we are executing when we click on the run button is this hello world example. So let's now start working on a real workflow execution task. I wanna develop this task inside of features workflows. And in here, I've created a new folder called tasks. And in here, let's create run workflow.ts file.
Let's start with the imports. Let's add Toposort and let's add Logger and Task from Trigger.dev SDK. Then let's import our GetWorkflow from Workflows data. Then let's define the RunWorkflowTask. Let's give it an ID of run-workflow.
And inside of the run, let's simply define an empty asynchronous function. Let's go ahead and make this asynchronous function accept a workflow ID and an organization ID inside of an object. Then using that organization ID and workflow ID, we can properly call our getWorkflow data fetcher, which gives us access to the workflow the user is trying to run. Next, let's check if the workflow isn't available or if the workflow doesn't have a graph. And if they don't, let's throw an early error, the workflow has no graph.
Then let's go ahead and destructure the nodes and the edges from the other workflows which actually have a graph, which have passed this validation. Then let's add a little helper called byId, so we create a new map and we put the nodes inside in this exact format. Then let's go ahead and create a new set and we use edges.flatmap. What we do here is we ensure that the nodes we are going to run are only those which are connected. So if we have any orphaned nodes which aren't connected to anything we're just going to drop them and we're not going to run them through the toposort and we're not going to execute them.
Then let's go ahead and order our nodes using toposort.array. Inside of the array in the first argument perform a map over nodes and in the second argument perform a map over edges. Make sure you produce an array which will end up being a matrix of sources and targets. And then filter by ID of each of those and make sure they appear in the connected set. So that's how we ensure that we only execute those nodes for which we found a connection to another node.
So there is a complete cycle. And let's add a little log here. Running workflow, workflow name, and let's prepare how many steps this is going to have. By steps we mean how many nodes this is going to have. Then let's do a for loop.
So for every order that we find, let's go ahead and find the node using our By ID helper. And let's simply log running step and then let's print out exactly which step that is. And let's say that to do, actually execute the node instead of just logging it and report its progress so the UI can watch the run line. So that is the next step. So just make sure that you are iterating over the order, right?
Because that's what we are doing. The order is essentially going to return an array of strings and that's what we have to follow. It's basically an array of IDs which have to be executed in that specific order. And in the end simply return the steps using order.length. Now that we have our new run workflow task, let's go back inside of our server actions and let's replace the usage of hello world task and replace it with our run workflow task.
So first let's add import type run workflow task from features workflows task run workflow. So yes, the run workflow task as defined and exported here is automatically a type. So there was nothing else we had to export this can serve both as the API and as the type. So now that we imported that let's go down until we find run workflow action and in here we trigger the Hello World task. So let's now replace this with the following.
Tasks.trigger type of Run Workflow Task which makes it required for us to name the workflow like this. If you try to enter something else, it's going to throw an error because run workflow task is clearly defined to have this specific ID. So make sure that your ID isn't misspelled. And then pass along the workflow ID and the organization ID. And then as a third argument, which is something we didn't have for the Hello World example, go ahead and add tags.
And tags is an array which has a constructed string workflow colon ID. If you try and click on the run button and then go inside of your trigger development server, you will notice that runs are not firing. So even though we correctly replaced which run we are invoking, it's not properly executing. The reason is because we didn't register the features folder to store any trigger.dev tasks. So we have to go back inside of the trigger.config.ts and replace trigger with features.
And then I would recommend restarting your server and let's try it one more time. So make sure you have your trigger server running and then go ahead and click run and just like that you will now see your runs. Besides verifying the runs inside of your CLI, I would recommend also visiting the trigger.dev dashboard so you can actually go inside of your latest runs and verify what's logging inside. Create the following scenario in your canvas. A start node connected to open URL 1 which is connected to open URL 2 and if you take a look at our run workflow task what we do is we topologically order the nodes based on their edges, based on the source and the target of the edge.
So we are aware which node is connected to which one, which means this for loop for this specific canvas should output start node followed by open URL node and then the last node that should be outputted is the open URL 2 node. So when I click run I'm going to go ahead and refresh my trigger.dev and this one which just completed, let's take a look at the logs here. The first step is start node, the second step is open URL1 and the third step is open URL2. Now I'm gonna try and purposely remove the edges and connect it the other way around, just to verify that this isn't a fluke. And just as we suspected, the order is now start, open URL2, and then open URL1, meaning that our algorithm for topological sort is correct.
And because we added a tag into our tasks.trigger inside of run workflow action, if you take a look at your latest runs and scroll to the right side, you will see that our newest runs have a workflow with their respective ID. Now, right now this isn't really useful for us, but later when we add the real-time feature of displaying logs of a specific run, this will come in very handy. We can now also remove the trigger folder and the example file from there because it's no longer used. Before we commit and end this chapter, let's ensure that we have the proper git changes. So we removed the example.ts from the trigger folder as well as the entire trigger folder.
We modified the trigger config so that directory of the tasks is now features and not trigger. We added a new run workflow task which topologically orders the connected nodes which it extracted from the workflow graph which we loaded using the organization id and the workflow id and inside of this topological order we iterate over those nodes and log which step is currently running to verify if that order is correct and matches what we see on the canvas. We then modified our actions.ts by adding that new run workflow task and appending it to the run workflow action. And that actually reminds me we should also remove the unused type hello world task from here. There we go.
And in the right sidebar we did this actually first I believe we wired in the run workflow action which is a server action and the validate graph and wired it into the run button so we can disable it while it's running and we can throw any errors if validate graph doesn't pass. Great. So now let's go ahead and git add and then git commit and git push. Amazing job and see you in the next chapter.