Interactive architecture lab

Run the workflow model by hand.

These demos mirror the core contracts: explicit state, renderings with callbacks, parent-child output routing, and render-scoped workers keyed by logical effect identity.

Demo 01

Simple counter workflow.

The UI only receives the current rendering. Each button sends a pure action into the runtime, producing a new state and sometimes an output.

Current count 0 No output yet
    counterWorkflow.ts state → rendering → action
    const counterWorkflow = {
      initialState: () => ({ count: 0 }),
      render: (_props, state, ctx) => {
        const increment = () => ctx.actionSink.send((s) => ({
          state: { count: Math.min(s.count + 1, 10) },
          output: s.count + 1 === 10 ? { type: 'reachedMax', value: 10 } : undefined,
        }));
        const decrement = () => ctx.actionSink.send((s) => ({
          state: { count: Math.max(s.count - 1, 0) },
          output: s.count - 1 === 0 ? { type: 'reachedZero' } : undefined,
        }));
        const reset = () => ctx.actionSink.send(() => ({ state: { count: 0 } }));
    
        if (state.count === 0) return { type: 'atZero', count: 0, increment };
        if (state.count === 10) return { type: 'atMax', count: 10, reset };
        return { type: 'counting', count: state.count, increment, decrement, reset };
      },
    };
    Demo 02

    Compose parent and child workflows.

    A parent renders a child with a stable key, receives child outputs, and maps them into parent actions. The child keeps its own state while the parent owns the wider flow.

    Parent workflow

    CartWorkflow

    state: editingCart

    Child workflow

    QuantityCounter

    0
      cartWorkflow.ts stable child key + output mapping
      render: (props, state, ctx) => {
        const quantity = ctx.renderChild(
          counterWorkflow,
          { max: 5 },
          'quantity',
          (childOutput) => (parentState) => ({
            state:
              childOutput.type === 'reachedMax'
                ? { ...parentState, banner: 'Bulk quantity selected' }
                : { ...parentState, banner: 'Quantity reset to zero' },
          }),
        );
      
        return { type: 'editingCart', quantity };
      };
      counterWorkflow.ts child workflow definition
      const counterWorkflow = {
        initialState: () => ({ count: 0 }),
        render: (_props, state, ctx) => {
          const increment = () => ctx.actionSink.send((s) => ({
            state: { count: Math.min(s.count + 1, 5) },
            output: s.count + 1 === 5 ? { type: 'reachedMax', value: 5 } : undefined,
          }));
          const decrement = () => ctx.actionSink.send((s) => ({
            state: { count: Math.max(s.count - 1, 0) },
            output: s.count - 1 === 0 ? { type: 'reachedZero' } : undefined,
          }));
          const reset = () => ctx.actionSink.send(() => ({ state: { count: 0 } }));
      
          return { type: 'counting', count: state.count, increment, decrement, reset };
        },
      };
      Demo 03

      Worker keys control lifetime.

      Start a worker in loading. Move to a state that also declares runWorker(..., 'load') and the same worker keeps running. Move to a state that does not declare that key and the runtime cancels it at the end of render.

      idle

      Press Start load to render loading and run the worker.

      worker key
      load
      rendering
      idle
        loadWorkflow.ts same key continues; missing key cancels