Symfony’s Workflow component models business processes as a series of places
(states or steps) connected by transitions (actions that move from one place
to another). An object progresses through the workflow, and its current position
is tracked by the marking, which stores which place(s) the object is in.
A key feature of workflows (as opposed to state machines) is that an object can
be in multiple places simultaneously. For example, when building a product,
you might assemble several components in parallel. However, until now, each
place could only record whether the object was there or not, like a binary flag.
Symfony 7.4 introduces multiplicity thanks to weighted transitions: a place
can now track how many times an object is in that place. This is useful when
you need multiple instances of something before proceeding. For example,
“collect 4 legs before assembling the table” or “wait for 3 approvals before publishing”.
How Weighted Transitions Work
You can assign a weight to specify how many instances are produced or required:
- Weight on output (to): the transition places the object in the target
place N times; - Weight on input (from): the transition requires the object to be in the
source place N times before it can fire.
Without weights, every transition produces or consumes exactly one instance per place.
Example: Building a Table
Consider a workflow that models table construction. A table needs four legs,
one tabletop, and we also want to track timing. Here’s how weighted transitions
handle this:
# config/packages/workflow.yaml
framework:
workflows:
make_table:
transitions:
start:
from: init
to:
- place: prepare_leg
weight: 4
- place: prepare_top
weight: 1
- place: stopwatch_running
weight: 1
build_leg:
from: prepare_leg
to: leg_created
build_top:
from: prepare_top
to: top_created
join:
from:
- place: leg_created
weight: 4
- top_created
- stopwatch_running
to: finished
When using the following code:
$subject = new Subject();
$workflow = new Workflow($definition);
$workflow->apply($subject, 'start');
$workflow->apply($subject, 'build_leg');
$workflow->apply($subject, 'build_top');
$workflow->apply($subject, 'build_leg');
$workflow->apply($subject, 'build_leg');
$workflow->apply($subject, 'build_leg');
$workflow->apply($subject, 'join');
This is how the execution flow works:
- Firing
startmarks the object as being inprepare_legfour times, in
prepare_toponce, and instopwatch_runningonce; - Each time you fire
build_leg, it consumes one instance fromprepare_leg
and adds one toleg_created(fire it four times to accumulate four instances); - Firing
build_topcreates the single tabletop; - The
jointransition requires the object to be inleg_createdexactly
four times, plus once in each of the other places, before it can fire.
This creates a synchronization point: you cannot proceed to assembly until
all required components reach their specified quantities. The workflow enforces
these structural constraints at the model level.



