Skip to content

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"])