Most node-based libraries ship with an opinion about your data model. You hand them a nodes[] array and an edges[] array, and in return you get a rendered graph. That works fine in a demo.
It starts to break the moment your app grows up.
Your real domain model is not { id, x, y }. It is a workflow step, a call routing rule, an AI agent with a schema, a piece of business logic with validations attached.
Persistence is not "serialize the library's internal state to JSON". It is writing to your backend, your database, your existing document format.
In the default stateless setup, undo/redo is not the library's job. It is a feature of your editor, tied to your command history.
Optimistic updates, collaboration, offline sync ā all of that lives in your app, not in a graph library.
When a library owns the data, every one of those concerns has to go through an adapter. You spend more time translating between the library's model and yours than you spend building the actual editor.
Foblex Flow's core is stateless by default.
The library does not hold the definition of your graph. It renders what you pass in as Angular template children, listens for user interaction on those children, and emits events when something happens. Your application decides whether to apply the change.
That is the whole model.
This is not just a design preference. It is the decision that shaped every API that came after.
The default contract above remains unchanged. If an application wants the library to handle common graph bookkeeping, it can explicitly install the optional Managed Flow State plugin:
withFlowState() provides typed node, group, connection, selection and viewport signals, plus load(), snapshot() and batched undo/redo. Supported finished gestures update that opt-in store automatically. The same public events still fire, records keep the application's own fields, and the application can override or replace the default mutation behavior.
So "stateless" describes the core and the default integration mode. Managed state is an explicit provider installed by applications that prefer convenience over wiring every supported gesture themselves; it is not a hidden store imposed on every flow.
Most graph libraries keep a copy of your nodes and edges inside themselves. The library becomes the source of truth. Your app mirrors it.
Foblex Flow takes a different approach.
What the core stores in the default mode:
<f-node> and <f-connection> instances currently in the templateWhat the app stores in the stateless mode:
š In short: the library handles the UI layer, while your application owns the business logic.
The managed-state plugin can own graph records and interaction history, but it never owns what those records mean, how they are validated, or where they are persisted.
The golden rule of the stateless core: the library never mutates your application-owned data silently.
Every user interaction that would change the graph is surfaced as an event. In this mode, the library does not write back into your signals or services. It waits.
Concrete events on the fDraggable directive:
Node position, size, and rotation follow the same pattern. fNodePosition is a two-way binding, but the source of truth is still your signal. The library reports the new value at the end of a gesture. Writing it back is your call.
Rendering lifecycle is also surfaced as events on the
This happened. You decide what to do.
That one sentence is the whole interaction contract.
Installing withFlowState() is an explicit variation of that contract: supported gestures are applied to the provided store automatically, while their public events remain observable.
This is where the shape of the API diverges from what people expect the first time they try the library.
People usually reach for something like setState(ā¦) or loadFromJSON(ā¦). A method on the component that takes the full graph and renders it. That is the reducer-for-graphs mental model.
The core component does not work that way.
ā How people expect the API to work:
ā How the library actually works:
In the default mode there is no setState and no hidden graph store. The template is the graph ā driven by your signals, rendered by Angular, with the library wiring up interactivity on top.
Initialization works the same way in stateless mode. You hydrate your own signals from your backend or file, Angular renders the template, the library picks up the <f-node> and <f-connection> children and makes them interactive. When rendering settles, fFullRendered fires ā that is the place to call getState() for measured bounds and run "fit to content".
With Managed Flow State, initialization becomes state.load(...), the template renders state.nodes(), state.groups() and state.connections(), and state.snapshot() returns persistable graph records. The application still decides when to load and save them.
It is more accurate to say the Foblex Flow core is an interaction layer than a graph library. The graph is yours unless you explicitly place its generic records in Managed Flow State.
Every consequence of "stateless core" looks, on the surface, like a missing feature. None of them are.
Persistence is your job. The stateless core does not ship a toJSON() that serializes your graph, because it does not have your graph. Managed Flow State offers snapshot() for its opt-in records, but your application still decides the domain shape, storage format and persistence lifecycle.
Undo/redo belongs to the state owner. In stateless mode, that is your application's command history: it knows that adding a node, editing a label or changing a validation rule are meaningful actions. Managed Flow State provides a generic history stack for supported graph gestures and mutations when that is enough. Domain actions outside the plugin remain the application's responsibility.
Optimistic updates are your job. In stateless mode, write to your signal, let the view update, and reconcile later if the server disagrees. With Managed Flow State, reconcile against the explicit plugin store instead; it is still application-scoped and inspectable rather than hidden runtime data.
Collaboration is your job. Two users dragging the same node is a merge problem in your domain, not a rendering problem. The library hands you the gesture; your CRDT or your OT layer decides what it means, then writes to your state, and the view follows.
That sounds restrictive in a feature matrix, but it matters in real editors. The apps I have seen built on Foblex Flow all have non-trivial domain models ā call routing, AI agent graphs, workflow automation, ETL pipelines. In every one of them, the data model existed before the editor did. A library that insisted on owning it would have been a wall, not a tool.
A node editor library can do one of two things. It can be a platform that owns the graph and hands you hooks into it. Or it can be an interaction layer that renders what you already have.
Foblex Flow's core chose the second one on purpose. The library is not a small application you embed. It is a set of Angular primitives ā <f-flow>, <f-canvas>, fNode, <f-connection> ā that turn your existing data into something a user can drag, connect, and edit. Managed Flow State is an optional companion for applications that want those primitives plus a ready-made graph store.
In stateless mode, every event the library emits is an offer. This happened. You decide what to do with the change. Accept it by writing to your state and the view updates. Ignore it and nothing changes. Veto it by not writing, and the user's gesture was advisory. With Managed Flow State, supported gestures use the plugin's overridable handlers instead of waiting for application writeback.
For me, this is the point. An editor is a conversation between a user's intent and an application's rules. The stateless core reports gestures and waits. The managed-state plugin applies only the supported behavior an application explicitly opted into and exposes override points where its rules differ.
Small, but it shapes everything downstream. The stateless default is why persistence, collaboration and domain-specific history can stay in your app instead of fighting a hidden store. Managed Flow State is there when you deliberately want the library to own common graph records and interaction history without changing that default for everyone else.
This is Part 3 of the Inside Foblex Flow series.
Part 4 will look at the rendering pipeline: how <f-node> and <f-connection> elements are picked up from the template, how measurement and layout are coordinated, and how fNodesRendered and fFullRendered actually get decided under the hood.
If you're building a visual editor in Angular and want a native Angular solution (not a React wrapper) ā take a look.
And if you like what I'm building, please consider starring the repo ā
It helps the project a lot.
<f-flow> component: flow.foblex.com/docs/f-flow-componentfNode directive: flow.foblex.com/docs/f-node-directive<f-connection> component: flow.foblex.com/docs/f-connection-component