MVP to Production AI

How to Implement an Embedded Workflow Builder that your Users will love Creating Automations With

Users want to build workflows that can action on data in multiple platforms. This article will show users how to create workflow interfaces (workflow builders) from the ground up, from UI to data flows to adding hundreds of integrations. Readers should walk away with a good idea on how to build workflow builders that their users will love building on.

What do Spotify, Raycast, and your home office have in common? While they may not all be perfect products, all of them invite their users to build their own experiences - whether it’s playlists you’ve curated, keyboard shortcuts you’ve tinkered with, or that optimal desk-seat height combination you’ve set up.

N8n has captured that customizability (where users build their own workflows) with value (workflows automate tedious tasks for users).

For many use cases, embedded workflow builders are the perfect interface for users to build deterministic workflows with non-deterministic AI functions. Marrying the customizability of workflows with the value of AI.

In this tutorial, we’ll go over how to build the core components of a workflow product, starting with:

  1. UI

  2. Data flows

  3. Triggers and actions

  4. Observability

UI: Designing the workflow canvas

Set aside the data and operations that need to occur in a workflow product. What you’re left with is a simple canvas UI where users can add steps (nodes) with connectors (edges) signifying when steps should occur or how data should flow.

ReactFlow is a library that makes it easy for you to get started with a workflow canvas with zoom-in/out, drag & drop, and state change handling that comes out-of-the-box [1]. The canvas is all driven from the main ReactFlow component.

<div className="w-full h-full">
	<ReactFlow 
	  nodes={nodes}
		edges={edges}
		nodeTypes={nodeTypes}
		onNodesChange={onNodesChange}
		onEdgesChange={onEdgesChange}
		onConnect={onConnect}
	/>
</div>

Based off node type, you can style your nodes with icons, text, and inputs. For our working example (which you can try for yourself in our ActionKit playground), we customized our nodes with 3rd-party integration icons, a step description, connection handler locations, and click behavior.

export function ActionNode({ id, data }: NodeProps<ActionNodeType>) {
	const setSelectedNode = useWorkflowStore((state) => state.setSelectedNode);
	
	return (
		<div className="border w-52 h-16 flex p-1 justify-center items-center 
			rounded-sm bg-background-muted/10 hover:bg-background-muted/30
			cursor-pointer space-x-1 shadow-lg relative"
			onClick={() => setSelectedNode(id)}>
			<Handle type="target" position={Position.Top} />
			<div className="flex items-center overflow-hidden">
				<img alt="integration icon" src={data.icon} className="w-4 h-4 mr-1" />
				{data.action.title}
			</div>
			<Handle type="source" position={Position.Bottom} />
		</div>
	);
}

Data Flows: Building workflow execution

If you’re familiar with data structures and models, you may already know what a “directed graph” is. If you aren’t familiar, it’s not that hard!

“Graphs” are a way to model systems with nodes and edges (just like a workflow). The “directed” part of “directed graph” means that edges have direction. This is important because generally workflow steps need to be run in a specific order where one step happens after another and may use data from the preceding step.

Another example of a directed graph is street maps. Locations are the nodes, and roads are the edges. These edges have direction, as roads can be one-way or two-way. When Google Maps gives you directions from one location (node) to another, it’s deciding on the optimal edge path to minimize your travel time.

Ok, back to workflows. For your application to properly execute workflows for your users, you need to:

  1. Store node state - each step in your workflow should be able to hold data

  2. Execute the steps in your workflow according to the edges set by your users

Node state

Because nodes are objects, you can store properties within nodes (Neo4j has built their entire graph databases around property graphs [2]). ReactFlow’s built-in node types keep properties like position {x: 100, y: 100}. However, you can extend ReactFlow’s node types to include data necessary for your use case.

In our Playground workflow builder example, we kept track of user inputs and outputs for each ActionNode.

export type ActionNodeType = Node<{
	action: ParagonAction,
	icon: string,
	integration: string,
	inputValues: Record<string, ConnectInputValue>;
	output?: string;
}, 'actionNode'

Using zustand [3] for client-side state management, we could make this state available across our frontend without issues like prop-drilling.

