<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Workflow Builder</title>
    <description>The latest articles on DEV Community by Workflow Builder (workflowbuilder).</description>
    <link>https://dev.to/workflowbuilder</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F13501%2F64ca000e-b872-411c-a228-258f05840d5a.jpg</url>
      <title>DEV Community: Workflow Builder</title>
      <link>https://dev.to/workflowbuilder</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/workflowbuilder"/>
    <language>en</language>
    <item>
      <title>Parameters change everything: passing data between workflow nodes</title>
      <dc:creator>Piotr Błaszczyk</dc:creator>
      <pubDate>Tue, 21 Jul 2026 12:34:45 +0000</pubDate>
      <link>https://dev.to/workflowbuilder/parameters-change-everything-passing-data-between-workflow-nodes-9mb</link>
      <guid>https://dev.to/workflowbuilder/parameters-change-everything-passing-data-between-workflow-nodes-9mb</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; &lt;a href="https://www.workflowbuilder.io/" rel="noopener noreferrer"&gt;Workflow Builder&lt;/a&gt; is an open-source (Apache 2.0) React SDK for embedding a visual workflow editor in your own product. It is built on React Flow: the canvas comes from React Flow, Workflow Builder is the layer above it (nodes as JSON schemas, properties panel, validation, execution). This article is about one feature, the Variable Picker, which lets one node read another node's output. Type &lt;code&gt;{{&lt;/code&gt; into a field and the editor offers only the data that exists at that point in the graph, already type-checked. Underneath it took output schemas in JSON, a graph reachability walk for scoping, and a strict template resolver. Real code included.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;em&gt;Disclosure: I am the creator of the Workflow Builder SDK and have been building it since the first commit. Keep that in mind while reading.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This article first appeared on the &lt;a href="https://www.workflowbuilder.io/blog/variable-picker-passing-data-between-workflow-nodes" rel="noopener noreferrer"&gt;Workflow Builder blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Drop a few boxes on a canvas, connect them with arrows, and you have a workflow. Do this, then this, then that. It runs, top to bottom, each box doing its one fixed thing.&lt;/p&gt;

&lt;p&gt;It works, but it is a trivial program. Nothing one box produces can reach the next, so no step can build on what another did. It only becomes a real program once one node can use the output of another.&lt;/p&gt;

&lt;h2&gt;
  
  
  A node is a function
&lt;/h2&gt;

&lt;p&gt;Here is why nothing flows between the boxes. A node is a function, and right now it receives no inputs. Think about how that works in code. A function with no arguments is rarely interesting.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;sendEmail&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What does it send? To whom? It is a black box with a label.&lt;/p&gt;

&lt;p&gt;Now give it parameters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;sendEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Suddenly it is a tool. The same function serves a thousand different cases, because the caller decides what flows in. The signature is a contract. It says "give me these three things and I will do the rest."&lt;/p&gt;

&lt;p&gt;A node in &lt;a href="https://www.workflowbuilder.io/" rel="noopener noreferrer"&gt;Workflow Builder&lt;/a&gt; is exactly this. An empty action node is &lt;code&gt;sendEmail()&lt;/code&gt;. A configured one is &lt;code&gt;sendEmail(to, subject, body)&lt;/code&gt;. The whole value of the editor lives in that gap.&lt;/p&gt;

&lt;p&gt;So the interesting problem in building a workflow editor is not the canvas. It is this: how does a node get its inputs?&lt;/p&gt;

&lt;h2&gt;
  
  
  Where do the arguments come from
&lt;/h2&gt;

&lt;p&gt;An argument gets its value from one of two sources. The first is a literal. You type the value in by hand. &lt;code&gt;subject = "Welcome aboard"&lt;/code&gt;. This is fine for constants, and useless for anything that depends on what happened earlier in the run.&lt;/p&gt;

&lt;p&gt;The second is a reference. You point at the output of a step that already ran, and let the real value be filled in when the workflow executes. The email subject is not a string you typed. It is "whatever the Classify step decided this request was about."&lt;/p&gt;

&lt;p&gt;That second source is the Variable Picker. Say an upstream Classify node has just tagged an incoming support ticket as &lt;code&gt;Billing&lt;/code&gt;. In the email node's Subject field you type &lt;code&gt;{{&lt;/code&gt;, and instead of guessing at variable names, you get a panel of everything that is actually available at that point in the graph. You pick one. The field shows a friendly chip, &lt;code&gt;{{ Classify · Category }}&lt;/code&gt;, while the diagram quietly stores the durable form:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight liquid"&gt;&lt;code&gt;&lt;span class="cp"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;classify-1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;category&lt;/span&gt;&lt;span class="cp"&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the same idea as wiring one function's return value into another function's argument. We made the wiring visual, and we made it impossible to wire to something that is not there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Types, because data has a shape
&lt;/h2&gt;

&lt;p&gt;As soon as data flows between steps, it has a shape, and shapes can be wrong.&lt;/p&gt;

&lt;p&gt;A Decision node comparing "is greater than" only makes sense for numbers and dates. Feeding it a boolean is not a clever edge case. It is a bug the user did not mean to write. So every output a node can emit declares its type, drawn from a small, deliberate set:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;VariableTypePrimitive&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;number&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;boolean&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;datetime&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;date&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;VariableType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;VariableTypePrimitive&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;object&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;array&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The picker reads those types and filters itself. When a field expects a number, the panel only offers numbers. When it expects a date, it also offers datetimes, because a datetime is a date with extra precision. The available comparison operators shift with the type too. Strings get "contains." Numbers get "greater than." Dates get "before" and "after." The interface never lets the user assemble a comparison that cannot mean anything.&lt;/p&gt;

&lt;p&gt;This is types-as-validation, except the user never sees a red error. They simply are not offered the wrong choice in the first place.&lt;/p&gt;

&lt;p&gt;The filter is strict but not naive. User input arrives as strings, so a few cross-type pairings are allowed on purpose:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A number can fill a &lt;code&gt;string&lt;/code&gt; field.&lt;/li&gt;
&lt;li&gt;A valid date string satisfies a &lt;code&gt;date&lt;/code&gt; field, and &lt;code&gt;date&lt;/code&gt; and &lt;code&gt;datetime&lt;/code&gt; are interchangeable.&lt;/li&gt;
&lt;li&gt;The only boolean literals it accepts are &lt;code&gt;''&lt;/code&gt;, &lt;code&gt;'true'&lt;/code&gt;, and &lt;code&gt;'false'&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything else is refused. The picker coerces where the meaning is unambiguous and refuses where it is not.&lt;/p&gt;

&lt;h2&gt;
  
  
  A node can only see what runs before it
&lt;/h2&gt;

&lt;p&gt;In a programming language, a variable has a scope. You cannot read a value that has not been assigned yet, and you cannot reach into a block that never ran. A workflow has exactly the same rule, drawn in two dimensions on a canvas, and a serious editor has to respect it.&lt;/p&gt;

&lt;p&gt;Some variables are global. Secrets, tenant configuration, an API base URL. Available everywhere, from the first node to the last.&lt;/p&gt;

