ArchitectureHigh Level Overview

Architecture

High Level Overview

Get a high level understanding of how Workflow Kit is architected.


High Level Overview

Workflow Kit is built around a Model View Controller architecture, where the workflow state, its visual representation, and the logic that connects them are separated into distinct layers. The diagram below illustrates how its core types and functions interact.

Architecture diagram showing the MVC data flow in Workflow Kit. The Workflow model (top-left) transforms into Flows via workflowView() (top-left to bottom-left). User interactions on Flows produce FlowsChange[] via onFlowsChange() (bottom-left to bottom-right). FlowsChange[] is converted to WorkflowChange[] via toWorkflowChanges() (bottom-right to top-right). Finally, WorkflowChange[] is applied back to Workflow via applyWorkflowChanges() (top-right to top-left), completing the cycle.

Types:

  • Workflow: The current workflow state (Model).
  • Flows: The visual representation of the workflow (View).
  • FlowsChange: An event captured from the visual representation (select, drag, or drop).
  • WorkflowChange: A change that occurred in the workflow.

Functions:

  • workflowView: Transforms a Workflow into its visual Flows representation.
  • onFlowsChange: Triggered whenever the user interacts with the Flows by selecting, dragging, or dropping a node, producing FlowsChange objects.
  • toWorkflowChanges: Converts FlowsChange[] into WorkflowChange[], acting as the Controller that bridges the View and the Model.
  • applyWorkflowChanges: Applies the provided WorkflowChange[] against the current workflow to produce an updated Workflow.

Where It All Starts

We can take a look at how this architecture is implemented by going at the following file.

// src/app/page.tsx

// ...

const schema: WorkflowSchema = {
  start: { message: "" },
  elements: [],
  end: { message: "" },
};

export default function Home() {
  const [state, dispatch] = useWorkflow(schema);

  const onWorkflowChange = useCallback(
    (changes: WorkflowChange[]) => {
      dispatch({ type: "applyWorkflowChanges", changes });
    },
    [dispatch],
  );

  return (
    <div className="flex h-screen flex-col bg-neutral-900">
      <Topbar state={state} dispatch={dispatch} />
      <div className="relative grow">
        <WorkflowView
          workflow={state.history.current}
          center={state.center}
          onWorkflowChange={onWorkflowChange}
        />
      </div>
    </div>
  );
}

// ...

The useWorkflow reducer returns a state that is passed to WorkflowView. Whenever the workflow needs to update, onWorkflowChange is called to apply the changes.

The Workflow's Visual Interface

The code of the WorkflowView component can be found in the following file.

// src/shared/kits/workflow/components/workflow-view.tsx

// ...

interface WorkflowViewProps {
  workflow: Workflow;
  center: string | null;
  onWorkflowChange: OnWorkflowChange;
}

export function WorkflowView({
  workflow,
  center,
  onWorkflowChange,
}: WorkflowViewProps) {
  const flows = useMemo(
    () => workflowView(workflow, onWorkflowChange),
    [workflow, onWorkflowChange],
  );

  const onFlowsChange = useCallback(
    (changes: TypedFlowsChange[]) => {
      const array = toWorkflowChanges(changes, workflow, flows);
      if (array.length) onWorkflowChange(array);
    },
    [onWorkflowChange, workflow, flows],
  );

  useCenter(flows, center);

  return (
    <>
      <FlowsView
        flows={flows}
        onFlowsChange={onFlowsChange}
        nodeTypes={nodeTypes}
        edgeTypes={edgeTypes}
      >
        <Controls />
        <MiniMap />
      </FlowsView>
      {widgetView(workflow, onWorkflowChange)}
    </>
  );
}

// ...

The workflow prop is the model, holding the state; workflowView serves as the view, transforming that state into the visual representation; and toWorkflowChanges functions as the controller, mapping user interactions into updates to the workflow.

React Flow Abstraction Layer

As you can see, instead of using React Flow directly, WorkflowView relies on an abstraction layer that is located in src/shared/lib/flows that is more suited to our use case.

You can see how this abstraction layer is using React Flow by going to the following file.

// src/shared/lib/flows/components/flows-view/index.tsx

// ...

export function FlowsView<T extends NodeEntity, U extends EdgeEntity>({
  flows,
  onFlowsChange,
  nodeTypes,
  edgeTypes,
  children,
  ...props
}: FlowsViewProps<T, U>) {
  // ...

  return (
    <ReactFlow
      nodes={nodes}
      edges={edges}
      onNodesChange={onNodesChange}
      onEdgesChange={onEdgesChange}
      nodeTypes={nodeTypes}
      edgeTypes={edgeTypes}
      fitView={true}
      maxZoom={constants.flows.config.maxZoom}
      minZoom={constants.flows.config.minZoom}
      panOnScroll={true}
      selectionKeyCode={null}
      multiSelectionKeyCode={null}
      nodesConnectable={false}
      proOptions={{
        hideAttribution: true,
      }}
      {...props}
    >
      {children}
      <Background
        color="var(--color-neutral-700)"
        variant={BackgroundVariant.Dots}
      />
    </ReactFlow>
  );
}