What Happens When You Start a Flow (core)
This diagram follows player.start(flow) from a raw payload to a rendered view.
Controller setup. setupFlow first lets plugins transform the raw flow (resolveFlowContent), constructs the FlowController, then wires up the core controllers in order — BindingParser, SchemaController, ValidationController, DataController (with its ValidationMiddleware and format/deformat/default-value taps), ExpressionEvaluator, and ViewController. Each exposes a hook so plugins can tap it. The ViewController then subscribes to the flow and to the data controller’s update/delete hooks.
Flow state machine. flowController.start() creates a FlowInstance, and Player taps its transition hooks. Each transition(value) first checks skipTransition — the validation "navigation" phase — and if validation blocks, Player waits for the user to fix errors or dismiss warnings. Otherwise it resolves strings on the transitions, looks up the next state (falling back to *), pushes history, and fires the transition hooks. The next state is dispatched by type: ACTION/ASYNC_ACTION evaluate an expression and loop back to transition, FLOW recurses into a nested instance, END resolves the flow result, and VIEW hands off to rendering.
View pipeline. For a VIEW state, resolveView lets plugins swap the view, a ViewInstance is created, and the view plugins are applied in a fixed order — Asset, Switch, Applicability, AssetTransform, StringResolver, Template, then MultiNode. On the first update the Parser builds the AST and a Resolver is created; subsequent updates reuse it. The Resolver computes the tree, and at each node the DependencyModel decides whether to reuse the cached subtree or recompute. Recomputation runs applicability/switch/template filtering, then the StringResolver ({{bindings}} via model.get, @[expr]@ via the evaluator, plus asset transforms), then recurses into children. The final resolved tree is emitted to the renderer.
Once rendered, a user action re-enters the state machine at transition, while a data change hands off to the data-change pipeline.
%% Player: start(flow) -> rendered view
%% Shows the controllers spun up by Player.start, the FlowController state machine,
%% and the ViewController -> Parser -> Resolver pipeline that produces a rendered view.
flowchart TD
Start(["player.start(flow)"]) --> SetupFlow["setupFlow(payload)"]
SetupFlow --> ResolveContent["hooks.resolveFlowContent<br/>(plugins transform raw flow)"]
ResolveContent --> NewFC["new FlowController(navigation)"]
NewFC --> OnStartHook["hooks.onStart"]
OnStartHook --> FCHook["hooks.flowController<br/>(plugin tap)"]
FCHook --> BP["new BindingParser<br/>hooks.bindingParser"]
BP --> Schema["new SchemaController<br/>hooks.schema"]
Schema --> ValC["new ValidationController<br/>hooks.validationController"]
ValC --> DC["new DataController<br/>(+ ValidationMiddleware)<br/>hooks.dataController"]
DC --> DCTaps["Tap data hooks:<br/>format / deformat /<br/>resolveDefaultValue<br/>(from SchemaController)"]
DCTaps --> Expr["new ExpressionEvaluator<br/>hooks.expressionEvaluator"]
Expr --> VC["new ViewController<br/>hooks.viewController"]
VC --> VCTaps["ViewController taps:<br/>flowController.hooks.flow<br/>dataController.hooks.onUpdate<br/>dataController.hooks.onDelete"]
VCTaps --> StartFC["flowController.start()"]
StartFC --> NewFI["FlowInstance created<br/>flowController.hooks.flow fires"]
NewFI --> FITaps["Player taps FlowInstance hooks:<br/>beforeTransition / skipTransition /<br/>resolveTransitionNode / transition /<br/>afterTransition"]
FITaps --> Transition["FlowInstance.transition(value)"]
Transition --> Skip{"skipTransition?<br/>(validation 'navigation' phase)"}
Skip -- blocked --> WaitFix["Wait for user fix /<br/>warning dismissal"]
WaitFix --> Transition
Skip -- proceed --> Before["beforeTransition<br/>resolveStrings on transitions[]<br/>evaluate onEnd"]
Before --> Lookup["lookup transitions[value]<br/>fallback '*'"]
Lookup --> Push["pushHistory(nextState)"]
Push --> RTN["resolveTransitionNode<br/>resolveStrings on ref / param"]
RTN --> TransitionHook["transition hook<br/>(reset validation if leaving VIEW)"]
TransitionHook --> AfterT["afterTransition hook"]
AfterT --> StateType{"state type?"}
StateType -- ACTION --> ActExp["expressionEvaluator.evaluate(exp)"]
ActExp --> Transition
StateType -- ASYNC_ACTION --> AsyncExp["await exp"]
AsyncExp --> Transition
StateType -- FLOW --> SubFlow["nested FlowInstance<br/>(FlowController.hooks.flow taps)"]
SubFlow --> Transition
StateType -- END --> EndHook["hooks.onEnd<br/>flowResult resolves"]
StateType -- VIEW --> OnView["ViewController.onView(viewRef)"]
OnView --> ResolveView["hooks.resolveView<br/>(waterfall, plugins swap view)"]
ResolveView --> MkVI["new ViewInstance"]
MkVI --> ApplyPlugins["applyViewPlugins (in order):<br/>1 AssetPlugin<br/>2 SwitchPlugin<br/>3 ApplicabilityPlugin<br/>4 AssetTransformCorePlugin<br/>5 StringResolverPlugin<br/>6 TemplatePlugin<br/>7 MultiNodePlugin"]
ApplyPlugins --> ViewHook["hooks.view (ViewInstance)<br/>ValidationBindingTracker taps -><br/>validations 'load' phase"]
ViewHook --> VIUpdate["ViewInstance.update()"]
VIUpdate --> ParseOnce{"first update?"}
ParseOnce -- yes --> Parse["Parser.parseView()<br/>parser hooks:<br/>onParseObject / parseNode /<br/>onCreateASTNode"]
Parse --> NewResolver["new Resolver(AST)<br/>resolver hooks registered<br/>by view plugins"]
NewResolver --> ResolverUpdate
ParseOnce -- no --> ResolverUpdate["Resolver.update(dataChanges, nodeChanges)"]
ResolverUpdate --> BeforeU["beforeUpdate hook"]
BeforeU --> Compute["computeTree(root)"]
Compute --> ResolveOpts["resolveOptions hook"]
ResolveOpts --> SkipR{"skipResolve?<br/>(DependencyModel:<br/>any tracked dep changed?)"}
SkipR -- cached --> Reuse["reuse cached subtree"]
SkipR -- recompute --> BeforeR["beforeResolve<br/>ApplicabilityPlugin filters<br/>SwitchPlugin picks case<br/>TemplatePlugin expands"]
BeforeR --> ResolveH["resolve hook<br/>StringResolverPlugin:<br/>{{bindings}} via model.get<br/>@[expr]@ via evaluator<br/>AssetTransform applied"]
ResolveH --> Children["recurse children<br/>(MultiNodePlugin handles arrays)"]
Children --> AfterR["afterResolve hook<br/>final node value"]
AfterR --> NodeUp["afterNodeUpdate hook"]
Reuse --> AfterU
NodeUp --> AfterU["afterUpdate hook"]
AfterU --> OnUpdate["ViewInstance.hooks.onUpdate<br/>(resolved tree -> renderer)"]
OnUpdate --> Rendered(["rendered view"])
Rendered -.user action.-> Transition
Rendered -.data change.-> DataPipe(["see data-change-pipeline.mmd"])