import { create } from 'zustand';

export const useWorkflowStore = create<WorkflowState>((set, get) => ({
	nodes: [{
		id: "trigger",
		type: 'triggerNode',
		position: { x: 100, y: 100 },
		data: {
			trigger: null,
			inputValues: {},
		}
	}],
...
	setNodes: (nodes: WorkflowNode[]) => {
		set({ nodes: nodes });
	},
});

Workflow execution

In your workflow engine, this is where the directed graph comes in. Because workflows have an initial trigger and data flows top-down, your workflow engine must execute steps in order. In our playground, steps can have multiple child steps, so we used a breadth-first pattern to make sure every step at a level was executed before going to the next level.

queue.push('trigger');
while (queue.length > 0) {
	const nodeId: string = queue.shift();
	const selectedNode: WorkflowNode = nodeMap.get(nodeId);
	const res: string = await performAction(selectedNode, userId) ?? triggerInput;
	newNodes = setSelectedNodeOutput(newNodes, selectedNode, res);
	if (edgeMap.has(nodeId)) {
		for (const id of edgeMap.get(nodeId)!) {
			queue.push(id);
		}
	}
}

Triggers and Actions: Arming your users

We’ve gone over the skeleton of your workflow product - the UI and the execution. However, your users get their value from your product through the actions users can automate. These actions should be a combination of unique actions they can only do in your product AND actions in integrated platforms.

For example, Copy.ai workflows have internal actions that happen on their platform - like scraping, research, and content generations - as well as external actions that occur in their integrations - like Microsoft Teams, Slack, and Hubspot.

These integration actions connect your product with other products in your users’ work ecosystem. For your workflow product to be truly integrated, it needs not only integration actions, but integration triggers as well.

Integration triggers kick off your users’ defined workflows whenever an event occurs in an integrated platform. In the Copy.ai example above, they’ve implemented CRM triggers that kick off automations whenever a new lead is created in Hubspot or Salesforce.

In our ActionKit Playground example, we have a Slack trigger that can initiate a variety of pre-built integration actions from ActionKit.

Observability: Adding self-service debugging

Because workflows are like open-world games, there’s an infinite number of combinations that users can create. Users will find unexpected edge cases, realize triggers aren’t firing, or type malformed inputs. When that occurs, there should be a way for users to debug their own issues before filing a support ticket or reaching out to your team.

The good news is, you already know what good observability and debugging looks like as your product and engineering team use products like the browser debugger, Sentry, and CloudWatch, every day.

Like these products, workflow products should provide users the ability to quickly test and iterate on their workflow development. Deployed workflows should also have run history and logs to see when workflow executions succeed or fail.

We’ve implemented an example for testing steps and tracking run history in our Playground.

If you’d like to see a production example that’s used by hundreds of enterprise customers, you can check out Paragon’s Workflow Builder and Task History.

Wrapping Up

In this post, we went over the core components of a workflow product and how your team can implement embedded workflows in your application. Workflows are a great interface for users to customize their experience and find value through automating tasks inside and outside of your application.

For integration actions that work outside of your application, check out ActionKit and try out actions for yourself in our ActionKit Playground.

If you’re interested in Paragon’s full suite of products with integration actions, triggers, automations, and syncs, sign up for a free trial and book a call with our team.

Sources and Acknowledgements

[1] The xyflow team behind ReactFlow also supports the SvelteFlow library for the Svelte framework

[2] Neo4j - popular graph database used for network analysis and graph-based RAG

[3] Zustand is a great library for client-side state; also check out ReactFlow’s example using Zustand

TABLE OF CONTENTS
    Table of contents will appear here.
Jack Mu
,

Developer Advocate

mins to read

Ship native integrations 7x faster with Paragon

Ready to get started?

Join hundreds of SaaS companies that are scaling their integration roadmaps with Paragon

Ready to get started?

Join hundreds of SaaS companies that are scaling their integration roadmaps with Paragon

Ready to get started?

Join hundreds of SaaS companies that are scaling their integration roadmaps with Paragon

Ready to get started?

Join hundreds of SaaS companies that are scaling their integration roadmaps with Paragon