&lt;p&gt;But most variables are earned. A node can only reference the output of a step that genuinely runs before it. Concretely, that means a step somewhere upstream, reachable by walking the arrows backward from the current node. The picker computes this directly. It runs a breadth-first search back through the edges to collect every ancestor of the selected node, and offers their outputs and nothing else:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// BFS backward through edges to find all ancestor nodes&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ancestors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Set&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;queue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;nodeId&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;shift&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;edge&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;edges&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;edge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;ancestors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;edge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;ancestors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;edge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;edge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The consequence is exactly the scoping rule you would want. A node sees what flows into it. It cannot see a sibling on a parallel branch that never reaches it. It cannot see a node downstream of itself either, because that data does not exist yet when this step runs. The graph topology &lt;em&gt;is&lt;/em&gt; the scope. We did not invent a new mental model. We took the one every programmer already has and rendered it on the canvas.&lt;/p&gt;

&lt;p&gt;One honest note on that code. This is really a reachability walk, not a meaningful breadth-first one. Order does not matter, because we collect the whole set of ancestors, so depth-first would behave the same. The inner loop rescans every edge on each step, so it runs in O(ancestors × edges). That is fine at workflow scale. It would not be fine in a general graph library.&lt;/p&gt;

&lt;h2&gt;
  
  
  Simple when it works, loud when it fails
&lt;/h2&gt;

&lt;p&gt;Everything above is plumbing. For the end user it collapses into one gesture: focus a field, type &lt;code&gt;{{&lt;/code&gt;, pick from a panel grouped by upstream step, and keep typing around the chip. The editor only ever offers things that work, and the hard part disappears into a two-character keystroke.&lt;/p&gt;

&lt;p&gt;We took one more deliberate stance for the moment things go wrong. A reference is strict by default. If a path cannot be resolved at run time, the run fails loudly with &lt;code&gt;Unresolved template reference&lt;/code&gt;, rather than silently substituting an empty string and shipping it to an LLM. A typo in a prompt should break on the first run, not three weeks later in production. When absence is genuinely expected, the user opts into a fallback on purpose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight liquid"&gt;&lt;code&gt;&lt;span class="cp"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;classify-1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;category?}}&lt;span class="w"&gt;                          &lt;/span&gt;//&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;resolves&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;missing&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;{{nodes.classify-1.category&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s1"&gt;'unclassified'&lt;/span&gt;&lt;span class="cp"&gt;}}&lt;/span&gt;  // resolves to a default
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Strictness needs one more distinction to be safe. The resolver parses in two stages. An outer pass catches anything that looks like a reference: a &lt;code&gt;{{namespace.path}}&lt;/code&gt; block with a dot, the marker that the author meant a reference and not literal text. An inner pass checks that body against the real grammar. So a token has three fates, not two:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;{{nodes.foo?bar}}&lt;/code&gt; looks like a reference but breaks the grammar. It throws &lt;code&gt;Malformed&lt;/code&gt; instead of leaking a broken token into a prompt.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;{{total}}&lt;/code&gt; has no namespace dot, so the outer pass never claims it. It stays literal text.&lt;/li&gt;
&lt;li&gt;A well-formed path that points at nothing throws &lt;code&gt;Unresolved&lt;/code&gt;. Only the opt-in &lt;code&gt;?&lt;/code&gt; and &lt;code&gt;default:&lt;/code&gt; quiet this last case.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Strict by default, lenient on request. The safe thing is automatic, the risky thing is a choice you make with your eyes open.&lt;/p&gt;

&lt;h2&gt;
  
  
  Because in Workflow Builder, everything is data
&lt;/h2&gt;

&lt;p&gt;None of this would be possible if a node were just a React component with some props hardcoded in its source.&lt;/p&gt;

&lt;p&gt;A node in Workflow Builder is a data definition. It always carried a &lt;code&gt;schema&lt;/code&gt;, the JSON description of its own configurable inputs, the part that says "I am a function with this signature." Building the Variable Picker meant the other half of that contract had to exist as data too. So we extended the definition with an &lt;code&gt;outputSchema&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;OutputProperty&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;VariableType&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;description&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the whole trick. The inputs of a node are data. The outputs of a node are now data. And because both halves of the contract are just JSON, the picker can read them, the type filter can reason over them, and the ancestor search can connect one node's outputs to another node's inputs. The SDK stops there. It stores the wiring as text and runs nothing itself. A backend resolves the references when the workflow runs. Workflow Builder ships a reference backend so you can watch the whole loop work, then swap in your own. A node that wants to participate adds an &lt;a href="https://www.workflowbuilder.io/docs/guides/use-variable-picker/" rel="noopener noreferrer"&gt;&lt;code&gt;outputSchema&lt;/code&gt;&lt;/a&gt;. A node that does not, simply never shows up in any downstream picker. No special cases. No component knows about any other component. The graph and the schemas carry all the meaning.&lt;/p&gt;

&lt;p&gt;This is how we tend to think on this project. When we want a new capability, the first question is rarely "what component do we build." It is "what does this need to become data, and what can read that data." Make the contract explicit and make it JSON. After that, a new capability is mostly a question of what each node reads and what it exposes.&lt;/p&gt;

&lt;p&gt;The Variable Picker looks like a small convenience: two braces and a dropdown. Underneath, it is the feature that finally lets one box pass a value to the next. That is the line between "do this, then this" and a real program. Cross it and you have a tool. Stop short and you have a diagram.&lt;/p&gt;

</description>
      <category>react</category>
      <category>typescript</category>
      <category>webdev</category>
      <category>opensource</category>
    </item>
    <item>
      <title>From React Flow to Workflow Builder: what you keep, and what you gain</title>
      <dc:creator>Dominika Pacholec</dc:creator>
      <pubDate>Thu, 16 Jul 2026 09:35:23 +0000</pubDate>
      <link>https://dev.to/workflowbuilder/from-react-flow-to-workflow-builder-what-you-keep-and-what-you-gain-4b9m</link>
      <guid>https://dev.to/workflowbuilder/from-react-flow-to-workflow-builder-what-you-keep-and-what-you-gain-4b9m</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foz80vta1dza374z4u48c.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foz80vta1dza374z4u48c.jpg" alt=" " width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; I've spent 7+ years building diagram-heavy and node-based UIs, so when I dug into Workflow Builder – our SDK at Synergy Codes – I went to the public source to see what it actually does to a React Flow app. Short version: it's a superset, not a replacement. React Flow stays underneath as a peer dependency, and your React Flow knowledge carries over. Where the work is: graph state moves into the SDK's store, and custom nodes get rewritten as node templates. This is an honest breakdown of what stays, what changes, and what you gain – every claim checked against the public source. If you're a React Flow dev weighing build-vs-buy for a workflow editor, this one's for you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Disclaimer:&lt;/strong&gt; I'm a frontend engineer at Synergy Codes, the company behind Workflow Builder. I build diagram and node-based UIs for a living, but I'm not on the Workflow Builder SDK team – I went through the public repo and docs to write this, and kept it honest, including the parts that cost you effort.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://www.workflowbuilder.io/blog/from-react-flow-to-workflow-builder" rel="noopener noreferrer"&gt;Workflow Builder blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A production workflow editor on React Flow isn't one feature – it's a properties panel, undo/redo, a palette, persistence, layout, validation. Workflow Builder gives you that whole layer while keeping React Flow underneath.&lt;/p&gt;

&lt;h2&gt;
  
  
  Can you still do everything you did in React Flow?
