Skip to content

Case study / B2B SaaS platform / Part 1 of 2

Separating SaaS workflow templates from customer state

Short answerReusable steps can serve different services when customer documents and progress belong to each purchase, rather than to the shared template.

A B2B SaaS product sold services with different operational requirements. One needed documents from the customer, another required an expert review, and another followed an application through several stages. The operations team needed to configure these services without waiting for a developer to build a new flow each time.

I built a Laravel workflow using a fixed set of step types. The important boundary was between the shared service definition and the work already in progress for a particular customer. They could use the same components without sharing mutable state.

Separating the template from the work in progress

An administrator chooses and orders steps in the management panel. Purchasing a service creates customer-owned instances of those steps. Documents, progress and expert notes belong to the instances rather than the shared template.

An expert can therefore work on one customer's file without changing another customer's experience. A step type has a reusable interface, but the state shown in that interface comes from the individual instance. Configuring a service and delivering it remain separate operations.

Connecting steps through events

The domain rules were still changing, so I kept the backend in a modular monolith. Splitting the workflow into independently deployed services would have added coordination before those boundaries were clear. Within the application, one step does not directly invoke another.

Instead, completing a step emits an event. Listeners recalculate visibility and progress. That boundary proved useful when adding automatic invoicing: a payment made inside a service could reach the invoice workflow through a listener. Document collection and expert review did not need to know how invoicing worked.

Why revision stopped being a step type

The original design treated revision as a separate step. Rejecting a document opened a new revision task with its own notes and deadline. The general workflow engine then accumulated special rules to hide inactive revisions and control the original upload step.

The model did not match the customer's work. A revision was not an independent stage to pass through; it was a change to a document that already belonged to an existing stage. Keeping it separate made the engine responsible for exceptions that belonged in the document model.

I moved revision into document state. When a review needs another upload, the document step reopens and the visibility of subsequent steps is recalculated. Existing data remains available. The customer continues the same piece of work instead of entering a parallel workflow.

A boundary for future step types

The result is a configurable workflow, with a deliberate limit on what becomes a new type. Before adding one, I ask whether the customer is entering a distinct stage or whether an existing record has changed state. That distinction determines whether the product needs another component or a better model for the work it already supports.

Work together

Thinking about a similar project?

Tell me about your project