&lt;/h2&gt;

&lt;p&gt;Yes. Almost everything you reach for in a React Flow editor works the same way inside Workflow Builder, because React Flow is still doing the work. Here is how the common building blocks map across:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What you do in React Flow&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;th&gt;In Workflow Builder&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Render custom nodes&lt;/td&gt;
&lt;td&gt;This changes&lt;/td&gt;
&lt;td&gt;Start from 7 built-in node templates (trigger, action, conditional, decision, delay, notification, AI agent), or write your own as a node template with a schema for its data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Build custom edges (&lt;code&gt;EdgeProps&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Same as React Flow&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;EdgeProps&lt;/code&gt; straight from React Flow, plus ready components like labeled and self-connecting edges&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Configure the canvas and handle events (zoom, pan, fit view, &lt;code&gt;onNodeClick&lt;/code&gt;, &lt;code&gt;onPaneClick&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Same as React Flow&lt;/td&gt;
&lt;td&gt;Props pass straight through to the underlying &lt;code&gt;&amp;lt;ReactFlow&amp;gt;&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manage graph state (read and update)&lt;/td&gt;
&lt;td&gt;This changes&lt;/td&gt;
&lt;td&gt;Workflow Builder sits above React Flow and owns its state – the SDK store is the source of truth, and you read and update the graph through it. React Flow's hooks (&lt;code&gt;useReactFlow&lt;/code&gt;, &lt;code&gt;useNodes&lt;/code&gt;) stay available for reading and inspecting the live canvas&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Build the core editor layer yourself&lt;/td&gt;
&lt;td&gt;New in WB&lt;/td&gt;
&lt;td&gt;A node library, a schema-driven properties panel, a drag-and-drop palette, read-only mode, and typed persistence (localStorage, REST, or callback)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wire up editor basics yourself&lt;/td&gt;
&lt;td&gt;New in WB&lt;/td&gt;
&lt;td&gt;Undo / redo and copy / paste ship as free, open plugins on the public plugin API&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Graph state is the one area that works differently. Workflow Builder runs React Flow in controlled mode and owns the graph in its own store, so the store is the source of truth: you read and update the graph through the SDK's actions. React Flow's hooks still return the live canvas state for reads and inspection, but the store – not direct &lt;code&gt;nodes&lt;/code&gt; / &lt;code&gt;edges&lt;/code&gt; mutation – is how you drive changes. Keeping state in a dedicated store is also a performance choice: it follows the patterns Synergy Codes' &lt;a href="https://www.synergycodes.com/webbook/guide-to-optimize-react-flow-project-performance" rel="noopener noreferrer"&gt;guide to optimizing React Flow performance&lt;/a&gt; recommends for keeping larger graphs responsive.&lt;/p&gt;

&lt;p&gt;Everything else passes through. You hand React Flow props to Workflow Builder, and they land on the underlying canvas untouched. When you build nodes, edges, or plugins, you still import &lt;code&gt;Handle&lt;/code&gt;, &lt;code&gt;Position&lt;/code&gt;, and &lt;code&gt;EdgeProps&lt;/code&gt; from &lt;code&gt;@xyflow/react&lt;/code&gt; – same package, same docs, same upgrade path. And when you need the raw canvas API for something the SDK does not surface, it is still reachable.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you gain on top
&lt;/h2&gt;

&lt;p&gt;Workflow Builder adds the layer React Flow deliberately leaves to you – the part that turns a working canvas into a product. React Flow gives you a fast, flexible renderer. It does not give you a properties panel, history, or a palette, because those are application concerns. That is precisely the layer you would build by hand, and it is what Workflow Builder ships ready.&lt;/p&gt;

&lt;p&gt;The table above lists what that layer includes. What a list cannot show is why each piece is worth having ready – most of it is work that looks small and turns out not to be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A node library&lt;/strong&gt; – the built-in node types (trigger, action, conditional, decision, delay, notification, AI agent) mean you are not modeling and styling the common workflow primitives from zero. You extend or replace them instead of inventing them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A schema-driven properties panel&lt;/strong&gt; – one schema drives the form, its validation, and the node's data shape, so they stay in sync as nodes evolve instead of drifting apart across three hand-maintained places.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Undo / redo&lt;/strong&gt; – history is one of those features that looks simple and rarely is. You get a working implementation, built on a public change-tracking primitive, to keep as-is, swap for your own model, or drop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A node palette&lt;/strong&gt; – a ready drag-and-drop panel for adding nodes to the canvas, already connected to the store so dropped nodes land in the graph correctly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read-only mode&lt;/strong&gt; – locking the canvas for viewers while keeping full inspection is state you would otherwise thread through every interaction by hand.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A typed workflow document&lt;/strong&gt; – instead of persisting loose JSON and hoping it loads back correctly, the graph has one canonical, typed shape, older formats are migrated forward, and saving to localStorage, a REST endpoint, or a callback is a thin adapter on top.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;React Flow renders a canvas but leaves the surrounding UI to you. Workflow Builder ships the same component library and design system it is built on, so the editor and the controls around it share one consistent look out of the box. It is fully tokenized, with theming and white-label support, so you can override tokens to make the editor match the app it is embedded in. That is polish React Flow leaves entirely to you.&lt;/p&gt;

&lt;p&gt;The layer is &lt;strong&gt;plugin-first&lt;/strong&gt;: optional features register through a public plugin API and can be added or removed without breaking the editor. The same plugin surface that builds the reference undo / redo also lets you add toolbar buttons, inject UI into known slots, and ship your own translations.&lt;/p&gt;

&lt;p&gt;This is not theoretical. The demo app ships two full, open plugins built on the public plugin API – &lt;a href="https://github.com/synergycodes/workflowbuilder/tree/main/apps/demo/src/app/plugins" rel="noopener noreferrer"&gt;copy / paste and undo / redo&lt;/a&gt; – which prove the plugin API works and show the pattern you'd follow for your own. Heavier diagram operations – smart edge routing and node avoidance, edge reshaping, ELK auto-layout, and PDF export – are Enterprise features delivered through Overflow, and the other folders in the demo are placeholders.&lt;/p&gt;

&lt;p&gt;Some of the heavier features sit in the Enterprise Edition, and it is worth being clear about the line. The open SDK exposes layout-direction control and fit-view helpers. Production-grade &lt;strong&gt;ELK auto-layout&lt;/strong&gt;, &lt;strong&gt;smart edge routing&lt;/strong&gt;, on-canvas &lt;strong&gt;edge reshaping&lt;/strong&gt;, &lt;strong&gt;PDF export&lt;/strong&gt;, and an in-browser &lt;strong&gt;flow runner&lt;/strong&gt; are delivered through Overflow in the &lt;a href="https://www.workflowbuilder.io/blog/edge-routing-in-workflow-editors-technical-deep-dive" rel="noopener noreferrer"&gt;Enterprise Edition&lt;/a&gt;. Either way, React Flow stays the renderer underneath – these features layer on top, they do not replace it.&lt;/p&gt;

&lt;p&gt;So the trade is the opposite of the one developers fear. You keep React Flow's full range, and you add the editor features that are surprisingly hard to build well. If something is possible in React Flow, it is possible in a Workflow Builder editor – plus a great deal you would otherwise build yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  When does moving from React Flow to Workflow Builder make sense?
&lt;/h2&gt;

&lt;p&gt;A React Flow prototype that works eventually has to &lt;a href="https://www.workflowbuilder.io/blog/from-react-flow-templates-to-production-when-a-workflow-editor-becomes-a-real-product-feature" rel="noopener noreferrer"&gt;become a product&lt;/a&gt; – and that is when the properties panel, undo / redo, palette, and persistence stop being nice-to-haves. The choice at that point is whether to build the editor layer around React Flow yourself or start from one that already exists.&lt;/p&gt;

&lt;p&gt;Adopting Workflow Builder does not mean rebuilding the canvas. It does mean changing where two things live. You mount &lt;code&gt;&amp;lt;WorkflowBuilder.Root&amp;gt;&lt;/code&gt; instead of rendering &lt;code&gt;&amp;lt;ReactFlow&amp;gt;&lt;/code&gt; yourself, and graph state moves into the SDK store. Your edge components carry over unchanged. Your custom nodes are rewritten as node templates, with their data described as a schema – that is the real migration work, and it is honest to call it work. What transfers is your React Flow knowledge; what changes is where the nodes and state live.&lt;/p&gt;

&lt;p&gt;Because the SDK sits on React Flow instead of forking it, you are not locked in. React Flow stays underneath, the raw canvas stays reachable, and you keep its API and upgrade path. Backing out still means unwinding the node and state changes you made, but you are building on the library you already know, not a fork you would have to escape.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.workflowbuilder.io/" rel="noopener noreferrer"&gt;Workflow Builder&lt;/a&gt; is a production-ready React SDK for embedding visual workflow editors, built by Synergy Codes on top of React Flow. It ships as Community Edition (open source, Apache 2.0) and Enterprise Edition, and the team spent the &lt;a href="https://www.workflowbuilder.io/blog/build-vs-buy-workflow-editor-hidden-cost-react-flow" rel="noopener noreferrer"&gt;14–25 weeks of canvas work&lt;/a&gt; so you can start from the layer above. If you are weighing a React Flow workflow editor against building one from scratch, that head start is the whole point.&lt;/p&gt;

&lt;p&gt;The open-source Community Edition is a solid start on its own, and enough to ship many editors. If you are building something more complex, Synergy Codes also runs &lt;a href="https://www.workflowbuilder.io/consulting" rel="noopener noreferrer"&gt;discovery workshops&lt;/a&gt; to shape the product with your team – optional help, not a prerequisite for using the SDK.&lt;/p&gt;

</description>
      <category>react</category>
      <category>reactflow</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>How we turned our workflow editor into a real SDK</title>
      <dc:creator>Jan Librowski</dc:creator>
      <pubDate>Tue, 02 Jun 2026 15:18:42 +0000</pubDate>
      <link>https://dev.to/workflowbuilder/how-we-turned-our-workflow-editor-into-a-real-sdk-bg8</link>
      <guid>https://dev.to/workflowbuilder/how-we-turned-our-workflow-editor-into-a-real-sdk-bg8</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz87jf54ga7kpa3dfes2v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz87jf54ga7kpa3dfes2v.png" alt="Workflow Builder editor: Nodes Library on the left, a Sales Inquiry Pipeline workflow on the canvas, and the Properties panel on the right showing the selected Route by Type node" width="800" height="433"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.workflowbuilder.io/" rel="noopener noreferrer"&gt;Workflow Builder&lt;/a&gt; is a visual editor for workflow graphs in React apps. For a long time it was a strong editor that had not yet grown into an SDK. The 2.0 release closes that gap. Same canvas, palette, and properties panel as before – but now it is something you adopt as a library, not a codebase you grow your product inside.&lt;/p&gt;

&lt;p&gt;The difference between an editor and an SDK turned out not to be features. It was boundaries. This post is about the three we drew.&lt;/p&gt;

&lt;h2&gt;
  
  
  In short
&lt;/h2&gt;

&lt;p&gt;A UI component library and an SDK differ by their boundaries, not their features. Workflow Builder could draw, configure, validate, and serialize a workflow graph, but adopting it meant taking the codebase, extending it meant reaching into internals, and running it meant building your own execution. Workflow Builder 2.0 draws three contracts – for installing, extending, and running it – that make it a library you build against.&lt;/p&gt;

&lt;h2&gt;
  
  
  From editor to SDK: what adoption actually takes
&lt;/h2&gt;

&lt;p&gt;Workflow Builder began as a capable workflow-editor toolkit. Teams liked the canvas right away, then kept meeting the same three edges when they tried to ship it inside their own product.&lt;/p&gt;

&lt;p&gt;First, there was no package to install. In practice you took the codebase and grew your product inside it.&lt;/p&gt;

&lt;p&gt;Second, extending the editor meant reaching into its internals. Plugins leaned on private files and module load order, so a plugin written for one build rarely moved cleanly to another.&lt;/p&gt;

&lt;p&gt;Third, the editor could draw, configure, validate, and serialize a graph, but it could not run one. Every team rebuilt the same execution plumbing – a job runner, retries, audit logs, persistence – in whatever stack they were already on.&lt;/p&gt;

&lt;p&gt;None of these are missing features. They are missing &lt;strong&gt;boundaries&lt;/strong&gt; – the contracts a team builds against instead of building around. That is the line between an editor and an SDK.&lt;/p&gt;

&lt;h2&gt;
  
  
  What makes something an SDK, not just a library
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4lmdl2cx4sg0v7io3rk5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4lmdl2cx4sg0v7io3rk5.png" alt="Three contracts that turn an editor into an SDK" width="800" height="186"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;An SDK is defined by its contracts, not its features. Workflow Builder 2.0 draws three – one for installing it, one for extending it, one for running it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;installing it&lt;/strong&gt; – one public surface, with internals kept private;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;extending it&lt;/strong&gt; – you add behavior by composing values, not by touching internals;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;running it&lt;/strong&gt; – the editor defines what a workflow needs to run, not which engine runs it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  No forking, just install it
&lt;/h2&gt;

&lt;p&gt;Before 2.0, adopting Workflow Builder meant taking its codebase – in practice it was a rich template you forked and grew. 2.0 packages it as a real library with a single public surface: you install it, import from one entry point, and your app stays yours. Internals stay private, so you can't build on something we may change. Giving it the SDK name was part of this move to a package, not a relabel of the old setup.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// npm install @workflowbuilder/sdk&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;WorkflowBuilder&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@workflowbuilder/sdk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@workflowbuilder/sdk/style.css&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;App&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;WorkflowBuilder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Root&lt;/span&gt; &lt;span class="na"&gt;nodeTypes&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;myNodeTypes&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh9emkplrp81ejlh286aj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh9emkplrp81ejlh286aj.png" alt="Packaging boundary: from a codebase you fork to a library you install" width="726" height="316"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Extend it without touching internals
&lt;/h2&gt;

&lt;p&gt;Before, plugins ran side effects the moment they were imported and reached into internal files to do their work. That made them capable and unportable at the same time. The philosophy now is that extension is composition: you hand the editor the behavior you want, you do not patch the editor to get it.&lt;/p&gt;

&lt;p&gt;A plugin is a value. You pass an array of them to the editor, and it runs them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// A plugin is a value – a function that calls the SDK's register* APIs:&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;copyPastePlugin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;WorkflowBuilderPlugin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;registerComponentDecorator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;OptionalHooks&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CopyPasteProvider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;CopyPasteProvider&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// You compose them on the editor:&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;WorkflowBuilder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Root&lt;/span&gt; &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;copyPastePlugin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;undoRedoPlugin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;helpPlugin&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each plugin registers through documented extension points, never private internals. So a plugin written in our demo drops into your app unchanged.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run it on any engine
&lt;/h2&gt;

&lt;p&gt;The editor used to draw graphs but never execute them, so every team rebuilt execution from scratch. The fix was to make the editor define the execution contract and stay out of the engine business: submit a workflow, run a node, emit events. Any engine that fulfills the contract can run the graph.&lt;/p&gt;

&lt;p&gt;We picked &lt;a href="https://temporal.io/" rel="noopener noreferrer"&gt;&lt;strong&gt;Temporal&lt;/strong&gt;&lt;/a&gt; as the first reference engine on purpose. Temporal has the strictest execution model we know of – a V8 sandbox, determinism constraints, replay-driven recovery. If our port contract holds up against that, easier engines – an in-memory test double, a BullMQ queue, or JSON-first platforms like Inngest or Restate – plug in through the same two interfaces. We're shipping Temporal first; the rest is one adapter away. Temporal also gives us, for free, the primitives every team kept rebuilding by hand: durable retries, idempotent activities, an audit trail by construction, and cancellation as a first-class signal.&lt;/p&gt;

&lt;p&gt;The fit on the integration side was uncannily clean. Temporal's Workflow/Activity split maps one-to-one onto our pure-domain runner plus &lt;code&gt;ActivityRunnerPort&lt;/code&gt;: the deterministic graph traversal lives in the Workflow, the side-effectful node executions live in Activities. &lt;code&gt;proxyActivities&lt;/code&gt; lets us give each activity its own retry profile – short, aggressive retries for database writes; longer timeouts with fewer retries for node executions that may call LLMs. That separation falls out of Temporal's model; we did not engineer around it.&lt;/p&gt;

&lt;p&gt;The graph runner itself is just a function. It takes the workflow plus your engine's two runtime hooks – a node runner and an event sink – and nothing else:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// What an engine has to fulfill – two small interfaces:&lt;/span&gt;
&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Runner&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;executeNode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;NodeResult&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Events&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;emitEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;?):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;updateStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// The graph runner: a function taking the workflow and those two hooks:&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;runGraph&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;runner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;events&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Temporal adapter is what those hooks look like wired to Temporal primitives:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// apps/execution-worker/src/engines/temporal/workflows/run-workflow.ts (abridged)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;databaseActivities&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;proxyActivities&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;Pick&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Activities&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;emitEvent&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;updateStatus&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;startToCloseTimeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;30s&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;retry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;maximumAttempts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nodeActivities&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;proxyActivities&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;Pick&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Activities&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;executeNode&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;startToCloseTimeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;10m&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;retry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;maximumAttempts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;runner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ActivityRunnerPort&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;AiStudioNode&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;executeNode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;nodeActivities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;executeNode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;events&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;EventEmitterPort&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;emitEvent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;executionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;nodeId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="nx"&gt;databaseActivities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;emitEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;executionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;nodeId&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;updateStatus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;executionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;errorMessage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="nx"&gt;databaseActivities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;updateStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;executionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;errorMessage&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;runWorkflow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;WorkflowExecutionInput&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;AiStudioNode&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;runGraph&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;runner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;events&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Swap Temporal for Inngest, Restate, or an in-memory test double by implementing those two interfaces against that engine's primitives. The editor does not change. We picked Temporal first as the hardest test on purpose – the simpler engines have less to ask of the contract, not more.&lt;/p&gt;

&lt;h2&gt;
  
  
  We proved it by building on it: AI Studio
&lt;/h2&gt;

&lt;p&gt;The most reliable test of a contract is building something real on it. AI Studio is a full reference app – a workflow palette, real execution against the backend, and a live log that highlights nodes as they run. The AI Agent nodes call an LLM through OpenRouter, so set &lt;code&gt;OPENROUTER_API_KEY&lt;/code&gt; in &lt;code&gt;apps/execution-worker/.env&lt;/code&gt; before running a workflow (full setup in 'How to try it' below).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9jgzaewj8venlvs0jfw6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9jgzaewj8venlvs0jfw6.png" alt="AI Studio running: the executed path (Classify &amp;amp; Extract → Route by Type → Pricing Specialist → Final QA Check) highlighted green; Execution Log panel on the right shows node events and the Pricing Specialist's drafted response" width="800" height="433"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It rides the public SDK surface and the shared runtime-contract types – no reaching into editor internals. It runs the graph through the runtime contract, adds its execution UI through documented extension points, and talks to the backend through its own adapter. Nothing reaches around a boundary. That is the proof we wanted: a non-trivial product is buildable on the contract, not on our internals.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to try it
&lt;/h2&gt;

&lt;p&gt;Source at &lt;a href="https://github.com/synergycodes/workflowbuilder" rel="noopener noreferrer"&gt;github.com/synergycodes/workflowbuilder&lt;/a&gt;.&lt;br&gt;
Docs at &lt;a href="https://www.workflowbuilder.io/docs/overview/" rel="noopener noreferrer"&gt;workflowbuilder.io/docs&lt;/a&gt;.&lt;br&gt;
The AI Agent nodes call an LLM through OpenRouter, so set a key before the stack runs a workflow end to end:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/synergycodes/workflowbuilder
&lt;span class="nb"&gt;cd &lt;/span&gt;workflowbuilder
pnpm &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;span class="c"&gt;# copy apps/execution-worker/.env.example to apps/execution-worker/.env and set OPENROUTER_API_KEY&lt;/span&gt;
pnpm preflight   &lt;span class="c"&gt;# checks Node / pnpm / Docker / ports / .env before the stack starts&lt;/span&gt;
pnpm dev:ai-studio
&lt;span class="c"&gt;# open http://localhost:4201&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is Workflow Builder?
&lt;/h3&gt;

&lt;p&gt;Workflow Builder is a React SDK for embedding visual workflow editors, built by &lt;a href="https://www.synergycodes.com/" rel="noopener noreferrer"&gt;Synergy Codes&lt;/a&gt;. It ships as an open-source Community Edition and a commercial Enterprise Edition with full source included.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I swap the execution engine?
&lt;/h3&gt;

&lt;p&gt;Yes. The editor defines a runtime contract – submit a workflow, run a node, emit events – and stays out of the engine business. Temporal is the reference engine; implement the same contract to run on another.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;We shipped the SDK before the platform on purpose. The boundary is the product: three contracts you can build against. There is still a lot to do – more reference adapters, more documented extension points, more sharp edges to find – and we are actively iterating. We would rather learn from what you build on the contracts than guess at the features behind them.&lt;br&gt;
If you build something, share it with us or let us know what broke – &lt;a href="https://github.com/synergycodes/workflowbuilder/issues" rel="noopener noreferrer"&gt;open an issue&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>react</category>
      <category>typescript</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I built a React circuit editor in a day with Workflow Builder</title>
      <dc:creator>Kacper Cierzniewski</dc:creator>
      <pubDate>Mon, 01 Jun 2026 14:00:00 +0000</pubDate>
      <link>https://dev.to/workflowbuilder/i-built-a-react-circuit-editor-in-a-day-with-workflow-builder-12a0</link>
      <guid>https://dev.to/workflowbuilder/i-built-a-react-circuit-editor-in-a-day-with-workflow-builder-12a0</guid>
      <description>&lt;h2&gt;
  
  
  In short
&lt;/h2&gt;

&lt;p&gt;Workflow Builder is generic enough to build non-workflow tools. In one day I built an interactive electrical-circuit editor with custom Battery, Switch, Lightbulb and Resistor nodes, a sub-200-line plugin that solves Ohm's law on every diagram change, and an LED-vs-incandescent cost comparison showing ~7× lower running cost for LEDs at the same brightness.&lt;/p&gt;

&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;p&gt;As a frontend engineer at Synergy Codes I usually work on projects built from scratch. In my latest project we spent a lot of time building a custom diagram tool for electrical circuits, and I wanted to see how far I could get with Workflow Builder instead, in roughly one day of work.&lt;/p&gt;

&lt;p&gt;The end result is a small interactive circuit editor with batteries, switches, resistors, lightbulbs, meters and an energy-cost calculator that compares LED and incandescent bulbs. This is the writeup of that day.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading the docs first
&lt;/h2&gt;

&lt;p&gt;Before opening the editor I went through the documentation. Two things stood out.&lt;/p&gt;

&lt;p&gt;First, there is a lot of it. Not only an API reference (which I would expect), but a proper introduction, an architecture overview, and a set of guides covering the things you actually need: custom nodes, plugins, persistence, JsonForms control. For a relatively young SDK that was a pleasant surprise.&lt;/p&gt;

&lt;p&gt;Second, the docs are very explicit that Workflow Builder is the editor layer and execution is up to you. The framing matters, because it sets expectations: the SDK is going to give you the hooks rather than the answers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Genericity is the keyword
&lt;/h3&gt;

&lt;p&gt;The thing that makes Workflow Builder usable for something like a circuit editor is that almost nothing about it is workflow-specific.&lt;/p&gt;

&lt;h3&gt;
  
  
  Building custom nodes
&lt;/h3&gt;

&lt;p&gt;Each node is described by a JSON Schema and a UI schema. There is a long list of building blocks already in the SDK (text inputs, labels, dropdowns, switches, accordions), and you arrange them in a vertical layout that drives the properties panel. The pattern is 4 small files per node, documented in &lt;a href="https://www.workflowbuilder.io/docs/guides/add-a-custom-node/" rel="noopener noreferrer"&gt;Add a custom node&lt;/a&gt;. I will show what that looks like further down.&lt;/p&gt;

&lt;h3&gt;
  
  
  Embedding it as a React component
&lt;/h3&gt;

&lt;p&gt;Workflow Builder ships as a React component on npm (&lt;code&gt;@workflowbuilder/sdk&lt;/code&gt;), so dropping it into an existing app is a one-liner. You install the package, import &lt;code&gt;WorkflowBuilder&lt;/code&gt; and its stylesheet, and mount &lt;code&gt;&amp;lt;WorkflowBuilder.Root /&amp;gt;&lt;/code&gt; anywhere in your tree. All configuration – node types, plugins, persistence strategy, layout direction – is passed as props on that component, so the editor stays declarative and there is no separate factory or builder step to keep in sync.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;WorkflowBuilder&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@workflowbuilder/sdk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@workflowbuilder/sdk/style.css&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;demoPaletteItems&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./features/workflowbuilder/palette&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;plugin&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;circuitSolverPlugin&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./features/workflowbuilder/plugins/circuit-solver/plugin-exports&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;plugin&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;circuitReadoutsPlugin&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./features/workflowbuilder/plugins/circuit-readouts/plugin-exports&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;App&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;WorkflowBuilder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Root&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Bulb Comparison"&lt;/span&gt;
      &lt;span class="na"&gt;layoutDirection&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"RIGHT"&lt;/span&gt;
      &lt;span class="na"&gt;nodeTypes&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;demoPaletteItems&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;integration&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;localStorage&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;circuitSolverPlugin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;circuitReadoutsPlugin&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the entire &lt;code&gt;App.tsx&lt;/code&gt; for the circuit editor. Everything else in this writeup lives in the &lt;code&gt;nodeTypes&lt;/code&gt; and &lt;code&gt;plugins&lt;/code&gt; arrays.&lt;/p&gt;

&lt;h3&gt;
  
  
  Different built-in ways of data storing
&lt;/h3&gt;

&lt;p&gt;Another thing that surprised me. Workflow Builder does not force any specific way of saving the diagram. There are 3 persistence strategies, picked via the &lt;code&gt;integration&lt;/code&gt; prop on &lt;code&gt;&amp;lt;WorkflowBuilder.Root /&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The first one is &lt;strong&gt;localStorage&lt;/strong&gt;. This is the default, so if you do nothing you already have it. The SDK reads the diagram from &lt;code&gt;localStorage&lt;/code&gt; when the editor mounts and writes it back when the user clicks Save (or when they close the tab, thanks to a &lt;code&gt;beforeunload&lt;/code&gt; hook). Zero configuration, no backend, everything stays in the browser. Limitations are the obvious ones: data is per-browser and per-origin, capped around 5 MB.&lt;/p&gt;

&lt;p&gt;The second one is &lt;strong&gt;REST API&lt;/strong&gt;. You give the SDK 2 endpoints (one to &lt;code&gt;GET&lt;/code&gt; the initial diagram from, one to &lt;code&gt;POST&lt;/code&gt; the saved diagram to) and it handles the wiring for you.&lt;/p&gt;

&lt;p&gt;The third one is &lt;strong&gt;callback (&lt;code&gt;props&lt;/code&gt; strategy)&lt;/strong&gt;. You pass an &lt;code&gt;onDataSave&lt;/code&gt; function and the SDK calls it whenever the diagram needs to persist. This is the escape hatch: if you need auth headers, custom error handling, file uploads, or any non-standard shape, this is the option that gives you full control.&lt;/p&gt;

&lt;p&gt;For my one-day experiment I stayed with &lt;code&gt;localStorage&lt;/code&gt;, but it is nice to know I will not have to rewrite anything when a real backend comes into the picture.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let's build something
&lt;/h2&gt;

&lt;p&gt;The starting point is a plain Vite + React app with &lt;code&gt;@workflowbuilder/sdk&lt;/code&gt; installed and the snippet above as &lt;code&gt;App.tsx&lt;/code&gt;. Standard commands:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pnpm i&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pnpm dev&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And the editor is up with the default (empty) diagram.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F01wod576a9ir2i05cbzo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F01wod576a9ir2i05cbzo.png" alt="Default Workflow Builder editor on first start" width="800" height="467"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Tinker with the code
&lt;/h3&gt;

&lt;p&gt;The first thing I wanted to modify is the node library. Based on the docs section on Node schemas, it looks like I just need to add a few schema files. Under &lt;code&gt;src/feWorkflow Builder is great as a visualization tool and very generic, so iatures/workflowbuilder/nodes/&lt;/code&gt; I added the nodes I needed for a simple circuit: Battery, Switch, Lightbulb, Ammeter, Voltmeter.&lt;/p&gt;

&lt;h4&gt;
  
  
  How I did it: the Battery example
&lt;/h4&gt;

&lt;p&gt;For each node I created a folder with 4 files, following the canonical &lt;code&gt;action&lt;/code&gt; example in the demo app. For Battery it looks like this.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;battery/schema.ts&lt;/code&gt; describes the data the node holds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;sharedProperties&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;statusOptions&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@workflowbuilder/sdk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NodeSchema&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@workflowbuilder/sdk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;sharedProperties&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;statusOptions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;voltage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;number&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;minimum&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nx"&gt;satisfies&lt;/span&gt; &lt;span class="nx"&gt;NodeSchema&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;BatteryNodeSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;sharedProperties&lt;/code&gt; spread gives me the standard &lt;code&gt;label&lt;/code&gt; and &lt;code&gt;description&lt;/code&gt; fields every node has, so I do not have to redeclare them.&lt;br&gt;
Workflow Builder is great as a visualization tool and very generic, so i&lt;br&gt;
&lt;code&gt;battery/uischema.ts&lt;/code&gt; tells the SDK how to render the properties panel, in this case just a Text input for the voltage. &lt;code&gt;battery/default-properties-data.ts&lt;/code&gt; holds the default values when the user drags a new Battery onto the canvas (I went with 9 V).&lt;/p&gt;

&lt;p&gt;And finally &lt;code&gt;battery/battery.ts&lt;/code&gt; ties everything together into a &lt;code&gt;PaletteItem&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;PaletteItem&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@workflowbuilder/sdk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;defaultPropertiesData&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./default-properties-data&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;BatteryNodeSchema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;schema&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./schema&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;uischema&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./uischema&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;battery&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PaletteItem&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;BatteryNodeSchema&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;battery&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;icon&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;BatteryFull&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Battery&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Power source providing voltage to the circuit.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;defaultPropertiesData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;uischema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The icon name (&lt;code&gt;BatteryFull&lt;/code&gt;) is one of many ready-made icons exposed by the SDK – you reference them by string, no extra import needed. The last step is registering the node in &lt;code&gt;src/features/workflowbuilder/palette.ts&lt;/code&gt; so it shows up in the left sidebar. That is the whole node. The other 3 (Switch, Lightbulb, Ammeter) follow exactly the same pattern.&lt;/p&gt;

&lt;h4&gt;
  
  
  Build the schema
&lt;/h4&gt;

&lt;p&gt;Now I can see all the defined nodes in the palette:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxzpdmjst5n0h668msdjv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxzpdmjst5n0h668msdjv.png" alt="Editor palette with the custom electrical nodes" width="409" height="977"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I can drag and drop them onto the canvas and try to build something:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6ura4cbzwd64b9ug0t3g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6ura4cbzwd64b9ug0t3g.png" alt="Simple circuit assembled from the custom nodes" width="800" height="374"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It is not interactive yet – this is only a visual presentation – but to this point it took me roughly 2 hours, including reading docs and writing this article, to get a working visualization. As I still had a lot of spare time, I figured I could try to make it more interactive. For example, when I turn off the switch, there should be no current and the bulb should stop glowing. The plugin API is well documented, so this should be doable.&lt;/p&gt;

&lt;h3&gt;
  
  
  The idea is crystallizing
&lt;/h3&gt;

&lt;p&gt;I started with a simple lightbulb schema, but it looks like we can do a lot more. It should be possible to add some real logic and compare an LED bulb to a traditional one in terms of efficiency and cost. That is what I want to build.&lt;/p&gt;

&lt;h4&gt;
  
  
  Small adjustments
&lt;/h4&gt;

&lt;p&gt;To make this work I added a few missing nodes. A Wattmeter to read the power going through a load. An Efficiency property on the Lightbulb (lumens per watt) so I can describe LED vs incandescent behavior. A Resistor so the circuit's total resistance is more than just the bulbs. And an Energy Stats node to show how much power and how much money the circuit costs over time.&lt;/p&gt;

&lt;h4&gt;
  
  
  Let's implement it
&lt;/h4&gt;

&lt;p&gt;To make the circuit actually react to changes I wrote a small plugin called &lt;code&gt;circuit-solver&lt;/code&gt;. The pattern is exactly what the docs describe. I registered a hook via the SDK plugin API (the &lt;code&gt;OptionalHooks&lt;/code&gt; slot) that runs every time any node or edge in the diagram changes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;registerComponentDecorator&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@workflowbuilder/sdk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;CircuitSolverRunner&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./circuit-solver-runner&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;plugin&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;registerComponentDecorator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;OptionalHooks&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CircuitSolverRunner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;CircuitSolver&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The runner reads &lt;code&gt;nodes&lt;/code&gt; and &lt;code&gt;edges&lt;/code&gt; from the SDK store via &lt;code&gt;useStore&lt;/code&gt; and runs a pure solver function. The solver walks the diagram starting from every Battery node, follows the outgoing edges around the loop, sums the resistances of every Resistor and Lightbulb it passes through (Lightbulb resistance is derived from &lt;code&gt;V_nominal² / P_nominal&lt;/code&gt;), applies Ohm's law (&lt;code&gt;I = V / R&lt;/code&gt;), and writes the resulting voltage, current and power back for every node in the loop. A Switch in the loop with &lt;code&gt;isOn === false&lt;/code&gt; drops the current to zero everywhere.&lt;/p&gt;

&lt;p&gt;The computed values land in a separate Zustand store. The readout plugin (which is the thing that paints the values directly on each node) just subscribes to that store. The solver stays pure, the readout stays React, and the two never collide. The whole solver fits in one file of around 250 lines.&lt;/p&gt;

&lt;p&gt;One simplification: this only handles series circuits, one loop per battery, no branches. Parallel circuits and proper mesh analysis need Kirchhoff's laws, which is a much bigger task and not what I want to spend my one day on.&lt;/p&gt;

&lt;p&gt;Now I need to draw the whole circuit:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frp2gk1sj2dkm4mok3m36.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frp2gk1sj2dkm4mok3m36.png" alt="Circuit diagram with the switch in the OFF position" width="800" height="526"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  It's alive!
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdegxwbl7gebvx7jg6xem.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdegxwbl7gebvx7jg6xem.png" alt="Same circuit after turning the switch on" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When I turned the switch on, the logic from the plugin kicked in for all the new nodes and it looks like it is working correctly. The bulbs are shining and the Resistor value is taken into account. When I increase the resistance, the bulbs are barely glowing and the Energy Stats reacts:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffbvl2em07aqxmz0bikx7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffbvl2em07aqxmz0bikx7.png" alt="Higher resistance, bulbs barely glow, Energy Stats drops" width="800" height="526"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Compare LED vs traditional bulb
&lt;/h3&gt;

&lt;p&gt;To make a proper comparison I need two separate circuits and then I can check the Energy Stats node on each. Everything is customizable here, so you can easily adjust the electricity cost in your area and how many hours per day each bulb is on.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0abka1mdm53ulq710awd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0abka1mdm53ulq710awd.png" alt="Two separate circuits, incandescent on the left, LED on the right" width="800" height="461"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One small thing about the Energy Stats node. When I first built it, the cost was just a number with no currency unit, which on a diagram looks a bit weird. I went back and added a &lt;code&gt;currency&lt;/code&gt; text field that defaults to &lt;code&gt;$&lt;/code&gt; but accepts anything: &lt;code&gt;PLN&lt;/code&gt;, &lt;code&gt;€&lt;/code&gt;, &lt;code&gt;zł&lt;/code&gt;, whatever fits. It is a tiny change, but a nice example of how cheap adding a property is in this architecture. One new entry in the schema, one in the UI schema, one default value, and the readout picks it up.&lt;/p&gt;

&lt;h4&gt;
  
  
  Cost comparison
&lt;/h4&gt;

&lt;p&gt;To compare costs properly I tuned the bulbs' nominal power so both produce the same brightness in lumens. To keep it realistic I aimed for 1000 lm, which is a moderate amount of light for a small room.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fetx1c1tc9ir4edmu1dby.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fetx1c1tc9ir4edmu1dby.png" alt="Cost comparison: incandescent vs LED" width="800" height="346"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The focus now is on the Energy Stats nodes. About &lt;code&gt;$20&lt;/code&gt; per year for the traditional bulb running 5 hours a day, and only about &lt;code&gt;$3&lt;/code&gt; for the LED. That is roughly 7 times less cost for the same brightness. And that is one bulb.&lt;/p&gt;

&lt;h4&gt;
  
  
  Efficiency comparison
&lt;/h4&gt;

&lt;p&gt;I already added the logic that makes a bulb dimmer when it runs below its rated voltage, so I can also check how efficient the LED is compared to the traditional bulb at the same power consumption.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffp24g2xtcx2het4ntnvr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffp24g2xtcx2het4ntnvr.png" alt="Same power, different efficiency, LED visibly brighter" width="800" height="548"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Of course this is just a representation, real bulbs do not literally glow at these exact intensities, but Workflow Builder is flexible enough that I can adjust it for my needs. The point comes across: the LED produces much more visible light for the same wattage, while the Energy Stats confirms identical consumption.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Overall, a great experience
&lt;/h3&gt;

&lt;p&gt;The architecture is generic enough to build well outside the workflow domain – this article is one example. An electrical circuit editor is about as far from a "workflow" as you can get, and I did not have to fight the SDK at any point. At the same time the genericity does not come at the cost of structure. There are clear patterns to follow (the 4-file node, the plugin decorators), so you are not dropped into a freeform mess. Without too much effort I was able to create proper node schemas to represent my needs.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The plugin API let me add domain-specific logic without touching the app core, which is a sign of good architecture. With more time I can imagine designing more nodes for other electrical devices and building a full home circuit that calculates how much power it draws.&lt;/li&gt;
&lt;li&gt;Workflow Builder ships with a design system, including dark mode out of the box:&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fagp2ealv6g7fmo5q3mh1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fagp2ealv6g7fmo5q3mh1.png" alt="The circuit editor in dark mode" width="800" height="456"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Idea for improvement
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;The default node template gives you one input on the left and one output on the right, which fits a workflow but not a symmetric domain like circuits, where a battery has + and -, a switch has two poles, and a resistor has two terminals. In this writeup I worked around it by placing nodes carefully on the canvas. The SDK has a proper way to do this: &lt;code&gt;defineNodeTemplate&lt;/code&gt; lets you author a custom React component for a node type and declare whatever port topology you need, registered through the &lt;code&gt;nodeTemplates&lt;/code&gt; prop on &lt;code&gt;&amp;lt;WorkflowBuilder.Root /&amp;gt;&lt;/code&gt;. A natural next step for this project would be a dedicated two-terminal template for the passive components so the wiring matches real schematics instead of relying on layout tricks.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Is it possible to check the source code?
&lt;/h3&gt;

&lt;p&gt;Yes, here are the links:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/kacpercierzniewski/electric-schema-workflowbuilder/" rel="noopener noreferrer"&gt;Repository of the project&lt;/a&gt;,&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/synergycodes/workflowbuilder/" rel="noopener noreferrer"&gt;Workflow Builder&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Can Workflow Builder be used for non-workflow domains?
&lt;/h3&gt;

&lt;p&gt;Yes. The SDK is generic – nodes are described by JSON Schema and a UI schema, and plugins can react to any diagram change. The only constraint I hit was port topology (one input on the left, one output on the right), which is limiting for symmetric domains like electrical circuits.&lt;/p&gt;

&lt;h3&gt;
  
  
  How long does it take to add a new node type?
&lt;/h3&gt;

&lt;p&gt;About 15-20 minutes per node once you know the pattern. Each node is 4 files (data schema, UI schema, default-properties, PaletteItem) plus 1 line in &lt;code&gt;palette.ts&lt;/code&gt; to register it.&lt;/p&gt;

&lt;h3&gt;
  
  
  How does the plugin API work?
&lt;/h3&gt;

&lt;p&gt;Plugins register decorators through &lt;code&gt;registerComponentDecorator(slot, { content, name })&lt;/code&gt;. The slot tells the SDK where to mount your component – &lt;code&gt;OptionalHooks&lt;/code&gt; runs every time nodes or edges change. From there you read the SDK store with &lt;code&gt;useStore&lt;/code&gt; and write derived data back into your own Zustand store. The circuit-solver in this article fits in around 250 lines this way.&lt;/p&gt;

&lt;h3&gt;
  
  
  Which persistence strategies does Workflow Builder support?
&lt;/h3&gt;

&lt;p&gt;3 strategies, picked via the &lt;code&gt;integration&lt;/code&gt; prop on &lt;code&gt;&amp;lt;WorkflowBuilder.Root /&amp;gt;&lt;/code&gt;: &lt;code&gt;localStorage&lt;/code&gt; (default, ~5 MB cap), REST API (you supply a GET and a POST endpoint), and &lt;code&gt;props&lt;/code&gt; callback (you pass &lt;code&gt;onDataSave&lt;/code&gt; and handle everything yourself).&lt;/p&gt;

&lt;h3&gt;
  
  
  Can Synergy Codes build a domain-specific editor like this?
&lt;/h3&gt;

&lt;p&gt;Yes. Synergy Codes specializes in diagramming interfaces, workflow editors, and visualization tools, and has delivered 170+ custom editors for clients including Siemens, BMW, and Canon. Workflow Builder, ngDiagram, and Overflow are the open-source products we use as starting points – see &lt;a href="https://www.synergycodes.com/" rel="noopener noreferrer"&gt;synergycodes.com&lt;/a&gt; for case studies.&lt;/p&gt;

&lt;h2&gt;
  
  
  See also
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.workflowbuilder.io/docs/guides/add-a-custom-node/" rel="noopener noreferrer"&gt;Add a custom node&lt;/a&gt;, the four-file pattern this article leans on&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.workflowbuilder.io/docs/guides/build-a-plugin/" rel="noopener noreferrer"&gt;Build a plugin&lt;/a&gt;, the registration API used by the circuit-solver plugin&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.workflowbuilder.io/docs/guides/configuring-the-editor/" rel="noopener noreferrer"&gt;Configuring the editor&lt;/a&gt;, where &lt;code&gt;nodeTypes&lt;/code&gt;, &lt;code&gt;plugins&lt;/code&gt; and &lt;code&gt;integration&lt;/code&gt; live&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>javascript</category>
      <category>react</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
