diff --git a/.all-contributorsrc b/.all-contributorsrc
index 90d43d944..4cdd4875e 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -98,6 +98,28 @@
       "contributions": [
         "design"
       ]
+    },
+    {
+      "login": "lexfm",
+      "name": "Alejandro Fimbres",
+      "avatar_url": "https://avatars.githubusercontent.com/u/9345943?v=4",
+      "profile": "https://github.com/lexfm",
+      "contributions": [
+        "doc",
+        "test",
+        "code"
+      ]
+    },
+    {
+      "login": "rafbcampos",
+      "name": "Rafael Campos",
+      "avatar_url": "https://avatars.githubusercontent.com/u/26394217?v=4",
+      "profile": "https://github.com/rafbcampos",
+      "contributions": [
+        "doc",
+        "test",
+        "code"
+      ]
     }
   ],
   "contributorsPerLine": 6
diff --git a/.circleci/config.yml b/.circleci/config.yml
index a10fb8c7f..0d6af02cb 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -168,7 +168,7 @@ jobs:
       - run: cd xcode && bundle exec pod install
       - run: bazel build --config=ci -- //:PlayerUI //:PlayerUI-Demo //:PlayerUI_Pod
       # TODO: the timeout should be added to the test itself
-      - run: bazel test --test_env=APPLITOOLS_API_KEY=${APPLITOOLS_API_KEY} --test_env=APPLITOOLS_BATCH_ID=${CIRCLE_SHA1} --test_env=APPLITOOLS_PR_NUMBER=${CIRCLE_PULL_REQUEST##*/} --test_timeout=600 --jobs=1 --verbose_failures --config=ci  -- //:PlayerUI-Unit-Unit //:PlayerUI-UI-ViewInspectorTests //:PlayerUI-UI-XCUITests
+      - run: bazel test --test_env=APPLITOOLS_API_KEY=${APPLITOOLS_API_KEY} --test_env=APPLITOOLS_BATCH_ID=${CIRCLE_SHA1} --test_env=APPLITOOLS_PR_NUMBER=${CIRCLE_PULL_REQUEST##*/} --test_timeout=1200 --jobs=1 --verbose_failures --config=ci  -- //:PlayerUI-Unit-Unit //:PlayerUI-UI-ViewInspectorTests //:PlayerUI-UI-XCUITests
 
       - run: 
           when: always
diff --git a/core/player/BUILD b/core/player/BUILD
index 976578948..b33c16f5a 100644
--- a/core/player/BUILD
+++ b/core/player/BUILD
@@ -20,6 +20,7 @@ javascript_pipeline(
     ],
     test_data = [
         "//core/make-flow:@player-ui/make-flow",
+        "//plugins/common-types/core:@player-ui/common-types-plugin",
     ],
     library_name = "Player",
     bundle_entry = 'bundle.entry.js'
diff --git a/core/player/src/__tests__/data.test.ts b/core/player/src/__tests__/data.test.ts
index 7c930e6c0..c7b60ca66 100644
--- a/core/player/src/__tests__/data.test.ts
+++ b/core/player/src/__tests__/data.test.ts
@@ -143,7 +143,6 @@ describe('delete', () => {
 
     controller.delete('foo.baz');
 
-    expect(controller.getTrash()).toStrictEqual(new Set());
     expect(controller.get('')).toStrictEqual({
       foo: { bar: 'Some Data' },
     });
@@ -166,9 +165,6 @@ describe('delete', () => {
 
     controller.delete('foo.bar');
 
-    expect(controller.getTrash()).toStrictEqual(
-      new Set([parser.parse('foo.bar')])
-    );
     expect(controller.get('')).toStrictEqual({ foo: {} });
   });
 
@@ -187,9 +183,6 @@ describe('delete', () => {
 
     controller.delete('foo.0');
 
-    expect(controller.getTrash()).toStrictEqual(
-      new Set([parser.parse('foo.0')])
-    );
     expect(controller.get('')).toStrictEqual({ foo: [] });
   });
 
@@ -208,7 +201,6 @@ describe('delete', () => {
 
     controller.delete('foo.1');
 
-    expect(controller.getTrash()).toStrictEqual(new Set());
     expect(controller.get('')).toStrictEqual({ foo: ['Some Data'] });
   });
 
@@ -230,7 +222,6 @@ describe('delete', () => {
 
     controller.delete('foo');
 
-    expect(controller.getTrash()).toStrictEqual(new Set([parser.parse('foo')]));
     expect(controller.get('')).toStrictEqual({ baz: 'Other data' });
   });
 
@@ -251,7 +242,6 @@ describe('delete', () => {
 
     controller.delete('');
 
-    expect(controller.getTrash()).toStrictEqual(new Set());
     expect(controller.get('')).toStrictEqual({
       foo: {
         bar: 'Some Data',
@@ -295,6 +285,9 @@ describe('formatting', () => {
 
     controller.set([['foo.baz', 'should-deformat']], { formatted: true });
     expect(controller.get('foo.baz')).toBe('deformatted!');
+    expect(controller.get('foo.baz', { formatted: false })).toBe(
+      'deformatted!'
+    );
   });
 });
 
@@ -430,3 +423,30 @@ it('should not send update for deeply equal data', () => {
 
   expect(onUpdateCallback).not.toBeCalled();
 });
+
+it('should handle deleting non-existent value + parent value', () => {
+  const model = {
+    user: {
+      name: 'frodo',
+      age: 3,
+    },
+  };
+
+  const localData = new LocalModel(model);
+
+  const parser = new BindingParser({
+    get: localData.get,
+    set: localData.set,
+  });
+  const controller = new DataController({}, { pathResolver: parser });
+  controller.hooks.resolveDataStages.tap('basic', () => [localData]);
+
+  controller.delete('user.email');
+
+  expect(controller.get('user')).toStrictEqual({
+    name: 'frodo',
+    age: 3,
+  });
+
+  controller.delete('foo.bar');
+});
diff --git a/core/player/src/__tests__/helpers/binding.plugin.ts b/core/player/src/__tests__/helpers/binding.plugin.ts
index a576cc4e2..3a2722e08 100644
--- a/core/player/src/__tests__/helpers/binding.plugin.ts
+++ b/core/player/src/__tests__/helpers/binding.plugin.ts
@@ -102,12 +102,14 @@ export default class TrackBindingPlugin implements PlayerPlugin {
         view.hooks.resolver.tap('test', (resolver) => {
           resolver.hooks.resolve.tap('test', (val, node, options) => {
             if (val?.binding) {
+              const currentValue = options?.data.model.get(val.binding);
               options.validation?.track(val.binding);
               const valObj = options.validation?.get(val.binding);
 
               if (valObj) {
                 return {
                   ...val,
+                  value: currentValue,
                   validation: valObj,
                   allValidations: options.validation?.getAll(),
                 };
diff --git a/core/player/src/__tests__/validation.test.ts b/core/player/src/__tests__/validation.test.ts
index dc51de272..35704f985 100644
--- a/core/player/src/__tests__/validation.test.ts
+++ b/core/player/src/__tests__/validation.test.ts
@@ -6,6 +6,7 @@ import type { SchemaController } from '../schema';
 import type { BindingParser } from '../binding';
 import TrackBindingPlugin, { addValidator } from './helpers/binding.plugin';
 import { Player } from '..';
+import { VALIDATION_PROVIDER_NAME_SYMBOL } from '../controllers/validation';
 import type { ValidationController } from '../controllers/validation';
 import type { InProgressState } from '../types';
 import TestExpressionPlugin, {
@@ -48,6 +49,7 @@ const simpleFlow: Flow = {
           {
             type: 'names',
             names: ['frodo', 'sam'],
+            trigger: 'navigation',
             severity: 'warning',
           },
         ],
@@ -57,6 +59,7 @@ const simpleFlow: Flow = {
         validation: [
           {
             type: 'names',
+            trigger: 'navigation',
             names: ['frodo', 'sam'],
             severity: 'warning',
           },
@@ -82,6 +85,7 @@ const simpleFlow: Flow = {
     },
   },
 };
+
 const simpleExpressionFlow: Flow = {
   id: 'test-flow',
   views: [
@@ -387,6 +391,77 @@ const flowWithApplicability: Flow = {
   },
 };
 
+const flowWithItemsInArray: Flow = {
+  id: 'test-flow',
+  views: [
+    {
+      id: 'view-1',
+      type: 'view',
+      pets: [
+        {
+          asset: {
+            type: 'whatevs',
+            id: 'thing1',
+            binding: 'pets.0.name',
+          },
+        },
+        {
+          asset: {
+            type: 'whatevs',
+            id: 'thing2',
+            binding: 'pets.1.name',
+          },
+        },
+        {
+          asset: {
+            type: 'whatevs',
+            id: 'thing2',
+            binding: 'pets.2.name',
+          },
+        },
+      ],
+    },
+  ],
+  data: {
+    pets: [],
+  },
+  schema: {
+    ROOT: {
+      pets: {
+        type: 'PetType',
+        isArray: true,
+      },
+    },
+    PetType: {
+      name: {
+        type: 'string',
+        validation: [
+          {
+            type: 'required',
+          },
+        ],
+      },
+    },
+  },
+  navigation: {
+    BEGIN: 'FLOW_1',
+    FLOW_1: {
+      startState: 'VIEW_1',
+      VIEW_1: {
+        state_type: 'VIEW',
+        ref: 'view-1',
+        transitions: {
+          '*': 'END_1',
+        },
+      },
+      END_1: {
+        state_type: 'END',
+        outcome: 'test',
+      },
+    },
+  },
+};
+
 test('alt APIs', async () => {
   const player = new Player();
 
@@ -584,6 +659,83 @@ describe('validation', () => {
     });
   });
 
+  describe('data model delete', () => {
+    it('deletes the validation when the data is deleted', async () => {
+      const state = player.getState() as InProgressState;
+
+      const { validation, data, binding, view } = state.controllers;
+      const thing2Binding = binding.parse('data.thing2');
+
+      expect(validation.getBindings().has(thing2Binding)).toBe(true);
+
+      await waitFor(() => {
+        expect(
+          view.currentView?.lastUpdate?.thing2.asset.validation
+        ).toBeUndefined();
+      });
+
+      data.set([['data.thing2', 'gandalf']]);
+
+      await waitFor(() => {
+        expect(
+          view.currentView?.lastUpdate?.thing2.asset.validation?.message
+        ).toBe('Names just be in: frodo,sam');
+      });
+
+      data.delete('data.thing2');
+      expect(data.get('data.thing2', { includeInvalid: true })).toBe(undefined);
+
+      await waitFor(() => {
+        expect(
+          view.currentView?.lastUpdate?.thing2.asset.validation
+        ).toBeUndefined();
+      });
+
+      data.set([['data.thing2', 'gandalf']]);
+      await waitFor(() => {
+        expect(
+          view.currentView?.lastUpdate?.thing2.asset.validation?.message
+        ).toBe('Names just be in: frodo,sam');
+      });
+    });
+
+    it('handles arrays', async () => {
+      player.start(flowWithItemsInArray);
+      const state = player.getState() as InProgressState;
+      const { data, binding, view } = state.controllers;
+
+      await waitFor(() => {
+        expect(
+          view.currentView?.lastUpdate?.pets[1].asset.validation
+        ).toBeUndefined();
+      });
+
+      // Trigger validation for the second item
+      data.set([['pets.1.name', '']]);
+      expect(
+        schema.getType(binding.parse('pets.1.name'))?.validation
+      ).toHaveLength(1);
+
+      await waitFor(() => {
+        expect(
+          view.currentView?.lastUpdate?.pets[1].asset.validation?.message
+        ).toBe('A value is required');
+      });
+
+      // Delete the first item, the items should shift up and validation moves to the first item
+      data.delete('pets.0');
+
+      await waitFor(() => {
+        expect(
+          view.currentView?.lastUpdate?.pets[1].asset.validation
+        ).toBeUndefined();
+        expect(
+          view.currentView?.lastUpdate?.pets[0].asset.validation?.message
+        ).toBe('A value is required');
+      });
+    });
+  });
+
   describe('state', () => {
     it('updates when setting data', async () => {
       const state = player.getState() as InProgressState;
@@ -639,6 +791,8 @@ describe('validation', () => {
           displayTarget: 'field',
           trigger: 'change',
           type: 'names',
+          blocking: true,
+          [VALIDATION_PROVIDER_NAME_SYMBOL]: 'schema',
         })
       );
 
@@ -685,7 +839,7 @@ describe('validation', () => {
       expect(result.endState.outcome).toBe('test');
     });
 
-    it('doesnt remove existing warnings if a new warning is triggered', async () => {
+    it('block navigation after data changes on first input, show warning on second input, then navigation succeeds', async () => {
       player.start(simpleFlow);
       const state = player.getState() as InProgressState;
       const { flowResult } = state;
@@ -721,26 +875,10 @@ describe('validation', () => {
       // Try to transition
       state.controllers.flow.transition('foo');
 
-      // Stays on the same view
+      // Should transition to end since data changes already occured on first input
       expect(
         state.controllers.flow.current?.currentState?.value.state_type
-      ).toBe('VIEW');
-
-      // New validation warning
-      expect(
-        state.controllers.view.currentView?.lastUpdate?.thing1.asset.validation
-      ).not.toBe(undefined);
-
-      // Existing warning stays
-      expect(
-        state.controllers.view.currentView?.lastUpdate?.thing2.asset.validation
-      ).not.toBe(undefined);
-
-      state.controllers.data.set([['data.thing1', 'frodo']]);
-      state.controllers.data.set([['data.thing2', 'sam']]);
-
-      // Try to transition again
-      state.controllers.flow.transition('foo');
+      ).toBe('END');
 
       // Should work now that there's no error
       const result = await flowResult;
@@ -838,7 +976,7 @@ describe('validation', () => {
       expect(result.endState.outcome).toBe('test');
     });
 
-    it('doesnt remove existing warnings if a new warning is triggered - manual dismiss', async () => {
+    it('autodismiss if data change already took place on input with warning, manually dismiss second warning', async () => {
       player.start(simpleFlow);
       const state = player.getState() as InProgressState;
       const { flowResult } = state;
@@ -875,30 +1013,83 @@ describe('validation', () => {
       // Try to transition
       state.controllers.flow.transition('foo');
 
-      // Stays on the same view
+      // Since data change (setting "sam") already triggered validation next step is auto dismiss
       expect(
         state.controllers.flow.current?.currentState?.value.state_type
-      ).toBe('VIEW');
+      ).toBe('END');
 
-      // New validation warning
-      expect(
-        state.controllers.view.currentView?.lastUpdate?.thing1.asset.validation
-      ).not.toBe(undefined);
+      // Should work now that there's no error
+      const result = await flowResult;
+      expect(result.endState.outcome).toBe('test');
+    });
+  });
 
-      // Existing warning stays
-      expect(
-        state.controllers.view.currentView?.lastUpdate?.thing2.asset.validation
-      ).toBe(undefined);
+  describe('introspection and filtering', () => {
+    /**
+     *
+     */
+    const getAllKnownValidations = () => {
+      const allBindings = validationController.getBindings();
+      const allValidations = Array.from(allBindings).flatMap((b) => {
+        const validatedBinding =
+          validationController.getValidationForBinding(b);
+
+        if (!validatedBinding) {
+          return [];
+        }
+
+        return validatedBinding.allValidations.map((v) => {
+          return {
+            binding: b,
+            validation: v,
+            response: validationController.validationRunner(v.value, b),
+          };
+        });
+      });
 
-      state.controllers.data.set([['data.thing1', 'frodo']]);
-      // state.controllers.data.set([['data.thing2', 'sam']]);
+      return allValidations;
+    };
 
-      // Try to transition again
-      state.controllers.flow.transition('foo');
+    it('can query all triggered validations', async () => {
+      const state = player.getState() as InProgressState;
+      state.controllers.data.set([['data.thing4', 'not-sam']]);
 
-      // Should work now that there's no error
-      const result = await flowResult;
-      expect(result.endState.outcome).toBe('test');
+      await waitFor(() => {
+        expect(
+          state.controllers.view.currentView?.lastUpdate?.alreadyInvalidData
+            .asset.validation.message
+        ).toBe('Names just be in: sam');
+      });
+
+      const currentValidations = getAllKnownValidations();
+
+      expect(currentValidations).toHaveLength(5);
+      expect(
+        currentValidations[0].validation.value[VALIDATION_PROVIDER_NAME_SYMBOL]
+      ).toBe('schema');
+    });
+
+    it('can compute new validations without dismissing existing ones', async () => {
+      const updatedFlow = {
+        ...flowWithThings,
+        views: [
+          {
+            ...flowWithThings.views?.[0],
+            validation: [
+              {
+                type: 'expression',
+                ref: 'data.thing2',
+                message: 'Both need to equal 100',
+                exp: '{{data.thing1}} + {{data.thing2}} == 100',
+              },
+            ],
+          },
+        ],
+      };
+
+      player.start(updatedFlow as any);
+      const currentValidations = getAllKnownValidations();
+      expect(currentValidations).toHaveLength(6);
     });
   });
 });
@@ -1017,6 +1208,7 @@ test('shows errors on load', () => {
     displayTarget: 'field',
   });
 });
+
 describe('errors', () => {
   const errorFlow = makeFlow({
     id: 'view-1',
@@ -1071,7 +1263,7 @@ describe('errors', () => {
       {
         type: 'required',
         ref: 'foo.data.thing1',
-        trigger: 'load',
+        trigger: 'navigation',
         severity: 'error',
         blocking: 'once',
       },
@@ -1159,8 +1351,141 @@ describe('errors', () => {
     );
   });
 });
+
+test('validations return non-blocking errors', async () => {
+  const flow = makeFlow({
+    id: 'view-1',
+    type: 'view',
+    blocking: {
+      asset: {
+        id: 'thing-1',
+        binding: 'foo.blocking',
+        type: 'input',
+      },
+    },
+    nonblocking: {
+      asset: {
+        id: 'thing-2',
+        binding: 'foo.nonblocking',
+        type: 'input',
+      },
+    },
+  });
+
+  flow.schema = {
+    ROOT: {
+      foo: {
+        type: 'FooType',
+      },
+    },
+    FooType: {
+      blocking: {
+        type: 'TestType',
+        validation: [
+          {
+            type: 'required',
+          },
+        ],
+      },
+      nonblocking: {
+        type: 'TestType',
+        validation: [
+          {
+            type: 'required',
+            blocking: false,
+          },
+        ],
+      },
+    },
+  };
+
+  const player = new Player({ plugins: [new TrackBindingPlugin()] });
+  player.start(flow);
+
+  /**
+   *
+   */
+  const getState = () => player.getState() as InProgressState;
+
+  /**
+   *
+   */
+  const getCurrentView = () =>
+    getState().controllers.view.currentView?.lastUpdate;
+
+  // No errors show up initially
+
+  await waitFor(() => {
+    expect(getState().controllers.view.currentView?.lastUpdate?.id).toBe(
+      'view-1'
+    );
+  });
+
+  expect(getCurrentView()?.blocking.asset.validation).toBeUndefined();
+  expect(getCurrentView()?.nonblocking.asset.validation).toBeUndefined();
+
+  getState().controllers.flow.transition('next');
+  expect(
+    getState().controllers.flow.current?.currentState?.value.state_type
+  ).toBe('VIEW');
+
+  expect(player.getState().status).toBe('in-progress');
+
+  await waitFor(() => {
+    expect(getCurrentView()?.blocking.asset.validation).toMatchObject({
+      message: 'A value is required',
+      severity: 'error',
+      displayTarget: 'field',
+    });
+
+    expect(getCurrentView()?.nonblocking.asset.validation).toMatchObject({
+      message: 'A value is required',
+      severity: 'error',
+      displayTarget: 'field',
+    });
+  });
+
+  getState().controllers.data.set([['foo.blocking', 'foo']]);
+
+  await waitFor(() => {
+    expect(getCurrentView()?.blocking.asset.validation).toBeUndefined();
+
+    expect(getCurrentView()?.nonblocking.asset.validation).toMatchObject({
+      message: 'A value is required',
+      severity: 'error',
+      displayTarget: 'field',
+    });
+  });
+
+  getState().controllers.flow.transition('next');
+
+  await waitFor(() => {
+    expect(player.getState().status).toBe('completed');
+  });
+});
+
 describe('warnings', () => {
-  const warningFlow = makeFlow({
+  const warningFlowOnNavigation = makeFlow({
+    id: 'view-1',
+    type: 'view',
+    thing1: {
+      asset: {
+        id: 'thing-1',
+        binding: 'foo.data.thing1',
+        type: 'input',
+      },
+    },
+    validation: [
+      {
+        type: 'required',
+        ref: 'foo.data.thing1',
+        trigger: 'navigation',
+        severity: 'warning',
+      },
+    ],
+  });
+
+  const warningFlowOnLoad = makeFlow({
     id: 'view-1',
     type: 'view',
     thing1: {
@@ -1215,34 +1540,37 @@ describe('warnings', () => {
       {
         type: 'required',
         ref: 'foo.data.thing1',
-        trigger: 'load',
+        trigger: 'navigation',
         blocking: 'once',
         severity: 'warning',
       },
     ],
   });
 
-  it('shows warnings on load', () => {
-    const player = new Player({ plugins: [new TrackBindingPlugin()] });
-    player.start(warningFlow);
-    const state = player.getState() as InProgressState;
-
-    // Validation starts with a warning on load
-    expect(
-      omit(
-        state.controllers.view.currentView?.lastUpdate?.thing1.asset.validation,
-        'dismiss'
-      )
-    ).toMatchObject({
-      message: 'A value is required',
-      severity: 'warning',
-      displayTarget: 'field',
-    });
+  const onceBlockingWarningFlowWithChangeTrigger = makeFlow({
+    id: 'view-1',
+    type: 'view',
+    thing1: {
+      asset: {
+        id: 'thing-1',
+        binding: 'foo.data.thing1',
+        type: 'input',
+      },
+    },
+    validation: [
+      {
+        type: 'required',
+        ref: 'foo.data.thing1',
+        trigger: 'change',
+        blocking: 'once',
+        severity: 'warning',
+      },
+    ],
   });
 
-  it('auto-dismiss on double-navigation', async () => {
+  it('shows warnings on load', () => {
     const player = new Player({ plugins: [new TrackBindingPlugin()] });
-    player.start(warningFlow);
+    player.start(warningFlowOnLoad);
     const state = player.getState() as InProgressState;
 
     // Validation starts with a warning on load
@@ -1256,6 +1584,12 @@ describe('warnings', () => {
       severity: 'warning',
       displayTarget: 'field',
     });
+  });
+
+  it('auto-dismiss on double-navigation', async () => {
+    const player = new Player({ plugins: [new TrackBindingPlugin()] });
+    player.start(warningFlowOnNavigation);
+    const state = player.getState() as InProgressState;
 
     // Try to navigate, should prevent the navigation and keep the warning
     state.controllers.flow.transition('next');
@@ -1286,18 +1620,6 @@ describe('warnings', () => {
     player.start(blockingWarningFlow);
     const state = player.getState() as InProgressState;
 
-    // Validation starts with a warning on load
-    expect(
-      omit(
-        state.controllers.view.currentView?.lastUpdate?.thing1.asset.validation,
-        'dismiss'
-      )
-    ).toMatchObject({
-      message: 'A value is required',
-      severity: 'warning',
-      displayTarget: 'field',
-    });
-
     // Try to navigate, should prevent the navigation and keep the warning
     state.controllers.flow.transition('next');
     expect(state.controllers.flow.current?.currentState?.value.state_type).toBe(
@@ -1428,7 +1750,11 @@ describe('warnings', () => {
     player.start(onceBlockingWarningFlow);
     const state = player.getState() as InProgressState;
 
-    // Validation starts with a warning on load
+    // Try to navigate, should prevent the navigation and keep the warning
+    state.controllers.flow.transition('next');
+    expect(state.controllers.flow.current?.currentState?.value.state_type).toBe(
+      'VIEW'
+    );
     expect(
       omit(
         state.controllers.view.currentView?.lastUpdate?.thing1.asset.validation,
@@ -1440,7 +1766,26 @@ describe('warnings', () => {
       displayTarget: 'field',
     });
 
-    // Try to navigate, should prevent the navigation and keep the warning
+    // Navigate _again_ this should dismiss it
+    state.controllers.flow.transition('next');
+    // We make it to the next state
+
+    expect(state.controllers.flow.current?.currentState?.value.state_type).toBe(
+      'END'
+    );
+  });
+
+  it('once blocking warnings with change trigger auto-dismiss on double-navigation', async () => {
+    const player = new Player({ plugins: [new TrackBindingPlugin()] });
+    player.start(onceBlockingWarningFlowWithChangeTrigger);
+    const state = player.getState() as InProgressState;
+
+    // Validation starts with no warnings on load
+    expect(
+      state.controllers.view.currentView?.lastUpdate?.thing1.asset.validation
+    ).toBeUndefined();
+
+    // Try to navigate, should prevent the navigation and show the warning
     state.controllers.flow.transition('next');
     expect(state.controllers.flow.current?.currentState?.value.state_type).toBe(
       'VIEW'
@@ -1459,14 +1804,17 @@ describe('warnings', () => {
     // Navigate _again_ this should dismiss it
     state.controllers.flow.transition('next');
     // We make it to the next state
-    expect(state.controllers.flow.current?.currentState?.value.state_type).toBe(
-      'END'
-    );
+
+    await waitFor(() => {
+      expect(
+        state.controllers.flow.current?.currentState?.value.state_type
+      ).toBe('END');
+    });
   });
 
   it('triggers re-render on dismiss call', () => {
     const player = new Player({ plugins: [new TrackBindingPlugin()] });
-    player.start(warningFlow);
+    player.start(warningFlowOnLoad);
     const state = player.getState() as InProgressState;
 
     // Validation starts with a warning on load
@@ -1555,33 +1903,33 @@ describe('validation within arrays', () => {
 
     // Error if set to an falsy value
     state.controllers.data.set([['thing.1.data.3.name', '']]);
-    await waitFor(() =>
+    await waitFor(() => {
       expect(
         state.controllers.view.currentView?.lastUpdate?.thing1.asset.validation
       ).toMatchObject({
         severity: 'error',
         message: 'A value is required',
         displayTarget: 'field',
-      })
-    );
-    expect(
-      state.controllers.view.currentView?.lastUpdate?.thing2.asset.validation
-    ).toBe(undefined);
+      });
+      expect(
+        state.controllers.view.currentView?.lastUpdate?.thing2.asset.validation
+      ).toBe(undefined);
+    });
 
     // Other one gets error if i try to navigate
     state.controllers.data.set([['thing.1.data.3.name', 'adam']]);
     state.controllers.flow.transition('anything');
-    await waitFor(() =>
+    await waitFor(() => {
       expect(
         state.controllers.view.currentView?.lastUpdate?.thing1.asset.validation
-      ).toBe(undefined)
-    );
-    expect(
-      state.controllers.view.currentView?.lastUpdate?.thing2.asset.validation
-    ).toMatchObject({
-      severity: 'error',
-      message: 'A value is required',
-      displayTarget: 'field',
+      ).toBe(undefined);
+      expect(
+        state.controllers.view.currentView?.lastUpdate?.thing2.asset.validation
+      ).toMatchObject({
+        severity: 'error',
+        message: 'A value is required',
+        displayTarget: 'field',
+      });
     });
   });
 });
@@ -1693,18 +2041,18 @@ test('validations can run against formatted or deformatted values', async () =>
   ).toBeUndefined();
 
   state.controllers.data.set([['person.name', 'adam']], { formatted: true });
-  await waitFor(() =>
+  await waitFor(() => {
     expect(
       state.controllers.view.currentView?.lastUpdate?.validation.message
-    ).toBe('Names just be in: frodo,sam')
-  );
+    ).toBe('Names just be in: frodo,sam');
+  });
 
   state.controllers.data.set([['person.name', 'sam']], { formatted: true });
-  await waitFor(() =>
+  await waitFor(() => {
     expect(
       state.controllers.view.currentView?.lastUpdate?.validation
-    ).toBeUndefined()
-  );
+    ).toBeUndefined();
+  });
 });
 
 test('tracking a binding commits the default value', () => {
@@ -1754,7 +2102,7 @@ test('tracking a binding commits the default value', () => {
   });
 });
 
-test('validates on expressions outside of view', async () => {
+test('does not validate on expressions outside of view', async () => {
   const flowWithExp: Flow = {
     id: 'flow-with-exp',
     views: [
@@ -1823,7 +2171,7 @@ test('validates on expressions outside of view', async () => {
   state.controllers.flow.transition('Next');
 
   const response = await outcome;
-  expect(response.data).toStrictEqual({ person: { name: 'frodo' } });
+  expect(response.data).toStrictEqual({ person: { name: 'invalid' } });
 });
 
 describe('Validation applicability', () => {
@@ -1881,7 +2229,7 @@ describe('Validation applicability', () => {
       });
 
       state.controllers.data.set([['independentBinding', false]]);
-      await await waitFor(() => {
+      await waitFor(() => {
         expect(state.controllers.data.get('independentBinding')).toStrictEqual(
           false
         );
@@ -1897,172 +2245,292 @@ describe('Validation applicability', () => {
   });
 });
 
-describe('Validations with custom field messages', () => {
-  it('can evaluate expressions in message', async () => {
-    const flow = makeFlow({
-      id: 'view-1',
-      type: 'view',
-      thing1: {
-        asset: {
-          id: 'thing-1',
-          binding: 'foo.data.thing1',
-          type: 'input',
-        },
-      },
-      validation: [
-        {
-          type: 'expression',
-          ref: 'foo.data.thing1',
-          message: 'The entered value {{foo.data.thing1}} is greater than 100',
-          exp: '{{foo.data.thing1}} < 100',
-        },
-      ],
-    });
-    const player = new Player({
-      plugins: [new TrackBindingPlugin()],
-    });
-    player.start(flow);
-    const state = player.getState() as InProgressState;
-
-    state.controllers.data.set([['foo.data.thing1', 200]]);
-    state.controllers.flow.transition('next');
-    expect(
-      state.controllers.view.currentView?.lastUpdate?.thing1.asset.validation
-    ).toMatchObject({
-      severity: 'error',
-      message: 'The entered value 200 is greater than 100',
-      displayTarget: 'field',
-    });
-  });
-
-  it('can templatize messages', async () => {
-    const errFlow = makeFlow({
-      id: 'view-1',
-      type: 'view',
-      thing1: {
-        asset: {
-          id: 'thing-1',
-          binding: 'foo.data.thing1',
-          type: 'integer',
-        },
-      },
-      validation: [
-        {
-          type: 'integer',
-          ref: 'foo.data.thing1',
-          message:
-            'foo.data.thing1 is a number. You have provided a value of %type, which is correct. But floored value, %flooredValue is not equal to entered value, %value',
-          trigger: 'load',
-          severity: 'error',
-        },
-      ],
-    });
-
-    const player = new Player({ plugins: [new TrackBindingPlugin()] });
-    player.start(errFlow);
-    const state = player.getState() as InProgressState;
-
-    state.controllers.data.set([['foo.data.thing1', 200.567]]);
-
-    await waitFor(() =>
-      expect(
-        state.controllers.view.currentView?.lastUpdate?.thing1.asset.validation
-      ).toMatchObject({
-        message:
-          'foo.data.thing1 is a number. You have provided a value of number, which is correct. But floored value, 200 is not equal to entered value, 200.567',
-        severity: 'error',
-        displayTarget: 'field',
-      })
-    );
-  });
-});
-
-describe('Validations with multiple inputs', () => {
-  const complexValidation = makeFlow({
+test('updating a binding only updates its data and not other bindings due to weak binding connections', async () => {
+  const flow = makeFlow({
     id: 'view-1',
     type: 'view',
     thing1: {
       asset: {
         id: 'thing-1',
-        binding: 'foo.a',
-        type: 'input',
+        binding: 'input.text',
       },
     },
     thing2: {
       asset: {
         id: 'thing-2',
-        binding: 'foo.b',
-        type: 'input',
+        binding: 'input.check',
       },
     },
     validation: [
       {
-        type: 'expression',
-        ref: 'foo.a',
-        message: 'Both need to equal 100',
-        exp: 'sumValues(["foo.a", "foo.b"]) == 100',
-        severity: 'error',
-        trigger: 'load',
+        type: 'requiredIf',
+        ref: 'input.text',
+        param: 'input.check',
       },
     ],
   });
 
-  let player: Player;
-  let validationController: ValidationController;
-  let schema: SchemaController;
-  let parser: BindingParser;
-
-  beforeEach(() => {
-    player = new Player({
-      plugins: [new TrackBindingPlugin(), new TestExpressionPlugin()],
-    });
-    player.hooks.validationController.tap('test', (vc) => {
-      validationController = vc;
-    });
-    player.hooks.schema.tap('test', (s) => {
-      schema = s;
-    });
-    player.hooks.bindingParser.tap('test', (p) => {
-      parser = p;
-    });
-
-    player.start(flowWithThings);
-  });
+  flow.data = {
+    someOtherParam: 'notFoo',
+  };
 
-  it('Throws errors when a weak referenced field is changed', async () => {
-    complexValidation.data = {
-      foo: {
-        a: 90,
-        b: 10,
+  flow.schema = {
+    ROOT: {
+      input: {
+        type: 'InputType',
       },
-    };
-
-    player.start(complexValidation);
-    const state = player.getState() as InProgressState;
-
-    expect(
-      state.controllers.view.currentView?.lastUpdate?.thing1.asset.validation
-    ).toBeUndefined();
-
-    state.controllers.data.set([['foo.b', 70]]);
-    await waitFor(() =>
-      expect(
-        state.controllers.view.currentView?.lastUpdate?.thing1.asset.validation
-      ).toMatchObject({
-        severity: 'error',
-        message: 'Both need to equal 100',
-      })
-    );
-
-    expect(state.controllers.data.get('')).toMatchObject({
-      foo: {
-        a: 90,
-        b: 70,
+    },
+    InputType: {
+      text: {
+        type: 'DateType',
+        validation: [
+          {
+            type: 'paramIsFoo',
+            param: 'someOtherParam',
+          },
+        ],
       },
+      check: {
+        type: 'BooleanType',
+        validation: [
+          {
+            type: 'required',
+          },
+        ],
+      },
+    },
+  };
+
+  const basicValidationPlugin = {
+    name: 'basic-validation',
+    apply: (player: Player) => {
+      player.hooks.schema.tap('basic-validation', (schema) => {
+        schema.addDataTypes([
+          {
+            type: 'DateType',
+            validation: [{ type: 'date' }],
+          },
+          {
+            type: 'BooleanType',
+            validation: [{ type: 'boolean' }],
+          },
+        ]);
+      });
+
+      player.hooks.validationController.tap('basic-validation', (vc) => {
+        vc.hooks.createValidatorRegistry.tap('basic-validation', (registry) => {
+          registry.register('date', (ctx, value) => {
+            if (value === undefined) {
+              return;
+            }
+
+            return value.match(/^\d{4}-\d{2}-\d{2}$/)
+              ? undefined
+              : { message: 'Not a date' };
+          });
+          registry.register('boolean', (ctx, value) => {
+            if (value === undefined || value === true || value === false) {
+              return;
+            }
+
+            return {
+              message: 'Not a boolean',
+            };
+          });
+
+          registry.register('required', (ctx, value) => {
+            if (value === undefined) {
+              return {
+                message: 'Required',
+              };
+            }
+          });
+
+          registry.register<any>('requiredIf', (ctx, value, { param }) => {
+            const paramValue = ctx.model.get(param);
+            if (paramValue === undefined) {
+              return;
+            }
+
+            if (value === undefined) {
+              return {
+                message: 'Required',
+              };
+            }
+          });
+
+          registry.register<any>('paramIsFoo', (ctx, value, { param }) => {
+            const paramValue = ctx.model.get(param);
+            if (paramValue === 'foo') {
+              return;
+            }
+
+            if (value === undefined) {
+              return {
+                message: 'Must be foo',
+              };
+            }
+          });
+        });
+      });
+    },
+  };
+
+  const player = new Player({
+    plugins: [new TrackBindingPlugin(), basicValidationPlugin],
+  });
+  player.start(flow);
+  const state = player.getState() as InProgressState;
+
+  state.controllers.flow.transition('next');
+  await waitFor(() => {
+    state.controllers.data.set([['input.text', '']]);
+  });
+
+  await waitFor(() => {
+    state.controllers.data.set([['input.check', true]]);
+  });
+
+  await waitFor(() => {
+    const finalState = player.getState() as InProgressState;
+    const otherParam = finalState.controllers.data.get('someOtherParam');
+    expect(otherParam).toBe('notFoo');
+  });
+});
+
+describe('Validations with custom field messages', () => {
+  it('can evaluate expressions in message', async () => {
+    const flow = makeFlow({
+      id: 'view-1',
+      type: 'view',
+      thing1: {
+        asset: {
+          id: 'thing-1',
+          binding: 'foo.data.thing1',
+          type: 'input',
+        },
+      },
+      validation: [
+        {
+          type: 'expression',
+          ref: 'foo.data.thing1',
+          message: 'The entered value {{foo.data.thing1}} is greater than 100',
+          exp: '{{foo.data.thing1}} < 100',
+        },
+      ],
+    });
+    const player = new Player({
+      plugins: [new TrackBindingPlugin()],
+    });
+    player.start(flow);
+    const state = player.getState() as InProgressState;
+
+    state.controllers.data.set([['foo.data.thing1', 200]]);
+    state.controllers.flow.transition('next');
+    expect(
+      state.controllers.view.currentView?.lastUpdate?.thing1.asset.validation
+    ).toMatchObject({
+      severity: 'error',
+      message: 'The entered value 200 is greater than 100',
+      displayTarget: 'field',
     });
   });
 
-  it('Clears errors when a weak referenced field is changed', async () => {
+  it('can templatize messages', async () => {
+    const errFlow = makeFlow({
+      id: 'view-1',
+      type: 'view',
+      thing1: {
+        asset: {
+          id: 'thing-1',
+          binding: 'foo.data.thing1',
+          type: 'integer',
+        },
+      },
+      validation: [
+        {
+          type: 'integer',
+          ref: 'foo.data.thing1',
+          message:
+            'foo.data.thing1 is a number. You have provided a value of %type, which is correct. But floored value, %flooredValue is not equal to entered value, %value',
+          trigger: 'load',
+          severity: 'error',
+        },
+      ],
+    });
+
+    const player = new Player({ plugins: [new TrackBindingPlugin()] });
+    player.start(errFlow);
+    const state = player.getState() as InProgressState;
+
+    state.controllers.data.set([['foo.data.thing1', 200.567]]);
+
+    await waitFor(() => {
+      expect(
+        state.controllers.view.currentView?.lastUpdate?.thing1.asset.validation
+      ).toMatchObject({
+        message:
+          'foo.data.thing1 is a number. You have provided a value of number, which is correct. But floored value, 200 is not equal to entered value, 200.567',
+        severity: 'error',
+        displayTarget: 'field',
+      });
+    });
+  });
+});
+
+describe('Validations with multiple inputs', () => {
+  const complexValidation = makeFlow({
+    id: 'view-1',
+    type: 'view',
+    thing1: {
+      asset: {
+        id: 'thing-1',
+        binding: 'foo.a',
+        type: 'input',
+      },
+    },
+    thing2: {
+      asset: {
+        id: 'thing-2',
+        binding: 'foo.b',
+        type: 'input',
+      },
+    },
+    validation: [
+      {
+        type: 'expression',
+        ref: 'foo.a',
+        message: 'Both need to equal 100',
+        exp: 'sumValues(["foo.a", "foo.b"]) == 100',
+        severity: 'error',
+        trigger: 'load',
+      },
+    ],
+  });
+
+  let player: Player;
+  let validationController: ValidationController;
+  let schema: SchemaController;
+  let parser: BindingParser;
+
+  beforeEach(() => {
+    player = new Player({
+      plugins: [new TrackBindingPlugin(), new TestExpressionPlugin()],
+    });
+    player.hooks.validationController.tap('test', (vc) => {
+      validationController = vc;
+    });
+    player.hooks.schema.tap('test', (s) => {
+      schema = s;
+    });
+    player.hooks.bindingParser.tap('test', (p) => {
+      parser = p;
+    });
+
+    player.start(flowWithThings);
+  });
+
+  it('Throws errors when a weak referenced field is changed', async () => {
     complexValidation.data = {
       foo: {
         a: 90,
@@ -2077,58 +2545,90 @@ describe('Validations with multiple inputs', () => {
       state.controllers.view.currentView?.lastUpdate?.thing1.asset.validation
     ).toBeUndefined();
 
-    state.controllers.data.set([['foo.a', 15]]);
-    await waitFor(() =>
+    state.controllers.data.set([['foo.b', 70]]);
+    await waitFor(() => {
       expect(
         state.controllers.view.currentView?.lastUpdate?.thing1.asset.validation
       ).toMatchObject({
         severity: 'error',
         message: 'Both need to equal 100',
-      })
-    );
+      });
 
-    expect(
-      state.controllers.data.get('', { includeInvalid: false })
-    ).toMatchObject({
-      foo: {
-        a: 90,
-        b: 10,
-      },
+      expect(state.controllers.data.get('')).toMatchObject({
+        foo: {
+          a: 90,
+          b: 70,
+        },
+      });
     });
+  });
 
-    expect(
-      state.controllers.data.get('', { includeInvalid: true })
-    ).toMatchObject({
+  it('Clears errors when a weak referenced field is changed', async () => {
+    complexValidation.data = {
       foo: {
-        a: 15,
+        a: 90,
         b: 10,
       },
-    });
+    };
 
-    state.controllers.data.set([['foo.b', 85]]);
+    player.start(complexValidation);
+    const state = player.getState() as InProgressState;
+
+    expect(
+      state.controllers.view.currentView?.lastUpdate?.thing1.asset.validation
+    ).toBeUndefined();
 
-    await waitFor(() =>
+    state.controllers.data.set([['foo.a', 15]]);
+    await waitFor(() => {
       expect(
         state.controllers.view.currentView?.lastUpdate?.thing1.asset.validation
-      ).toBeUndefined()
-    );
+      ).toMatchObject({
+        severity: 'error',
+        message: 'Both need to equal 100',
+      });
 
-    expect(
-      state.controllers.data.get('', { includeInvalid: false })
-    ).toMatchObject({
-      foo: {
-        a: 15,
-        b: 85,
-      },
+      expect(
+        state.controllers.data.get('', { includeInvalid: false })
+      ).toMatchObject({
+        foo: {
+          a: 90,
+          b: 10,
+        },
+      });
+
+      expect(
+        state.controllers.data.get('', { includeInvalid: true })
+      ).toMatchObject({
+        foo: {
+          a: 15,
+          b: 10,
+        },
+      });
     });
 
-    expect(
-      state.controllers.data.get('', { includeInvalid: true })
-    ).toMatchObject({
-      foo: {
-        a: 15,
-        b: 85,
-      },
+    state.controllers.data.set([['foo.b', 85]]);
+    await waitFor(() => {
+      expect(
+        state.controllers.view.currentView?.lastUpdate?.thing1.asset.validation
+      ).toBeUndefined();
+
+      expect(
+        state.controllers.data.get('', { includeInvalid: false })
+      ).toMatchObject({
+        foo: {
+          a: 15,
+          b: 85,
+        },
+      });
+
+      expect(
+        state.controllers.data.get('', { includeInvalid: true })
+      ).toMatchObject({
+        foo: {
+          a: 15,
+          b: 85,
+        },
+      });
     });
   });
 });
@@ -2255,170 +2755,295 @@ describe('weak binding edge cases', () => {
     await waitFor(() => {
       state.controllers.data.set([['input.text', '1999-12-31']]);
     });
-
     await waitFor(() => {
       state.controllers.data.set([['input.check', true]]);
     });
-
     await waitFor(() => {
       state.controllers.flow.transition('next');
     });
-
     await waitFor(() => {
       expect(player.getState().status).toBe('completed');
     });
   });
 });
 
-test('updating a binding only updates its data and not other bindings due to weak binding connections', async () => {
-  const flow = makeFlow({
-    id: 'view-1',
-    type: 'view',
-    thing1: {
-      asset: {
-        id: 'thing-1',
-        binding: 'input.text',
+describe('Validation Providers', () => {
+  it('uses a locally defined handler', async () => {
+    let shouldError = true;
+
+    const player = new Player({
+      plugins: [
+        new TrackBindingPlugin(),
+
+        {
+          name: 'basic-validation',
+          apply: (p: Player) => {
+            p.hooks.validationController.tap('basic-validation', (vc) => {
+              vc.hooks.resolveValidationProviders.tap(
+                'basic-validation',
+                (providers) => {
+                  return [
+                    ...providers,
+                    {
+                      source: 'local-test',
+                      provider: {
+                        getValidationsForBinding(binding) {
+                          if (binding.asString() === 'data.thing1') {
+                            return [
+                              {
+                                type: 'custom',
+                                trigger: 'load',
+                                severity: 'error',
+                                handler: (ctx, value) => {
+                                  if (shouldError) {
+                                    return {
+                                      message: 'Local Error',
+                                    };
+                                  }
+                                },
+                              },
+                            ];
+                          }
+                        },
+                      },
+                    },
+                  ];
+                }
+              );
+            });
+          },
+        },
+      ],
+    });
+
+    player.start(simpleFlow);
+
+    /**
+     *
+     */
+    const getControllers = () => {
+      const state = player.getState() as InProgressState;
+      return state.controllers;
+    };
+
+    /**
+     *
+     */
+    const getFirstInput = () => {
+      return getControllers().view.currentView?.lastUpdate?.thing1.asset;
+    };
+
+    expect(getFirstInput()?.validation?.message).toBe('Local Error');
+    getControllers().data.set([['data.thing1', 'foo']]);
+    expect(getFirstInput()?.validation?.message).toBe('Local Error');
+
+    shouldError = false;
+
+    getControllers().data.set([['data.thing1', 'sam']]);
+
+    await waitFor(() => {
+      expect(getFirstInput()?.validation?.message).toBe(undefined);
+    });
+  });
+});
+
+describe('Validation + Default Data', () => {
+  it('triggers validation default data is invalid', async () => {
+    const flow = makeFlow({
+      id: 'view-1',
+      type: 'view',
+      requiredField: {
+        asset: {
+          id: 'required-field',
+          type: 'input',
+          binding: 'input.text',
+        },
       },
-    },
-    thing2: {
-      asset: {
-        id: 'thing-2',
-        binding: 'input.check',
+      thing2: {
+        asset: {
+          id: 'thing-2',
+          binding: 'input.check',
+        },
       },
-    },
-    validation: [
-      {
-        type: 'requiredIf',
-        ref: 'input.text',
-        param: 'input.check',
+      validation: [
+        {
+          type: 'requiredIf',
+          ref: 'input.text',
+          param: 'input.check',
+        },
+      ],
+    });
+
+    flow.schema = {
+      ROOT: {
+        input: {
+          type: 'InputType',
+        },
       },
-    ],
-  });
+      InputType: {
+        text: {
+          type: 'StringType',
+          // The default value is an empty string, which is invalid b/c of the required check
+          default: '',
+          validation: [
+            {
+              type: 'required',
+            },
+          ],
+        },
+      },
+    };
 
-  flow.data = {
-    someOtherParam: 'notFoo',
-  };
+    const player = new Player({
+      plugins: [new TrackBindingPlugin()],
+    });
 
-  flow.schema = {
-    ROOT: {
-      input: {
-        type: 'InputType',
-      },
-    },
-    InputType: {
-      text: {
-        type: 'DateType',
-        validation: [
-          {
-            type: 'paramIsFoo',
-            param: 'someOtherParam',
+    player.start(flow);
+
+    /**
+     *
+     */
+    const getControllers = () => {
+      const state = player.getState() as InProgressState;
+      return state.controllers;
+    };
+
+    /**
+     *
+     */
+    const getFirstInput = () => {
+      return getControllers().view.currentView?.lastUpdate?.requiredField.asset;
+    };
+
+    await waitFor(() => {
+      expect(getFirstInput()?.validation).toBeUndefined();
+    });
+
+    // Set the value to the same as the default
+    getControllers().data.set([['input.text', '']]);
+
+    await waitFor(() => {
+      expect(getFirstInput()?.validation.message).toBe('A value is required');
+    });
+
+    // Set the value to something else
+    getControllers().data.set([['input.text', 'foo']]);
+    await waitFor(() => {
+      expect(getFirstInput()?.validation).toBeUndefined();
+    });
+  });
+});
+
+describe('Validation in subflow', () => {
+  it('validations are evaluated when in a subflow', async () => {
+    const flow = {
+      id: 'input-validation-flow',
+      views: [
+        {
+          id: 'view-1',
+          type: 'input',
+          binding: 'foo.requiredInput',
+          label: {
+            asset: {
+              id: 'input-required-label',
+              type: 'text',
+              value: 'This input is required',
+            },
           },
-        ],
-      },
-      check: {
-        type: 'BooleanType',
-        validation: [
-          {
-            type: 'required',
+        },
+      ],
+      schema: {
+        ROOT: {
+          foo: {
+            type: 'FooType',
           },
-        ],
+        },
+        FooType: {
+          requiredInput: {
+            type: 'StringType',
+            validation: [
+              {
+                type: 'required',
+              },
+            ],
+          },
+        },
       },
-    },
-  };
-
-  const basicValidationPlugin = {
-    name: 'basic-validation',
-    apply: (player: Player) => {
-      player.hooks.schema.tap('basic-validation', (schema) => {
-        schema.addDataTypes([
-          {
-            type: 'DateType',
-            validation: [{ type: 'date' }],
+      data: {},
+      navigation: {
+        BEGIN: 'FLOW_1',
+        FLOW_1: {
+          startState: 'SUBFLOW',
+          SUBFLOW: {
+            state_type: 'FLOW',
+            ref: 'FLOW_2',
+            transitions: {
+              '*': 'END_Done',
+            },
           },
-          {
-            type: 'BooleanType',
-            validation: [{ type: 'boolean' }],
+          END_Done: {
+            state_type: 'END',
+            outcome: 'done',
           },
-        ]);
-      });
-
-      player.hooks.validationController.tap('basic-validation', (vc) => {
-        vc.hooks.createValidatorRegistry.tap('basic-validation', (registry) => {
-          registry.register('date', (ctx, value) => {
-            if (value === undefined) {
-              return;
-            }
-
-            return value.match(/^\d{4}-\d{2}-\d{2}$/)
-              ? undefined
-              : { message: 'Not a date' };
-          });
-          registry.register('boolean', (ctx, value) => {
-            if (value === undefined || value === true || value === false) {
-              return;
-            }
-
-            return {
-              message: 'Not a boolean',
-            };
-          });
+        },
+        FLOW_2: {
+          startState: 'VIEW_1',
+          VIEW_1: {
+            state_type: 'VIEW',
+            ref: 'view-1',
+            transitions: {
+              '*': 'END_Done',
+            },
+          },
+          END_Done: {
+            state_type: 'END',
+            outcome: 'done',
+          },
+        },
+      },
+    } as Flow;
 
-          registry.register('required', (ctx, value) => {
-            if (value === undefined) {
-              return {
-                message: 'Required',
-              };
-            }
-          });
+    const player = new Player({
+      plugins: [new TrackBindingPlugin()],
+    });
 
-          registry.register<any>('requiredIf', (ctx, value, { param }) => {
-            const paramValue = ctx.model.get(param);
-            if (paramValue === undefined) {
-              return;
-            }
+    player.start(flow);
 
-            if (value === undefined) {
-              return {
-                message: 'Required',
-              };
-            }
-          });
+    const getControllers = () => {
+      const state = player.getState() as InProgressState;
+      return state.controllers;
+    };
 
-          registry.register<any>('paramIsFoo', (ctx, value, { param }) => {
-            const paramValue = ctx.model.get(param);
-            if (paramValue === 'foo') {
-              return;
-            }
+    const getValidationMessage = () => {
+      return getControllers().view.currentView?.lastUpdate?.validation;
+    };
 
-            if (value === undefined) {
-              return {
-                message: 'Must be foo',
-              };
-            }
-          });
-        });
-      });
-    },
-  };
+    const attemptTransition = () => {
+      getControllers().flow.transition('next');
+    };
 
-  const player = new Player({
-    plugins: [new TrackBindingPlugin(), basicValidationPlugin],
-  });
-  player.start(flow);
-  const state = player.getState() as InProgressState;
+    await waitFor(() => {
+      expect(getControllers().view.currentView?.lastUpdate?.id).toStrictEqual(
+        'view-1'
+      );
+    });
 
-  state.controllers.flow.transition('next');
-  waitFor(() => {
-    state.controllers.data.set([['input.text', '']]);
-  });
+    attemptTransition();
+    expect(getControllers().view.currentView?.lastUpdate?.id).toStrictEqual(
+      'view-1'
+    );
+    const firstRequiredValidation = getValidationMessage();
+    expect(firstRequiredValidation.message).toStrictEqual(
+      'A value is required'
+    );
+    getControllers().data.set([['foo.requiredInput', 1]]);
 
-  waitFor(() => {
-    state.controllers.data.set([['input.check', true]]);
-  });
+    await waitFor(() => {
+      attemptTransition();
+    });
 
-  waitFor(() => {
-    const finalState = player.getState() as InProgressState;
-    const otherParam = finalState.controllers.data.get('someOtherParam');
-    expect(otherParam).toBe('notFoo');
+    await waitFor(() => {
+      expect(player.getState().status).toStrictEqual('completed');
+    });
   });
 });
diff --git a/core/player/src/binding/__tests__/index.test.ts b/core/player/src/binding/__tests__/index.test.ts
index 34b665d8a..b8e5a9596 100644
--- a/core/player/src/binding/__tests__/index.test.ts
+++ b/core/player/src/binding/__tests__/index.test.ts
@@ -10,6 +10,24 @@ test('caches bindings', () => {
   expect(b1).toBe(b3);
 });
 
+test('does not call the update hook when readOnly is true', () => {
+  const onSetHook = jest.fn();
+  const onGetHook = jest.fn();
+
+  const parser = new BindingParser({
+    get: (b) => {
+      onGetHook(b);
+      return [{ bar: 'blah' }];
+    },
+    set: onSetHook,
+    readOnly: true,
+  });
+
+  parser.parse('foo[bar="baz"].blah');
+  expect(onGetHook).toBeCalledWith(parser.parse('foo'));
+  expect(onSetHook).not.toHaveBeenCalled();
+});
+
 test('calls the update hook when data needs to be changed', () => {
   const onSetHook = jest.fn();
   const onGetHook = jest.fn();
diff --git a/core/player/src/binding/binding.ts b/core/player/src/binding/binding.ts
index 68586f489..2dd21364c 100644
--- a/core/player/src/binding/binding.ts
+++ b/core/player/src/binding/binding.ts
@@ -14,6 +14,14 @@ export interface BindingParserOptions {
    * Get the result of evaluating an expression
    */
   evaluate: (exp: string) => any;
+
+  /**
+   * Without readOnly, if a binding such as this is used: arr[key='does not exist'],
+   * then an object with that key will be created.
+   * This is done to make assignment such as arr[key='abc'].val = 'foo' work smoothly.
+   * Setting readOnly to true will prevent this behavior, avoiding unintended data changes.
+   */
+  readOnly?: boolean;
 }
 
 export type Getter = (path: BindingInstance) => any;
diff --git a/core/player/src/binding/index.ts b/core/player/src/binding/index.ts
index eb2e2a4ae..c2b4f4b07 100644
--- a/core/player/src/binding/index.ts
+++ b/core/player/src/binding/index.ts
@@ -172,7 +172,7 @@ export class BindingParser {
 
     const updateKeys = Object.keys(updates);
 
-    if (updateKeys.length > 0) {
+    if (!options.readOnly && updateKeys.length > 0) {
       const updateTransaction = updateKeys.map<[BindingInstance, any]>(
         (updatedBinding) => [
           this.parse(updatedBinding),
diff --git a/core/player/src/controllers/constants/index.ts b/core/player/src/controllers/constants/index.ts
index 3b7876596..bf28b9df5 100644
--- a/core/player/src/controllers/constants/index.ts
+++ b/core/player/src/controllers/constants/index.ts
@@ -10,7 +10,7 @@ export interface ConstantsProvider {
   addConstants(data: Record<string, any>, namespace: string): void;
 
   /**
-   * Function to retreive constants from the providers store
+   * Function to retrieve constants from the providers store
    *  - @param key Key used for the store access
    *  - @param namespace namespace values were loaded under (defined in the plugin)
    *  - @param fallback Optional - if key doesn't exist in namespace what to return (will return unknown if not provided)
@@ -77,9 +77,13 @@ export class ConstantsController implements ConstantsProvider {
     }
   }
 
-  clearTemporaryValues(): void {
-    this.tempStore.forEach((value: LocalModel) => {
-      value.reset();
-    });
+  clearTemporaryValues(namespace?: string): void {
+    if (namespace) {
+      this.tempStore.get(namespace)?.reset();
+    } else {
+      this.tempStore.forEach((value: LocalModel) => {
+        value.reset();
+      });
+    }
   }
 }
diff --git a/core/player/src/controllers/data.ts b/core/player/src/controllers/data.ts
index e2f43f167..5281c6c3a 100644
--- a/core/player/src/controllers/data.ts
+++ b/core/player/src/controllers/data.ts
@@ -25,11 +25,15 @@ export class DataController implements DataModelWithParser<DataModelOptions> {
     resolveDefaultValue: new SyncBailHook<[BindingInstance], any>(),
 
     onDelete: new SyncHook<[any]>(),
+
     onSet: new SyncHook<[BatchSetTransaction]>(),
+
     onGet: new SyncHook<[any, any]>(),
+
     onUpdate: new SyncHook<[Updates, DataModelOptions | undefined]>(),
 
     format: new SyncWaterfallHook<[any, BindingInstance]>(),
+
     deformat: new SyncWaterfallHook<[any, BindingInstance]>(),
 
     serialize: new SyncWaterfallHook<[any]>(),
@@ -119,18 +123,24 @@ export class DataController implements DataModelWithParser<DataModelOptions> {
       (updates, [binding, newVal]) => {
         const oldVal = this.get(binding, { includeInvalid: true });
 
-        if (!dequal(oldVal, newVal)) {
-          updates.push({
-            binding,
-            newValue: newVal,
-            oldValue: oldVal,
-          });
+        const update = {
+          binding,
+          newValue: newVal,
+          oldValue: oldVal,
+        };
+
+        if (dequal(oldVal, newVal)) {
+          this.logger?.debug(
+            `Skipping update for path: ${binding.asString()}. Value was unchanged: ${oldVal}`
+          );
+        } else {
+          updates.push(update);
+
+          this.logger?.debug(
+            `Setting path: ${binding.asString()} from: ${oldVal} to: ${newVal}`
+          );
         }
 
-        this.logger?.debug(
-          `Setting path: ${binding.asString()} from: ${oldVal} to: ${newVal}`
-        );
-
         return updates;
       },
       []
@@ -164,15 +174,17 @@ export class DataController implements DataModelWithParser<DataModelOptions> {
     return result;
   }
 
-  private resolve(binding: BindingLike): BindingInstance {
+  private resolve(binding: BindingLike, readOnly: boolean): BindingInstance {
     return Array.isArray(binding) || typeof binding === 'string'
-      ? this.pathResolver.parse(binding)
+      ? this.pathResolver.parse(binding, { readOnly })
       : binding;
   }
 
   public get(binding: BindingLike, options?: DataModelOptions) {
     const resolved =
-      binding instanceof BindingInstance ? binding : this.resolve(binding);
+      binding instanceof BindingInstance
+        ? binding
+        : this.resolve(binding, true);
     let result = this.getModel().get(resolved, options);
 
     if (result === undefined && !options?.ignoreDefaultValue) {
@@ -185,6 +197,8 @@ export class DataController implements DataModelWithParser<DataModelOptions> {
 
     if (options?.formatted) {
       result = this.hooks.format.call(result, resolved);
+    } else if (options?.formatted === false) {
+      result = this.hooks.deformat.call(result, resolved);
     }
 
     this.hooks.onGet.call(binding, result);
@@ -192,53 +206,36 @@ export class DataController implements DataModelWithParser<DataModelOptions> {
     return result;
   }
 
-  public delete(binding: BindingLike) {
-    if (binding === undefined || binding === null) {
-      throw new Error(`Invalid arguments: delete expects a data path (string)`);
+  public delete(binding: BindingLike, options?: DataModelOptions) {
+    if (
+      typeof binding !== 'string' &&
+      !Array.isArray(binding) &&
+      !(binding instanceof BindingInstance)
+    ) {
+      throw new Error('Invalid arguments: delete expects a data path (string)');
     }
 
-    const resolved = this.resolve(binding);
-    this.hooks.onDelete.call(resolved);
-    this.deleteData(resolved);
-  }
-
-  public getTrash(): Set<BindingInstance> {
-    return this.trash;
-  }
-
-  private addToTrash(binding: BindingInstance) {
-    this.trash.add(binding);
-  }
+    const resolved =
+      binding instanceof BindingInstance
+        ? binding
+        : this.resolve(binding, false);
 
-  private deleteData(binding: BindingInstance) {
-    const parentBinding = binding.parent();
-    const parentPath = parentBinding.asString();
-    const property = binding.key();
+    const parentBinding = resolved.parent();
+    const property = resolved.key();
+    const parentValue = this.get(parentBinding);
 
-    const existedBeforeDelete = Object.prototype.hasOwnProperty.call(
-      this.get(parentBinding),
-      property
-    );
+    const existedBeforeDelete =
+      typeof parentValue === 'object' &&
+      parentValue !== null &&
+      Object.prototype.hasOwnProperty.call(parentValue, property);
 
-    if (property !== undefined) {
-      const parent = parentBinding ? this.get(parentBinding) : undefined;
+    this.getModel().delete(resolved, options);
 
-      // If we're deleting an item in an array, we just splice it out
-      // Don't add it to the trash
-      if (parentPath && Array.isArray(parent)) {
-        if (parent.length > property) {
-          this.set([[parentBinding, removeAt(parent, property as number)]]);
-        }
-      } else if (parentPath && parent[property]) {
-        this.set([[parentBinding, omit(parent, property as string)]]);
-      } else if (!parentPath) {
-        this.getModel().reset(omit(this.get(''), property as string));
-      }
+    if (existedBeforeDelete && !this.get(resolved)) {
+      this.trash.add(resolved);
     }
 
-    if (existedBeforeDelete && !this.get(binding)) {
-      this.addToTrash(binding);
-    }
+    this.hooks.onDelete.call(resolved);
   }
 
   public serialize(): object {
diff --git a/core/player/src/controllers/flow/controller.ts b/core/player/src/controllers/flow/controller.ts
index b73c77656..d5049da77 100644
--- a/core/player/src/controllers/flow/controller.ts
+++ b/core/player/src/controllers/flow/controller.ts
@@ -29,6 +29,7 @@ export class FlowController {
     this.start = this.start.bind(this);
     this.run = this.run.bind(this);
     this.transition = this.transition.bind(this);
+    this.addNewFlow = this.addNewFlow.bind(this);
   }
 
   /** Navigate to another state in the state-machine */
@@ -40,20 +41,9 @@ export class FlowController {
     this.current.transition(stateTransition, options);
   }
 
-  private async addNewFlow(flow: FlowInstance) {
+  private addNewFlow(flow: FlowInstance) {
     this.navStack.push(flow);
     this.current = flow;
-    flow.hooks.transition.tap(
-      'flow-controller',
-      async (_oldState, newState: NamedState) => {
-        if (newState.value.state_type === 'FLOW') {
-          this.log?.debug(`Got FLOW state. Loading flow ${newState.value.ref}`);
-          const endState = await this.run(newState.value.ref);
-          this.log?.debug(`Flow ended. Using outcome: ${endState.outcome}`);
-          flow.transition(endState.outcome);
-        }
-      }
-    );
     this.hooks.flow.call(flow);
   }
 
@@ -74,6 +64,20 @@ export class FlowController {
 
     const flow = new FlowInstance(startState, startFlow, { logger: this.log });
     this.addNewFlow(flow);
+
+    flow.hooks.afterTransition.tap('flow-controller', (flowInstance) => {
+      if (flowInstance.currentState?.value.state_type === 'FLOW') {
+        const subflowId = flowInstance.currentState?.value.ref;
+        this.log?.debug(`Loading subflow ${subflowId}`);
+        this.run(subflowId).then((subFlowEndState) => {
+          this.log?.debug(
+            `Subflow ended. Using outcome: ${subFlowEndState.outcome}`
+          );
+          flowInstance.transition(subFlowEndState?.outcome);
+        });
+      }
+    });
+
     const end = await flow.start();
     this.navStack.pop();
 
diff --git a/core/player/src/controllers/flow/flow.ts b/core/player/src/controllers/flow/flow.ts
index 8beae91ba..3d35dacec 100644
--- a/core/player/src/controllers/flow/flow.ts
+++ b/core/player/src/controllers/flow/flow.ts
@@ -58,6 +58,9 @@ export class FlowInstance {
 
     /** A callback when a transition from 1 state to another was made */
     transition: new SyncHook<[NamedState | undefined, NamedState]>(),
+
+    /** A callback to run actions after a transition occurs */
+    afterTransition: new SyncHook<[FlowInstance]>(),
   };
 
   constructor(
@@ -131,7 +134,7 @@ export class FlowInstance {
 
       if (skipTransition) {
         this.log?.debug(
-          `Skipping transition from ${this.currentState} b/c hook told us to`
+          `Skipping transition from ${this.currentState.name} b/c hook told us to`
         );
         return;
       }
@@ -201,5 +204,7 @@ export class FlowInstance {
     this.hooks.transition.call(prevState, {
       ...newCurrentState,
     });
+
+    this.hooks.afterTransition.call(this);
   }
 }
diff --git a/core/player/src/controllers/validation/binding-tracker.ts b/core/player/src/controllers/validation/binding-tracker.ts
index 9bb76354e..2dacd7a58 100644
--- a/core/player/src/controllers/validation/binding-tracker.ts
+++ b/core/player/src/controllers/validation/binding-tracker.ts
@@ -13,6 +13,9 @@ const CONTEXT = 'validation-binding-tracker';
 export interface BindingTracker {
   /** Get the bindings currently being tracked for validation */
   getBindings(): Set<BindingInstance>;
+
+  /** Add a binding to the tracked set */
+  trackBinding(binding: BindingInstance): void;
 }
 interface Options {
   /** Parse a binding from a view */
@@ -42,6 +45,16 @@ export class ValidationBindingTrackerViewPlugin
     return this.trackedBindings;
   }
 
+  /** Add a binding to the tracked set */
+  trackBinding(binding: BindingInstance) {
+    if (this.trackedBindings.has(binding)) {
+      return;
+    }
+
+    this.trackedBindings.add(binding);
+    this.options.callbacks?.onAdd?.(binding);
+  }
+
   /** Attach hooks to the given resolver */
   applyResolver(resolver: Resolver) {
     this.trackedBindings.clear();
@@ -52,9 +65,6 @@ export class ValidationBindingTrackerViewPlugin
     /** Each Node is a registered section or page that maps to a set of nodes in its section */
     const sections = new Map<Node.Node, Set<Node.Node>>();
 
-    /** Keep track of all seen bindings so we can notify people when we hit one for the first time */
-    const seenBindings = new Set<BindingInstance>();
-
     let lastViewUpdateChangeSet: Set<BindingInstance> | undefined;
 
     const nodeTree = new Map<Node.Node, Set<Node.Node>>();
@@ -129,10 +139,8 @@ export class ValidationBindingTrackerViewPlugin
           }
         }
 
-        if (!seenBindings.has(parsed)) {
-          seenBindings.add(parsed);
-          this.options.callbacks?.onAdd?.(parsed);
-        }
+        this.trackedBindings.add(parsed);
+        this.options.callbacks?.onAdd?.(parsed);
       };
 
       return {
@@ -144,23 +152,36 @@ export class ValidationBindingTrackerViewPlugin
               track(binding);
             }
 
-            const eow = options.validation?._getValidationForBinding(binding);
+            const eows = options.validation
+              ?._getValidationForBinding(binding)
+              ?.getAll(getOptions);
+
+            const firstFieldEOW = eows?.find(
+              (eow) =>
+                eow.displayTarget === 'field' || eow.displayTarget === undefined
+            );
 
-            if (
-              eow?.displayTarget === undefined ||
-              eow?.displayTarget === 'field'
-            ) {
-              return eow;
+            return firstFieldEOW;
+          },
+          getValidationsForBinding(binding, getOptions) {
+            if (getOptions?.track) {
+              track(binding);
             }
 
-            return undefined;
+            return (
+              options.validation
+                ?._getValidationForBinding(binding)
+                ?.getAll(getOptions) ?? []
+            );
           },
-          getChildren: (type: Validation.DisplayTarget) => {
+          getChildren: (type?: Validation.DisplayTarget) => {
             const validations = new Array<ValidationResponse>();
             lastComputedBindingTree.get(node)?.forEach((binding) => {
-              const eow = options.validation?._getValidationForBinding(binding);
+              const eow = options.validation
+                ?._getValidationForBinding(binding)
+                ?.get();
 
-              if (eow && type === eow.displayTarget) {
+              if (eow && (type === undefined || type === eow.displayTarget)) {
                 validations.push(eow);
               }
             });
@@ -170,7 +191,9 @@ export class ValidationBindingTrackerViewPlugin
           getValidationsForSection: () => {
             const validations = new Array<ValidationResponse>();
             lastSectionBindingTree.get(node)?.forEach((binding) => {
-              const eow = options.validation?._getValidationForBinding(binding);
+              const eow = options.validation
+                ?._getValidationForBinding(binding)
+                ?.get();
 
               if (eow && eow.displayTarget === 'section') {
                 validations.push(eow);
@@ -213,7 +236,7 @@ export class ValidationBindingTrackerViewPlugin
       }
 
       if (node === resolver.root) {
-        this.trackedBindings = currentBindingTree.get(node) ?? new Set();
+        this.trackedBindings = new Set(currentBindingTree.get(node));
         lastComputedBindingTree = currentBindingTree;
 
         lastSectionBindingTree.clear();
diff --git a/core/player/src/controllers/validation/controller.ts b/core/player/src/controllers/validation/controller.ts
index fd749bd3b..a164bf56c 100644
--- a/core/player/src/controllers/validation/controller.ts
+++ b/core/player/src/controllers/validation/controller.ts
@@ -8,13 +8,18 @@ import type { SchemaController } from '../../schema';
 import type {
   ErrorValidationResponse,
   ValidationObject,
+  ValidationObjectWithHandler,
   ValidatorContext,
   ValidationProvider,
   ValidationResponse,
   WarningValidationResponse,
   StrongOrWeakBinding,
 } from '../../validator';
-import { ValidationMiddleware, ValidatorRegistry } from '../../validator';
+import {
+  ValidationMiddleware,
+  ValidatorRegistry,
+  removeBindingAndChildrenFromMap,
+} from '../../validator';
 import type { Logger } from '../../logger';
 import { ProxyLogger } from '../../logger';
 import type { Resolve, ViewInstance } from '../../view';
@@ -28,7 +33,22 @@ import type {
 import type { BindingTracker } from './binding-tracker';
 import { ValidationBindingTrackerViewPlugin } from './binding-tracker';
 
-type SimpleValidatorContext = Omit<ValidatorContext, 'validation'>;
+export const SCHEMA_VALIDATION_PROVIDER_NAME = 'schema';
+export const VIEW_VALIDATION_PROVIDER_NAME = 'view';
+
+export const VALIDATION_PROVIDER_NAME_SYMBOL: unique symbol = Symbol.for(
+  'validation-provider-name'
+);
+
+export type ValidationObjectWithSource = ValidationObjectWithHandler & {
+  /** The name of the validation */
+  [VALIDATION_PROVIDER_NAME_SYMBOL]: string;
+};
+
+type SimpleValidatorContext = Omit<
+  ValidatorContext,
+  'validation' | 'schemaType'
+>;
 
 interface BaseActiveValidation<T> {
   /** The validation is being actively shown */
@@ -52,7 +72,10 @@ type StatefulWarning = {
   type: 'warning';
 
   /** The underlying validation this tracks */
-  value: ValidationObject;
+  value: ValidationObjectWithSource;
+
+  /** If this is currently preventing navigation from continuing */
+  isBlockingNavigation: boolean;
 } & (
   | {
       /** warnings start with no state, but can active or dismissed */
@@ -67,7 +90,10 @@ type StatefulError = {
   type: 'error';
 
   /** The underlying validation this tracks */
-  value: ValidationObject;
+  value: ValidationObjectWithSource;
+
+  /** If this is currently preventing navigation from continuing */
+  isBlockingNavigation: boolean;
 } & (
   | {
       /** Errors start with no state an can be activated */
@@ -80,16 +106,17 @@ export type StatefulValidationObject = StatefulWarning | StatefulError;
 
 /** Helper for initializing a validation object that tracks state */
 function createStatefulValidationObject(
-  obj: ValidationObject
+  obj: ValidationObjectWithSource
 ): StatefulValidationObject {
   return {
     value: obj,
     type: obj.severity,
     state: 'none',
+    isBlockingNavigation: false,
   };
 }
 
-type ValidationRunner = (obj: ValidationObject) =>
+type ValidationRunner = (obj: ValidationObjectWithHandler) =>
   | {
       /** A validation message */
       message: string;
@@ -98,7 +125,7 @@ type ValidationRunner = (obj: ValidationObject) =>
 
 /** A class that manages validating bindings across phases */
 class ValidatedBinding {
-  private currentPhase?: Validation.Trigger;
+  public currentPhase?: Validation.Trigger;
   private applicableValidations: Array<StatefulValidationObject> = [];
   private validationsByState: Record<
     Validation.Trigger,
@@ -109,11 +136,16 @@ class ValidatedBinding {
     navigation: [],
   };
 
+  public get allValidations(): Array<StatefulValidationObject> {
+    return Object.values(this.validationsByState).flat();
+  }
+
   public weakBindings: Set<BindingInstance>;
+
   private onDismiss?: () => void;
 
   constructor(
-    possibleValidations: Array<ValidationObject>,
+    possibleValidations: Array<ValidationObjectWithSource>,
     onDismiss?: () => void,
     log?: Logger,
     weakBindings?: Set<BindingInstance>
@@ -123,9 +155,8 @@ class ValidatedBinding {
       const { trigger } = vObj;
 
       if (this.validationsByState[trigger]) {
-        this.validationsByState[trigger].push(
-          createStatefulValidationObject(vObj)
-        );
+        const statefulValidationObject = createStatefulValidationObject(vObj);
+        this.validationsByState[trigger].push(statefulValidationObject);
       } else {
         log?.warn(`Unknown validation trigger: ${trigger}`);
       }
@@ -133,15 +164,41 @@ class ValidatedBinding {
     this.weakBindings = weakBindings ?? new Set();
   }
 
+  private checkIfBlocking(statefulObj: StatefulValidationObject) {
+    if (statefulObj.state === 'active') {
+      const { isBlockingNavigation } = statefulObj;
+      return isBlockingNavigation;
+    }
+
+    return false;
+  }
+
+  public getAll(): Array<ValidationResponse> {
+    return this.applicableValidations.reduce((all, statefulObj) => {
+      if (statefulObj.state === 'active' && statefulObj.response) {
+        return [
+          ...all,
+          {
+            ...statefulObj.response,
+            blocking: this.checkIfBlocking(statefulObj),
+          },
+        ];
+      }
+
+      return all;
+    }, [] as Array<ValidationResponse>);
+  }
+
   public get(): ValidationResponse | undefined {
-    const firstError = this.applicableValidations.find((statefulObj) => {
-      const blocking =
-        this.currentPhase === 'navigation' ? statefulObj.value.blocking : true;
-      return statefulObj.state === 'active' && blocking !== false;
+    const firstInvalid = this.applicableValidations.find((statefulObj) => {
+      return statefulObj.state === 'active' && statefulObj.response;
     });
 
-    if (firstError?.state === 'active') {
-      return firstError.response;
+    if (firstInvalid?.state === 'active') {
+      return {
+        ...firstInvalid.response,
+        blocking: this.checkIfBlocking(firstInvalid),
+      };
     }
   }
 
@@ -158,8 +215,10 @@ class ValidatedBinding {
 
       const blocking =
         obj.value.blocking ??
-        ((obj.value.severity === 'warning' && 'once') ||
-          (obj.value.severity === 'error' && true));
+        ((obj.value.severity === 'warning' && 'once') || true);
+
+      const isBlockingNavigation =
+        blocking === true || (blocking === 'once' && !canDismiss);
 
       const dismissable = canDismiss && blocking === 'once';
 
@@ -178,12 +237,6 @@ class ValidatedBinding {
 
           return obj;
         }
-
-        if (obj.value.severity === 'error') {
-          const err = obj as StatefulError;
-          err.state = 'none';
-          return obj;
-        }
       }
 
       const response = runner(obj.value);
@@ -192,6 +245,7 @@ class ValidatedBinding {
         type: obj.type,
         value: obj.value,
         state: response ? 'active' : 'none',
+        isBlockingNavigation,
         dismissable:
           obj.value.severity === 'warning' &&
           this.currentPhase === 'navigation',
@@ -292,21 +346,48 @@ export class ValidationController implements BindingTracker {
     onRemoveValidation: new SyncWaterfallHook<
       [ValidationResponse, BindingInstance]
     >(),
+
+    resolveValidationProviders: new SyncWaterfallHook<
+      [
+        Array<{
+          /** The name of the provider */
+          source: string;
+          /** The provider itself */
+          provider: ValidationProvider;
+        }>
+      ],
+      {
+        /** The view this is triggered for  */
+        view?: ViewInstance;
+      }
+    >(),
+
+    /** A hook called when a binding is added to the tracker */
+    onTrackBinding: new SyncHook<[BindingInstance]>(),
   };
 
   private tracker: BindingTracker | undefined;
   private validations = new Map<BindingInstance, ValidatedBinding>();
   private validatorRegistry?: ValidatorRegistry;
   private schema: SchemaController;
-  private providers: Array<ValidationProvider>;
+
+  private providers:
+    | Array<{
+        /** The name of the provider */
+        source: string;
+        /** The provider itself */
+        provider: ValidationProvider;
+      }>
+    | undefined;
+
+  private viewValidationProvider?: ValidationProvider;
   private options?: SimpleValidatorContext;
   private weakBindingTracker = new Set<BindingInstance>();
-  private lastActiveBindings = new Set<BindingInstance>();
 
   constructor(schema: SchemaController, options?: SimpleValidatorContext) {
     this.schema = schema;
     this.options = options;
-    this.providers = [schema];
+    this.reset();
   }
 
   setOptions(options: SimpleValidatorContext) {
@@ -316,6 +397,22 @@ export class ValidationController implements BindingTracker {
   /** Return the middleware for the data-model to stop propagation of invalid data */
   public getDataMiddleware(): Array<DataModelMiddleware> {
     return [
+      {
+        set: (transaction, options, next) => {
+          return next?.set(transaction, options) ?? [];
+        },
+        get: (binding, options, next) => {
+          return next?.get(binding, options);
+        },
+        delete: (binding, options, next) => {
+          this.validations = removeBindingAndChildrenFromMap(
+            this.validations,
+            binding
+          );
+
+          return next?.delete(binding, options);
+        },
+      },
       new ValidationMiddleware(
         (binding) => {
           if (!this.options) {
@@ -323,7 +420,6 @@ export class ValidationController implements BindingTracker {
           }
 
           this.updateValidationsForBinding(binding, 'change', this.options);
-
           const strongValidation = this.getValidationForBinding(binding);
 
           // return validation issues directly on bindings first
@@ -342,15 +438,17 @@ export class ValidationController implements BindingTracker {
               weakValidation?.get()?.severity === 'error'
             ) {
               weakValidation?.weakBindings.forEach((weakBinding) => {
-                weakBinding === strongBinding
-                  ? newInvalidBindings.add({
-                      binding: weakBinding,
-                      isStrong: true,
-                    })
-                  : newInvalidBindings.add({
-                      binding: weakBinding,
-                      isStrong: false,
-                    });
+                if (weakBinding === strongBinding) {
+                  newInvalidBindings.add({
+                    binding: weakBinding,
+                    isStrong: true,
+                  });
+                } else {
+                  newInvalidBindings.add({
+                    binding: weakBinding,
+                    isStrong: false,
+                  });
+                }
               });
             }
           });
@@ -364,9 +462,44 @@ export class ValidationController implements BindingTracker {
     ];
   }
 
-  public onView(view: ViewInstance): void {
+  private getValidationProviders() {
+    if (this.providers) {
+      return this.providers;
+    }
+
+    this.providers = this.hooks.resolveValidationProviders.call([
+      {
+        source: SCHEMA_VALIDATION_PROVIDER_NAME,
+        provider: this.schema,
+      },
+      {
+        source: VIEW_VALIDATION_PROVIDER_NAME,
+        provider: {
+          getValidationsForBinding: (
+            binding: BindingInstance
+          ): Array<ValidationObject> | undefined => {
+            return this.viewValidationProvider?.getValidationsForBinding?.(
+              binding
+            );
+          },
+
+          getValidationsForView: (): Array<ValidationObject> | undefined => {
+            return this.viewValidationProvider?.getValidationsForView?.();
+          },
+        },
+      },
+    ]);
+
+    return this.providers;
+  }
+
+  public reset() {
     this.validations.clear();
+    this.tracker = undefined;
+  }
 
+  public onView(view: ViewInstance): void {
+    this.validations.clear();
     if (!this.options) {
       return;
     }
@@ -375,7 +508,10 @@ export class ValidationController implements BindingTracker {
       ...this.options,
       callbacks: {
         onAdd: (binding) => {
-          if (!this.options) {
+          if (
+            !this.options ||
+            this.getValidationForBinding(binding) !== undefined
+          ) {
             return;
           }
 
@@ -400,30 +536,43 @@ export class ValidationController implements BindingTracker {
               view.update(new Set([binding]));
             }
           );
+
+          this.hooks.onTrackBinding.call(binding);
         },
       },
     });
 
     this.tracker = bindingTrackerPlugin;
-    this.providers = [this.schema, view];
+    this.viewValidationProvider = view;
 
     bindingTrackerPlugin.apply(view);
   }
 
-  private updateValidationsForBinding(
+  updateValidationsForBinding(
     binding: BindingInstance,
     trigger: Validation.Trigger,
-    context: SimpleValidatorContext,
+    validationContext?: SimpleValidatorContext,
     onDismiss?: () => void
   ): void {
+    const context = validationContext ?? this.options;
+
+    if (!context) {
+      throw new Error(`Context is required for executing validations`);
+    }
+
     if (trigger === 'load') {
       // Get all of the validations from each provider
-      const possibleValidations = this.providers.reduce<
-        Array<ValidationObject>
+      const possibleValidations = this.getValidationProviders().reduce<
+        Array<ValidationObjectWithSource>
       >(
         (vals, provider) => [
           ...vals,
-          ...(provider.getValidationsForBinding?.(binding) ?? []),
+          ...(provider.provider
+            .getValidationsForBinding?.(binding)
+            ?.map((valObj) => ({
+              ...valObj,
+              [VALIDATION_PROVIDER_NAME_SYMBOL]: provider.source,
+            })) ?? []),
         ],
         []
       );
@@ -444,7 +593,7 @@ export class ValidationController implements BindingTracker {
 
     const trackedValidations = this.validations.get(binding);
     trackedValidations?.update(trigger, true, (validationObj) => {
-      const response = this.validationRunner(validationObj, context, binding);
+      const response = this.validationRunner(validationObj, binding, context);
 
       if (this.weakBindingTracker.size > 0) {
         const t = this.validations.get(binding) as ValidatedBinding;
@@ -464,8 +613,8 @@ export class ValidationController implements BindingTracker {
           validation.update(trigger, true, (validationObj) => {
             const response = this.validationRunner(
               validationObj,
-              context,
-              vBinding
+              vBinding,
+              context
             );
             return response ? { message: response.message } : undefined;
           });
@@ -474,21 +623,28 @@ export class ValidationController implements BindingTracker {
     }
   }
 
-  private validationRunner(
-    validationObj: ValidationObject,
-    context: SimpleValidatorContext,
-    binding: BindingInstance
+  validationRunner(
+    validationObj: ValidationObjectWithHandler,
+    binding: BindingInstance,
+    context: SimpleValidatorContext | undefined = this.options
   ) {
-    const handler = this.getValidator(validationObj.type);
+    if (!context) {
+      throw new Error('No context provided to validation runner');
+    }
+
+    const handler =
+      validationObj.handler ?? this.getValidator(validationObj.type);
+
     const weakBindings = new Set<BindingInstance>();
 
     // For any data-gets in the validation runner, default to using the _invalid_ value (since that's what we're testing against)
     const model: DataModelWithParser = {
-      get(b, options = { includeInvalid: true }) {
+      get(b, options) {
         weakBindings.add(isBinding(b) ? binding : context.parseBinding(b));
-        return context.model.get(b, options);
+        return context.model.get(b, { ...options, includeInvalid: true });
       },
       set: context.model.set,
+      delete: context.model.delete,
     };
 
     const result = handler?.(
@@ -500,6 +656,7 @@ export class ValidationController implements BindingTracker {
         ) => context.evaluate(exp, options),
         model,
         validation: validationObj,
+        schemaType: this.schema.getType(binding),
       },
       context.model.get(binding, {
         includeInvalid: true,
@@ -519,7 +676,6 @@ export class ValidationController implements BindingTracker {
           model,
           evaluate: context.evaluate,
         });
-
         if (parameters) {
           message = replaceParams(message, parameters);
         }
@@ -532,24 +688,33 @@ export class ValidationController implements BindingTracker {
   }
 
   private updateValidationsForView(trigger: Validation.Trigger): void {
-    const { activeBindings } = this;
-
-    const canDismiss =
-      trigger !== 'navigation' ||
-      this.setCompare(this.lastActiveBindings, activeBindings);
-
-    this.getBindings().forEach((binding) => {
-      this.validations.get(binding)?.update(trigger, canDismiss, (obj) => {
-        if (!this.options) {
-          return;
-        }
+    const isNavigationTrigger = trigger === 'navigation';
+    const lastActiveBindings = this.activeBindings;
+
+    /** Run validations for all bindings in view */
+    const updateValidations = (dismissValidations: boolean) => {
+      this.getBindings().forEach((binding) => {
+        this.validations
+          .get(binding)
+          ?.update(trigger, dismissValidations, (obj) => {
+            if (!this.options) {
+              return;
+            }
 
-        return this.validationRunner(obj, this.options, binding);
+            return this.validationRunner(obj, binding, this.options);
+          });
       });
-    });
+    };
+
+    // Should dismiss for non-navigation triggers.
+    updateValidations(!isNavigationTrigger);
 
-    if (trigger === 'navigation') {
-      this.lastActiveBindings = activeBindings;
+    if (isNavigationTrigger) {
+      // If validations didn't change since last update, dismiss all dismissible validations.
+      const { activeBindings } = this;
+      if (this.setCompare(lastActiveBindings, activeBindings)) {
+        updateValidations(true);
+      }
     }
   }
 
@@ -583,6 +748,10 @@ export class ValidationController implements BindingTracker {
     return this.tracker?.getBindings() ?? new Set();
   }
 
+  trackBinding(binding: BindingInstance): void {
+    this.tracker?.trackBinding(binding);
+  }
+
   /** Executes all known validations for the tracked bindings using the given model */
   validateView(trigger: Validation.Trigger = 'navigation'): {
     /** Indicating if the view can proceed without error */
@@ -595,26 +764,35 @@ export class ValidationController implements BindingTracker {
 
     const validations = new Map<BindingInstance, ValidationResponse>();
 
+    let canTransition = true;
+
     this.getBindings().forEach((b) => {
-      const invalid = this.getValidationForBinding(b)?.get();
+      const allValidations = this.getValidationForBinding(b)?.getAll();
+
+      allValidations?.forEach((v) => {
+        if (trigger === 'navigation' && v.blocking) {
+          this.options?.logger.debug(
+            `Validation on binding: ${b.asString()} is preventing navigation. ${JSON.stringify(
+              v
+            )}`
+          );
 
-      if (invalid) {
-        this.options?.logger.debug(
-          `Validation on binding: ${b.asString()} is preventing navigation. ${JSON.stringify(
-            invalid
-          )}`
-        );
+          canTransition = false;
+        }
 
-        validations.set(b, invalid);
-      }
+        if (!validations.has(b)) {
+          validations.set(b, v);
+        }
+      });
     });
 
     return {
-      canTransition: validations.size === 0,
+      canTransition,
       validations: validations.size ? validations : undefined,
     };
   }
 
+  /** Get the current tracked validation for the given binding */
   public getValidationForBinding(
     binding: BindingInstance
   ): ValidatedBinding | undefined {
@@ -626,11 +804,10 @@ export class ValidationController implements BindingTracker {
       _getValidationForBinding: (binding) => {
         return this.getValidationForBinding(
           isBinding(binding) ? binding : parser(binding)
-        )?.get();
+        );
       },
       getAll: () => {
         const bindings = this.getBindings();
-
         if (bindings.size === 0) {
           return undefined;
         }
@@ -653,6 +830,9 @@ export class ValidationController implements BindingTracker {
       get() {
         throw new Error('Error Access be provided by the view plugin');
       },
+      getValidationsForBinding() {
+        throw new Error('Error rollup should be provided by the view plugin');
+      },
       getChildren() {
         throw new Error('Error rollup should be provided by the view plugin');
       },
@@ -664,7 +844,7 @@ export class ValidationController implements BindingTracker {
       },
       register: () => {
         throw new Error(
-          'Section funcationality hould be provided by the view plugin'
+          'Section functionality should be provided by the view plugin'
         );
       },
       type: (binding) =>
diff --git a/core/player/src/controllers/view/asset-transform.ts b/core/player/src/controllers/view/asset-transform.ts
index a5f9e5a9c..81bdff2c1 100644
--- a/core/player/src/controllers/view/asset-transform.ts
+++ b/core/player/src/controllers/view/asset-transform.ts
@@ -92,7 +92,10 @@ export class AssetTransformCorePlugin {
             const transform = this.registry.get(node.value);
 
             if (transform?.beforeResolve) {
-              const store = getStore(node, this.beforeResolveSymbol);
+              const store = getStore(
+                options.node ?? node,
+                this.beforeResolveSymbol
+              );
 
               return transform.beforeResolve(node, options, store);
             }
diff --git a/core/player/src/controllers/view/controller.ts b/core/player/src/controllers/view/controller.ts
index 406b0c3ab..677a04a23 100644
--- a/core/player/src/controllers/view/controller.ts
+++ b/core/player/src/controllers/view/controller.ts
@@ -75,14 +75,31 @@ export class ViewController {
       }
     );
 
-    options.model.hooks.onUpdate.tap('viewController', (updates) => {
+    /** Trigger a view update */
+    const update = (updates: Set<BindingInstance>) => {
       if (this.currentView) {
         if (this.optimizeUpdates) {
-          this.queueUpdate(new Set(updates.map((t) => t.binding)));
+          this.queueUpdate(updates);
         } else {
           this.currentView.update();
         }
       }
+    };
+
+    options.model.hooks.onUpdate.tap('viewController', (updates) => {
+      update(new Set(updates.map((t) => t.binding)));
+    });
+
+    options.model.hooks.onDelete.tap('viewController', (binding) => {
+      const parentBinding = binding.parent();
+      const property = binding.key();
+
+      // Deleting an array item will trigger an update for the entire array
+      if (typeof property === 'number' && parentBinding) {
+        update(new Set([parentBinding]));
+      } else {
+        update(new Set([binding]));
+      }
     });
   }
 
diff --git a/core/player/src/data/__tests__/model.test.ts b/core/player/src/data/__tests__/model.test.ts
index df763f69a..eeb44a5ef 100644
--- a/core/player/src/data/__tests__/model.test.ts
+++ b/core/player/src/data/__tests__/model.test.ts
@@ -1,6 +1,7 @@
-import { BindingParser } from '../../binding';
+import { BindingInstance, BindingParser } from '../../binding';
 import type { DataModelMiddleware } from '..';
 import { LocalModel, PipelinedDataModel } from '..';
+import { withParser } from '../model';
 import type { BatchSetTransaction } from '../model';
 
 const { parse } = new BindingParser({
@@ -27,7 +28,8 @@ describe('model', () => {
             newTransaction.push([binding, val]);
           }
         });
-        next?.set(newTransaction, options);
+
+        return next?.set(newTransaction, options) ?? [];
       },
     };
 
@@ -45,4 +47,31 @@ describe('model', () => {
     expect(localModel.get(parse('foo.bar'))).toBe(undefined);
     expect(localModel.get(parse('foo.baz'))).toBe('good');
   });
+
+  it('works with withParser', () => {
+    const mockParse = jest.fn(() => new BindingInstance(['some', 'binding']));
+
+    const modelWithParser = withParser(model, mockParse);
+
+    modelWithParser.get('some.binding');
+
+    expect(mockParse).toHaveBeenCalledWith(
+      'some.binding',
+      expect.objectContaining({ readOnly: true })
+    );
+
+    modelWithParser.set([['some.binding', 'test']]);
+
+    expect(mockParse).toHaveBeenCalledWith(
+      'some.binding',
+      expect.objectContaining({ readOnly: false })
+    );
+
+    modelWithParser.delete(['some.binding']);
+
+    expect(mockParse).toHaveBeenCalledWith(
+      'some.binding',
+      expect.objectContaining({ readOnly: false })
+    );
+  });
 });
diff --git a/core/player/src/data/dependency-tracker.ts b/core/player/src/data/dependency-tracker.ts
index fd9a813a3..0f947511a 100644
--- a/core/player/src/data/dependency-tracker.ts
+++ b/core/player/src/data/dependency-tracker.ts
@@ -157,6 +157,15 @@ export class DependencyMiddleware
 
     return next?.get(binding, options);
   }
+
+  public delete(
+    binding: BindingInstance,
+    options?: DataModelOptions,
+    next?: DataModelImpl | undefined
+  ) {
+    this.addWriteDep(binding);
+    return next?.delete(binding, options);
+  }
 }
 
 /** A data-model that tracks dependencies of read/written data */
@@ -184,4 +193,9 @@ export class DependencyModel<Options = DataModelOptions>
 
     return this.rootModel.get(binding, options);
   }
+
+  public delete(binding: BindingInstance, options?: Options) {
+    this.addWriteDep(binding);
+    return this.rootModel.delete(binding, options);
+  }
 }
diff --git a/core/player/src/data/local-model.ts b/core/player/src/data/local-model.ts
index 9a82eadf4..e182b6960 100644
--- a/core/player/src/data/local-model.ts
+++ b/core/player/src/data/local-model.ts
@@ -1,5 +1,5 @@
 import get from 'dlv';
-import { setIn } from 'timm';
+import { setIn, omit, removeAt } from 'timm';
 import type { BindingInstance } from '../binding';
 import type { BatchSetTransaction, DataModelImpl, Updates } from './model';
 
@@ -38,4 +38,28 @@ export class LocalModel implements DataModelImpl {
     });
     return effectiveOperations;
   }
+
+  public delete(binding: BindingInstance) {
+    const parentBinding = binding.parent();
+
+    if (parentBinding) {
+      const parentValue = this.get(parentBinding);
+
+      if (parentValue !== undefined) {
+        if (Array.isArray(parentValue)) {
+          this.model = setIn(
+            this.model,
+            parentBinding.asArray(),
+            removeAt(parentValue, binding.key() as number)
+          ) as any;
+        } else {
+          this.model = setIn(
+            this.model,
+            parentBinding.asArray(),
+            omit(parentValue, binding.key() as string)
+          ) as any;
+        }
+      }
+    }
+  }
 }
diff --git a/core/player/src/data/model.ts b/core/player/src/data/model.ts
index 4bb963419..cdeac1bfb 100644
--- a/core/player/src/data/model.ts
+++ b/core/player/src/data/model.ts
@@ -56,11 +56,13 @@ export interface DataModelOptions {
 export interface DataModelWithParser<Options = DataModelOptions> {
   get(binding: BindingLike, options?: Options): any;
   set(transaction: [BindingLike, any][], options?: Options): Updates;
+  delete(binding: BindingLike, options?: Options): void;
 }
 
 export interface DataModelImpl<Options = DataModelOptions> {
   get(binding: BindingInstance, options?: Options): any;
   set(transaction: BatchSetTransaction, options?: Options): Updates;
+  delete(binding: BindingInstance, options?: Options): void;
 }
 
 export interface DataModelMiddleware {
@@ -72,11 +74,19 @@ export interface DataModelMiddleware {
     options?: DataModelOptions,
     next?: DataModelImpl
   ): Updates;
+
   get(
     binding: BindingInstance,
     options?: DataModelOptions,
     next?: DataModelImpl
   ): any;
+
+  delete?(
+    binding: BindingInstance,
+    options?: DataModelOptions,
+    next?: DataModelImpl
+  ): void;
+
   reset?(): void;
 }
 
@@ -86,12 +96,16 @@ export function withParser<Options = unknown>(
   parseBinding: BindingFactory
 ): DataModelWithParser<Options> {
   /** Parse something into a binding if it requires it */
-  function maybeParse(binding: BindingLike): BindingInstance {
+  function maybeParse(
+    binding: BindingLike,
+    readOnly: boolean
+  ): BindingInstance {
     const parsed = isBinding(binding)
       ? binding
       : parseBinding(binding, {
           get: model.get,
           set: model.set,
+          readOnly,
         });
 
     if (!parsed) {
@@ -103,14 +117,17 @@ export function withParser<Options = unknown>(
 
   return {
     get(binding, options?: Options) {
-      return model.get(maybeParse(binding), options);
+      return model.get(maybeParse(binding, true), options);
     },
     set(transaction, options?: Options) {
       return model.set(
-        transaction.map(([key, val]) => [maybeParse(key), val]),
+        transaction.map(([key, val]) => [maybeParse(key, false), val]),
         options
       );
     },
+    delete(binding, options?: Options) {
+      return model.delete(maybeParse(binding, false), options);
+    },
   };
 }
 
@@ -125,10 +142,33 @@ export function toModel(
   }
 
   return {
-    get: (binding: BindingInstance, options?: DataModelOptions) =>
-      middleware.get(binding, options ?? defaultOptions, next),
-    set: (transaction: BatchSetTransaction, options?: DataModelOptions) =>
-      middleware.set(transaction, options ?? defaultOptions, next),
+    get: (binding: BindingInstance, options?: DataModelOptions) => {
+      const resolvedOptions = options ?? defaultOptions;
+
+      if (middleware.get) {
+        return middleware.get(binding, resolvedOptions, next);
+      }
+
+      return next?.get(binding, resolvedOptions);
+    },
+    set: (transaction: BatchSetTransaction, options?: DataModelOptions) => {
+      const resolvedOptions = options ?? defaultOptions;
+
+      if (middleware.set) {
+        return middleware.set(transaction, resolvedOptions, next);
+      }
+
+      return next?.set(transaction, resolvedOptions);
+    },
+    delete: (binding: BindingInstance, options?: DataModelOptions) => {
+      const resolvedOptions = options ?? defaultOptions;
+
+      if (middleware.delete) {
+        return middleware.delete(binding, resolvedOptions, next);
+      }
+
+      return next?.delete(binding, resolvedOptions);
+    },
   };
 }
 
@@ -145,7 +185,7 @@ export function constructModelForPipeline(
   }
 
   if (pipeline.length === 1) {
-    return pipeline[0];
+    return toModel(pipeline[0]);
   }
 
   /** Default and propagate the options into the nested calls */
@@ -166,6 +206,9 @@ export function constructModelForPipeline(
     set: (transaction, options) => {
       return createModelWithOptions(options)?.set(transaction, options);
     },
+    delete: (binding, options) => {
+      return createModelWithOptions(options)?.delete(binding, options);
+    },
   };
 }
 
@@ -218,4 +261,8 @@ export class PipelinedDataModel implements DataModelImpl {
   public get(binding: BindingInstance, options?: DataModelOptions): any {
     return this.effectiveDataModel.get(binding, options);
   }
+
+  public delete(binding: BindingInstance, options?: DataModelOptions): void {
+    return this.effectiveDataModel.delete(binding, options);
+  }
 }
diff --git a/core/player/src/data/noop-model.ts b/core/player/src/data/noop-model.ts
index 635f523c6..5de123541 100644
--- a/core/player/src/data/noop-model.ts
+++ b/core/player/src/data/noop-model.ts
@@ -12,6 +12,8 @@ export class NOOPDataModel implements DataModelImpl {
   set() {
     return [];
   }
+
+  delete() {}
 }
 
 /** You only really need 1 instance of the NOOP model */
diff --git a/core/player/src/expressions/__tests__/evaluator.test.ts b/core/player/src/expressions/__tests__/evaluator.test.ts
index 21d2727a1..0b1d00392 100644
--- a/core/player/src/expressions/__tests__/evaluator.test.ts
+++ b/core/player/src/expressions/__tests__/evaluator.test.ts
@@ -2,6 +2,7 @@ import type { DataModelWithParser } from '../../data';
 import { LocalModel, withParser } from '../../data';
 import type { BindingLike } from '../../binding';
 import { BindingParser } from '../../binding';
+import type { ExpressionType } from '..';
 import { ExpressionEvaluator } from '..';
 
 describe('evaluator', () => {
@@ -14,7 +15,7 @@ describe('evaluator', () => {
     const bindingParser = new BindingParser({
       get: localModel.get,
       set: localModel.set,
-      evaluate: (exp) => {
+      evaluate: (exp: ExpressionType) => {
         return evaluator.evaluate(exp);
       },
     });
@@ -24,6 +25,23 @@ describe('evaluator', () => {
     evaluator = new ExpressionEvaluator({ model });
   });
 
+  test('resolveOptions hook', () => {
+    model = withParser(new LocalModel({ foo: 2 }), parseBinding);
+    evaluator = new ExpressionEvaluator({ model });
+
+    const testFn = jest.fn();
+
+    evaluator.hooks.resolveOptions.tap('test', (hookOptions) => {
+      testFn.mockImplementation((value: any) => hookOptions.model.set(value));
+
+      return { ...hookOptions, model: { ...hookOptions.model, set: testFn } };
+    });
+
+    evaluator.evaluate('{{foo}} = 3');
+    expect(model.get('foo')).toStrictEqual(3);
+    expect(testFn).toBeCalled();
+  });
+
   test('member expression', () => {
     evaluator.setExpressionVariable('foo', { bar: 'baz' });
     expect(evaluator.evaluate('foo["bar"]')).toStrictEqual('baz');
@@ -82,7 +100,7 @@ describe('evaluator', () => {
       },
     });
 
-    expect(evaluator.evaluate({ foo: '1 + 2' })).toStrictEqual(3);
+    expect(evaluator.evaluate({ value: '1 + 2' })).toStrictEqual(3);
   });
   test('functions', () => {
     model = withParser(new LocalModel({ test: 2 }), parseBinding);
@@ -267,6 +285,15 @@ describe('evaluator', () => {
       });
       expect(model.get('foo')).toStrictEqual(2);
     });
+    test('conditional', () => {
+      expect(
+        evaluator.evaluate('conditional(true, true, false)')
+      ).toStrictEqual(true);
+
+      expect(
+        evaluator.evaluate('conditional(false, true, false)')
+      ).toStrictEqual(false);
+    });
   });
   describe('not supported', () => {
     test('this ref', () => {
@@ -279,7 +306,7 @@ describe('evaluator', () => {
   });
 
   describe('error handling', () => {
-    test('skips throwing error when handler is provided', () => {
+    test('skips throwing error when handler is provided, but not when throwErrors is true', () => {
       const errorHandler = jest.fn();
 
       evaluator.hooks.onError.tap('test', (e) => {
@@ -291,6 +318,10 @@ describe('evaluator', () => {
       evaluator.evaluate('foo()');
 
       expect(errorHandler).toBeCalledTimes(1);
+
+      expect(() =>
+        evaluator.evaluate('foo()', { throwErrors: true, model })
+      ).toThrowError();
     });
   });
 
@@ -358,4 +389,21 @@ describe('evaluator', () => {
       'Unknown expression function: foo'
     );
   });
+
+  test('enables hooks to change expression', () => {
+    evaluator.hooks.beforeEvaluate.tap('test', (expression) => {
+      return `'foo' == 'bar'`;
+    });
+
+    expect(evaluator.evaluate('bar()')).toStrictEqual(false);
+  });
+
+  test('ignores props other than value on expression', () => {
+    expect(
+      evaluator.evaluate({
+        _comment: 'hello world',
+        value: true,
+      } as any)
+    ).toStrictEqual(true);
+  });
 });
diff --git a/core/player/src/expressions/evaluator-functions.ts b/core/player/src/expressions/evaluator-functions.ts
index 16f5db68a..3759b4521 100644
--- a/core/player/src/expressions/evaluator-functions.ts
+++ b/core/player/src/expressions/evaluator-functions.ts
@@ -1,7 +1,11 @@
 import type { Binding } from '@player-ui/types';
 
 import type { BindingLike } from '../binding';
-import type { ExpressionHandler, ExpressionContext } from './types';
+import type {
+  ExpressionHandler,
+  ExpressionContext,
+  ExpressionNode,
+} from './types';
 
 /** Sets a value to the data-model */
 export const setDataVal: ExpressionHandler<[Binding, any], any> = (
@@ -25,5 +29,23 @@ export const deleteDataVal: ExpressionHandler<[Binding], void> = (
   _context: ExpressionContext,
   binding
 ) => {
-  return _context.model.set([[binding as BindingLike, undefined]]);
+  return _context.model.delete(binding);
 };
+
+/** Conditional expression handler */
+export const conditional: ExpressionHandler<
+  [ExpressionNode, ExpressionNode, ExpressionNode?]
+> = (ctx, condition, ifTrue, ifFalse) => {
+  const resolution = ctx.evaluate(condition);
+  if (resolution) {
+    return ctx.evaluate(ifTrue);
+  }
+
+  if (ifFalse) {
+    return ctx.evaluate(ifFalse);
+  }
+
+  return null;
+};
+
+conditional.resolveParams = false;
diff --git a/core/player/src/expressions/evaluator.ts b/core/player/src/expressions/evaluator.ts
index 667da3522..e754e3538 100644
--- a/core/player/src/expressions/evaluator.ts
+++ b/core/player/src/expressions/evaluator.ts
@@ -2,6 +2,7 @@ import { SyncWaterfallHook, SyncBailHook } from 'tapable-ts';
 import { parseExpression } from './parser';
 import * as DEFAULT_EXPRESSION_HANDLERS from './evaluator-functions';
 import { isExpressionNode } from './types';
+import { isObjectExpression } from './utils';
 import type {
   ExpressionNode,
   BinaryOperator,
@@ -71,6 +72,11 @@ const DEFAULT_UNARY_OPERATORS: Record<string, UnaryOperator> = {
 export interface HookOptions extends ExpressionContext {
   /** Given an expression node  */
   resolveNode: (node: ExpressionNode) => any;
+
+  /** Enabling this flag skips calling the onError hook, and just throws errors back to the caller.
+   * The caller is responsible for handling the error.
+   */
+  throwErrors?: boolean;
 }
 
 export type ExpressionEvaluatorOptions = Omit<
@@ -92,6 +98,12 @@ export class ExpressionEvaluator {
     /** Resolve an AST node for an expression to a value */
     resolve: new SyncWaterfallHook<[any, ExpressionNode, HookOptions]>(),
 
+    /** Gets the options that will be passed in calls to the resolve hook */
+    resolveOptions: new SyncWaterfallHook<[HookOptions]>(),
+
+    /** Allows users to change the expression to be evaluated before processing */
+    beforeEvaluate: new SyncWaterfallHook<[ExpressionType, HookOptions]>(),
+
     /**
      * An optional means of handling an error in the expression execution
      * Return true if handled, to stop propagation of the error
@@ -128,14 +140,22 @@ export class ExpressionEvaluator {
   }
 
   public evaluate(
-    expression: ExpressionType,
+    expr: ExpressionType,
     options?: ExpressionEvaluatorOptions
   ): any {
-    const opts = {
+    const resolvedOpts = this.hooks.resolveOptions.call({
       ...this.defaultHookOptions,
       ...options,
-      resolveNode: (node: ExpressionNode) => this._execAST(node, opts),
-    };
+      resolveNode: (node: ExpressionNode) => this._execAST(node, resolvedOpts),
+    });
+
+    let expression = this.hooks.beforeEvaluate.call(expr, resolvedOpts) ?? expr;
+
+    // Unwrap any returned expression type
+    // Since this could also be an object type, we need to recurse through it until we find the end
+    while (isObjectExpression(expression)) {
+      expression = expression.value;
+    }
 
     // Check for literals
     if (
@@ -149,21 +169,17 @@ export class ExpressionEvaluator {
 
     // Skip doing anything with objects that are _actually_ just parsed expression nodes
     if (isExpressionNode(expression)) {
-      return this._execAST(expression, opts);
+      return this._execAST(expression, resolvedOpts);
     }
 
-    if (typeof expression === 'object') {
-      const values = Array.isArray(expression)
-        ? expression
-        : Object.values(expression);
-
-      return values.reduce(
+    if (Array.isArray(expression)) {
+      return expression.reduce(
         (_nothing, exp) => this.evaluate(exp, options),
         null
       );
     }
 
-    return this._execString(String(expression), opts);
+    return this._execString(String(expression), resolvedOpts);
   }
 
   public addExpressionFunction<T extends readonly unknown[], R>(
@@ -217,8 +233,8 @@ export class ExpressionEvaluator {
 
       return this._execAST(expAST, options);
     } catch (e: any) {
-      if (!this.hooks.onError.call(e)) {
-        // Only throw the error if it's not handled by the hook
+      if (options.throwErrors || !this.hooks.onError.call(e)) {
+        // Only throw the error if it's not handled by the hook, or throwErrors is true
         throw e;
       }
     }
@@ -305,35 +321,23 @@ export class ExpressionEvaluator {
     if (node.type === 'CallExpression') {
       const expressionName = node.callTarget.name;
 
-      // Treat the conditional operator as special.
-      // Don't exec the arguments that don't apply
-      if (expressionName === 'conditional') {
-        const condition = resolveNode(node.args[0]);
-
-        if (condition) {
-          return resolveNode(node.args[1]);
-        }
-
-        if (node.args[2]) {
-          return resolveNode(node.args[2]);
-        }
-
-        return null;
-      }
-
       const operator = this.operators.expressions.get(expressionName);
 
       if (!operator) {
         throw new Error(`Unknown expression function: ${expressionName}`);
       }
 
+      if ('resolveParams' in operator && operator.resolveParams === false) {
+        return operator(expressionContext, ...node.args);
+      }
+
       const args = node.args.map((n) => resolveNode(n));
 
       return operator(expressionContext, ...args);
     }
 
     if (node.type === 'ModelRef') {
-      return model.get(node.ref);
+      return model.get(node.ref, { context: { model: options.model } });
     }
 
     if (node.type === 'MemberExpression') {
diff --git a/core/player/src/expressions/types.ts b/core/player/src/expressions/types.ts
index ae9622dc6..583c3c170 100644
--- a/core/player/src/expressions/types.ts
+++ b/core/player/src/expressions/types.ts
@@ -1,17 +1,24 @@
 import type { DataModelWithParser } from '../data';
 import type { Logger } from '../logger';
 
+export type ExpressionObjectType = {
+  /** The expression to eval */
+  value: BasicExpressionTypes;
+};
+
 export type ExpressionLiteralType =
   | string
   | number
   | boolean
   | undefined
   | null;
-export type ExpressionType =
-  | object
+
+export type BasicExpressionTypes =
   | ExpressionLiteralType
-  | Array<ExpressionLiteralType>
-  | ExpressionNode;
+  | ExpressionObjectType
+  | Array<ExpressionLiteralType | ExpressionObjectType>;
+
+export type ExpressionType = BasicExpressionTypes | ExpressionNode;
 
 export interface OperatorProcessingOptions {
   /**
@@ -53,7 +60,12 @@ export const ExpNodeOpaqueIdentifier = Symbol('Expression Node ID');
 
 /** Checks if the input is an already processed Expression node */
 export function isExpressionNode(x: any): x is ExpressionNode {
-  return typeof x === 'object' && x.__id === ExpNodeOpaqueIdentifier;
+  return (
+    typeof x === 'object' &&
+    x !== null &&
+    !Array.isArray(x) &&
+    x.__id === ExpNodeOpaqueIdentifier
+  );
 }
 
 export interface NodePosition {
diff --git a/core/player/src/expressions/utils.ts b/core/player/src/expressions/utils.ts
index b74a1da2d..d48ea8873 100644
--- a/core/player/src/expressions/utils.ts
+++ b/core/player/src/expressions/utils.ts
@@ -1,6 +1,9 @@
+import { isExpressionNode } from './types';
 import type {
   ExpressionHandler,
   ExpressionNode,
+  ExpressionObjectType,
+  ExpressionType,
   NodeLocation,
   NodePosition,
 } from './types';
@@ -129,3 +132,19 @@ export function findClosestNodeAtPosition(
     return node;
   }
 }
+
+/** Checks if the expression is a simple type */
+export function isObjectExpression(
+  expr: ExpressionType
+): expr is ExpressionObjectType {
+  if (isExpressionNode(expr)) {
+    return false;
+  }
+
+  return (
+    typeof expr === 'object' &&
+    expr !== null &&
+    !Array.isArray(expr) &&
+    'value' in expr
+  );
+}
diff --git a/core/player/src/player.ts b/core/player/src/player.ts
index dcb310971..d465650e4 100644
--- a/core/player/src/player.ts
+++ b/core/player/src/player.ts
@@ -1,12 +1,11 @@
 import { setIn } from 'timm';
 import deferred from 'p-defer';
-import queueMicrotask from 'queue-microtask';
 import type { Flow as FlowType, FlowResult } from '@player-ui/types';
 
 import { SyncHook, SyncWaterfallHook } from 'tapable-ts';
 import type { Logger } from './logger';
 import { TapableLogger } from './logger';
-import type { ExpressionHandler } from './expressions';
+import type { ExpressionType } from './expressions';
 import { ExpressionEvaluator } from './expressions';
 import { SchemaController } from './schema';
 import { BindingParser } from './binding';
@@ -289,10 +288,11 @@ export class Player {
     });
 
     /** Resolve any data references in a string */
-    function resolveStrings<T>(val: T) {
+    function resolveStrings<T>(val: T, formatted?: boolean) {
       return resolveDataRefs(val, {
         model: dataController,
         evaluate: expressionEvaluator.evaluate,
+        formatted,
       });
     }
 
@@ -306,7 +306,7 @@ export class Player {
           if (typeof state.onEnd === 'object' && 'exp' in state.onEnd) {
             expressionEvaluator?.evaluate(state.onEnd.exp);
           } else {
-            expressionEvaluator?.evaluate(state.onEnd);
+            expressionEvaluator?.evaluate(state.onEnd as ExpressionType);
           }
         }
 
@@ -353,7 +353,7 @@ export class Player {
           newState = setIn(
             state,
             ['param'],
-            resolveStrings(state.param)
+            resolveStrings(state.param, false)
           ) as any;
         }
 
@@ -361,26 +361,18 @@ export class Player {
       });
 
       flow.hooks.transition.tap('player', (_oldState, newState) => {
-        if (newState.value.state_type === 'ACTION') {
-          const { exp } = newState.value;
-
-          // The nested transition call would trigger another round of the flow transition hooks to be called.
-          // This created a weird timing where this nested transition would happen before the view had a chance to respond to the first one
-
-          // Additionally, because we are using queueMicrotask, errors could get swallowed in the detached queue
-          // Use a try catch and fail player explicitly if any errors are caught in the nested transition/state
-          queueMicrotask(() => {
-            try {
-              flowController?.transition(
-                String(expressionEvaluator?.evaluate(exp))
-              );
-            } catch (error) {
-              const state = this.getState();
-              if (error instanceof Error && state.status === 'in-progress') {
-                state.fail(error);
-              }
-            }
-          });
+        if (newState.value.state_type !== 'VIEW') {
+          validationController.reset();
+        }
+      });
+
+      flow.hooks.afterTransition.tap('player', (flowInstance) => {
+        const value = flowInstance.currentState?.value;
+        if (value && value.state_type === 'ACTION') {
+          const { exp } = value;
+          flowController?.transition(
+            String(expressionEvaluator?.evaluate(exp))
+          );
         }
 
         expressionEvaluator.reset();
@@ -402,6 +394,11 @@ export class Player {
       parseBinding,
       transition: flowController.transition,
       model: dataController,
+      utils: {
+        findPlugin: <Plugin = unknown>(pluginSymbol: symbol) => {
+          return this.findPlugin(pluginSymbol) as unknown as Plugin;
+        },
+      },
       logger: this.logger,
       flowController,
       schema,
@@ -419,6 +416,7 @@ export class Player {
         ...validationController.forView(parseBinding),
         type: (b) => schema.getType(parseBinding(b)),
       },
+      constants: this.constantsController,
     });
     viewController.hooks.view.tap('player', (view) => {
       validationController.onView(view);
@@ -432,7 +430,7 @@ export class Player {
           .start()
           .then((endState) => {
             const flowResult: FlowResult = {
-              endState: resolveStrings(endState),
+              endState: resolveStrings(endState, false),
               data: dataController.serialize(),
             };
 
@@ -503,7 +501,6 @@ export class Player {
         ref,
         status: 'completed',
         flow: state.flow,
-        dataModel: state.controllers.data.getModel(),
       } as const;
 
       return maybeUpdateState({
diff --git a/core/player/src/plugins/flow-exp-plugin.ts b/core/player/src/plugins/flow-exp-plugin.ts
index e6ec6903a..5050e30c8 100644
--- a/core/player/src/plugins/flow-exp-plugin.ts
+++ b/core/player/src/plugins/flow-exp-plugin.ts
@@ -3,7 +3,7 @@ import type {
   ExpressionObject,
   NavigationFlowState,
 } from '@player-ui/types';
-import type { ExpressionEvaluator } from '../expressions';
+import type { ExpressionEvaluator, ExpressionType } from '../expressions';
 import type { FlowInstance } from '../controllers';
 import type { Player, PlayerPlugin } from '../player';
 
@@ -28,7 +28,7 @@ export class FlowExpPlugin implements PlayerPlugin {
         if (typeof exp === 'object' && 'exp' in exp) {
           expressionEvaluator?.evaluate(exp.exp);
         } else {
-          expressionEvaluator?.evaluate(exp);
+          expressionEvaluator?.evaluate(exp as ExpressionType);
         }
       }
     };
diff --git a/core/player/src/string-resolver/__tests__/index.test.ts b/core/player/src/string-resolver/__tests__/index.test.ts
index 7f46d276d..93667a8a1 100644
--- a/core/player/src/string-resolver/__tests__/index.test.ts
+++ b/core/player/src/string-resolver/__tests__/index.test.ts
@@ -1,5 +1,7 @@
 import type { Expression } from '@player-ui/types';
-
+import { CommonTypesPlugin } from '@player-ui/common-types-plugin';
+import { Player } from '../../player';
+import type { InProgressState } from '../../types';
 import { BindingParser } from '../../binding';
 import { LocalModel, withParser } from '../../data';
 import { resolveDataRefs, resolveExpressionsInString } from '..';
@@ -214,3 +216,118 @@ test('resolves expressions', () => {
 
   expect(resolveDataRefs('@[{{adam.age}} + 10]@', options)).toBe(36);
 });
+
+describe('Returns unformatted values for requests', () => {
+  const player = new Player({ plugins: [new CommonTypesPlugin()] });
+
+  const endStateFlow = {
+    id: 'minimal-player-response-format',
+    topic: 'MOCK',
+    schema: {
+      ROOT: {
+        phoneNumber: {
+          type: 'PhoneType',
+          default: 'false',
+        },
+      },
+    },
+    data: {
+      phoneNumber: '1234567890',
+    },
+    views: [
+      {
+        actions: [
+          {
+            asset: {
+              id: 'action-1',
+              type: 'action',
+              value: 'Next',
+              label: {
+                asset: {
+                  id: 'Action-Label-Next',
+                  type: 'text',
+                  value: 'Continue',
+                },
+              },
+            },
+          },
+        ],
+        id: 'KitchenSink-View1',
+        title: {
+          asset: {
+            id: 'KitchenSink-View1-Title',
+            type: 'text',
+            value: '{{phoneNumber}}',
+          },
+        },
+        type: 'questionAnswer',
+      },
+    ],
+    navigation: {
+      BEGIN: 'KitchenSinkFlow',
+      KitchenSinkFlow: {
+        END_Done: {
+          outcome: '{{phoneNumber}}',
+          state_type: 'END',
+        },
+        VIEW_KitchenSink_1: {
+          ref: 'KitchenSink-View1',
+          state_type: 'VIEW',
+          transitions: {
+            param: 'END_invokeWithParam',
+            '*': 'END_Done',
+          },
+        },
+        END_invokeWithParam: {
+          state_type: 'END',
+          outcome: '{{phoneNumber}}',
+          param: {
+            type: 'someTopic',
+            topicId: 'someTopicId',
+            navData: {
+              topic: 'someTopic',
+              op: 'EDIT',
+              param: {
+                phone: '{{phoneNumber}}',
+              },
+            },
+          },
+        },
+        startState: 'VIEW_KitchenSink_1',
+      },
+    },
+  };
+
+  test('unformatted endState', async () => {
+    player.start(endStateFlow as any);
+
+    const state = player.getState() as InProgressState;
+
+    state.controllers.flow.transition('foo');
+
+    const { flowResult } = state;
+
+    const result = await flowResult;
+
+    expect(result.endState).toStrictEqual({
+      outcome: '1234567890',
+      state_type: 'END',
+    });
+  });
+
+  test('unformatted "param"', async () => {
+    player.start(endStateFlow as any);
+
+    const state = player.getState() as InProgressState;
+
+    state.controllers.flow.transition('param');
+
+    const { flowResult } = state;
+
+    const result = await flowResult;
+
+    const param = result.endState.param as any;
+
+    expect(param.navData.param.phone).toBe('1234567890');
+  });
+});
diff --git a/core/player/src/string-resolver/index.ts b/core/player/src/string-resolver/index.ts
index 89383d710..5b53757cf 100644
--- a/core/player/src/string-resolver/index.ts
+++ b/core/player/src/string-resolver/index.ts
@@ -17,6 +17,11 @@ export interface Options {
    * Passing `false` will skip trying to evaluate any expressions (@[ foo() ]@)
    */
   evaluate: false | ((exp: Expression) => any);
+
+  /**
+   * Optionaly resolve binding without formatting in case Type format applies
+   */
+  formatted?: boolean;
 }
 
 /** Search the given string for the coordinates of the next expression to resolve */
@@ -116,7 +121,7 @@ export function resolveExpressionsInString(
 
 /** Return a string with all data model references resolved */
 export function resolveDataRefsInString(val: string, options: Options): string {
-  const { model } = options;
+  const { model, formatted = true } = options;
   let workingString = resolveExpressionsInString(val, options);
 
   if (
@@ -144,7 +149,7 @@ export function resolveDataRefsInString(val: string, options: Options): string {
       )
       .trim();
 
-    const evaledVal = model.get(binding, { formatted: true });
+    const evaledVal = model.get(binding, { formatted });
 
     // Exit early if the string is _just_ a model lookup
     // If the result is a string, we may need further processing for nested bindings
diff --git a/core/player/src/types.ts b/core/player/src/types.ts
index 2296a62e9..20c8a5ac3 100644
--- a/core/player/src/types.ts
+++ b/core/player/src/types.ts
@@ -85,10 +85,7 @@ export type InProgressState = BaseFlowState<'in-progress'> &
 /** The flow completed properly */
 export type CompletedState = BaseFlowState<'completed'> &
   PlayerFlowExecutionData &
-  FlowResult & {
-    /** The top-level data-model for the flow */
-    dataModel: DataModelWithParser;
-  };
+  FlowResult;
 
 /** The flow finished but not successfully */
 export type ErrorState = BaseFlowState<'error'> & {
diff --git a/core/player/src/validator/__tests__/binding-map-splice.test.ts b/core/player/src/validator/__tests__/binding-map-splice.test.ts
new file mode 100644
index 000000000..53b2a0de8
--- /dev/null
+++ b/core/player/src/validator/__tests__/binding-map-splice.test.ts
@@ -0,0 +1,52 @@
+import { BindingParser } from '../../binding';
+import { removeBindingAndChildrenFromMap } from '../binding-map-splice';
+
+const parser = new BindingParser({
+  get: () => undefined,
+  set: () => undefined,
+  evaluate: () => undefined,
+});
+
+describe('removeBindingAndChildrenFromMap', () => {
+  it('removes a binding and its chidlren', () => {
+    const sourceMap = new Map([
+      [parser.parse('foo.bar.baz'), 1],
+      [parser.parse('foo.bar'), 2],
+      [parser.parse('foo.baz'), 3],
+    ]);
+
+    const result = removeBindingAndChildrenFromMap(
+      sourceMap,
+      parser.parse('foo.bar')
+    );
+
+    expect(result).toStrictEqual(new Map([[parser.parse('foo.baz'), 3]]));
+  });
+
+  it('splices a binding and its children', () => {
+    const sourceMap = new Map([
+      [parser.parse('foo.bar.0.aaa'), 1],
+      [parser.parse('foo.bar.0.aab'), 2],
+      [parser.parse('foo.bar.1.bba'), 3],
+      [parser.parse('foo.bar.1.bbb'), 4],
+      [parser.parse('foo.bar.2.cca'), 5],
+      [parser.parse('foo.bar.2.ccb'), 6],
+      [parser.parse('foo.baz'), 3],
+    ]);
+
+    const result = removeBindingAndChildrenFromMap(
+      sourceMap,
+      parser.parse('foo.bar.1')
+    );
+
+    expect(result).toStrictEqual(
+      new Map([
+        [parser.parse('foo.bar.0.aaa'), 1],
+        [parser.parse('foo.bar.0.aab'), 2],
+        [parser.parse('foo.bar.1.cca'), 5],
+        [parser.parse('foo.bar.1.ccb'), 6],
+        [parser.parse('foo.baz'), 3],
+      ])
+    );
+  });
+});
diff --git a/core/player/src/validator/__tests__/validation-middleware.test.ts b/core/player/src/validator/__tests__/validation-middleware.test.ts
index b8d8059c6..9a83935ef 100644
--- a/core/player/src/validator/__tests__/validation-middleware.test.ts
+++ b/core/player/src/validator/__tests__/validation-middleware.test.ts
@@ -10,6 +10,7 @@ const parser = new BindingParser({
   evaluate: () => undefined,
 });
 const foo = parser.parse('foo');
+const bar = parser.parse('bar');
 
 describe('middleware', () => {
   /**
@@ -50,6 +51,41 @@ describe('middleware', () => {
     ).toStrictEqual('baz');
     expect(baseDataModel.get(foo)).toStrictEqual('bar');
   });
+
+  it('only returns updates for bindings set in the same transaction', () => {
+    // Setup the invalid data
+    const invalidUpdates = dataModelWithMiddleware.set(
+      [[foo, 'baz']],
+      undefined,
+      baseDataModel
+    );
+
+    expect(invalidUpdates).toMatchInlineSnapshot(`
+      Array [
+        Object {
+          "binding": BindingInstance {
+            "factory": [Function],
+            "joined": "foo",
+            "split": Array [
+              "foo",
+            ],
+          },
+          "force": true,
+          "newValue": "baz",
+          "oldValue": "baz",
+        },
+      ]
+    `);
+
+    // Set some unrelated data
+    const validUpdates = dataModelWithMiddleware.set(
+      [[bar, 'baz']],
+      undefined,
+      baseDataModel
+    );
+
+    expect(validUpdates).toHaveLength(1);
+  });
 });
 
 test('merges invalid', () => {
diff --git a/core/player/src/validator/binding-map-splice.ts b/core/player/src/validator/binding-map-splice.ts
new file mode 100644
index 000000000..25520b60f
--- /dev/null
+++ b/core/player/src/validator/binding-map-splice.ts
@@ -0,0 +1,59 @@
+import type { BindingInstance } from '../binding';
+
+/**
+ * Remove a binding, and any children from from the map
+ * If the binding is an array-item, then it will be spliced from the array and the others will be shifted down
+ *
+ * @param sourceMap - A map of bindings to values
+ * @param binding - The binding to remove from the map
+ */
+export function removeBindingAndChildrenFromMap<T>(
+  sourceMap: Map<BindingInstance, T>,
+  binding: BindingInstance
+): Map<BindingInstance, T> {
+  const targetMap = new Map(sourceMap);
+
+  const parentBinding = binding.parent();
+  const property = binding.key();
+
+  // Clear out any that are sub-bindings of this binding
+
+  targetMap.forEach((_value, trackedBinding) => {
+    if (binding === trackedBinding || binding.contains(trackedBinding)) {
+      targetMap.delete(trackedBinding);
+    }
+  });
+
+  if (typeof property === 'number') {
+    // Splice out this index from the rest
+
+    // Order matters here b/c we are shifting items in the array
+    // Start with the smallest index and work our way down
+    const bindingsToRewrite = Array.from(sourceMap.keys())
+      .filter((b) => {
+        if (parentBinding.contains(b)) {
+          const [childIndex] = b.relative(parentBinding);
+          return typeof childIndex === 'number' && childIndex > property;
+        }
+
+        return false;
+      })
+      .sort();
+
+    bindingsToRewrite.forEach((trackedBinding) => {
+      // If the tracked binding is a sub-binding of the parent binding, then we need to
+      // update the path to reflect the new index
+
+      const [childIndex, ...childPath] = trackedBinding.relative(parentBinding);
+
+      if (typeof childIndex === 'number') {
+        const newSegments = [childIndex - 1, ...childPath];
+        const newChildBinding = parentBinding.descendent(newSegments);
+        targetMap.set(newChildBinding, targetMap.get(trackedBinding) as T);
+        targetMap.delete(trackedBinding);
+      }
+    });
+  }
+
+  return targetMap;
+}
diff --git a/core/player/src/validator/index.ts b/core/player/src/validator/index.ts
index feca52b30..6bdf33ebb 100644
--- a/core/player/src/validator/index.ts
+++ b/core/player/src/validator/index.ts
@@ -1,3 +1,4 @@
 export * from './validation-middleware';
 export * from './types';
 export * from './registry';
+export * from './binding-map-splice';
diff --git a/core/player/src/validator/types.ts b/core/player/src/validator/types.ts
index 65d105702..77206f350 100644
--- a/core/player/src/validator/types.ts
+++ b/core/player/src/validator/types.ts
@@ -1,4 +1,4 @@
-import type { Validation } from '@player-ui/types';
+import type { Schema, Validation } from '@player-ui/types';
 
 import type { BindingInstance, BindingFactory } from '../binding';
 import type { DataModelWithParser } from '../data';
@@ -40,12 +40,17 @@ type RequiredValidationKeys = 'severity' | 'trigger';
 export type ValidationObject = Validation.Reference &
   Required<Pick<Validation.Reference, RequiredValidationKeys>>;
 
+export type ValidationObjectWithHandler = ValidationObject & {
+  /** A predefined handler for this validation object */
+  handler?: ValidatorFunction;
+};
+
 export interface ValidationProvider {
   getValidationsForBinding?(
     binding: BindingInstance
-  ): Array<ValidationObject> | undefined;
+  ): Array<ValidationObjectWithHandler> | undefined;
 
-  getValidationsForView?(): Array<ValidationObject> | undefined;
+  getValidationsForView?(): Array<ValidationObjectWithHandler> | undefined;
 }
 
 export interface ValidatorContext {
@@ -66,6 +71,9 @@ export interface ValidatorContext {
 
   /** The constants for messages */
   constants: ConstantsProvider;
+
+  /** The type in the schema that triggered the validation if there is one */
+  schemaType: Schema.DataType | undefined;
 }
 
 export type ValidatorFunction<Options = unknown> = (
diff --git a/core/player/src/validator/validation-middleware.ts b/core/player/src/validator/validation-middleware.ts
index 7c622dceb..f77c17a53 100644
--- a/core/player/src/validator/validation-middleware.ts
+++ b/core/player/src/validator/validation-middleware.ts
@@ -11,6 +11,7 @@ import { toModel } from '../data';
 import type { Logger } from '../logger';
 
 import type { ValidationResponse } from './types';
+import { removeBindingAndChildrenFromMap } from './binding-map-splice';
 
 /**
  * A BindingInstance with an indicator of whether or not it's a strong binding
@@ -37,17 +38,21 @@ export class ValidationMiddleware implements DataModelMiddleware {
   public validator: MiddlewareChecker;
   public shadowModelPaths: Map<BindingInstance, any>;
   private logger?: Logger;
+  private shouldIncludeInvalid?: (options?: DataModelOptions) => boolean;
 
   constructor(
     validator: MiddlewareChecker,
     options?: {
       /** A logger instance */
       logger?: Logger;
+      /** Optional function to include data staged in shadowModel */
+      shouldIncludeInvalid?: (options?: DataModelOptions) => boolean;
     }
   ) {
     this.validator = validator;
     this.shadowModelPaths = new Map();
     this.logger = options?.logger;
+    this.shouldIncludeInvalid = options?.shouldIncludeInvalid;
   }
 
   public set(
@@ -58,8 +63,11 @@ export class ValidationMiddleware implements DataModelMiddleware {
     const asModel = toModel(this, { ...options, includeInvalid: true }, next);
     const nextTransaction: BatchSetTransaction = [];
 
+    const includedBindings = new Set<BindingInstance>();
+
     transaction.forEach(([binding, value]) => {
       this.shadowModelPaths.set(binding, value);
+      includedBindings.add(binding);
     });
 
     const invalidBindings: Array<BindingInstance> = [];
@@ -79,7 +87,8 @@ export class ValidationMiddleware implements DataModelMiddleware {
             nextTransaction.push([validation.binding, value]);
           }
         });
-      } else {
+      } else if (includedBindings.has(binding)) {
+        invalidBindings.push(binding);
         this.logger?.debug(
           `Invalid value for path: ${binding.asString()} - ${
             validations.severity
@@ -88,6 +97,8 @@ export class ValidationMiddleware implements DataModelMiddleware {
       }
     });
 
+    let validResults: Updates = [];
+
     if (next && nextTransaction.length > 0) {
       // defer clearing the shadow model to prevent validations that are run twice due to weak binding refs still needing the data
       nextTransaction.forEach(([binding]) =>
@@ -97,9 +108,11 @@ export class ValidationMiddleware implements DataModelMiddleware {
       if (invalidBindings.length === 0) {
         return result;
       }
+
+      validResults = result;
     }
 
-    return invalidBindings.map((binding) => {
+    const invalidResults = invalidBindings.map((binding) => {
       return {
         binding,
         oldValue: asModel.get(binding),
@@ -107,6 +120,8 @@ export class ValidationMiddleware implements DataModelMiddleware {
         force: true,
       };
     });
+
+    return [...validResults, ...invalidResults];
   }
 
   public get(
@@ -116,7 +131,10 @@ export class ValidationMiddleware implements DataModelMiddleware {
   ) {
     let val = next?.get(binding, options);
 
-    if (options?.includeInvalid === true) {
+    if (
+      this.shouldIncludeInvalid?.(options) ??
+      options?.includeInvalid === true
+    ) {
       this.shadowModelPaths.forEach((shadowValue, shadowBinding) => {
         if (shadowBinding === binding) {
           val = shadowValue;
@@ -132,4 +150,17 @@ export class ValidationMiddleware implements DataModelMiddleware {
 
     return val;
   }
+
+  public delete(
+    binding: BindingInstance,
+    options?: DataModelOptions,
+    next?: DataModelImpl
+  ) {
+    this.shadowModelPaths = removeBindingAndChildrenFromMap(
+      this.shadowModelPaths,
+      binding
+    );
+
+    return next?.delete(binding, options);
+  }
 }
diff --git a/core/player/src/view/parser/__tests__/__snapshots__/parser.test.ts.snap b/core/player/src/view/parser/__tests__/__snapshots__/parser.test.ts.snap
index 0789b5d28..29a84841f 100644
--- a/core/player/src/view/parser/__tests__/__snapshots__/parser.test.ts.snap
+++ b/core/player/src/view/parser/__tests__/__snapshots__/parser.test.ts.snap
@@ -50,6 +50,115 @@ Object {
 }
 `;
 
+exports[`generates the correct AST when using switch plugin works with asset wrapped objects 1`] = `
+Object {
+  "children": Array [
+    Object {
+      "path": Array [
+        "title",
+        "asset",
+      ],
+      "value": Object {
+        "parent": [Circular],
+        "type": "asset",
+        "value": Object {
+          "id": "businessprofile-tile-screen-yoy-subtitle",
+          "type": "text",
+          "value": "If it's changed since last year, let us know. Feel free to pick more than one.",
+        },
+      },
+    },
+  ],
+  "type": "value",
+  "value": Object {
+    "id": "toughView",
+    "type": "view",
+  },
+}
+`;
+
+exports[`generates the correct AST when using switch plugin works with objects in a multiNode 1`] = `
+Object {
+  "children": Array [
+    Object {
+      "path": Array [
+        "title",
+        "asset",
+      ],
+      "value": Object {
+        "children": Array [
+          Object {
+            "path": Array [
+              "values",
+            ],
+            "value": Object {
+              "override": true,
+              "parent": [Circular],
+              "type": "multi-node",
+              "values": Array [
+                Object {
+                  "children": Array [
+                    Object {
+                      "path": Array [
+                        "asset",
+                      ],
+                      "value": Object {
+                        "parent": [Circular],
+                        "type": "asset",
+                        "value": Object {
+                          "id": "businessprofile-tile-screen-yoy-subtitle-1",
+                          "type": "text",
+                          "value": "If it's changed since last year, let us know. Feel free to pick more than one.",
+                        },
+                      },
+                    },
+                  ],
+                  "parent": [Circular],
+                  "type": "value",
+                  "value": undefined,
+                },
+                Object {
+                  "children": Array [
+                    Object {
+                      "path": Array [
+                        "asset",
+                      ],
+                      "value": Object {
+                        "parent": [Circular],
+                        "type": "asset",
+                        "value": Object {
+                          "id": "businessprofile-tile-screen-yoy-subtitle-2",
+                          "type": "text",
+                          "value": "More text",
+                        },
+                      },
+                    },
+                  ],
+                  "parent": [Circular],
+                  "type": "value",
+                  "value": undefined,
+                },
+              ],
+            },
+          },
+        ],
+        "parent": [Circular],
+        "type": "asset",
+        "value": Object {
+          "id": "someMultiNode",
+          "type": "type",
+        },
+      },
+    },
+  ],
+  "type": "value",
+  "value": Object {
+    "id": "toughView",
+    "type": "view",
+  },
+}
+`;
+
 exports[`generates the correct AST works with applicability things 1`] = `
 Object {
   "expression": "{{baz}}",
@@ -116,6 +225,88 @@ Object {
 }
 `;
 
+exports[`generates the correct AST works with applicability things 3`] = `
+Object {
+  "children": Array [
+    Object {
+      "path": Array [
+        "asset",
+      ],
+      "value": Object {
+        "children": Array [
+          Object {
+            "path": Array [
+              "someProp",
+            ],
+            "value": Object {
+              "expression": "{{foo}}",
+              "parent": [Circular],
+              "type": "applicability",
+              "value": Object {
+                "parent": [Circular],
+                "type": "value",
+                "value": Object {
+                  "description": Object {
+                    "value": "description",
+                  },
+                  "label": Object {
+                    "value": "label",
+                  },
+                },
+              },
+            },
+          },
+        ],
+        "parent": [Circular],
+        "type": "asset",
+        "value": undefined,
+      },
+    },
+  ],
+  "type": "value",
+  "value": undefined,
+}
+`;
+
+exports[`generates the correct AST works with applicability things 4`] = `
+Object {
+  "children": Array [
+    Object {
+      "path": Array [
+        "asset",
+      ],
+      "value": Object {
+        "children": Array [
+          Object {
+            "path": Array [
+              "someProp",
+              "asset",
+            ],
+            "value": Object {
+              "expression": "{{foo}}",
+              "parent": [Circular],
+              "type": "applicability",
+              "value": Object {
+                "parent": [Circular],
+                "type": "asset",
+                "value": Object {
+                  "type": "someAsset",
+                },
+              },
+            },
+          },
+        ],
+        "parent": [Circular],
+        "type": "asset",
+        "value": undefined,
+      },
+    },
+  ],
+  "type": "value",
+  "value": undefined,
+}
+`;
+
 exports[`parseView parses a simple view 1`] = `
 Object {
   "children": Array [
diff --git a/core/player/src/view/parser/__tests__/parser.test.ts b/core/player/src/view/parser/__tests__/parser.test.ts
index bb3df5347..74e2689f2 100644
--- a/core/player/src/view/parser/__tests__/parser.test.ts
+++ b/core/player/src/view/parser/__tests__/parser.test.ts
@@ -73,6 +73,35 @@ describe('generates the correct AST', () => {
         },
       })
     ).toMatchSnapshot();
+
+    expect(
+      parser.parseObject({
+        asset: {
+          someProp: {
+            applicability: '{{foo}}',
+            label: {
+              value: 'label',
+            },
+            description: {
+              value: 'description',
+            },
+          },
+        },
+      })
+    ).toMatchSnapshot();
+
+    expect(
+      parser.parseObject({
+        asset: {
+          someProp: {
+            asset: {
+              applicability: '{{foo}}',
+              type: 'someAsset',
+            },
+          },
+        },
+      })
+    ).toMatchSnapshot();
   });
 
   test('parses an object', () => {
@@ -161,3 +190,74 @@ describe('parseView', () => {
     ).toMatchSnapshot();
   });
 });
+
+describe('generates the correct AST when using switch plugin', () => {
+  const toughStaticSwitchView = {
+    id: 'toughView',
+    type: 'view',
+    title: {
+      staticSwitch: [
+        {
+          case: "'true'",
+          asset: {
+            id: 'businessprofile-tile-screen-yoy-subtitle',
+            type: 'text',
+            value:
+              "If it's changed since last year, let us know. Feel free to pick more than one.",
+          },
+        },
+      ],
+    },
+  };
+
+  const toughStaticSwitchMultiNodeView = {
+    id: 'toughView',
+    type: 'view',
+    title: {
+      asset: {
+        id: 'someMultiNode',
+        type: 'type',
+        values: [
+          {
+            staticSwitch: [
+              {
+                case: "'true'",
+                asset: {
+                  id: 'businessprofile-tile-screen-yoy-subtitle-1',
+                  type: 'text',
+                  value:
+                    "If it's changed since last year, let us know. Feel free to pick more than one.",
+                },
+              },
+            ],
+          },
+          {
+            asset: {
+              id: 'businessprofile-tile-screen-yoy-subtitle-2',
+              type: 'text',
+              value: 'More text',
+            },
+          },
+        ],
+      },
+    },
+  };
+
+  const switchPlugin = new SwitchPlugin({
+    evaluate: () => {
+      return true;
+    },
+  } as any);
+  const parser = new Parser();
+  switchPlugin.applyParser(parser);
+
+  test('works with asset wrapped objects', () => {
+    expect(parser.parseObject(toughStaticSwitchView)).toMatchSnapshot();
+  });
+
+  test('works with objects in a multiNode', () => {
+    expect(
+      parser.parseObject(toughStaticSwitchMultiNodeView)
+    ).toMatchSnapshot();
+  });
+});
diff --git a/core/player/src/view/parser/index.ts b/core/player/src/view/parser/index.ts
index ce57f8318..a558157c1 100644
--- a/core/player/src/view/parser/index.ts
+++ b/core/player/src/view/parser/index.ts
@@ -83,6 +83,21 @@ export class Parser {
     return tapped;
   }
 
+  /**
+   * Checks if there are templated values in the object
+   *
+   * @param obj - The Parsed Object to check to see if we have a template array type for
+   * @param localKey - The key being checked
+   */
+  private hasTemplateValues(obj: any, localKey: string) {
+    return (
+      Object.hasOwnProperty.call(obj, 'template') &&
+      Array.isArray(obj?.template) &&
+      obj.template.length &&
+      obj.template.find((tmpl: any) => tmpl.output === localKey)
+    );
+  }
+
   public parseObject(
     obj: object,
     type: Node.ChildrenTypes = NodeType.Value,
@@ -172,6 +187,13 @@ export class Parser {
                 template
               );
 
+              if (templateAST?.type === NodeType.MultiNode) {
+                templateAST.values.forEach((v) => {
+                  // eslint-disable-next-line no-param-reassign
+                  v.parent = templateAST;
+                });
+              }
+
               if (templateAST) {
                 return {
                   path: [...path, template.output],
@@ -199,6 +221,26 @@ export class Parser {
             NodeType.Switch
           );
 
+          if (
+            localSwitch &&
+            localSwitch.type === NodeType.Value &&
+            localSwitch.children?.length === 1 &&
+            localSwitch.value === undefined
+          ) {
+            const firstChild = localSwitch.children[0];
+
+            return {
+              ...rest,
+              children: [
+                ...children,
+                {
+                  path: [...path, localKey, ...firstChild.path],
+                  value: firstChild.value,
+                },
+              ],
+            };
+          }
+
           if (localSwitch) {
             return {
               ...rest,
@@ -222,7 +264,7 @@ export class Parser {
             const multiNode = this.hooks.onCreateASTNode.call(
               {
                 type: NodeType.MultiNode,
-                override: true,
+                override: !this.hasTemplateValues(localObj, localKey),
                 values: childValues,
               },
               localValue
@@ -255,7 +297,7 @@ export class Parser {
           if (determineNodeType === NodeType.Applicability) {
             const parsedNode = this.hooks.parseNode.call(
               localValue,
-              type,
+              NodeType.Value,
               options,
               determineNodeType
             );
diff --git a/core/player/src/view/plugins/__tests__/__snapshots__/template.test.ts.snap b/core/player/src/view/plugins/__tests__/__snapshots__/template.test.ts.snap
index fd5dd14ac..468be4a9f 100644
--- a/core/player/src/view/plugins/__tests__/__snapshots__/template.test.ts.snap
+++ b/core/player/src/view/plugins/__tests__/__snapshots__/template.test.ts.snap
@@ -1,5 +1,69 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
+exports[`dynamic templates Works with template items plus value items Should show template item first when coming before values on lexical order 1`] = `
+Object {
+  "asset": Object {
+    "id": "overviewItem3",
+    "label": Object {
+      "asset": Object {
+        "id": "overviewItem3-label",
+        "type": "text",
+        "value": "1099-A",
+      },
+    },
+    "type": "overviewItem",
+    "values": Array [
+      Object {
+        "asset": Object {
+          "id": "overviewItem3-year",
+          "type": "text",
+          "value": "Desciption of concept 1099 1",
+        },
+      },
+      Object {
+        "asset": Object {
+          "id": "loverviewItem3-cy",
+          "type": "text",
+          "value": "4000",
+        },
+      },
+    ],
+  },
+}
+`;
+
+exports[`dynamic templates Works with template items plus value items Should show template item last when coming after values on lexical order 1`] = `
+Object {
+  "asset": Object {
+    "id": "overviewItem1",
+    "label": Object {
+      "asset": Object {
+        "id": "overviewItem1-label",
+        "type": "text",
+        "value": "First Summary",
+      },
+    },
+    "type": "overviewItem",
+    "values": Array [
+      Object {
+        "asset": Object {
+          "id": "overviewItem1-year",
+          "type": "text",
+          "value": "Desciption of year summary 1",
+        },
+      },
+      Object {
+        "asset": Object {
+          "id": "loverviewItem1-cy",
+          "type": "text",
+          "value": "14000",
+        },
+      },
+    ],
+  },
+}
+`;
+
 exports[`templates works with nested templates 1`] = `
 Object {
   "children": Array [
@@ -8,6 +72,7 @@ Object {
         "values",
       ],
       "value": Object {
+        "override": false,
         "parent": [Circular],
         "type": "multi-node",
         "values": Array [
@@ -24,6 +89,7 @@ Object {
                         "values",
                       ],
                       "value": Object {
+                        "override": false,
                         "parent": [Circular],
                         "type": "multi-node",
                         "values": Array [
@@ -71,6 +137,7 @@ Object {
                         "values",
                       ],
                       "value": Object {
+                        "override": false,
                         "parent": [Circular],
                         "type": "multi-node",
                         "values": Array [
@@ -118,6 +185,7 @@ Object {
                         "values",
                       ],
                       "value": Object {
+                        "override": false,
                         "parent": [Circular],
                         "type": "multi-node",
                         "values": Array [
@@ -172,6 +240,7 @@ Object {
         "values",
       ],
       "value": Object {
+        "override": false,
         "parent": [Circular],
         "type": "multi-node",
         "values": Array [
diff --git a/core/player/src/view/plugins/__tests__/applicability.test.ts b/core/player/src/view/plugins/__tests__/applicability.test.ts
index d8ed830b4..37c62e038 100644
--- a/core/player/src/view/plugins/__tests__/applicability.test.ts
+++ b/core/player/src/view/plugins/__tests__/applicability.test.ts
@@ -5,6 +5,7 @@ import { ExpressionEvaluator } from '../../../expressions';
 import { SchemaController } from '../../../schema';
 import type { Resolve } from '../../resolver';
 import { Resolver } from '../../resolver';
+import type { Node } from '../../parser';
 import { Parser } from '../../parser';
 import { ApplicabilityPlugin, StringResolverPlugin } from '..';
 
@@ -32,6 +33,40 @@ describe('applicability', () => {
     };
   });
 
+  it('undefined does not remove asset', () => {
+    const aP = new ApplicabilityPlugin();
+    const sP = new StringResolverPlugin();
+
+    aP.applyParser(parser);
+
+    const root = parser.parseObject({
+      asset: {
+        values: [
+          {
+            applicability: '{{foo}}',
+            value: 'foo',
+          },
+          {
+            value: 'bar',
+          },
+        ],
+      },
+    });
+
+    const resolver = new Resolver(root as Node.Node, resolverOptions);
+
+    aP.applyResolver(resolver);
+    sP.applyResolver(resolver);
+
+    expect(resolver.update()).toStrictEqual({
+      asset: { values: [{ value: 'foo' }, { value: 'bar' }] },
+    });
+    model.set([['foo', false]]);
+    expect(resolver.update()).toStrictEqual({
+      asset: { values: [{ value: 'bar' }] },
+    });
+  });
+
   it('removes empty objects', () => {
     new ApplicabilityPlugin().applyParser(parser);
     const root = parser.parseObject({
@@ -48,7 +83,7 @@ describe('applicability', () => {
       },
     });
     model.set([['foo', true]]);
-    const resolver = new Resolver(root!, resolverOptions);
+    const resolver = new Resolver(root as Node.Node, resolverOptions);
 
     new ApplicabilityPlugin().applyResolver(resolver);
     new StringResolverPlugin().applyResolver(resolver);
@@ -77,7 +112,7 @@ describe('applicability', () => {
       },
     });
     model.set([['foo', true]]);
-    const resolver = new Resolver(root!, resolverOptions);
+    const resolver = new Resolver(root as Node.Node, resolverOptions);
 
     new ApplicabilityPlugin().applyResolver(resolver);
     new StringResolverPlugin().applyResolver(resolver);
@@ -126,7 +161,7 @@ describe('applicability', () => {
       [fooBinding, true],
       [barBinding, true],
     ]);
-    const resolver = new Resolver(root!, resolverOptions);
+    const resolver = new Resolver(root as Node.Node, resolverOptions);
 
     new ApplicabilityPlugin().applyResolver(resolver);
     new StringResolverPlugin().applyResolver(resolver);
diff --git a/core/player/src/view/plugins/__tests__/template.test.ts b/core/player/src/view/plugins/__tests__/template.test.ts
index 5de7be084..032655217 100644
--- a/core/player/src/view/plugins/__tests__/template.test.ts
+++ b/core/player/src/view/plugins/__tests__/template.test.ts
@@ -9,6 +9,372 @@ import { ViewInstance } from '../../view';
 import type { Options } from '../options';
 import TemplatePlugin from '../template-plugin';
 
+const templateJoinValues = {
+  id: 'snippet-of-json',
+  topic: 'Snippet',
+  schema: {},
+  data: {
+    forms: {
+      '1099-A': [
+        {
+          description: 'Desciption of concept 1099 1',
+          amount: 'Help',
+        },
+      ],
+      '1099-B': [
+        {
+          description: 'Desciption of concept 1099 2',
+          amount: 'Help',
+        },
+      ],
+    },
+  },
+  views: [
+    {
+      id: 'overviewGroup',
+      type: 'overviewGroup',
+      metaData: {
+        role: 'stateful',
+      },
+      modifiers: [
+        {
+          type: 'tag',
+          value: 'fancy-header',
+        },
+      ],
+      headers: {
+        label: {
+          asset: {
+            id: 'line-of-work-summary-gh-header-label',
+            type: 'text',
+            value: 'Header',
+          },
+        },
+        values: [
+          {
+            asset: {
+              id: 'line-of-work-summary-gh-expenses-simple-header-previous-year',
+              type: 'text',
+              value: 'Type',
+            },
+          },
+          {
+            asset: {
+              id: 'line-of-work-summary-gh-expenses-simple-header-cy',
+              type: 'text',
+              value: '2022',
+            },
+          },
+        ],
+      },
+      template: [
+        {
+          data: 'forms.1099-A',
+          output: 'values',
+          value: {
+            asset: {
+              id: 'overviewItem3',
+              type: 'overviewItem',
+              label: {
+                asset: {
+                  id: 'overviewItem3-label',
+                  type: 'text',
+                  value: '1099-A',
+                },
+              },
+              values: [
+                {
+                  asset: {
+                    id: 'overviewItem3-year',
+                    type: 'text',
+                    value: 'Desciption of concept 1099 1',
+                  },
+                },
+                {
+                  asset: {
+                    id: 'loverviewItem3-cy',
+                    type: 'text',
+                    value: '4000',
+                  },
+                },
+              ],
+            },
+          },
+        },
+        {
+          data: 'forms.1099-B',
+          output: 'values',
+          value: {
+            asset: {
+              id: 'overviewItem4',
+              type: 'overviewItem',
+              label: {
+                asset: {
+                  id: 'overviewItem4-label',
+                  type: 'text',
+                  value: '1099-B',
+                },
+              },
+              values: [
+                {
+                  asset: {
+                    id: 'overviewItem4-year',
+                    type: 'text',
+                    value: 'Desciption of concept 1099 2',
+                  },
+                },
+                {
+                  asset: {
+                    id: 'loverviewItem3-cy',
+                    type: 'text',
+                    value: '6000',
+                  },
+                },
+              ],
+            },
+          },
+        },
+      ],
+      values: [
+        {
+          asset: {
+            id: 'overviewItem1',
+            type: 'overviewItem',
+            label: {
+              asset: {
+                id: 'overviewItem1-label',
+                type: 'text',
+                value: 'First Summary',
+              },
+            },
+            values: [
+              {
+                asset: {
+                  id: 'overviewItem1-year',
+                  type: 'text',
+                  value: 'Desciption of year summary 1',
+                },
+              },
+              {
+                asset: {
+                  id: 'loverviewItem1-cy',
+                  type: 'text',
+                  value: '14000',
+                },
+              },
+            ],
+          },
+        },
+        {
+          asset: {
+            id: 'overviewItem2',
+            type: 'overviewItem',
+            label: {
+              asset: {
+                id: 'overviewItem2-label',
+                type: 'text',
+                value: 'Second year Summary',
+              },
+            },
+            values: [
+              {
+                asset: {
+                  id: 'overviewItem2-year',
+                  type: 'text',
+                  value: 'Desciption of year summary item 2',
+                },
+              },
+              {
+                asset: {
+                  id: 'loverviewItem1-cy',
+                  type: 'text',
+                  value: '19000',
+                },
+              },
+            ],
+          },
+        },
+      ],
+    },
+    {
+      id: 'overviewGroup',
+      type: 'overviewGroup',
+      metaData: {
+        role: 'stateful',
+      },
+      modifiers: [
+        {
+          type: 'tag',
+          value: 'fancy-header',
+        },
+      ],
+      headers: {
+        label: {
+          asset: {
+            id: 'line-of-work-summary-gh-header-label',
+            type: 'text',
+            value: 'Header',
+          },
+        },
+        values: [
+          {
+            asset: {
+              id: 'line-of-work-summary-gh-expenses-simple-header-previous-year',
+              type: 'text',
+              value: 'Type',
+            },
+          },
+          {
+            asset: {
+              id: 'line-of-work-summary-gh-expenses-simple-header-cy',
+              type: 'text',
+              value: '2022',
+            },
+          },
+        ],
+      },
+      values: [
+        {
+          asset: {
+            id: 'overviewItem1',
+            type: 'overviewItem',
+            label: {
+              asset: {
+                id: 'overviewItem1-label',
+                type: 'text',
+                value: 'First Summary',
+              },
+            },
+            values: [
+              {
+                asset: {
+                  id: 'overviewItem1-year',
+                  type: 'text',
+                  value: 'Desciption of year summary 1',
+                },
+              },
+              {
+                asset: {
+                  id: 'loverviewItem1-cy',
+                  type: 'text',
+                  value: '14000',
+                },
+              },
+            ],
+          },
+        },
+        {
+          asset: {
+            id: 'overviewItem2',
+            type: 'overviewItem',
+            label: {
+              asset: {
+                id: 'overviewItem2-label',
+                type: 'text',
+                value: 'Second year Summary',
+              },
+            },
+            values: [
+              {
+                asset: {
+                  id: 'overviewItem2-year',
+                  type: 'text',
+                  value: 'Desciption of year summary item 2',
+                },
+              },
+              {
+                asset: {
+                  id: 'loverviewItem1-cy',
+                  type: 'text',
+                  value: '19000',
+                },
+              },
+            ],
+          },
+        },
+      ],
+      template: [
+        {
+          data: 'forms.1099-A',
+          output: 'values',
+          value: {
+            asset: {
+              id: 'overviewItem3',
+              type: 'overviewItem',
+              label: {
+                asset: {
+                  id: 'overviewItem3-label',
+                  type: 'text',
+                  value: '1099-A',
+                },
+              },
+              values: [
+                {
+                  asset: {
+                    id: 'overviewItem3-year',
+                    type: 'text',
+                    value: 'Desciption of concept 1099 1',
+                  },
+                },
+                {
+                  asset: {
+                    id: 'loverviewItem3-cy',
+                    type: 'text',
+                    value: '4000',
+                  },
+                },
+              ],
+            },
+          },
+        },
+        {
+          data: 'forms.1099-B',
+          output: 'values',
+          value: {
+            asset: {
+              id: 'overviewItem4',
+              type: 'overviewItem',
+              label: {
+                asset: {
+                  id: 'overviewItem4-label',
+                  type: 'text',
+                  value: '1099-B',
+                },
+              },
+              values: [
+                {
+                  asset: {
+                    id: 'overviewItem4-year',
+                    type: 'text',
+                    value: 'Desciption of concept 1099 2',
+                  },
+                },
+                {
+                  asset: {
+                    id: 'loverviewItem3-cy',
+                    type: 'text',
+                    value: '6000',
+                  },
+                },
+              ],
+            },
+          },
+        },
+      ],
+    },
+  ],
+  navigation: {
+    BEGIN: 'SnippetFlow',
+    SnippetFlow: {
+      startState: 'VIEW_Snippet-View1',
+      'VIEW_Snippet-View1': {
+        ref: 'overviewGroup',
+        state_type: 'VIEW',
+      },
+    },
+  },
+};
+
 const parseBinding = new BindingParser().parse;
 
 describe('templates', () => {
@@ -302,4 +668,39 @@ describe('dynamic templates', () => {
       },
     });
   });
+
+  describe('Works with template items plus value items', () => {
+    const model = withParser(
+      new LocalModel(templateJoinValues.data),
+      parseBinding
+    );
+    const evaluator = new ExpressionEvaluator({ model });
+
+    it('Should show template item first when coming before values on lexical order', () => {
+      const view = new ViewInstance(templateJoinValues.views[0], {
+        model,
+        parseBinding,
+        evaluator,
+        schema: new SchemaController(),
+      });
+
+      const resolved = view.update();
+
+      expect(resolved.values).toHaveLength(4);
+      expect(resolved.values[0]).toMatchSnapshot();
+    });
+    it('Should show template item last when coming after values on lexical order', () => {
+      const view = new ViewInstance(templateJoinValues.views[1], {
+        model,
+        parseBinding,
+        evaluator,
+        schema: new SchemaController(),
+      });
+
+      const resolved = view.update();
+
+      expect(resolved.values).toHaveLength(4);
+      expect(resolved.values[0]).toMatchSnapshot();
+    });
+  });
 });
diff --git a/core/player/src/view/plugins/applicability.ts b/core/player/src/view/plugins/applicability.ts
index 9b720bc9d..ddfffeee3 100644
--- a/core/player/src/view/plugins/applicability.ts
+++ b/core/player/src/view/plugins/applicability.ts
@@ -16,7 +16,7 @@ export default class ApplicabilityPlugin implements ViewPlugin {
         if (node?.type === NodeType.Applicability) {
           const isApplicable = options.evaluate(node.expression);
 
-          if (!isApplicable) {
+          if (isApplicable === false) {
             return null;
           }
 
diff --git a/core/player/src/view/plugins/string-resolver.ts b/core/player/src/view/plugins/string-resolver.ts
index ec05600c2..341251c40 100644
--- a/core/player/src/view/plugins/string-resolver.ts
+++ b/core/player/src/view/plugins/string-resolver.ts
@@ -87,15 +87,19 @@ export function resolveAllRefs(
 }
 
 /** Traverse up the node tree finding the first available 'path' */
-const findBasePath = (node: Node.Node): Node.PathSegment[] => {
+const findBasePath = (
+  node: Node.Node,
+  resolver: Resolver
+): Node.PathSegment[] => {
   const parentNode = node.parent;
   if (!parentNode) {
     return [];
   }
 
   if ('children' in parentNode) {
+    const original = resolver.getSourceNode(node);
     return (
-      parentNode.children?.find((child) => child.value === node)?.path ?? []
+      parentNode.children?.find((child) => child.value === original)?.path ?? []
     );
   }
 
@@ -103,7 +107,7 @@ const findBasePath = (node: Node.Node): Node.PathSegment[] => {
     return [];
   }
 
-  return findBasePath(parentNode);
+  return findBasePath(parentNode, resolver);
 };
 
 /** A plugin that resolves all string references for each node */
@@ -148,7 +152,7 @@ export default class StringResolverPlugin implements ViewPlugin {
           propsToSkip = new Set(['exp']);
         }
 
-        const nodePath = findBasePath(node);
+        const nodePath = findBasePath(node, resolver);
 
         /** If the path includes something that is supposed to be skipped, this node should be skipped too. */
         if (
diff --git a/core/player/src/view/plugins/template-plugin.ts b/core/player/src/view/plugins/template-plugin.ts
index 950c862fe..6dc4bf0b2 100644
--- a/core/player/src/view/plugins/template-plugin.ts
+++ b/core/player/src/view/plugins/template-plugin.ts
@@ -95,16 +95,11 @@ export default class TemplatePlugin implements ViewPlugin {
     });
 
     const result: Node.MultiNode = {
-      parent: node.parent,
       type: NodeType.MultiNode,
+      override: false,
       values,
     };
 
-    result.values.forEach((innerNode) => {
-      // eslint-disable-next-line no-param-reassign
-      innerNode.parent = result;
-    });
-
     return result;
   }
 
diff --git a/core/player/src/view/resolver/__tests__/edgecases.test.ts b/core/player/src/view/resolver/__tests__/edgecases.test.ts
index 1985631b3..036bce0d3 100644
--- a/core/player/src/view/resolver/__tests__/edgecases.test.ts
+++ b/core/player/src/view/resolver/__tests__/edgecases.test.ts
@@ -1,4 +1,4 @@
-import { replaceAt, set } from 'timm';
+import { replaceAt, set, omit } from 'timm';
 
 import { BindingParser } from '../../../binding';
 import { ExpressionEvaluator } from '../../../expressions';
@@ -7,6 +7,7 @@ import { SchemaController } from '../../../schema';
 import type { Logger } from '../../../logger';
 import { TapableLogger } from '../../../logger';
 import { Resolver } from '..';
+import type { Node } from '../../parser';
 import { NodeType, Parser } from '../../parser';
 import { StringResolverPlugin } from '../../plugins';
 
@@ -143,6 +144,174 @@ describe('Dynamic AST Transforms', () => {
       },
     });
   });
+
+  it('Nodes are properly cached on rerender', () => {
+    const model = new LocalModel({
+      year: '2021',
+    });
+    const parser = new Parser();
+    const bindingParser = new BindingParser();
+    const inputBinding = bindingParser.parse('year');
+    const rootNode = parser.parseObject(content);
+
+    const resolver = new Resolver(rootNode!, {
+      model,
+      parseBinding: bindingParser.parse.bind(bindingParser),
+      parseNode: parser.parseObject.bind(parser),
+      evaluator: new ExpressionEvaluator({
+        model: withParser(model, bindingParser.parse),
+      }),
+      schema: new SchemaController(),
+    });
+
+    resolver.update();
+
+    const resolveCache = resolver.getResolveCache();
+
+    resolver.update(new Set([inputBinding]));
+
+    const newResolveCache = resolver.getResolveCache();
+
+    expect(resolveCache.size).toBe(newResolveCache.size);
+
+    // The cached items between each re-render should stay the same
+    for (const [k, v] of resolveCache) {
+      const excludingUpdated = omit(v, 'updated');
+
+      expect(newResolveCache.has(k)).toBe(true);
+      expect(newResolveCache.get(k)).toMatchObject(excludingUpdated);
+    }
+  });
+
+  it('Cached node points to the correct parent node', () => {
+    const view = {
+      id: 'main-view',
+      type: 'questionAnswer',
+      title: [
+        {
+          asset: {
+            id: 'title',
+            type: 'text',
+            value: 'Cool Page',
+          },
+        },
+      ],
+      primaryInfo: [
+        {
+          asset: {
+            id: 'input',
+            type: 'input',
+            value: '{{year}}',
+            label: {
+              asset: {
+                id: 'label',
+                type: 'text',
+                value: 'label',
+              },
+            },
+          },
+        },
+      ],
+    };
+
+    const model = new LocalModel({
+      year: '2021',
+    });
+    const parser = new Parser();
+    const bindingParser = new BindingParser();
+    const inputBinding = bindingParser.parse('year');
+    const rootNode = parser.parseObject(view);
+
+    const resolver = new Resolver(rootNode!, {
+      model,
+      parseBinding: bindingParser.parse.bind(bindingParser),
+      parseNode: parser.parseObject.bind(parser),
+      evaluator: new ExpressionEvaluator({
+        model: withParser(model, bindingParser.parse),
+      }),
+      schema: new SchemaController(),
+    });
+
+    let inputNode: Node.Node | undefined;
+    let labelNode: Node.Node | undefined;
+
+    resolver.hooks.beforeResolve.tap('test', (node, options) => {
+      if (node?.type === 'asset' && node.value.id === 'input') {
+        // Add to dependencies
+        options.data.model.get(inputBinding);
+      }
+
+      return node;
+    });
+
+    resolver.hooks.afterResolve.tap('test', (value, node) => {
+      if (node.type === 'asset') {
+        const { id } = node.value;
+
+        if (id === 'input') inputNode = node;
+
+        if (id === 'label') labelNode = node;
+      }
+
+      return value;
+    });
+
+    resolver.update();
+
+    model.set([[inputBinding, '2022']]);
+
+    resolver.update(new Set([inputBinding]));
+
+    // Check that label (which is cached) still points to the correct parent node.
+    expect(labelNode?.parent).toBe(inputNode ?? {});
+  });
+
+  it('Fixes parent references when beforeResolve taps make changes', () => {
+    const model = new LocalModel({
+      year: '2021',
+    });
+    const parser = new Parser();
+    const bindingParser = new BindingParser();
+    const rootNode = parser.parseObject(content);
+
+    const resolver = new Resolver(rootNode!, {
+      model,
+      parseBinding: bindingParser.parse.bind(bindingParser),
+      parseNode: parser.parseObject.bind(parser),
+      evaluator: new ExpressionEvaluator({
+        model: withParser(model, bindingParser.parse),
+      }),
+      schema: new SchemaController(),
+    });
+
+    let parent;
+    resolver.hooks.beforeResolve.tap('test', (node) => {
+      if (node?.type !== NodeType.Asset || node.value.id !== 'subtitle') {
+        return node;
+      }
+
+      parent = node.parent;
+      return {
+        ...node,
+        parent: undefined,
+      };
+    });
+
+    let resolvedNode: Node.Node | undefined;
+    resolver.hooks.afterResolve.tap('test', (resolvedValue, node) => {
+      if (node?.type === NodeType.Asset && node.value.id === 'subtitle') {
+        resolvedNode = node;
+      }
+
+      return resolvedValue;
+    });
+
+    resolver.update();
+
+    expect(parent).not.toBeUndefined();
+    expect(resolvedNode).not.toBeUndefined();
+    expect(resolvedNode?.parent).toBe(parent);
+  });
 });
 
 describe('Duplicate IDs', () => {
@@ -219,7 +388,7 @@ describe('Duplicate IDs', () => {
     expect(testLogger.error).toBeCalledWith(
       'Cache conflict: Found Asset/View nodes that have conflicting ids: action-1, may cause cache issues.'
     );
-    testLogger.error.mockClear();
+    (testLogger.error as jest.Mock).mockClear();
 
     expect(firstUpdate).toStrictEqual({
       id: 'action',
@@ -314,7 +483,7 @@ describe('Duplicate IDs', () => {
     expect(testLogger.info).toBeCalledWith(
       'Cache conflict: Found Value nodes that have conflicting ids: value-1, may cause cache issues. To improve performance make value node IDs globally unique.'
     );
-    testLogger.info.mockClear();
+    (testLogger.info as jest.Mock).mockClear();
     expect(firstUpdate).toStrictEqual(content);
 
     resolver.update();
@@ -390,3 +559,68 @@ describe('AST caching', () => {
     });
   });
 });
+
+describe('Root AST Immutability', () => {
+  it('modifying nodes in beforeResolve should not impact the original tree', () => {
+    const content = {
+      id: 'action',
+      type: 'collection',
+      values: [
+        {
+          id: 'value-1',
+          binding: 'count1',
+        },
+        {
+          id: 'value-1',
+          binding: 'count2',
+        },
+      ],
+    };
+
+    const model = new LocalModel();
+    const parser = new Parser();
+    const bindingParser = new BindingParser();
+    const rootNode = parser.parseObject(content, NodeType.View);
+    const resolver = new Resolver(rootNode!, {
+      model,
+      parseBinding: bindingParser.parse.bind(bindingParser),
+      parseNode: parser.parseObject.bind(parser),
+      evaluator: new ExpressionEvaluator({
+        model: withParser(model, bindingParser.parse),
+      }),
+      schema: new SchemaController(),
+    });
+    let finalNode;
+
+    resolver.hooks.beforeResolve.tap('beforeResolve', (node) => {
+      if (node?.type !== NodeType.View) return node;
+
+      // eslint-disable-next-line no-param-reassign
+      node.value.type = 'not-collection';
+      return node;
+    });
+
+    resolver.hooks.afterResolve.tap('afterResolve', (value, node) => {
+      if (node?.type === NodeType.View) {
+        finalNode = node;
+      }
+
+      return value;
+    });
+
+    resolver.update();
+
+    expect(rootNode).toBe(resolver.root);
+    expect(rootNode).not.toBe(finalNode);
+    expect(finalNode).toMatchObject({
+      value: {
+        type: 'not-collection',
+      },
+    });
+    expect(rootNode).toMatchObject({
+      value: {
+        type: 'collection',
+      },
+    });
+  });
+});
diff --git a/core/player/src/view/resolver/index.ts b/core/player/src/view/resolver/index.ts
index e7199c867..e46c2b84d 100644
--- a/core/player/src/view/resolver/index.ts
+++ b/core/player/src/view/resolver/index.ts
@@ -1,5 +1,5 @@
 import { SyncWaterfallHook, SyncHook } from 'tapable-ts';
-import { setIn, addLast } from 'timm';
+import { setIn, addLast, clone } from 'timm';
 import dlv from 'dlv';
 import { dequal } from 'dequal';
 import type { BindingInstance, BindingLike } from '../../binding';
@@ -42,6 +42,12 @@ const withContext = (model: DataModelWithParser): DataModelWithParser => {
         ...options,
       });
     },
+    delete: (binding: BindingLike, options?: DataModelOptions): void => {
+      return model.delete(binding, {
+        context: { model },
+        ...options,
+      });
+    },
   };
 };
 
@@ -141,6 +147,7 @@ export class Resolver {
     this.hooks.beforeUpdate.call(changes);
     const resolveCache = new Map<Node.Node, Resolve.ResolvedNode>();
     this.idCache.clear();
+    const prevASTMap = new Map(this.ASTMap);
     this.ASTMap.clear();
 
     const updated = this.computeTree(
@@ -148,7 +155,9 @@ export class Resolver {
       undefined,
       changes,
       resolveCache,
-      toNodeResolveOptions(this.options)
+      toNodeResolveOptions(this.options),
+      undefined,
+      prevASTMap
     );
     this.resolveCache = resolveCache;
     this.hooks.afterUpdate.call(updated.value);
@@ -156,6 +165,10 @@ export class Resolver {
     return updated.value;
   }
 
+  public getResolveCache() {
+    return new Map(this.resolveCache);
+  }
+
   private getNodeID(node?: Node.Node): string | undefined {
     if (!node) {
       return;
@@ -206,12 +219,29 @@ export class Resolver {
     return this.resolveCache.get(node);
   }
 
+  private cloneNode(node: any) {
+    const clonedNode = clone(node);
+
+    Object.keys(clonedNode).forEach((key) => {
+      if (key === 'parent') return;
+
+      const value = clonedNode[key];
+      if (typeof value === 'object' && value !== null) {
+        clonedNode[key] = Array.isArray(value) ? [...value] : { ...value };
+      }
+    });
+
+    return clonedNode;
+  }
+
   private computeTree(
     node: Node.Node,
     parent: Node.Node | undefined,
     dataChanges: Set<BindingInstance> | undefined,
     cacheUpdate: Map<Node.Node, Resolve.ResolvedNode>,
-    options: Resolve.NodeResolveOptions
+    options: Resolve.NodeResolveOptions,
+    parentNode: Node.Node | undefined,
+    prevASTMap: Map<Node.Node, Node.Node>
   ): NodeUpdate {
     const dependencyModel = new DependencyModel(options.data.model);
 
@@ -254,43 +284,65 @@ export class Resolver {
 
       /** Recursively repopulate the AST map given some AST Node and it's resolved AST representation */
       const repopulateASTMapFromCache = (
-        resolvedAST: Node.Node,
-        AST: Node.Node
+        resolvedNode: Resolve.ResolvedNode,
+        AST: Node.Node,
+        ASTParent: Node.Node | undefined
       ) => {
+        const { node: resolvedAST } = resolvedNode;
         this.ASTMap.set(resolvedAST, AST);
+        const resolvedUpdate = {
+          ...resolvedNode,
+          updated: false,
+        };
+        cacheUpdate.set(AST, resolvedUpdate);
+
+        /** Helper function for recursing over child node */
+        const handleChildNode = (childNode: Node.Node) => {
+          // In order to get the correct results, we need to use the node references from the last update.
+          const originalChildNode = prevASTMap.get(childNode) ?? childNode;
+          const previousChildResult = this.getPreviousResult(originalChildNode);
+          if (!previousChildResult) return;
+
+          repopulateASTMapFromCache(
+            previousChildResult,
+            originalChildNode,
+            AST
+          );
+        };
+
         if ('children' in resolvedAST) {
-          resolvedAST.children?.forEach(({ value: childAST }) => {
-            const { node: childResolvedAST } =
-              this.getPreviousResult(childAST) || {};
-            if (!childResolvedAST) return;
-
-            repopulateASTMapFromCache(childResolvedAST, childAST);
-
-            if (childResolvedAST.type === NodeType.MultiNode) {
-              childResolvedAST.values.forEach((mChildAST) => {
-                const { node: mChildResolvedAST } =
-                  this.getPreviousResult(mChildAST) || {};
-                if (!mChildResolvedAST) return;
-
-                repopulateASTMapFromCache(mChildResolvedAST, mChildAST);
-              });
-            }
-          });
+          resolvedAST.children?.forEach(({ value: childAST }) =>
+            handleChildNode(childAST)
+          );
+        } else if (resolvedAST.type === NodeType.MultiNode) {
+          resolvedAST.values.forEach(handleChildNode);
         }
+
+        this.hooks.afterNodeUpdate.call(AST, ASTParent, resolvedUpdate);
       };
 
-      const resolvedAST = previousResult.node;
-      repopulateASTMapFromCache(resolvedAST, node);
+      // Point the root of the cached node to the new resolved node.
+      previousResult.node.parent = parentNode;
 
-      this.hooks.afterNodeUpdate.call(node, parent, update);
+      repopulateASTMapFromCache(previousResult, node, parent);
 
       return update;
     }
 
-    const resolvedAST = this.hooks.beforeResolve.call(node, resolveOptions) ?? {
+    // Shallow clone the node so that changes to it during the resolve steps don't impact the original.
+    // We are trusting that this becomes a deep clone once the whole node tree has been traversed.
+    const clonedNode = {
+      ...this.cloneNode(node),
+      parent: parentNode,
+    };
+    const resolvedAST = this.hooks.beforeResolve.call(
+      clonedNode,
+      resolveOptions
+    ) ?? {
       type: NodeType.Empty,
     };
 
+    resolvedAST.parent = parentNode;
     resolveOptions.node = resolvedAST;
 
     this.ASTMap.set(resolvedAST, node);
@@ -311,43 +363,25 @@ export class Resolver {
     dependencyModel.trackSubset('children');
 
     if ('children' in resolvedAST) {
-      resolvedAST.children?.forEach((child) => {
+      const newChildren = resolvedAST.children?.map((child) => {
         const computedChildTree = this.computeTree(
           child.value,
           node,
           dataChanges,
           cacheUpdate,
-          resolveOptions
+          resolveOptions,
+          resolvedAST,
+          prevASTMap
         );
-        let { updated: childUpdated, value: childValue } = computedChildTree;
-        const { node: childNode, dependencies: childTreeDeps } =
-          computedChildTree;
+        const {
+          dependencies: childTreeDeps,
+          node: childNode,
+          updated: childUpdated,
+          value: childValue,
+        } = computedChildTree;
 
         childTreeDeps.forEach((binding) => childDependencies.add(binding));
 
-        if (childNode.type === NodeType.MultiNode) {
-          childValue = [];
-          childNode.values.forEach((mValue) => {
-            const mTree = this.computeTree(
-              mValue,
-              node,
-              dataChanges,
-              cacheUpdate,
-              resolveOptions
-            );
-
-            if (mTree.value !== undefined && mTree.value !== null) {
-              childValue.push(mTree.value);
-            }
-
-            mTree.dependencies.forEach((bindingDep) =>
-              childDependencies.add(bindingDep)
-            );
-
-            childUpdated = childUpdated || mTree.updated;
-          });
-        }
-
         if (childValue) {
           if (childNode.type === NodeType.MultiNode && !childNode.override) {
             const arr = addLast(
@@ -361,7 +395,38 @@ export class Resolver {
         }
 
         updated = updated || childUpdated;
+
+        return { ...child, value: childNode };
       });
+
+      resolvedAST.children = newChildren;
+    } else if (resolvedAST.type === NodeType.MultiNode) {
+      const childValue: any = [];
+      const newValues = resolvedAST.values.map((mValue) => {
+        const mTree = this.computeTree(
+          mValue,
+          node,
+          dataChanges,
+          cacheUpdate,
+          resolveOptions,
+          resolvedAST,
+          prevASTMap
+        );
+        if (mTree.value !== undefined && mTree.value !== null) {
+          childValue.push(mTree.value);
+        }
+
+        mTree.dependencies.forEach((bindingDep) =>
+          childDependencies.add(bindingDep)
+        );
+
+        updated = updated || mTree.updated;
+
+        return mTree.node;
+      });
+
+      resolvedAST.values = newValues;
+      resolved = childValue;
     }
 
     childDependencies.forEach((bindingDep) =>
diff --git a/core/player/src/view/resolver/types.ts b/core/player/src/view/resolver/types.ts
index 4b3ceec11..6e16d70a5 100644
--- a/core/player/src/view/resolver/types.ts
+++ b/core/player/src/view/resolver/types.ts
@@ -13,6 +13,7 @@ import type {
   DataModelImpl,
   DataModelOptions,
 } from '../../data';
+import type { ConstantsProvider } from '../../controllers/constants';
 import type { TransitionFunction } from '../../controllers';
 import type { ExpressionEvaluator, ExpressionType } from '../../expressions';
 import type { ValidationResponse } from '../../validator';
@@ -20,30 +21,64 @@ import type { Logger } from '../../logger';
 import type { SchemaController } from '../../schema';
 import type { Node } from '../parser';
 
+export interface ValidationGetResolveOptions {
+  /**
+   * If we should ignore any non-blocking validations in the return
+   * @default true
+   */
+  ignoreNonBlocking?: boolean;
+}
+
+export interface PlayerUtils {
+  findPlugin<Plugin = unknown>(symbol: symbol): Plugin | undefined;
+}
+
 export declare namespace Resolve {
   export interface Validation {
     /** Fetch the data-type for the given binding */
     type(binding: BindingLike): Schema.DataType | undefined;
 
     /** Get all currently applicable validation errors */
-    getAll(): Map<BindingInstance, ValidationResponse> | undefined;
+    getAll(
+      options?: ValidationGetResolveOptions
+    ): Map<BindingInstance, ValidationResponse> | undefined;
 
     /** Internal Method to lookup if there is a validation for the given binding */
-    _getValidationForBinding(
-      binding: BindingLike
-    ): ValidationResponse | undefined;
+    _getValidationForBinding(binding: BindingLike):
+      | {
+          /** Get the validation for the given binding */
+          get: (
+            options?: ValidationGetResolveOptions
+          ) => ValidationResponse | undefined;
+
+          /** Get all validations for the given binding */
+          getAll: (
+            options?: ValidationGetResolveOptions
+          ) => Array<ValidationResponse>;
+        }
+      | undefined;
 
     /** Get field level error for the specific binding */
     get(
       binding: BindingLike,
       options?: {
         /** If this binding should also be tracked for validations */
-        track: boolean;
-      }
+        track?: boolean;
+      } & ValidationGetResolveOptions
     ): ValidationResponse | undefined;
 
+    getValidationsForBinding(
+      binding: BindingLike,
+      options?: {
+        /** If this binding should also be tracked for validations */
+        track?: boolean;
+      } & ValidationGetResolveOptions
+    ): Array<ValidationResponse>;
+
     /** Get errors for all children regardless of section */
-    getChildren(type: ValidationTypes.DisplayTarget): Array<ValidationResponse>;
+    getChildren(
+      type?: ValidationTypes.DisplayTarget
+    ): Array<ValidationResponse>;
 
     /** Get errors for all children solely in this section */
     getValidationsForSection(): Array<ValidationResponse>;
@@ -62,6 +97,9 @@ export declare namespace Resolve {
     /** A logger to use */
     logger?: Logger;
 
+    /** Utils for various useful operations */
+    utils?: PlayerUtils;
+
     /** An optional set of validation features */
     validation?: Validation;
 
@@ -73,6 +111,9 @@ export declare namespace Resolve {
 
     /** The hub for data invariants and metaData associated with the data model */
     schema: SchemaController;
+
+    /** The constants for messages */
+    constants?: ConstantsProvider;
   }
 
   export interface NodeDataOptions {
diff --git a/core/types/src/index.ts b/core/types/src/index.ts
index 5b06c4860..9d846f601 100644
--- a/core/types/src/index.ts
+++ b/core/types/src/index.ts
@@ -398,6 +398,15 @@ export declare namespace Validation {
     /** Where the error should be displayed */
     displayTarget?: DisplayTarget;
 
+    /**
+     * If the validation blocks navigation
+     * true/false - always/never block navigation
+     * once - only block navigation if the validation has not been triggered before
+     *
+     * @default - true for errors, 'once' for warnings
+     */
+    blocking?: boolean | 'once';
+
     /** Additional props to send down to a Validator */
     [key: string]: unknown;
   }
diff --git a/docs/site/pages/plugins/markdown.mdx b/docs/site/pages/plugins/markdown.mdx
new file mode 100644
index 000000000..9ef462fff
--- /dev/null
+++ b/docs/site/pages/plugins/markdown.mdx
@@ -0,0 +1,52 @@
+---
+title: Markdown
+platform: core
+---
+
+# Markdown Plugin
+
+The `markdown-plugin` adds support for parsing markdown content to Player Assets. This plugin is asset set agnostic, so it expects a mappers record to inform how to transform markdown content into valid Player Content with support from your asset set.
+
+## Usage
+
+<PlatformTabs>
+  <core>
+
+### Defining The Mappers
+
+```ts
+import type { Mappers } from '@player-ui/markdown-plugin';
+
+export const mappers: Mappers = {
+  text: ({ originalAsset, value }) => ({
+    id: `${originalAsset.id}-text`,
+    type: 'text',
+    value,
+  }),
+  image: ({ originalAsset, value, src }) => ({
+    id: `${originalAsset.id}-image`,
+    type: 'image',
+    accessibility: value,
+    metaData: {
+      ref: src,
+    },
+  }),
+  //...
+};
+```
+
+## Add the plugin to Player
+
+```ts
+import { MarkdownPlugin } from '@player-ui/markdown-plugin';
+import mappers from './mappers';
+
+const markdownPlugin = new MarkdownPlugin(myMarkdownMappers);
+// Add it to the player
+const player = new Player({
+  plugins: [markdownPlugin],
+});
+```
+  </core>
+</PlatformTabs>
+
diff --git a/docs/site/pages/plugins/stage-revert-data.mdx b/docs/site/pages/plugins/stage-revert-data.mdx
new file mode 100644
index 000000000..8eab2303a
--- /dev/null
+++ b/docs/site/pages/plugins/stage-revert-data.mdx
@@ -0,0 +1,47 @@
+---
+title: Stage Revert Data
+platform: core
+---
+
+# Stage Rvert Data
+
+This plugin enables users to temporarily stage data changes before committing to the actual data model
+
+A `stageData` property flag inside of the `view properties` must be added on the desired view configs.
+
+```json
+{
+  "VIEW_1": {
+    "state_type": "VIEW",
+    "ref": "view-1",
+    "attributes": {
+      "stageData": true,
+      "commitTransitions": ["VIEW_2"]
+    },
+    "transitions": {
+      "next": "VIEW_2",
+      "*": "ACTION_1"
+    }
+  }
+}
+```
+
+It also should include a list of acceptable `commitTransitions` valid `VIEW` name for the data to be committed when the transition occurs, A not included commit transition would trigger the staged data to be cleared. An acceptable transition will commit the data into the `data model`. e.g. as per the previous example transitioning to `VIEW_2` will trigger the staged data to get committed in the model, since the `next` transition property is pointing to it and is listed on the `commitTransitions` array parameter, otherwise it would get thrown away.
+
+## Example
+
+<PlatformTabs>
+  <core>
+
+Simply add the plugin to the config when constructing a player instance.
+
+```javascript
+import StageRevertPlugin from '@player/stage-revert-data';
+
+const player = new Player({
+  plugins: [new StageRevertPlugin()],
+});
+```
+
+  </core>
+</PlatformTabs>
\ No newline at end of file
diff --git a/docs/site/pages/tools/cli.mdx b/docs/site/pages/tools/cli.mdx
index 4f93d2adb..3936be02a 100644
--- a/docs/site/pages/tools/cli.mdx
+++ b/docs/site/pages/tools/cli.mdx
@@ -44,13 +44,7 @@ Plugins are the way to change runtime behavior of the CLI actions. This includes
 
 # Commands
 
-<!-- commands -->
-
 - [`player dsl compile`](#player-dsl-compile)
-<!-- - [`player json validate`](#player-json-validate) -->
-
-## `player dsl compile`
-
 Compile Player DSL files into JSON
 
 ```
@@ -69,8 +63,7 @@ DESCRIPTION
   Compile Player DSL files into JSON
 ```
 
-<!-- ## `player json validate`
-
+- [`player json validate`](#player-json-validate)
 Validate Player JSON content
 
 ```
@@ -84,6 +77,27 @@ FLAGS
 
 DESCRIPTION
   Validate Player JSON content
-``` -->
+```
 
-<!-- commandsstop -->
\ No newline at end of file
+- [`player dependency-versions check`](#player-dependency-versions-check)
+Checks for `@player-ui/@player-tools` dependency version mismatches and issues warnings/solutions accordingly
+
+```
+USAGE
+  $ player dependency-versions check [-c <value>] [-v] [-p] [-i <value>]
+FLAGS
+  -c, --config=<value>     Path to a specific config file to load.
+                           By default, will automatically search for an rc or config file to load
+  -i, --ignore=<value>...  Ignore the specified pattern(s) when outputting results. Note multiple patterns can be passed
+  -p, --path               Outputs full path to dependency
+  -v, --verbose            Give verbose description
+DESCRIPTION
+  Checks for `@player-ui/@player-tools` dependency version mismatches and issues warnings/solutions accordingly
+  Consider the following:
+  - The interpretation of TOP-LEVEL and NESTED dependencies is as follows:
+  a. TOP-LEVEL dependencies only have one 'node_modules' in their path
+  b. NESTED dependencies have more than one 'node_modules' in their path
+  - `@player-ui/@player-tools` dependencies are fetched not only from inside the 'node_modules' at the top of the repository in which it is run but also from 'node_modules' in sub-directories.
+  For example, if you have some 'node_modules' inside of a 'packages' folder that contains `@player-ui/@player-tools` dependencies, then these will also be fetched.
+  The display of such dependencies also depends on the first bullet point.
+```
\ No newline at end of file
diff --git a/docs/storybook/.storybook/preview.js b/docs/storybook/.storybook/preview.js
index 3b6557f6d..65305f9f0 100644
--- a/docs/storybook/.storybook/preview.js
+++ b/docs/storybook/.storybook/preview.js
@@ -2,11 +2,13 @@ import { PlayerDecorator } from '@player-ui/storybook';
 import { ReferenceAssetsPlugin } from '@player-ui/reference-assets-plugin-react';
 import { CommonTypesPlugin } from '@player-ui/common-types-plugin';
 import { DataChangeListenerPlugin } from '@player-ui/data-change-listener-plugin';
+import { ComputedPropertiesPlugin } from '@player-ui/computed-properties-plugin'
 export const parameters = {
   reactPlayerPlugins: [
     new ReferenceAssetsPlugin(),
     new CommonTypesPlugin(),
     new DataChangeListenerPlugin(),
+    new ComputedPropertiesPlugin(),
   ],
   options: {
     storySort: {
diff --git a/docs/storybook/BUILD b/docs/storybook/BUILD
index b5adae80b..f1b9c9f00 100644
--- a/docs/storybook/BUILD
+++ b/docs/storybook/BUILD
@@ -7,6 +7,7 @@ data = [
     "//plugins/reference-assets/react:@player-ui/reference-assets-plugin-react",
     "//plugins/reference-assets/mocks:@player-ui/reference-assets-plugin-mocks",
     "//plugins/data-change-listener/core:@player-ui/data-change-listener-plugin",
+    "//plugins/computed-properties/core:@player-ui/computed-properties-plugin",
     "//tools/storybook:@player-ui/storybook",
     "//react/player:@player-ui/react",
     "//:tsconfig.json",
diff --git a/jest.config.js b/jest.config.js
index 4a39adb51..17be76ca5 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -48,4 +48,6 @@ module.exports = {
     '!**/perf-core/**',
     '!**/_backup/**',
   ],
+  automock: false,
+  transformIgnorePatterns: ['node-modules', '!mdast-util-from-markdown'],
 };
diff --git a/package.json b/package.json
index 5a949cb38..cf8d05440 100644
--- a/package.json
+++ b/package.json
@@ -20,6 +20,7 @@
     "@babel/cli": "^7.15.7",
     "@babel/core": "^7.15.5",
     "@babel/eslint-parser": "^7.15.8",
+    "@babel/plugin-transform-numeric-separator": "^7.22.5",
     "@babel/plugin-transform-react-jsx-source": "^7.17.12",
     "@babel/plugin-transform-runtime": "^7.15.8",
     "@babel/preset-env": "^7.15.6",
@@ -133,7 +134,7 @@
     "log-update": "^4.0.0",
     "lunr": "^2.3.9",
     "lz-string": "^1.4.4",
-    "mdast-util-from-markdown": "^1.2.0",
+    "mdast-util-from-markdown": "^2.0.0",
     "mdast-util-toc": "^6.1.0",
     "mkdirp": "^1.0.4",
     "monaco-editor": "^0.31.1",
@@ -173,7 +174,7 @@
     "rollup-plugin-string": "^3.0.0",
     "rollup-plugin-styles": "^4.0.0",
     "signale": "^1.4.0",
-    "smooth-scroll-into-view-if-needed": "1.1.32",
+    "seamless-scroll-polyfill": "2.3.3",
     "sorted-array": "^2.0.4",
     "source-map-js": "^1.0.2",
     "std-mocks": "^1.0.1",
diff --git a/plugins/auto-scroll/react/BUILD b/plugins/auto-scroll/react/BUILD
index c3d2f6cfd..2734bdd30 100644
--- a/plugins/auto-scroll/react/BUILD
+++ b/plugins/auto-scroll/react/BUILD
@@ -3,7 +3,7 @@ load("//:index.bzl", "javascript_pipeline")
 javascript_pipeline(
     name = "@player-ui/auto-scroll-manager-plugin-react",
     dependencies = [
-        "@npm//smooth-scroll-into-view-if-needed",
+        "@npm//seamless-scroll-polyfill",
     ],
     peer_dependencies = [
         "//react/player:@player-ui/react",
diff --git a/plugins/auto-scroll/react/src/__tests__/plugin.test.tsx b/plugins/auto-scroll/react/src/__tests__/plugin.test.tsx
index 1d552c6b5..d1f2582f9 100644
--- a/plugins/auto-scroll/react/src/__tests__/plugin.test.tsx
+++ b/plugins/auto-scroll/react/src/__tests__/plugin.test.tsx
@@ -3,7 +3,7 @@ import React, { useLayoutEffect } from 'react';
 import type { InProgressState } from '@player-ui/react';
 import { ReactPlayer } from '@player-ui/react';
 
-import { findByRole, render, waitFor } from '@testing-library/react';
+import { findByRole, render, waitFor, act } from '@testing-library/react';
 import { makeFlow } from '@player-ui/make-flow';
 
 import {
@@ -14,7 +14,7 @@ import { Info, Action, Input } from '@player-ui/reference-assets-plugin-react';
 import { CommonTypesPlugin } from '@player-ui/common-types-plugin';
 import { AssetTransformPlugin } from '@player-ui/asset-transform-plugin';
 
-import scrollIntoView from 'smooth-scroll-into-view-if-needed';
+import scrollIntoViewWithOffset from '../scrollIntoViewWithOffset';
 
 import {
   AutoScrollManagerPlugin,
@@ -22,7 +22,7 @@ import {
   useRegisterAsScrollable,
 } from '..';
 
-jest.mock('smooth-scroll-into-view-if-needed');
+jest.mock('../scrollIntoViewWithOffset');
 
 /**
  * HOC to enable scrollable behavior for a given component
@@ -134,7 +134,107 @@ describe('auto-scroll plugin', () => {
       action.click();
     });
 
-    expect(scrollIntoView).toBeCalledTimes(1);
+    expect(scrollIntoViewWithOffset).toBeCalledTimes(1);
+  });
+
+  test('works with custom base element and offset', async () => {
+    const getBaseElementMock = jest.fn();
+
+    const wp = new ReactPlayer({
+      plugins: [
+        new AssetTransformPlugin([
+          [{ type: 'action' }, actionTransform],
+          [{ type: 'input' }, inputTransform],
+        ]),
+        new CommonTypesPlugin(),
+        new AutoScrollManagerPlugin({
+          autoFocusOnErrorField: true,
+          getBaseElement: getBaseElementMock,
+          offset: 40,
+        }),
+      ],
+    });
+    wp.assetRegistry.set({ type: 'info' }, Info);
+    wp.assetRegistry.set({ type: 'action' }, Action);
+    wp.assetRegistry.set({ type: 'input' }, withScrollable(Input));
+
+    wp.start(flow as any);
+
+    const { container } = render(
+      <div>
+        <React.Suspense fallback="loading...">
+          <wp.Component />
+        </React.Suspense>
+      </div>
+    );
+    await act(() => waitFor(() => {}));
+
+    getBaseElementMock.mockReturnValue({ id: 'view' });
+
+    const action = await findByRole(container, 'button');
+    act(() => action.click());
+    await act(() => waitFor(() => {}));
+
+    expect(scrollIntoViewWithOffset).toBeCalledWith(
+      expect.anything(),
+      expect.objectContaining({ id: 'view' }),
+      40
+    );
+
+    // Mock the case where the base element can't be found, so document.body is used as a fallback
+    getBaseElementMock.mockReturnValue(null);
+
+    act(() => action.click());
+    await act(() => waitFor(() => {}));
+
+    expect(scrollIntoViewWithOffset).toHaveBeenLastCalledWith(
+      expect.anything(),
+      document.body,
+      40
+    );
+  });
+
+  test('works without custom base element and offset provided', async () => {
+    const getBaseElementMock = jest.fn();
+
+    const wp = new ReactPlayer({
+      plugins: [
+        new AssetTransformPlugin([
+          [{ type: 'action' }, actionTransform],
+          [{ type: 'input' }, inputTransform],
+        ]),
+        new CommonTypesPlugin(),
+        new AutoScrollManagerPlugin({
+          autoFocusOnErrorField: true,
+        }),
+      ],
+    });
+    wp.assetRegistry.set({ type: 'info' }, Info);
+    wp.assetRegistry.set({ type: 'action' }, Action);
+    wp.assetRegistry.set({ type: 'input' }, withScrollable(Input));
+
+    wp.start(flow as any);
+
+    const { container } = render(
+      <div>
+        <React.Suspense fallback="loading...">
+          <wp.Component />
+        </React.Suspense>
+      </div>
+    );
+    await act(() => waitFor(() => {}));
+
+    getBaseElementMock.mockReturnValue({ id: 'view' });
+
+    const action = await findByRole(container, 'button');
+    act(() => action.click());
+    await act(() => waitFor(() => {}));
+
+    expect(scrollIntoViewWithOffset).toBeCalledWith(
+      expect.anything(),
+      document.body,
+      0
+    );
   });
 
   test('no error no scroll test', async () => {
@@ -171,7 +271,7 @@ describe('auto-scroll plugin', () => {
       action.click();
     });
 
-    expect(scrollIntoView).not.toBeCalled();
+    expect(scrollIntoViewWithOffset).not.toBeCalled();
   });
 });
 
diff --git a/plugins/auto-scroll/react/src/hooks.tsx b/plugins/auto-scroll/react/src/hooks.tsx
index 7968a2e81..5071f81cb 100644
--- a/plugins/auto-scroll/react/src/hooks.tsx
+++ b/plugins/auto-scroll/react/src/hooks.tsx
@@ -1,6 +1,6 @@
 import type { PropsWithChildren } from 'react';
 import React, { useEffect, useState } from 'react';
-import scrollIntoView from 'smooth-scroll-into-view-if-needed';
+import scrollIntoViewWithOffset from './scrollIntoViewWithOffset';
 import type { ScrollType } from './index';
 
 export interface AutoScrollProviderProps {
@@ -8,6 +8,10 @@ export interface AutoScrollProviderProps {
   getElementToScrollTo: (
     scrollableElements: Map<ScrollType, Set<string>>
   ) => string;
+  /** Optional function to get container element, which is used for calculating offset (default: document.body) */
+  getBaseElement: () => HTMLElement | undefined | null;
+  /** Additional offset to be used (default: 0) */
+  offset: number;
 }
 
 export interface RegisterData {
@@ -35,6 +39,8 @@ export const useRegisterAsScrollable = (): ScrollFunction => {
 /** Component to handle scrolling */
 export const AutoScrollProvider = ({
   getElementToScrollTo,
+  getBaseElement,
+  offset,
   children,
 }: PropsWithChildren<AutoScrollProviderProps>) => {
   // Tracker for what elements are registered to be scroll targets
@@ -68,10 +74,7 @@ export const AutoScrollProvider = ({
     const node = document.getElementById(getElementToScrollTo(scrollableMap));
 
     if (node) {
-      scrollIntoView(node, {
-        block: 'center',
-        inline: 'center',
-      });
+      scrollIntoViewWithOffset(node, getBaseElement() || document.body, offset);
     }
   });
 
diff --git a/plugins/auto-scroll/react/src/plugin.tsx b/plugins/auto-scroll/react/src/plugin.tsx
index 406460401..5ba575cc9 100644
--- a/plugins/auto-scroll/react/src/plugin.tsx
+++ b/plugins/auto-scroll/react/src/plugin.tsx
@@ -14,6 +14,10 @@ export interface AutoScrollManagerConfig {
   autoScrollOnLoad?: boolean;
   /** Config to auto-focus on an error */
   autoFocusOnErrorField?: boolean;
+  /** Optional function to get container element, which is used for calculating offset (default: document.body) */
+  getBaseElement?: () => HTMLElement | undefined | null;
+  /** Additional offset to be used (default: 0) */
+  offset?: number;
 }
 
 /** A plugin to manage scrolling behavior */
@@ -32,6 +36,12 @@ export class AutoScrollManagerPlugin implements ReactPlayerPlugin {
   /** tracks if the navigation failed */
   private failedNavigation: boolean;
 
+  /** function to return the base of the scrollable area */
+  private getBaseElement: () => HTMLElement | undefined | null;
+
+  /** static offset */
+  private offset: number;
+
   /** map of scroll type to set of ids that are registered under that type */
   private alreadyScrolledTo: Array<string>;
   private scrollFn: (
@@ -41,6 +51,8 @@ export class AutoScrollManagerPlugin implements ReactPlayerPlugin {
   constructor(config: AutoScrollManagerConfig) {
     this.autoScrollOnLoad = config.autoScrollOnLoad ?? false;
     this.autoFocusOnErrorField = config.autoFocusOnErrorField ?? false;
+    this.getBaseElement = config.getBaseElement ?? (() => null);
+    this.offset = config.offset ?? 0;
     this.initialRender = false;
     this.failedNavigation = false;
     this.alreadyScrolledTo = [];
@@ -123,6 +135,8 @@ export class AutoScrollManagerPlugin implements ReactPlayerPlugin {
           this.initialRender = true;
           this.failedNavigation = false;
           this.alreadyScrolledTo = [];
+          // Reset scroll position for new view
+          window.scroll(0, 0);
         });
         flow.hooks.skipTransition.intercept({
           call: () => {
@@ -136,9 +150,13 @@ export class AutoScrollManagerPlugin implements ReactPlayerPlugin {
   applyReact(reactPlayer: ReactPlayer) {
     reactPlayer.hooks.webComponent.tap(this.name, (Comp) => {
       return () => {
-        const { scrollFn } = this;
+        const { scrollFn, getBaseElement, offset } = this;
         return (
-          <AutoScrollProvider getElementToScrollTo={scrollFn}>
+          <AutoScrollProvider
+            getElementToScrollTo={scrollFn}
+            getBaseElement={getBaseElement}
+            offset={offset}
+          >
             <Comp />
           </AutoScrollProvider>
         );
diff --git a/plugins/auto-scroll/react/src/scrollIntoViewWithOffset.ts b/plugins/auto-scroll/react/src/scrollIntoViewWithOffset.ts
new file mode 100644
index 000000000..8203030be
--- /dev/null
+++ b/plugins/auto-scroll/react/src/scrollIntoViewWithOffset.ts
@@ -0,0 +1,22 @@
+/**
+ * Scroll to the given element
+-* @param node Element to scroll to
+-* @param baseElement Container element used to calculate offset
+-* @param offset Additional offset
+ */
+
+import { scrollTo } from 'seamless-scroll-polyfill';
+
+export default (
+  node: HTMLElement,
+  baseElement: HTMLElement,
+  offset: number
+) => {
+  scrollTo(window, {
+    behavior: 'smooth',
+    top:
+      node.getBoundingClientRect().top -
+      baseElement.getBoundingClientRect().top -
+      offset,
+  });
+};
diff --git a/plugins/check-path/core/src/index.test.ts b/plugins/check-path/core/src/index.test.ts
index 1d54787ef..5bbb0f06d 100644
--- a/plugins/check-path/core/src/index.test.ts
+++ b/plugins/check-path/core/src/index.test.ts
@@ -6,6 +6,7 @@ import { makeFlow } from '@player-ui/make-flow';
 import { AssetTransformPlugin } from '@player-ui/asset-transform-plugin';
 import type { Asset, AssetWrapper } from '@player-ui/types';
 import { CheckPathPlugin } from '.';
+import { CheckPathPluginSymbol } from './symbols';
 
 const nestedAssetFlow = makeFlow({
   id: 'view-1',
@@ -96,6 +97,49 @@ const applicableFlow = makeFlow({
   },
 });
 
+const bindingIdFlow = makeFlow({
+  id: '{{mysterious}}',
+  type: 'view',
+  fields: {
+    asset: {
+      id: 'fields',
+      type: 'any',
+      values: [
+        {
+          asset: {
+            id: 'asset-1',
+            type: 'asset',
+          },
+        },
+        {
+          asset: {
+            id: 'asset-2',
+            type: 'asset',
+          },
+        },
+        {
+          asset: {
+            id: '{{resolveToUndefined}}',
+            type: 'any',
+            values: [
+              {
+                asset: {
+                  id: 'asset-3',
+                  type: 'asset',
+                },
+              },
+            ],
+          },
+        },
+      ],
+    },
+  },
+});
+
+bindingIdFlow.data = {
+  mysterious: 'the-resolved-id',
+};
+
 interface ViewAsset extends Asset<'view'> {
   /**
    *
@@ -108,15 +152,20 @@ interface TransformedView extends ViewAsset {
    *
    */
   run: () => string;
+
+  /** */
+  checkPath?: CheckPathPlugin;
 }
 
 /**
  *
  */
 const ViewTransform: TransformFunction<ViewAsset, TransformedView> = (
-  view
+  view,
+  options
 ) => ({
   ...view,
+  checkPath: options.utils?.findPlugin<CheckPathPlugin>(CheckPathPluginSymbol),
   run() {
     return 'hello';
   },
@@ -143,6 +192,11 @@ describe('check path plugin', () => {
     expect(view.run()).toStrictEqual('hello');
   });
 
+  test('checkPath in resolveOptions', () => {
+    const view = checkPathPlugin.getAsset('view-1') as TransformedView;
+    expect(view.checkPath).toBeDefined();
+  });
+
   test('getAsset after setting data', () => {
     (player.getState() as InProgressState).controllers.data.set({ count: 5 });
     const view = checkPathPlugin.getAsset('view-1') as TransformedView;
@@ -193,6 +247,10 @@ describe('check path plugin', () => {
     expect(
       checkPathPlugin.getPath('coll-val-1-label', { type: 'input' })
     ).toStrictEqual(['label', 'asset']);
+
+    expect(
+      checkPathPlugin.getPath('coll-val-1-label', { type: 'not-found' })
+    ).toBeUndefined();
   });
 
   describe('hasParentContext', () => {
@@ -284,6 +342,11 @@ describe('check path plugin', () => {
     it('handles the root node not having a parent', () => {
       expect(checkPathPlugin.getParent('view-1')).toBeUndefined();
     });
+
+    it('handles parent ids that are unresolved bindings', () => {
+      player.start(bindingIdFlow);
+      expect(checkPathPlugin.getParent('fields')?.id).toBe('the-resolved-id');
+    });
   });
 });
 
@@ -297,7 +360,10 @@ describe('works with applicability', () => {
     player = new Player({
       plugins: [checkPathPlugin],
     });
-    player.start(applicableFlow);
+    player.start({
+      ...applicableFlow,
+      data: { foo: { bar: false, baz: false } },
+    });
     dataController = (player.getState() as InProgressState).controllers.data;
   });
 
diff --git a/plugins/check-path/core/src/index.ts b/plugins/check-path/core/src/index.ts
index 55539d48f..6f704b902 100644
--- a/plugins/check-path/core/src/index.ts
+++ b/plugins/check-path/core/src/index.ts
@@ -3,6 +3,7 @@ import type { Player, PlayerPlugin, Node, Resolver } from '@player-ui/player';
 import type { Asset } from '@player-ui/types';
 import { createObjectMatcher } from '@player-ui/partial-match-registry';
 import dlv from 'dlv';
+import { CheckPathPluginSymbol } from './symbols';
 
 export type QueryFunction = (asset: Asset) => boolean;
 export type Query = QueryFunction | string | object;
@@ -51,7 +52,7 @@ interface ViewInfo {
  */
 function getParent(
   node: Node.Node,
-  viewInfo: ViewInfo
+  viewInfo?: ViewInfo
 ): Node.ViewOrAsset | undefined {
   let working = node;
 
@@ -69,12 +70,7 @@ function getParent(
     parent &&
     (parent.type === NodeType.Asset || parent.type === NodeType.View)
   ) {
-    const sourceNode = viewInfo.resolver.getSourceNode(parent);
-    const viewOrAsset =
-      sourceNode?.type === NodeType.Applicability
-        ? sourceNode.value
-        : viewInfo.resolver.getSourceNode(parent);
-    return (viewOrAsset ?? parent) as Node.ViewOrAsset;
+    return parent;
   }
 }
 
@@ -85,6 +81,7 @@ function getParent(
 export class CheckPathPlugin implements PlayerPlugin {
   name = 'check-path';
   private viewInfo?: ViewInfo;
+  public readonly symbol = CheckPathPluginSymbol;
 
   apply(player: Player) {
     player.hooks.viewController.tap(this.name, (viewController) => {
@@ -139,14 +136,12 @@ export class CheckPathPlugin implements PlayerPlugin {
       return undefined;
     }
 
-    let potentialMatch = getParent(assetNode, this.viewInfo);
+    let potentialMatch = getParent(assetNode);
 
     // Handle the case of an empty query (just get the immediate parent)
     if (query === undefined) {
       if (potentialMatch) {
-        const resolved = this.viewInfo.resolvedMap.get(potentialMatch);
-
-        return resolved?.value;
+        return this.getAssetFromAssetNode(potentialMatch);
       }
 
       return;
@@ -166,18 +161,18 @@ export class CheckPathPlugin implements PlayerPlugin {
       }
 
       const matcher = createMatcher(parentQuery);
-      const resolved = this.viewInfo.resolvedMap.get(potentialMatch);
+      const resolved = this.getAssetFromAssetNode(potentialMatch);
 
-      if (resolved && matcher(resolved.value)) {
+      if (resolved && matcher(resolved)) {
         // This is the last match.
         if (queryArray.length === 0) {
-          return resolved.value;
+          return resolved;
         }
 
         parentQuery = queryArray.shift();
       }
 
-      potentialMatch = getParent(potentialMatch, this.viewInfo);
+      potentialMatch = getParent(potentialMatch);
     }
 
     return undefined;
@@ -200,9 +195,7 @@ export class CheckPathPlugin implements PlayerPlugin {
     let parent;
 
     while (working) {
-      parent =
-        working?.parent &&
-        this.viewInfo.resolvedMap.get(working.parent)?.resolved;
+      parent = working?.parent;
 
       if (
         parent &&
@@ -263,12 +256,9 @@ export class CheckPathPlugin implements PlayerPlugin {
       node.type === NodeType.View ||
       node.type === NodeType.Applicability
     ) {
-      const resolved =
-        node.type === NodeType.Applicability
-          ? this.viewInfo?.resolvedMap.get(node.value)
-          : this.viewInfo?.resolvedMap.get(node);
+      const resolvedValue = this.getResolvedValue(node);
       const includesSelf =
-        (includeSelfMatch && resolved && matcher(resolved.value)) ?? false;
+        (includeSelfMatch && matcher(resolvedValue)) ?? false;
       const childQuery = includesSelf ? rest : query;
 
       if (childQuery.length === 0 && includesSelf) {
@@ -329,6 +319,15 @@ export class CheckPathPlugin implements PlayerPlugin {
     const assetNode = this.viewInfo?.assetIdMap.get(id);
     if (!assetNode) return;
 
+    return this.getAssetFromAssetNode(assetNode);
+  }
+
+  /**
+   * Gets the value for an asset from an asset node
+   */
+  public getAssetFromAssetNode(
+    assetNode: Node.Asset | Node.View
+  ): Asset | undefined {
     const sourceNode = this.getSourceAssetNode(assetNode);
     if (!sourceNode) return;
 
@@ -367,23 +366,20 @@ export class CheckPathPlugin implements PlayerPlugin {
     };
 
     while (working !== undefined) {
-      const parent =
-        working?.parent && this.viewInfo.resolvedMap.get(working.parent);
+      const { parent } = working;
 
-      const parentNode = parent?.resolved;
-
-      if (parentNode) {
-        if (parentNode.type === NodeType.MultiNode) {
-          const index = parentNode.values.indexOf(working);
+      if (parent) {
+        if (parent.type === NodeType.MultiNode) {
+          const index = parent.values.indexOf(working);
 
           if (index !== -1) {
             const actualIndex =
               index -
-              parentNode.values
+              parent.values
                 .slice(0, index)
                 .reduce(
                   (undefCount, next) =>
-                    this.viewInfo?.resolvedMap.get(next)?.value === undefined
+                    this.getResolvedValue(next) === undefined
                       ? undefCount + 1
                       : undefCount,
                   0
@@ -391,18 +387,17 @@ export class CheckPathPlugin implements PlayerPlugin {
 
             path = [actualIndex, ...path];
           }
-        } else if ('children' in parentNode) {
-          const childProp = findWorkingChild(parentNode);
+        } else if ('children' in parent) {
+          const childProp = findWorkingChild(parent);
           path = [...(childProp?.path ?? []), ...path];
         }
-      }
-
-      if (parentQuery) {
-        const matcher = createMatcher(parentQuery);
 
-        if (matcher(parent?.value)) {
-          parentQuery = queryArray.shift();
-          if (!parentQuery) return path;
+        if (parentQuery) {
+          const matcher = createMatcher(parentQuery);
+          if (matcher(this.getResolvedValue(parent))) {
+            parentQuery = queryArray.shift();
+            if (!parentQuery) return path;
+          }
         }
       }
 
@@ -411,6 +406,11 @@ export class CheckPathPlugin implements PlayerPlugin {
 
     /* if at the end all queries haven't been consumed, 
        it means we couldn't find a path till the matching query */
-    return queryArray.length === 0 ? path : undefined;
+    return parentQuery ? undefined : path;
+  }
+
+  private getResolvedValue(node: Node.Node) {
+    const sourceNode = this.getSourceAssetNode(node);
+    return this.viewInfo?.resolvedMap.get(sourceNode ?? node)?.value;
   }
 }
diff --git a/plugins/check-path/core/src/symbols.ts b/plugins/check-path/core/src/symbols.ts
new file mode 100644
index 000000000..e26f50400
--- /dev/null
+++ b/plugins/check-path/core/src/symbols.ts
@@ -0,0 +1 @@
+export const CheckPathPluginSymbol = Symbol.for('CheckPathPlugin');
diff --git a/plugins/common-expressions/core/src/expressions/__tests__/expressions.test.ts b/plugins/common-expressions/core/src/expressions/__tests__/expressions.test.ts
index 16ef5c373..c36c05a23 100644
--- a/plugins/common-expressions/core/src/expressions/__tests__/expressions.test.ts
+++ b/plugins/common-expressions/core/src/expressions/__tests__/expressions.test.ts
@@ -248,6 +248,17 @@ describe('expr functions', () => {
       expect(propertyIndex).toBe(-1);
     });
 
+    test('undefined binding', () => {
+      const propertyIndex = findPropertyIndex(
+        context,
+        undefined as any,
+        'name',
+        'Tyler'
+      );
+
+      expect(propertyIndex).toBe(-1);
+    });
+
     test('non-existant model ref', () => {
       expect(findPropertyIndex(context, 'not-there', 'name', 'Adam')).toBe(-1);
       expect(
diff --git a/plugins/common-expressions/core/src/expressions/index.ts b/plugins/common-expressions/core/src/expressions/index.ts
index 23df7ff2e..fe31e1a78 100644
--- a/plugins/common-expressions/core/src/expressions/index.ts
+++ b/plugins/common-expressions/core/src/expressions/index.ts
@@ -115,14 +115,18 @@ export const sum = withoutContext<Array<number | string>, number>((...args) => {
 
 /** Finds the property in an array of objects */
 export const findPropertyIndex: ExpressionHandler<
-  [Array<any> | Binding, string | undefined, any],
+  [Array<any> | Binding | undefined, string | undefined, any],
   number
 > = <T = unknown>(
   context: ExpressionContext,
-  bindingOrModel: Binding | Array<Record<string, T>>,
+  bindingOrModel: Binding | Array<Record<string, T>> | undefined,
   propToCheck: string | undefined,
   valueToCheck: T
 ) => {
+  if (bindingOrModel === undefined) {
+    return -1;
+  }
+
   const searchArray: Array<Record<string, T>> = Array.isArray(bindingOrModel)
     ? bindingOrModel
     : context.model.get(bindingOrModel);
diff --git a/plugins/common-types/core/src/formats/__tests__/formats.test.ts b/plugins/common-types/core/src/formats/__tests__/formats.test.ts
index 9a530c469..62d246f75 100644
--- a/plugins/common-types/core/src/formats/__tests__/formats.test.ts
+++ b/plugins/common-types/core/src/formats/__tests__/formats.test.ts
@@ -238,11 +238,19 @@ describe('phone', () => {
     it('preserves formatting when value is well formatted', () => {
       expect(phone.format?.('(123) 123-1231')).toBe('(123) 123-1231');
     });
+
+    it(`doesn't add extra characters beyond mask value`, () => {
+      expect(phone.format?.('(123) 123-123145')).toBe('(123) 123-1231');
+    });
   });
 
   describe('deformatting', () => {
     it('removes masking chars', () => {
       expect(phone.deformat?.('(123) 123-1231')).toBe('1231231231');
     });
+
+    it(`doesn't remove characters when deformatting a deformatted value`, () => {
+      expect(phone.deformat?.('1231231231')).toBe('1231231231');
+    });
   });
 });
diff --git a/plugins/common-types/core/src/formats/__tests__/utils.test.ts b/plugins/common-types/core/src/formats/__tests__/utils.test.ts
new file mode 100644
index 000000000..738b4adfd
--- /dev/null
+++ b/plugins/common-types/core/src/formats/__tests__/utils.test.ts
@@ -0,0 +1,43 @@
+import { removeFormatCharactersFromMaskedString, PLACEHOLDER } from '../utils';
+
+describe('removeFormatCharactersFromMaskedString', () => {
+  it('removes formatted characters correctly', () => {
+    const result = removeFormatCharactersFromMaskedString(
+      '123-456_789',
+      '###-###_###'
+    );
+    expect(result).toBe('123456789');
+  });
+
+  it('removes formatted characters with multiple reserved keys', () => {
+    const result = removeFormatCharactersFromMaskedString(
+      '123-456',
+      '###-$$$',
+      [PLACEHOLDER, '$']
+    );
+    expect(result).toBe('123456');
+  });
+
+  it(`doesn't remove characters when value doesn't align with mask`, () => {
+    const result = removeFormatCharactersFromMaskedString('123456', '###-###');
+    expect(result).toBe('123456');
+  });
+
+  it(`doesn't add characters that extend beyond the length of the mask`, () => {
+    expect(removeFormatCharactersFromMaskedString('123456', '#####')).toBe(
+      '12345'
+    );
+    expect(
+      removeFormatCharactersFromMaskedString('123456', '#$#-##', [
+        PLACEHOLDER,
+        '$',
+      ])
+    ).toBe('12345');
+  });
+
+  it(`doesn't add characters beyond possible characters that can be replaced`, () => {
+    expect(removeFormatCharactersFromMaskedString('123456', '###-##')).toBe(
+      '12345'
+    );
+  });
+});
diff --git a/plugins/common-types/core/src/formats/utils.ts b/plugins/common-types/core/src/formats/utils.ts
index 148cb5055..825ee73a4 100644
--- a/plugins/common-types/core/src/formats/utils.ts
+++ b/plugins/common-types/core/src/formats/utils.ts
@@ -17,10 +17,34 @@ export const removeFormatCharactersFromMaskedString = (
   mask: string,
   reserved: string[] = [PLACEHOLDER]
 ): string => {
+  const reservedMatchesLength = mask
+    .split('')
+    .filter((val) => reserved.includes(val)).length;
+  let replacements = 0;
+
   return value.split('').reduce((newString, nextChar, nextIndex) => {
     const maskedVal = mask[nextIndex];
 
+    if (maskedVal === undefined) {
+      return newString;
+    }
+
+    if (reservedMatchesLength === replacements) {
+      return newString;
+    }
+
     if (reserved.includes(maskedVal)) {
+      replacements++;
+      return newString + nextChar;
+    }
+
+    /**
+     * Characters will match when the incoming value is formatted, but in cases
+     * where it's being pulled from the model and deformatted again, ensure we
+     * don't skip over characters.
+     */
+    if (maskedVal !== nextChar) {
+      replacements++;
       return newString + nextChar;
     }
 
diff --git a/plugins/common-types/core/src/validators/__tests__/index.test.ts b/plugins/common-types/core/src/validators/__tests__/index.test.ts
index f273dd34a..56bbebbc6 100644
--- a/plugins/common-types/core/src/validators/__tests__/index.test.ts
+++ b/plugins/common-types/core/src/validators/__tests__/index.test.ts
@@ -56,6 +56,7 @@ function create(
     validation: fullValidation,
     constants: new ConstantsController(),
     evaluate: new ExpressionEvaluator({ model }).evaluate,
+    schemaType: undefined,
   };
 
   return { context, validation: fullValidation };
@@ -175,9 +176,9 @@ describe('string', () => {
   });
 
   it('parameters on non-strings', () => {
-    expect(string(context, {})?.parameters.type).toBe('object');
-    expect(string(context, [])?.parameters.type).toBe('object');
-    expect(string(context, 1234)?.parameters.type).toBe('number');
+    expect(string(context, {})?.parameters?.type).toBe('object');
+    expect(string(context, [])?.parameters?.type).toBe('object');
+    expect(string(context, 1234)?.parameters?.type).toBe('number');
   });
 });
 
@@ -202,11 +203,11 @@ describe('integer', () => {
   });
 
   it('parameters on non-integers', () => {
-    expect(integer(context, 1234.567)?.parameters.type).toBe('number');
-    expect(integer(context, 1234.567)?.parameters.flooredValue).toBe(1234);
+    expect(integer(context, 1234.567)?.parameters?.type).toBe('number');
+    expect(integer(context, 1234.567)?.parameters?.flooredValue).toBe(1234);
 
-    expect(integer(context, 'test')?.parameters.type).toBe('string');
-    expect(integer(context, 'test')?.parameters.flooredValue).toBe(NaN);
+    expect(integer(context, 'test')?.parameters?.type).toBe('string');
+    expect(integer(context, 'test')?.parameters?.flooredValue).toBe(NaN);
   });
 
   it('errors on out of bounds integers', () => {
@@ -273,7 +274,7 @@ describe('oneOf', () => {
 });
 
 describe('regex', () => {
-  const { context } = create({ type: 'regex' });
+  const { context } = create({ type: 'regex' }, { foo: 'asset' });
 
   it('does nothing with invalid entries', () => {
     expect(regex(context, undefined)).toBe(undefined);
@@ -296,6 +297,19 @@ describe('regex', () => {
     );
     // i ignores case
     expect(regex(context, 'FOO', { regex: '/foo/i' })).toBe(undefined);
+
+    // Regex uses data reference
+    expect(regex(context, 'asset_text', { regex: '{{foo}}' })).toBe(undefined);
+    expect(regex(context, 'asset_text', { regex: '@[{{foo}}]@' })).toBe(
+      undefined
+    );
+
+    expect(regex(context, 'asset_text', { regex: 'a{{foo}}' })?.message).toBe(
+      'Invalid entry'
+    );
+    expect(regex(context, 'view_info', { regex: '{{foo}}' })?.message).toBe(
+      'Invalid entry'
+    );
   });
 });
 
diff --git a/plugins/common-types/core/src/validators/index.ts b/plugins/common-types/core/src/validators/index.ts
index ee3278ceb..54f0b9342 100644
--- a/plugins/common-types/core/src/validators/index.ts
+++ b/plugins/common-types/core/src/validators/index.ts
@@ -1,4 +1,5 @@
 import type { Expression } from '@player-ui/types';
+import { resolveDataRefs } from '@player-ui/player';
 import type { ValidatorFunction } from '@player-ui/player';
 
 // Shamelessly lifted from Scott Gonzalez via the Bassistance Validation plugin http://projects.scottsplayground.com/email_address_validation/
@@ -177,12 +178,13 @@ export const regex: ValidatorFunction<{
     return;
   }
 
+  const resolvedRegex = resolveDataRefs(options.regex, context);
   // Split up /pattern/flags into [pattern, flags]
-  const patternMatch = options.regex.match(/^\/(.*)\/(\w)*$/);
+  const patternMatch = resolvedRegex.match(/^\/(.*)\/(\w)*$/);
 
   const regexp = patternMatch
     ? new RegExp(patternMatch[1], patternMatch[2])
-    : new RegExp(options.regex);
+    : new RegExp(resolvedRegex);
 
   if (!regexp.test(value)) {
     const message = context.constants.getConstants(
diff --git a/plugins/computed-properties/core/BUILD b/plugins/computed-properties/core/BUILD
index d05a53f0c..e8973d469 100644
--- a/plugins/computed-properties/core/BUILD
+++ b/plugins/computed-properties/core/BUILD
@@ -10,5 +10,10 @@ javascript_pipeline(
     ],
     test_data = [
         "//core/make-flow:@player-ui/make-flow",
+        "//plugins/common-expressions/core:@player-ui/common-expressions-plugin",
+        "//plugins/common-types/core:@player-ui/common-types-plugin",
+        "//plugins/asset-transform/core:@player-ui/asset-transform-plugin",
+        "//core/make-flow:@player-ui/make-flow",
+        "//core/partial-match-registry:@player-ui/partial-match-registry"
     ]
 )
diff --git a/plugins/computed-properties/core/src/__tests__/index.test.ts b/plugins/computed-properties/core/src/__tests__/index.test.ts
index ed4fa6a0e..b44867b6c 100644
--- a/plugins/computed-properties/core/src/__tests__/index.test.ts
+++ b/plugins/computed-properties/core/src/__tests__/index.test.ts
@@ -1,5 +1,5 @@
 import { waitFor } from '@testing-library/react';
-import type { InProgressState } from '@player-ui/player';
+import type { InProgressState, Flow } from '@player-ui/player';
 import { Player } from '@player-ui/player';
 import { makeFlow } from '@player-ui/make-flow';
 import { ComputedPropertiesPlugin } from '..';
@@ -140,3 +140,80 @@ test('updates work across computations', async () => {
 
   await waitFor(() => expect(getView().label).toBe(undefined));
 });
+
+test('expressions update dependent expressions', async () => {
+  const flowWithDependentComputedValues: Flow = makeFlow({
+    views: [
+      {
+        id: 'view-1',
+        type: 'view',
+        computed: {
+          asset: {
+            id: 'computed',
+            type: 'text',
+            applicability: '{{foo.bar.computedValue}}',
+          },
+        },
+        dependent: {
+          asset: {
+            id: 'dependent',
+            type: 'text',
+            applicability: '{{foo.bar.dependentValue}}',
+          },
+        },
+      },
+    ],
+    data: {
+      foo: {
+        bar: {
+          sourceValue: false,
+        },
+      },
+    },
+    schema: {
+      ROOT: {
+        foo: {
+          type: 'FooType',
+        },
+      },
+      FooType: {
+        bar: {
+          type: 'BarType',
+        },
+      },
+      BarType: {
+        sourceValue: {
+          type: 'boolean',
+        },
+        computedValue: {
+          type: 'Expression',
+          exp: '{{foo.bar.sourceValue}} == true',
+        },
+        dependentValue: {
+          type: 'Expression',
+          exp: '{{foo.bar.computedValue}} == false',
+        },
+      },
+    },
+  });
+  const player = new Player({ plugins: [new ComputedPropertiesPlugin()] });
+  player.start(flowWithDependentComputedValues);
+
+  const getView = () => {
+    return (player.getState() as InProgressState).controllers.view.currentView
+      ?.lastUpdate as any;
+  };
+
+  // The label should start off as not there
+
+  expect(getView().computed).toBeUndefined();
+  expect(getView().dependent).toBeDefined();
+
+  (player.getState() as InProgressState).controllers.data.set([
+    ['foo.bar.sourceValue', true],
+  ]);
+  await waitFor(() => {
+    expect(getView().computed).toBeDefined();
+    expect(getView().dependent).toBeUndefined();
+  });
+});
diff --git a/plugins/computed-properties/core/src/__tests__/validation.test.ts b/plugins/computed-properties/core/src/__tests__/validation.test.ts
new file mode 100644
index 000000000..63df70c23
--- /dev/null
+++ b/plugins/computed-properties/core/src/__tests__/validation.test.ts
@@ -0,0 +1,218 @@
+/* eslint-disable jest/expect-expect */
+import { CommonExpressionsPlugin } from '@player-ui/common-expressions-plugin';
+import { CommonTypesPlugin } from '@player-ui/common-types-plugin';
+import { Registry } from '@player-ui/partial-match-registry';
+import { AssetTransformPlugin } from '@player-ui/asset-transform-plugin';
+import { makeFlow } from '@player-ui/make-flow';
+import { Player } from '@player-ui/player';
+import type {
+  Asset,
+  BindingInstance,
+  TransformFunction,
+  ValidationResponse,
+} from '@player-ui/player';
+import { ComputedPropertiesPlugin } from '..';
+
+// This is a flow that uses computed properties for evaluating cross field validation
+const flowWithComputedValidation = makeFlow({
+  id: 'view-1',
+  type: 'view',
+  template: [
+    {
+      data: 'items',
+      output: 'bindings',
+      value: 'items._index_.isRelevant',
+    },
+  ],
+  validation: [
+    {
+      type: 'expression',
+      ref: 'items.0.isRelevant',
+      message: 'Please select at least one item.',
+      exp: '{{expressions.SelectionValidation}}',
+    },
+  ],
+});
+
+flowWithComputedValidation.schema = {
+  ROOT: {
+    items: {
+      type: 'ItemType',
+      isArray: true,
+    },
+    expressions: {
+      type: 'ExpressionsType',
+    },
+  },
+  itemType: {
+    name: {
+      type: 'TextType',
+    },
+    isRelevant: {
+      type: 'BooleanType',
+    },
+  },
+  ExpressionsType: {
+    SelectionValidation: {
+      type: 'Expression',
+      exp: 'findProperty({{items}}, "isRelevant", true, "isRelevant", false)',
+    },
+  },
+};
+
+flowWithComputedValidation.data = {
+  items: [
+    {
+      name: 'One',
+      isRelevant: false,
+    },
+    {
+      name: 'Two',
+      isRelevant: false,
+    },
+  ],
+};
+
+interface ValidationView extends Asset<'view'> {
+  /**
+   *
+   */
+  bindings: string[];
+}
+
+/**
+ *
+ */
+const validationTrackerTransform: TransformFunction<
+  ValidationView,
+  ValidationView & {
+    /**
+     *
+     */
+    validation?: ValidationResponse;
+  }
+> = (asset, options) => {
+  const { bindings } = asset;
+  let validation: ValidationResponse | undefined;
+
+  // Setup tracking on each binding
+  bindings.forEach((binding) => {
+    validation =
+      options.validation?.get(binding, { track: true }) ?? validation;
+  });
+
+  return {
+    ...asset,
+    validation,
+  };
+};
+
+describe('cross field validation can use computed properties', () => {
+  /**
+   *
+   */
+  const baseValidationTest = (dataUpdate: Record<string, any>) => async () => {
+    const player = new Player({
+      plugins: [
+        // necessary for data middleware to reference {{expressions}}
+        new ComputedPropertiesPlugin(),
+
+        // necessary for expression type validation
+        new CommonTypesPlugin(),
+        // necessary for findProperty
+        new CommonExpressionsPlugin(),
+
+        // necessary to track validations
+        new AssetTransformPlugin(
+          new Registry([[{ type: 'view' }, validationTrackerTransform]])
+        ),
+      ],
+    });
+
+    const result = player.start(flowWithComputedValidation);
+
+    /**
+     *
+     */
+    const getControllers = () => {
+      const state = player.getState();
+      if (state.status === 'in-progress') {
+        return state.controllers;
+      }
+    };
+
+    /**
+     *
+     */
+    const getCurrentView = () => {
+      const controllers = getControllers();
+      return controllers ? controllers.view.currentView : undefined;
+    };
+
+    /**
+     *
+     */
+    const withValidations = (
+      assertions: (validations: {
+        /**
+         *
+         */
+        canTransition: boolean;
+        /**
+         *
+         */
+        validations?: Map<BindingInstance, ValidationResponse>;
+      }) => void
+    ) => assertions(getControllers()!.validation.validateView()!);
+
+    expect(getCurrentView()?.initialView.id).toBe('view-1');
+
+    withValidations(({ canTransition, validations }) => {
+      expect(canTransition).toBe(false);
+      expect(validations?.size).toBe(1);
+    });
+
+    getControllers()?.flow.transition('Next');
+
+    // Transition fails do to blocking validation
+    expect(getCurrentView()?.initialView.id).toBe('view-1');
+
+    getControllers()?.data.set(dataUpdate);
+
+    withValidations(({ canTransition, validations }) => {
+      expect(canTransition).toBe(true);
+      expect(validations).toBeUndefined();
+    });
+
+    getControllers()?.flow.transition('Next');
+
+    const { endState } = await result;
+    // eslint-disable-next-line jest/no-standalone-expect
+    expect(endState).toStrictEqual({
+      outcome: 'done',
+      state_type: 'END',
+    });
+  };
+
+  test(
+    'updating ref data should remove validation',
+    baseValidationTest({
+      'items.0.isRelevant': true,
+    })
+  );
+
+  test(
+    'updating non-ref data should remove validation',
+    baseValidationTest({
+      'items.1.isRelevant': true,
+    })
+  );
+
+  test(
+    'updating both should remove validation',
+    baseValidationTest({
+      'items.0.isRelevant': true,
+      'items.1.isRelevant': true,
+    })
+  );
+});
diff --git a/plugins/computed-properties/core/src/index.ts b/plugins/computed-properties/core/src/index.ts
index b0206624c..18bd37d42 100644
--- a/plugins/computed-properties/core/src/index.ts
+++ b/plugins/computed-properties/core/src/index.ts
@@ -67,6 +67,15 @@ export class ComputedPropertiesPlugin implements PlayerPlugin {
 
         return next?.set(transaction, options) ?? [];
       },
+      delete(binding, options, next) {
+        if (getExpressionType(binding)) {
+          throw new Error(
+            `Invalid 'delete' operation on computed property: ${binding.asString()}`
+          );
+        }
+
+        return next?.delete(binding, options);
+      },
     };
 
     player.hooks.dataController.tap(this.name, (dataController) => {
diff --git a/plugins/data-change-listener/core/src/index.test.ts b/plugins/data-change-listener/core/src/index.test.ts
index 57ba61fb2..e475d91c5 100644
--- a/plugins/data-change-listener/core/src/index.test.ts
+++ b/plugins/data-change-listener/core/src/index.test.ts
@@ -293,6 +293,10 @@ describe('Data-Change-Listener with Validations', () => {
     return getState().controllers.view.currentView?.lastUpdate?.fields.asset;
   }
 
+  const getCurrentView = () => {
+    return getState().controllers.view.currentView;
+  };
+
   beforeEach(() => {
     player = new Player({
       plugins: [
@@ -311,11 +315,11 @@ describe('Data-Change-Listener with Validations', () => {
         testExpression(...args);
       });
     });
-  });
 
-  it('bindings with a value that failed validation do not trigger listeners', async () => {
     player.start(flow);
+  });
 
+  it('bindings with a value that failed validation do not trigger listeners', async () => {
     expect(getInputAsset().validation).toBe(undefined);
 
     getInputAsset().set('AdamAdam');
@@ -326,8 +330,6 @@ describe('Data-Change-Listener with Validations', () => {
   });
 
   it('bindings with a successful validation trigger listeners', async () => {
-    player.start(flow);
-
     expect(getInputAsset().validation).toBe(undefined);
 
     getInputAsset().set('Adam');
@@ -336,4 +338,8 @@ describe('Data-Change-Listener with Validations', () => {
       expect(testExpression).toHaveBeenCalled();
     });
   });
+
+  it('removes listeners section after resolving', () => {
+    expect(getCurrentView()?.initialView?.listeners).toBeUndefined();
+  });
 });
diff --git a/plugins/data-change-listener/core/src/index.ts b/plugins/data-change-listener/core/src/index.ts
index a7710be65..6b9162f20 100644
--- a/plugins/data-change-listener/core/src/index.ts
+++ b/plugins/data-change-listener/core/src/index.ts
@@ -9,6 +9,7 @@ import type {
   BindingInstance,
   BindingParser,
 } from '@player-ui/player';
+import { isExpressionNode } from '@player-ui/player';
 
 const LISTENER_TYPES = {
   dataChange: 'dataChange.',
@@ -35,20 +36,24 @@ export type ViewListenerHandler = (
 
 /** Sub out any _index_ refs with the ones from the supplied list */
 function replaceExpressionIndexes(
-  exp: ExpressionType,
+  expression: ExpressionType,
   indexes: Array<string | number>
 ): ExpressionType {
   if (indexes.length === 0) {
-    return exp;
+    return expression;
   }
 
-  if (typeof exp === 'object' && exp !== null) {
-    return Object.values(exp).map((subExp) =>
+  if (isExpressionNode(expression)) {
+    return expression;
+  }
+
+  if (Array.isArray(expression)) {
+    return expression.map((subExp) =>
       replaceExpressionIndexes(subExp, indexes)
-    );
+    ) as any;
   }
 
-  let workingExp = String(exp);
+  let workingExp = String(expression);
 
   for (
     let replacementIndex = 0;
@@ -265,6 +270,12 @@ export class DataChangeListenerPlugin implements PlayerPlugin {
       this.name,
       (viewController: ViewController) => {
         viewController.hooks.resolveView.intercept(resolveViewInterceptor);
+
+        // remove listeners after extracting so that it does not get triggered in subsequent view updates
+        viewController.hooks.resolveView.tap(this.name, (view) => {
+          const { listeners, ...withoutListeners } = view as any;
+          return withoutListeners;
+        });
       }
     );
 
diff --git a/plugins/external-action/core/src/__tests__/index.test.ts b/plugins/external-action/core/src/__tests__/index.test.ts
index 0802b1b16..a73400619 100644
--- a/plugins/external-action/core/src/__tests__/index.test.ts
+++ b/plugins/external-action/core/src/__tests__/index.test.ts
@@ -1,4 +1,4 @@
-import type { Flow } from '@player-ui/player';
+import type { Flow, InProgressState } from '@player-ui/player';
 import { Player } from '@player-ui/player';
 import { ExternalActionPlugin } from '..';
 
@@ -105,3 +105,93 @@ test('allows multiple plugins', async () => {
   // Prev should win
   expect(completed.endState.outcome).toBe('BCK');
 });
+
+test('only transitions if player still on this external state', async () => {
+  let resolver: (() => void) | undefined;
+  const player = new Player({
+    plugins: [
+      new ExternalActionPlugin((state, options) => {
+        return new Promise((res) => {
+          // Only save resolver for first external action
+          if (!resolver) {
+            resolver = () => {
+              res(options.data.get('transitionValue'));
+            };
+          }
+        });
+      }),
+    ],
+  });
+
+  player.start({
+    id: 'test-flow',
+    data: {
+      transitionValue: 'Next',
+    },
+    navigation: {
+      BEGIN: 'FLOW_1',
+      FLOW_1: {
+        startState: 'EXT_1',
+        EXT_1: {
+          state_type: 'EXTERNAL',
+          ref: 'test-1',
+          transitions: {
+            Next: 'EXT_2',
+            Prev: 'END_BCK',
+          },
+        },
+        EXT_2: {
+          state_type: 'EXTERNAL',
+          ref: 'test-2',
+          transitions: {
+            Next: 'END_FWD',
+            Prev: 'END_BCK',
+          },
+        },
+        END_FWD: {
+          state_type: 'END',
+          outcome: 'FWD',
+        },
+        END_BCK: {
+          state_type: 'END',
+          outcome: 'BCK',
+        },
+      },
+    },
+  } as Flow);
+
+  let state = player.getState();
+  expect(state.status).toBe('in-progress');
+  expect(
+    (state as InProgressState).controllers.flow.current?.currentState?.name
+  ).toBe('EXT_1');
+
+  // probably dumb way to wait for async stuff to resolve
+  await new Promise<void>((res) => {
+    /**
+     *
+     */
+    function waitForResolver() {
+      if (resolver) res();
+      else setTimeout(waitForResolver, 50);
+    }
+
+    waitForResolver();
+  });
+
+  (state as InProgressState).controllers.flow.transition('Next');
+
+  state = player.getState();
+  expect(
+    (state as InProgressState).controllers.flow.current?.currentState?.name
+  ).toBe('EXT_2');
+
+  // Attempt to resolve _after_ Player has transitioned
+  resolver?.();
+
+  // Should be same as prev
+  state = player.getState();
+  expect(
+    (state as InProgressState).controllers.flow.current?.currentState?.name
+  ).toBe('EXT_2');
+});
diff --git a/plugins/external-action/core/src/index.ts b/plugins/external-action/core/src/index.ts
index 74975d4e3..ee9908344 100644
--- a/plugins/external-action/core/src/index.ts
+++ b/plugins/external-action/core/src/index.ts
@@ -1,4 +1,9 @@
-import type { Player, PlayerPlugin, InProgressState } from '@player-ui/player';
+import type {
+  Player,
+  PlayerPlugin,
+  InProgressState,
+  PlayerFlowState,
+} from '@player-ui/player';
 import type { NavigationFlowExternalState } from '@player-ui/types';
 
 export type ExternalStateHandler = (
@@ -22,16 +27,18 @@ export class ExternalActionPlugin implements PlayerPlugin {
       flowController.hooks.flow.tap(this.name, (flow) => {
         flow.hooks.transition.tap(this.name, (fromState, toState) => {
           const { value: state } = toState;
-
           if (state.state_type === 'EXTERNAL') {
             setTimeout(async () => {
-              const currentState = player.getState();
-
-              if (
+              /** Helper for ensuring state is still current relative to external state this is handling */
+              const shouldTransition = (
+                currentState: PlayerFlowState
+              ): currentState is InProgressState =>
                 currentState.status === 'in-progress' &&
                 currentState.controllers.flow.current?.currentState?.value ===
-                  state
-              ) {
+                  state;
+
+              const currentState = player.getState();
+              if (shouldTransition(currentState)) {
                 try {
                   const transitionValue = await this.handler(
                     state,
@@ -39,7 +46,15 @@ export class ExternalActionPlugin implements PlayerPlugin {
                   );
 
                   if (transitionValue !== undefined) {
-                    currentState.controllers.flow.transition(transitionValue);
+                    // Ensure the Player is still in the same state after waiting for transitionValue
+                    const latestState = player.getState();
+                    if (shouldTransition(latestState)) {
+                      latestState.controllers.flow.transition(transitionValue);
+                    } else {
+                      player.logger.warn(
+                        `External state resolved with [${transitionValue}], but Player already navigated away from [${toState.name}]`
+                      );
+                    }
                   }
                 } catch (error) {
                   if (error instanceof Error) {
diff --git a/plugins/markdown/core/BUILD b/plugins/markdown/core/BUILD
new file mode 100644
index 000000000..fc09c805a
--- /dev/null
+++ b/plugins/markdown/core/BUILD
@@ -0,0 +1,22 @@
+load("//:index.bzl", "javascript_pipeline")
+
+javascript_pipeline(
+    name = "@player-ui/markdown-plugin",
+    dependencies = [
+        "@npm//tapable-ts",
+        "@npm//mdast-util-from-markdown"
+    ],
+    peer_dependencies = [
+        "//core/player:@player-ui/player",
+        "//core/types:@player-ui/types",
+    ],
+    build_data = [
+        "@npm//babel-loader",
+        "@npm//@babel/plugin-transform-numeric-separator",
+    ],
+    test_data = [
+        "//core/partial-match-registry:@player-ui/partial-match-registry",
+        "//plugins/partial-match-fingerprint/core:@player-ui/partial-match-fingerprint-plugin",
+    ],
+    library_name = "MarkdownPlugin"
+)
diff --git a/plugins/markdown/core/src/__tests__/__snapshots__/index.test.ts.snap b/plugins/markdown/core/src/__tests__/__snapshots__/index.test.ts.snap
new file mode 100644
index 000000000..8364036c6
--- /dev/null
+++ b/plugins/markdown/core/src/__tests__/__snapshots__/index.test.ts.snap
@@ -0,0 +1,241 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`MarkdownPlugin parses the flow containing markdown into valid FRF, based on the given mappers 1`] = `
+Object {
+  "id": "markdown-view",
+  "primaryInfo": Object {
+    "asset": Object {
+      "id": "markdown-primaryInfo-collection",
+      "type": "collection",
+      "values": Array [
+        Object {
+          "asset": Object {
+            "id": "markdown-primaryInfo-collection-bold-composite-7",
+            "type": "composite",
+            "values": Array [
+              Object {
+                "asset": Object {
+                  "id": "markdown-primaryInfo-collection-bold-text-4",
+                  "type": "text",
+                  "value": "some ",
+                },
+              },
+              Object {
+                "asset": Object {
+                  "id": "markdown-primaryInfo-collection-bold-text-5",
+                  "modifiers": Array [
+                    Object {
+                      "type": "tag",
+                      "value": "important",
+                    },
+                  ],
+                  "type": "text",
+                  "value": "bold text",
+                },
+              },
+            ],
+          },
+        },
+        Object {
+          "asset": Object {
+            "id": "markdown-primaryInfo-collection-italic-text-8",
+            "modifiers": Array [
+              Object {
+                "type": "tag",
+                "value": "emphasis",
+              },
+            ],
+            "type": "text",
+            "value": "italicized text",
+          },
+        },
+        Object {
+          "asset": Object {
+            "id": "markdown-primaryInfo-collection-orderd-list-list-20",
+            "metaData": Object {
+              "listType": "ordered",
+            },
+            "type": "list",
+            "values": Array [
+              Object {
+                "asset": Object {
+                  "id": "markdown-primaryInfo-collection-orderd-list-text-11",
+                  "type": "text",
+                  "value": "First",
+                },
+              },
+              Object {
+                "asset": Object {
+                  "id": "markdown-primaryInfo-collection-orderd-list-text-14",
+                  "type": "text",
+                  "value": "Second",
+                },
+              },
+              Object {
+                "asset": Object {
+                  "id": "markdown-primaryInfo-collection-orderd-list-text-17",
+                  "type": "text",
+                  "value": "Third",
+                },
+              },
+            ],
+          },
+        },
+        Object {
+          "asset": Object {
+            "id": "markdown-primaryInfo-collection-unorderd-list-list-31",
+            "type": "list",
+            "values": Array [
+              Object {
+                "asset": Object {
+                  "id": "markdown-primaryInfo-collection-unorderd-list-text-21",
+                  "modifiers": Array [
+                    Object {
+                      "metaData": Object {
+                        "ref": "https://turbotax.intuit.ca",
+                      },
+                      "type": "link",
+                    },
+                  ],
+                  "type": "text",
+                  "value": "First",
+                },
+              },
+              Object {
+                "asset": Object {
+                  "id": "markdown-primaryInfo-collection-unorderd-list-text-25",
+                  "type": "text",
+                  "value": "Second",
+                },
+              },
+              Object {
+                "asset": Object {
+                  "id": "markdown-primaryInfo-collection-unorderd-list-text-28",
+                  "type": "text",
+                  "value": "Third",
+                },
+              },
+            ],
+          },
+        },
+        Object {
+          "asset": Object {
+            "accessibility": "alt text",
+            "id": "markdown-primaryInfo-collection-image-image-32",
+            "metaData": Object {
+              "ref": "image.png",
+            },
+            "type": "image",
+          },
+        },
+        Object {
+          "asset": Object {
+            "id": "markdown-primaryInfo-collection-unsupported-text-34",
+            "type": "text",
+            "value": "Highlights are ==not supported==",
+          },
+        },
+      ],
+    },
+  },
+  "title": Object {
+    "asset": Object {
+      "id": "markdown-view-title-composite-3",
+      "type": "composite",
+      "values": Array [
+        Object {
+          "asset": Object {
+            "id": "markdown-view-title-text-0",
+            "type": "text",
+            "value": "Learn more at ",
+          },
+        },
+        Object {
+          "asset": Object {
+            "id": "markdown-view-title-text-1",
+            "modifiers": Array [
+              Object {
+                "metaData": Object {
+                  "ref": "https://turbotax.intuit.ca",
+                },
+                "type": "link",
+              },
+            ],
+            "type": "text",
+            "value": "TurboTax Canada",
+          },
+        },
+      ],
+    },
+  },
+  "type": "questionAnswer",
+}
+`;
+
+exports[`MarkdownPlugin parses the flow, with only the required mappers 1`] = `
+Object {
+  "id": "markdown-view",
+  "primaryInfo": Object {
+    "asset": Object {
+      "id": "markdown-primaryInfo-collection",
+      "type": "collection",
+      "values": Array [
+        Object {
+          "asset": Object {
+            "id": "markdown-primaryInfo-collection-bold",
+            "type": "text",
+            "value": "some **bold text**",
+          },
+        },
+        Object {
+          "asset": Object {
+            "id": "markdown-primaryInfo-collection-italic",
+            "type": "text",
+            "value": "*italicized text*",
+          },
+        },
+        Object {
+          "asset": Object {
+            "id": "markdown-primaryInfo-collection-orderd-list",
+            "type": "text",
+            "value": "1. First
+2. Second
+3. Third",
+          },
+        },
+        Object {
+          "asset": Object {
+            "id": "markdown-primaryInfo-collection-unorderd-list",
+            "type": "text",
+            "value": "- [First](https://turbotax.intuit.ca)
+- Second
+- Third",
+          },
+        },
+        Object {
+          "asset": Object {
+            "id": "markdown-primaryInfo-collection-image",
+            "type": "text",
+            "value": "![alt text](image.png)",
+          },
+        },
+        Object {
+          "asset": Object {
+            "id": "markdown-primaryInfo-collection-unsupported-text-36",
+            "type": "text",
+            "value": "Highlights are ==not supported==",
+          },
+        },
+      ],
+    },
+  },
+  "title": Object {
+    "asset": Object {
+      "id": "markdown-view-title",
+      "type": "text",
+      "value": "Learn more at [TurboTax Canada](https://turbotax.intuit.ca)",
+    },
+  },
+  "type": "questionAnswer",
+}
+`;
diff --git a/plugins/markdown/core/src/__tests__/helpers/index.ts b/plugins/markdown/core/src/__tests__/helpers/index.ts
new file mode 100644
index 000000000..eb2b40053
--- /dev/null
+++ b/plugins/markdown/core/src/__tests__/helpers/index.ts
@@ -0,0 +1,154 @@
+import type { Asset, AssetWrapper } from '@player-ui/types';
+import type { Mappers } from '../../types';
+
+// Mock Asset Plugin implementation of the markdown plugin:
+
+let depth = 0;
+
+/**
+ * Wrap an Asset in an AssetWrapper
+ */
+function wrapAsset(asset: Asset): AssetWrapper {
+  return {
+    asset,
+  };
+}
+
+/**
+ * Flatten a composite Asset with a single element
+ */
+function flatSingleElementCompositeAsset(asset: Asset): Asset {
+  if (asset.type === 'composite' && (asset.values as Asset[]).length === 1) {
+    return (asset.values as AssetWrapper[])[0].asset;
+  }
+
+  return asset;
+}
+
+/**
+ * Recursively applies a modifier to an Asset and its children.
+ */
+function applyModifierToAssets({
+  types,
+  asset,
+  modifiers,
+}: {
+  /**
+   *  Types of Assets to apply the modifier to
+   */
+  types: string[];
+  /**
+   * Asset to be modified
+   */
+  asset: Asset;
+  /**
+   * Modifiers to be applied to the Asset
+   */
+  modifiers: any[];
+}): Asset {
+  let modifiedAsset = asset;
+  if (types.includes(asset.type)) {
+    modifiedAsset = {
+      ...asset,
+      modifiers: [...((asset.modifiers as any) || []), ...modifiers],
+    };
+  }
+
+  if (asset.values) {
+    modifiedAsset = {
+      ...modifiedAsset,
+      values: (asset.values as Asset[]).map((a) =>
+        applyModifierToAssets({ types, asset: a, modifiers })
+      ),
+    };
+  }
+
+  return modifiedAsset;
+}
+
+export const mockMappers: Mappers = {
+  text: ({ originalAsset, value }) => ({
+    id: `${originalAsset.id}-text-${depth++}`,
+    type: 'text',
+    value,
+  }),
+  strong: ({ originalAsset, value }) =>
+    flatSingleElementCompositeAsset({
+      id: `${originalAsset.id}-text-${depth++}`,
+      type: 'composite',
+      values: value.map((v) =>
+        wrapAsset(
+          applyModifierToAssets({
+            asset: v,
+            types: ['text'],
+            modifiers: [
+              {
+                type: 'tag',
+                value: 'important',
+              },
+            ],
+          })
+        )
+      ),
+    }),
+  emphasis: ({ originalAsset, value }) =>
+    flatSingleElementCompositeAsset({
+      id: `${originalAsset.id}-text-${depth++}`,
+      type: 'composite',
+      values: value.map((v) =>
+        wrapAsset(
+          applyModifierToAssets({
+            asset: v,
+            types: ['text'],
+            modifiers: [
+              {
+                type: 'tag',
+                value: 'emphasis',
+              },
+            ],
+          })
+        )
+      ),
+    }),
+  paragraph: ({ originalAsset, value }) =>
+    flatSingleElementCompositeAsset({
+      id: `${originalAsset.id}-composite-${depth++}`,
+      type: 'composite',
+      values: value.map(wrapAsset),
+    }),
+  list: ({ originalAsset, value, ordered }) => ({
+    id: `${originalAsset.id}-list-${depth++}`,
+    type: 'list',
+    values: value.map(wrapAsset),
+    ...(ordered && { metaData: { listType: 'ordered' } }),
+  }),
+  image: ({ originalAsset, value, src }) => ({
+    id: `${originalAsset.id}-image-${depth++}`,
+    type: 'image',
+    accessibility: value,
+    metaData: {
+      ref: src,
+    },
+  }),
+  link: ({ originalAsset, value, href }) =>
+    flatSingleElementCompositeAsset({
+      id: `${originalAsset.id}-link-${depth++}`,
+      type: 'composite',
+      values: value.map((v) =>
+        wrapAsset(
+          applyModifierToAssets({
+            asset: v,
+            types: ['text', 'image'],
+            modifiers: [
+              {
+                type: 'link',
+                metaData: {
+                  ref: href,
+                },
+              },
+            ],
+          })
+        )
+      ),
+    }),
+};
diff --git a/plugins/markdown/core/src/__tests__/index.test.ts b/plugins/markdown/core/src/__tests__/index.test.ts
new file mode 100644
index 000000000..3e6738367
--- /dev/null
+++ b/plugins/markdown/core/src/__tests__/index.test.ts
@@ -0,0 +1,185 @@
+import type { InProgressState } from '@player-ui/player';
+import { Player } from '@player-ui/player';
+import { Registry } from '@player-ui/partial-match-registry';
+import { PartialMatchFingerprintPlugin } from '@player-ui/partial-match-fingerprint-plugin';
+import type { Flow } from '@player-ui/types';
+import { mockMappers } from './helpers';
+import { MarkdownPlugin } from '..';
+
+const unparsedFlow: Flow = {
+  id: 'markdown-flow',
+  data: {
+    internal: {
+      locale: {
+        linkMarkdown:
+          'Learn more at [TurboTax Canada](https://turbotax.intuit.ca)',
+      },
+    },
+  },
+  views: [
+    {
+      id: 'markdown-view',
+      type: 'questionAnswer',
+      title: {
+        asset: {
+          id: 'markdown-view-title',
+          type: 'markdown',
+          value: '{{internal.locale.linkMarkdown}}',
+        },
+      },
+      primaryInfo: {
+        asset: {
+          id: 'markdown-primaryInfo-collection',
+          type: 'collection',
+          values: [
+            {
+              asset: {
+                id: 'markdown-primaryInfo-collection-bold',
+                type: 'markdown',
+                value: 'some **bold text**',
+              },
+            },
+            {
+              asset: {
+                id: 'markdown-primaryInfo-collection-italic',
+                type: 'markdown',
+                value: '*italicized text*',
+              },
+            },
+            {
+              asset: {
+                id: 'markdown-primaryInfo-collection-orderd-list',
+                type: 'markdown',
+                value: '1. First\n2. Second\n3. Third',
+              },
+            },
+            {
+              asset: {
+                id: 'markdown-primaryInfo-collection-unorderd-list',
+                type: 'markdown',
+                value:
+                  '- [First](https://turbotax.intuit.ca)\n- Second\n- Third',
+              },
+            },
+            {
+              asset: {
+                id: 'markdown-primaryInfo-collection-image',
+                type: 'markdown',
+                value: '![alt text](image.png)',
+              },
+            },
+            {
+              asset: {
+                id: 'markdown-primaryInfo-collection-unsupported',
+                type: 'markdown',
+                value: 'Highlights are ==not supported==',
+              },
+            },
+          ],
+        },
+      },
+    },
+  ],
+  navigation: {
+    BEGIN: 'FLOW_1',
+    FLOW_1: {
+      startState: 'VIEW_1',
+      VIEW_1: {
+        state_type: 'VIEW',
+        ref: 'markdown-view',
+        transitions: {
+          '*': 'END_Done',
+        },
+      },
+      END_Done: {
+        state_type: 'END',
+        outcome: 'done',
+      },
+    },
+  },
+};
+
+describe('MarkdownPlugin', () => {
+  it('parses the flow containing markdown into valid FRF, based on the given mappers', () => {
+    const player = new Player({
+      plugins: [new MarkdownPlugin(mockMappers)],
+    });
+    player.start(unparsedFlow);
+
+    const view = (player.getState() as InProgressState).controllers.view
+      .currentView?.lastUpdate;
+
+    expect(view).toMatchSnapshot();
+  });
+
+  it('parses the flow, with only the required mappers', () => {
+    const player = new Player({
+      plugins: [
+        new MarkdownPlugin({
+          text: mockMappers.text,
+          paragraph: mockMappers.paragraph,
+        }),
+      ],
+    });
+    player.start(unparsedFlow);
+
+    const view = (player.getState() as InProgressState).controllers.view
+      .currentView?.lastUpdate;
+
+    expect(view).toMatchSnapshot();
+  });
+
+  it('parses regular flow and maps assets', () => {
+    const fingerprint = new PartialMatchFingerprintPlugin(new Registry());
+
+    fingerprint.register({ type: 'action' }, 0);
+    fingerprint.register({ type: 'text' }, 1);
+    fingerprint.register({ type: 'composite' }, 2);
+
+    const player = new Player({
+      plugins: [fingerprint, new MarkdownPlugin(mockMappers)],
+    });
+
+    player.start({
+      id: 'action-with-expression',
+      views: [
+        {
+          id: 'action',
+          type: 'action',
+          exp: '{{count}} = {{count}} + 1',
+          label: {
+            asset: {
+              id: 'action-label',
+              type: 'markdown',
+              value: 'Clicked {{count}} *times*',
+            },
+          },
+        },
+      ],
+      data: {
+        count: 0,
+      },
+      navigation: {
+        BEGIN: 'FLOW_1',
+        FLOW_1: {
+          startState: 'VIEW_1',
+          VIEW_1: {
+            state_type: 'VIEW',
+            ref: 'action',
+            transitions: {
+              '*': 'END_Done',
+            },
+          },
+          END_Done: {
+            state_type: 'END',
+            outcome: 'done',
+          },
+        },
+      },
+    });
+
+    // the parser should create 2 text assets: `Clicked {{count}}` and a italicized `times`:
+    expect(fingerprint.get('action-label-text-38')).toBe(1);
+    expect(fingerprint.get('action-label-text-39')).toBe(1);
+  });
+});
diff --git a/plugins/markdown/core/src/index.ts b/plugins/markdown/core/src/index.ts
new file mode 100644
index 000000000..503b43b66
--- /dev/null
+++ b/plugins/markdown/core/src/index.ts
@@ -0,0 +1,54 @@
+import type { Player, PlayerPlugin } from '@player-ui/player';
+import { resolveDataRefsInString, NodeType } from '@player-ui/player';
+import type { Mappers } from './types';
+import { parseAssetMarkdownContent } from './utils';
+
+export * from './types';
+
+/**
+ * A plugin that parses markdown written into text assets using the given converters for markdown features into existing assets.
+ */
+export class MarkdownPlugin implements PlayerPlugin {
+  name = 'MarkdownPlugin';
+
+  private mappers: Mappers;
+
+  constructor(mappers: Mappers) {
+    this.mappers = mappers;
+  }
+
+  apply(player: Player) {
+    player.hooks.view.tap(this.name, (view) => {
+      view.hooks.resolver.tap(this.name, (resolver) => {
+        resolver.hooks.beforeResolve.tap(this.name, (node, options) => {
+          if (node?.type === NodeType.Asset && node.value.type === 'markdown') {
+            const resolvedContent = resolveDataRefsInString(
+              node.value.value as string,
+              {
+                evaluate: options.evaluate,
+                model: options.data.model,
+              }
+            );
+
+            const parsed = parseAssetMarkdownContent({
+              asset: {
+                ...node.value,
+                value: resolvedContent,
+              },
+              mappers: this.mappers,
+              parser: options.parseNode,
+            });
+
+            if (parsed.length === 1) {
+              return parsed[0];
+            }
+
+            return { ...node, nodeType: NodeType.MultiNode, values: parsed };
+          }
+
+          return node;
+        });
+      });
+    });
+  }
+}
diff --git a/plugins/markdown/core/src/types.ts b/plugins/markdown/core/src/types.ts
new file mode 100644
index 000000000..58906b01b
--- /dev/null
+++ b/plugins/markdown/core/src/types.ts
@@ -0,0 +1,140 @@
+import type { Asset } from '@player-ui/types';
+
+export interface BaseArgs {
+  /**
+   * Unparsed Asset
+   */
+  originalAsset: Asset;
+}
+
+export type LiteralMapper<T extends object = object> = (
+  args: {
+    /**
+     * markdown element value
+     */
+    value: string;
+  } & BaseArgs &
+    T
+) => Asset;
+
+export type CompositeMapper<T extends object = object> = (
+  args: {
+    /**
+     * array of assets resulted from the recursion over the AST node children
+     */
+    value: Asset[];
+  } & BaseArgs &
+    T
+) => Asset;
+
+export type FallbackMapper = (
+  args: {
+    /**
+     * markdown element value
+     */
+    value: string | Asset[];
+  } & BaseArgs
+) => Asset;
+
+export interface Mappers {
+  /**
+   * required text Asset
+   */
+  text: LiteralMapper;
+  /**
+   * required paragraph (composite) Asset
+   */
+  paragraph: CompositeMapper;
+  /**
+   * strong markdown (e.g. **bold**)
+   */
+  strong?: CompositeMapper;
+  /**
+   * emphasis markdown (e.g. *italic*)
+   */
+  emphasis?: CompositeMapper;
+  /**
+   * blockquote markdown (e.g. > blockquote)
+   */
+  blockquote?: CompositeMapper;
+  /**
+   * ordered or unordered list markdown (e.g. 1. item\n2. item, - item\n- item)
+   */
+  list?: CompositeMapper<{
+    /**
+     * Whether the list is ordered or not.
+     */
+    ordered: boolean;
+    /**
+     * The starting list number.
+     */
+    start?: number;
+  }>;
+  /**
+   * horizontalRule markdown (e.g. ---)
+   */
+  horizontalRule?: LiteralMapper;
+  /**
+   * link markdown (e.g. `[text](url)`)
+   */
+  link?: CompositeMapper<{
+    /**
+     * Link URL
+     */
+    href: string;
+  }>;
+  /**
+   * image markdown (e.g. `![alt](url)`)
+   */
+  image?: LiteralMapper<{
+    /**
+     * Image source URL
+     */
+    src: string;
+  }>;
+  /**
+   * code block markdown (e.g. ```code```)
+   */
+  code?: LiteralMapper<{
+    /**
+     * The language of the code block.
+     */
+    lang?: string;
+  }>;
+  /**
+   * heading markdown (e.g. # heading)
+   */
+  heading?: CompositeMapper<{
+    /**
+     * The heading depth.
+     */
+    depth: number;
+  }>;
+  /**
+   * inline code markdown (e.g. `code`)
+   */
+  inlineCode?: LiteralMapper;
+  /**
+   * list item markdown (e.g. - item)
+   */
+  listItem?: CompositeMapper;
+}
+
+export type Transformer<T = any> = (args: {
+  /**
+   * AST Node (e.g. Link)
+   */
+  astNode: T;
+  /**
+   * Player Asset
+   */
+  asset: Asset;
+  /**
+   * Record off mappers (markdown element -> Asset)
+   */
+  mappers: Mappers;
+  /**
+   * Record of parsers (e.g., { link :linkParser })
+   */
+  transformers: Record<string, Transformer>;
+}) => Asset;
diff --git a/plugins/markdown/core/src/utils/index.ts b/plugins/markdown/core/src/utils/index.ts
new file mode 100644
index 000000000..fa860de31
--- /dev/null
+++ b/plugins/markdown/core/src/utils/index.ts
@@ -0,0 +1,2 @@
+export * from './markdownParser';
+export * from './transformers';
diff --git a/plugins/markdown/core/src/utils/markdownParser.ts b/plugins/markdown/core/src/utils/markdownParser.ts
new file mode 100644
index 000000000..827790fd7
--- /dev/null
+++ b/plugins/markdown/core/src/utils/markdownParser.ts
@@ -0,0 +1,53 @@
+import type { Node, ParseObjectOptions } from '@player-ui/player';
+import { NodeType } from '@player-ui/player';
+import type { Asset } from '@player-ui/types';
+import { fromMarkdown } from 'mdast-util-from-markdown';
+import type { Mappers } from '../types';
+import { transformers } from './transformers';
+
+/**
+ * Parses markdown content using a provided mappers record.
+ */
+export function parseAssetMarkdownContent({
+  asset,
+  mappers,
+  parser,
+}: {
+  /**
+   * Asset to be parsed
+   */
+  asset: Asset;
+  /**
+   * Mappers record of AST Node to Player Content
+   *
+   * @see {@link Mappers}
+   */
+  mappers: Mappers;
+  /**
+   * Parser object to AST
+   */
+  parser?: (
+    obj: object,
+    type?: Node.ChildrenTypes,
+    options?: ParseObjectOptions
+  ) => Node.Node | null;
+}) {
+  const { children } = fromMarkdown(asset.value as string);
+
+  return children.map((node) => {
+    const transformer = transformers[node.type as keyof typeof transformers];
+    const content = transformer({
+      astNode: node as unknown,
+      asset,
+      mappers,
+      transformers,
+    });
+
+    return (
+      parser?.(
+        content,
+        children.length > 1 ? NodeType.Value : NodeType.Asset
+      ) || null
+    );
+  });
+}
diff --git a/plugins/markdown/core/src/utils/transformers.ts b/plugins/markdown/core/src/utils/transformers.ts
new file mode 100644
index 000000000..5ab85d9fe
--- /dev/null
+++ b/plugins/markdown/core/src/utils/transformers.ts
@@ -0,0 +1,327 @@
+import type { Asset } from '@player-ui/types';
+import type {
+  Blockquote,
+  Code,
+  Emphasis,
+  Heading,
+  Image,
+  InlineCode,
+  Link,
+  List,
+  ListItem,
+  Paragraph,
+  Strong,
+  Text,
+  ThematicBreak,
+} from 'mdast-util-from-markdown/lib';
+import type { Transformer } from '../types';
+
+/**
+ * Swap markdown type to text
+ */
+function swapMarkdownType(asset: Asset): Asset {
+  return { ...asset, type: 'text' };
+}
+
+/**
+ * Transforms Text AST Node into Player Content
+ */
+const textTransformer: Transformer<Text> = ({ astNode, asset, mappers }) => {
+  const { value } = astNode;
+
+  return mappers.text({
+    originalAsset: asset,
+    value,
+  });
+};
+
+/**
+ * Transforms Emphasis AST Node into Player Content
+ */
+const emphasisTransformer: Transformer<Emphasis> = ({
+  astNode,
+  asset,
+  mappers,
+  transformers,
+}) => {
+  if (mappers.emphasis) {
+    const { children } = astNode;
+
+    const value = children.map((node) =>
+      transformers[node.type]({ astNode: node, asset, mappers, transformers })
+    );
+
+    return mappers.emphasis({
+      originalAsset: asset,
+      value,
+    });
+  }
+
+  return swapMarkdownType(asset);
+};
+
+/**
+ * Transforms Strong AST Node into Player Content
+ */
+const strongTransformer: Transformer<Strong> = ({
+  astNode,
+  asset,
+  mappers,
+  transformers,
+}) => {
+  if (mappers.strong) {
+    const { children } = astNode;
+
+    const value = children.map((node) =>
+      transformers[node.type]({ astNode: node, asset, mappers, transformers })
+    );
+
+    return mappers.strong({
+      originalAsset: asset,
+      value,
+    });
+  }
+
+  return swapMarkdownType(asset);
+};
+
+/**
+ * Transforms Paragraph AST Node into Player Content
+ */
+const paragraphTransformer: Transformer<Paragraph> = ({
+  astNode,
+  asset,
+  mappers,
+  transformers,
+}) => {
+  const { children } = astNode;
+
+  if (
+    children.every(({ type }) => Boolean(mappers[type as keyof typeof mappers]))
+  ) {
+    const value = children.map((node) =>
+      transformers[node.type]({ astNode: node, asset, mappers, transformers })
+    );
+
+    return mappers.paragraph({
+      originalAsset: asset,
+      value,
+    });
+  }
+
+  return swapMarkdownType(asset);
+};
+
+/**
+ * Transforms List AST Node into Player Content
+ */
+const listTransformer: Transformer<List> = ({
+  astNode,
+  asset,
+  mappers,
+  transformers,
+}) => {
+  if (mappers.list) {
+    const { children, ordered, start } = astNode;
+
+    const value = children.map((node) =>
+      transformers[node.type]({ astNode: node, asset, mappers, transformers })
+    );
+
+    return mappers.list({
+      originalAsset: asset,
+      value,
+      ordered: Boolean(ordered),
+      start: start || undefined,
+    });
+  }
+
+  return swapMarkdownType(asset);
+};
+
+/**
+ * Transforms ListItem AST Node into Player Content
+ */
+const listItemTransformer: Transformer<ListItem> = ({
+  astNode,
+  asset,
+  mappers,
+  transformers,
+}) => {
+  const { children } = astNode;
+
+  const value = children.map((node) =>
+    transformers[node.type]({ astNode: node, asset, mappers, transformers })
+  );
+
+  const mapper = mappers.listItem || mappers.paragraph;
+
+  return mapper({
+    originalAsset: asset,
+    value,
+  });
+};
+
+/**
+ * Transforms Link AST Node into Player Content
+ */
+const linkTransformer: Transformer<Link> = ({
+  astNode,
+  asset,
+  mappers,
+  transformers,
+}) => {
+  if (mappers.link) {
+    const { children, url } = astNode;
+
+    const value = children.map((node) =>
+      transformers[node.type]({ astNode: node, asset, mappers, transformers })
+    );
+
+    return mappers.link({
+      originalAsset: asset,
+      href: url,
+      value,
+    });
+  }
+
+  return swapMarkdownType(asset);
+};
+
+/**
+ * Transforms Image AST Node into Player Content
+ */
+const imageTransformer: Transformer<Image> = ({ astNode, asset, mappers }) => {
+  if (mappers.image) {
+    const { title, url, alt } = astNode;
+
+    return mappers.image({
+      originalAsset: asset,
+      src: url,
+      value: title || alt || '',
+    });
+  }
+
+  return swapMarkdownType(asset);
+};
+
+/**
+ * Transforms Blockquote AST Node into Player Content
+ */
+const blockquoteTransformer: Transformer<Blockquote> = ({
+  astNode,
+  asset,
+  mappers,
+  transformers,
+}) => {
+  if (mappers.blockquote) {
+    const { children } = astNode;
+
+    const value = children.map((node) =>
+      transformers[node.type]({ astNode: node, asset, mappers, transformers })
+    );
+
+    return mappers.blockquote({
+      originalAsset: asset,
+      value,
+    });
+  }
+
+  return swapMarkdownType(asset);
+};
+
+/**
+ * Transforms InlineCode AST Node into Player Content
+ */
+const inlineCodeTransformer: Transformer<InlineCode> = ({
+  astNode,
+  asset,
+  mappers,
+}) => {
+  if (mappers.inlineCode) {
+    const { value } = astNode;
+
+    return mappers.inlineCode({
+      originalAsset: asset,
+      value,
+    });
+  }
+
+  return swapMarkdownType(asset);
+};
+
+/**
+ * Transforms Code block AST Node into Player Content
+ */
+const codeTransformer: Transformer<Code> = ({ astNode, asset, mappers }) => {
+  if (mappers.code) {
+    const { value, lang } = astNode;
+
+    return mappers.code({
+      originalAsset: asset,
+      value,
+      lang: lang || undefined,
+    });
+  }
+
+  return swapMarkdownType(asset);
+};
+
+/**
+ * Transforms Horizontal Rule (Thematic Break) AST Node into Player Content
+ */
+const horizontalRuleTransformer: Transformer<ThematicBreak> = ({
+  asset,
+  mappers,
+}) => {
+  if (mappers.horizontalRule) {
+    return mappers.horizontalRule({
+      originalAsset: asset,
+      value: '---',
+    });
+  }
+
+  return swapMarkdownType(asset);
+};
+
+/**
+ * Transforms Heading AST Node into Player Content
+ */
+const headingTransformer: Transformer<Heading> = ({
+  astNode,
+  asset,
+  mappers,
+  transformers,
+}) => {
+  if (mappers.heading) {
+    const { children, depth } = astNode;
+
+    const value = children.map((node) =>
+      transformers[node.type]({ astNode: node, asset, mappers, transformers })
+    );
+
+    return mappers.heading({
+      originalAsset: asset,
+      value,
+      depth,
+    });
+  }
+
+  return swapMarkdownType(asset);
+};
+
+export const transformers: Record<string, Transformer> = {
+  horizontalRule: horizontalRuleTransformer,
+  text: textTransformer,
+  emphasis: emphasisTransformer,
+  strong: strongTransformer,
+  blockquote: blockquoteTransformer,
+  list: listTransformer,
+  listItem: listItemTransformer,
+  link: linkTransformer,
+  image: imageTransformer,
+  paragraph: paragraphTransformer,
+  code: codeTransformer,
+  heading: headingTransformer,
+  inlineCode: inlineCodeTransformer,
+};
diff --git a/plugins/pubsub/core/src/__test__/handler.test.ts b/plugins/pubsub/core/src/__test__/handler.test.ts
new file mode 100644
index 000000000..30e5cb0d1
--- /dev/null
+++ b/plugins/pubsub/core/src/__test__/handler.test.ts
@@ -0,0 +1,88 @@
+import type { InProgressState } from '@player-ui/player';
+import { Player } from '@player-ui/player';
+import { PubSubPlugin } from '../plugin';
+import type { PubSubHandler } from '../handler';
+import { PubSubHandlerPlugin } from '../handler';
+import { pubsub as pubsubimpl } from '../pubsub';
+
+const customEventFlow = {
+  id: 'customEventFlow',
+  views: [
+    {
+      id: 'view-1',
+      type: 'info',
+    },
+  ],
+  navigation: {
+    BEGIN: 'FLOW_1',
+    FLOW_1: {
+      onStart: 'publish("customEvent", {{foo.bar}}, "daisy")',
+      startState: 'VIEW_1',
+      VIEW_1: {
+        ref: 'view-1',
+        state_type: 'VIEW',
+        transitions: {
+          Next: 'VIEW_2',
+          '*': 'END_Done',
+        },
+      },
+    },
+  },
+  data: {
+    foo: {
+      bar: 'ginger',
+      baz: '',
+    },
+  },
+};
+
+describe('PubSubHandlerPlugin', () => {
+  beforeEach(() => {
+    pubsubimpl.clear();
+  });
+
+  it('registers a new subscription handler', () => {
+    const pubsub = new PubSubPlugin();
+    const spy = jest.fn();
+
+    const player = new Player({
+      plugins: [
+        pubsub,
+        new PubSubHandlerPlugin(new Map([['customEvent', spy]])),
+      ],
+    });
+
+    player.start(customEventFlow as any);
+
+    expect(spy).toHaveBeenCalledWith(expect.anything(), 'ginger', 'daisy');
+    expect(pubsubimpl.count('customEvent')).toBe(1);
+  });
+
+  it('sets data in subscription', () => {
+    const pubsub = new PubSubPlugin();
+
+    /**
+     *
+     */
+    const customEventHandler: PubSubHandler<string[]> = (
+      context,
+      pet1,
+      pet2
+    ) => {
+      context.controllers.data.set([['foo.baz', pet2]]);
+    };
+
+    const subscriptions = new Map([['customEvent', customEventHandler]]);
+
+    const player = new Player({
+      plugins: [pubsub, new PubSubHandlerPlugin(subscriptions)],
+    });
+
+    player.start(customEventFlow as any);
+
+    expect(pubsubimpl.count('customEvent')).toBe(1);
+
+    const state = player.getState() as InProgressState;
+    expect(state.controllers.data.get('foo.baz')).toBe('daisy');
+  });
+});
diff --git a/plugins/pubsub/core/src/pubsub.test.ts b/plugins/pubsub/core/src/__test__/plugin.test.ts
similarity index 55%
rename from plugins/pubsub/core/src/pubsub.test.ts
rename to plugins/pubsub/core/src/__test__/plugin.test.ts
index e15757476..2aa8410fd 100644
--- a/plugins/pubsub/core/src/pubsub.test.ts
+++ b/plugins/pubsub/core/src/__test__/plugin.test.ts
@@ -1,6 +1,6 @@
 import { Player } from '@player-ui/player';
-import { PubSubPlugin } from './pubsub';
-import { PubSubPluginSymbol } from './symbols';
+import { PubSubPlugin } from '../plugin';
+import { PubSubPluginSymbol } from '../symbols';
 
 const minimal = {
   id: 'minimal',
@@ -27,6 +27,34 @@ const minimal = {
   },
 };
 
+const multistart = {
+  id: 'minimal',
+  views: [
+    {
+      id: 'view-1',
+      type: 'info',
+    },
+  ],
+  navigation: {
+    BEGIN: 'FLOW_1',
+    FLOW_1: {
+      onStart: [
+        'publish("pet", ["ginger", "daisy"])',
+        'customPublish("pet", ["ginger", "daisy"])',
+      ],
+      startState: 'VIEW_1',
+      VIEW_1: {
+        ref: 'view-1',
+        state_type: 'VIEW',
+        transitions: {
+          Next: 'VIEW_2',
+          '*': 'END_Done',
+        },
+      },
+    },
+  },
+};
+
 const customName = {
   id: 'custom',
   views: [
@@ -38,7 +66,7 @@ const customName = {
   navigation: {
     BEGIN: 'FLOW_1',
     FLOW_1: {
-      onStart: 'customPublish("pet.names", ["ginger", "daisy"])',
+      onStart: 'customPublish("pet.names", "ginger", "daisy")',
       startState: 'VIEW_1',
       VIEW_1: {
         ref: 'view-1',
@@ -52,33 +80,6 @@ const customName = {
   },
 };
 
-test('handles subscriptions', () => {
-  const pubsubPlugin = new PubSubPlugin();
-
-  const handler1 = jest.fn();
-  pubsubPlugin.subscribe('foo', handler1);
-
-  const handler2 = jest.fn();
-  const token2 = pubsubPlugin.subscribe('foo.bar', handler2);
-
-  pubsubPlugin.publish('foo.bar', 'baz');
-  expect(handler1).toBeCalledTimes(1);
-  expect(handler2).toBeCalledTimes(1);
-  expect(handler1).toBeCalledWith('foo.bar', 'baz');
-  expect(handler2).toBeCalledWith('foo.bar', 'baz');
-
-  pubsubPlugin.publish('foo', 'baz times 2');
-  expect(handler1).toBeCalledTimes(2);
-  expect(handler2).toBeCalledTimes(1);
-
-  pubsubPlugin.unsubscribe(token2);
-
-  pubsubPlugin.publish('foo.bar', 'go again!');
-  expect(handler1).toBeCalledTimes(3);
-  expect(handler2).toBeCalledTimes(1);
-  expect(handler1).toBeCalledWith('foo.bar', 'go again!');
-});
-
 test('loads an expression', () => {
   const pubsub = new PubSubPlugin();
 
@@ -115,10 +116,10 @@ test('handles custom expression names', () => {
   player.start(customName as any);
 
   expect(topLevel).toBeCalledTimes(1);
-  expect(topLevel).toBeCalledWith('pet.names', ['ginger', 'daisy']);
+  expect(topLevel).toBeCalledWith('pet.names', 'ginger', 'daisy');
 
   expect(nested).toBeCalledTimes(1);
-  expect(nested).toBeCalledWith('pet.names', ['ginger', 'daisy']);
+  expect(nested).toBeCalledWith('pet.names', 'ginger', 'daisy');
 });
 
 test('finds plugin', () => {
@@ -128,3 +129,34 @@ test('finds plugin', () => {
 
   expect(player.findPlugin<PubSubPlugin>(PubSubPluginSymbol)).toBe(pubsub);
 });
+
+test('only calls subscription once if multiple pubsub plugins are registered', () => {
+  const pubsub = new PubSubPlugin();
+  const pubsub2 = new PubSubPlugin();
+
+  const player = new Player({ plugins: [pubsub, pubsub2] });
+
+  const topLevel = jest.fn();
+  pubsub.subscribe('pet', topLevel);
+
+  player.start(minimal as any);
+
+  expect(topLevel).toBeCalledTimes(1);
+  expect(topLevel).toBeCalledWith('pet.names', ['ginger', 'daisy']);
+});
+
+test('calls subscription for each pubsub registered through pubsubplugin', () => {
+  const pubsub = new PubSubPlugin();
+  const pubsub2 = new PubSubPlugin({ expressionName: 'customPublish' });
+
+  const player = new Player({ plugins: [pubsub, pubsub2] });
+
+  const spy = jest.fn();
+  pubsub.subscribe('pet', spy);
+
+  player.start(multistart as any);
+
+  expect(spy).toBeCalledTimes(2);
+  expect(spy).toHaveBeenNthCalledWith(1, 'pet', ['ginger', 'daisy']);
+  expect(spy).toHaveBeenNthCalledWith(2, 'pet', ['ginger', 'daisy']);
+});
diff --git a/plugins/pubsub/core/src/__test__/pubsub.test.ts b/plugins/pubsub/core/src/__test__/pubsub.test.ts
new file mode 100644
index 000000000..280906576
--- /dev/null
+++ b/plugins/pubsub/core/src/__test__/pubsub.test.ts
@@ -0,0 +1,293 @@
+import { pubsub } from '../pubsub';
+
+describe('pubsub', () => {
+  beforeEach(() => {
+    pubsub.clear();
+  });
+
+  it('should call subscriber with single argument', () => {
+    const type = 'test';
+    const message = 'this is a test message';
+    const spy = jest.fn();
+
+    pubsub.subscribe(type, spy);
+    pubsub.publish(type, message);
+
+    expect(spy).toHaveBeenCalledTimes(1);
+    expect(spy).toHaveBeenCalledWith(type, message);
+  });
+
+  it('should call subscriber with multiple argument', () => {
+    const type = 'test';
+    const spy = jest.fn();
+
+    pubsub.subscribe(type, spy);
+    pubsub.publish(type, 1, 'two', 'three');
+
+    expect(spy).toHaveBeenCalledTimes(1);
+    expect(spy).toHaveBeenCalledWith(type, 1, 'two', 'three');
+  });
+
+  it('should handle different argument types', () => {
+    const type = 'test';
+    const spy = jest.fn();
+
+    pubsub.subscribe(type, spy);
+    pubsub.publish(type, 'one', 2, undefined, null, true, false);
+
+    expect(spy).toHaveBeenCalledTimes(1);
+    expect(spy).toHaveBeenCalledWith(
+      type,
+      'one',
+      2,
+      undefined,
+      null,
+      true,
+      false
+    );
+  });
+
+  it('should call all subscribers only once', () => {
+    const type = 'test';
+    const message = 'this is a test message';
+    const first = jest.fn();
+    const second = jest.fn();
+
+    pubsub.subscribe(type, first);
+    pubsub.subscribe(type, second);
+
+    pubsub.publish(type, message);
+
+    expect(first).toHaveBeenCalledTimes(1);
+    expect(first).toHaveBeenCalledWith(type, message);
+    expect(second).toHaveBeenCalledTimes(1);
+    expect(second).toHaveBeenCalledWith(type, message);
+  });
+
+  it('should call only subscribers of event', () => {
+    const message = 'this is a test message';
+    const first = jest.fn();
+    const second = jest.fn();
+
+    pubsub.subscribe('first', first);
+    pubsub.subscribe('second', second);
+
+    pubsub.publish('first', message);
+
+    expect(first).toHaveBeenCalledTimes(1);
+    expect(first).toHaveBeenCalledWith('first', message);
+    expect(second).not.toHaveBeenCalled();
+  });
+
+  it('should call subscriber for all publish types', () => {
+    const spy = jest.fn();
+
+    pubsub.subscribe('*', spy);
+
+    pubsub.publish('one', 'one');
+    pubsub.publish('two', 'two');
+    pubsub.publish('three', 'three');
+
+    expect(spy).toHaveBeenCalledTimes(3);
+    expect(spy).toHaveBeenNthCalledWith(1, 'one', 'one');
+    expect(spy).toHaveBeenNthCalledWith(2, 'two', 'two');
+    expect(spy).toHaveBeenNthCalledWith(3, 'three', 'three');
+  });
+
+  it('should call all levels of subscribers only once', () => {
+    const level1 = jest.fn();
+    const level2 = jest.fn();
+    const level3 = jest.fn();
+
+    pubsub.subscribe('foo', level1);
+    pubsub.subscribe('foo.bar', level2);
+    pubsub.subscribe('foo.bar.baz', level3);
+
+    pubsub.publish('foo.bar.baz', 'one', 'two', 'three');
+
+    expect(level1).toHaveBeenCalledTimes(1);
+    expect(level2).toHaveBeenCalledTimes(1);
+    expect(level3).toHaveBeenCalledTimes(1);
+    expect(level1).toHaveBeenCalledWith('foo.bar.baz', 'one', 'two', 'three');
+    expect(level2).toHaveBeenCalledWith('foo.bar.baz', 'one', 'two', 'three');
+    expect(level3).toHaveBeenCalledWith('foo.bar.baz', 'one', 'two', 'three');
+  });
+
+  it('should return unique symbols for each subscribe', () => {
+    const spy = jest.fn();
+    const spy2 = jest.fn();
+
+    const token1 = pubsub.subscribe('test', spy);
+    const token2 = pubsub.subscribe('test', spy);
+    const token3 = pubsub.subscribe('test', spy2);
+
+    const symbols = new Set([token1, token2, token3]);
+    expect(symbols.size).toBe(3);
+  });
+
+  it(`shouldn't error if subscribing to non string value`, () => {
+    const spy = jest.fn();
+    pubsub.subscribe(true as any, spy);
+    pubsub.publish(true as any, 'test');
+
+    expect(spy).not.toHaveBeenCalled();
+  });
+
+  it('should remove handler with token', () => {
+    const type = 'test';
+    const spy = jest.fn();
+    const token = pubsub.subscribe(type, spy);
+
+    expect(pubsub.count()).toBe(1);
+    expect(pubsub.count(type)).toBe(1);
+
+    pubsub.unsubscribe(token);
+
+    expect(pubsub.count()).toBe(0);
+    expect(pubsub.count(type)).toBe(0);
+  });
+
+  it('should only remove handler with passed token', () => {
+    const type = 'test';
+    const spy1 = jest.fn();
+    const spy2 = jest.fn();
+    const token1 = pubsub.subscribe(type, spy1);
+    const token2 = pubsub.subscribe(type, spy2);
+
+    expect(pubsub.count()).toBe(2);
+    expect(pubsub.count(type)).toBe(2);
+
+    pubsub.unsubscribe(token1);
+
+    expect(pubsub.count()).toBe(1);
+    expect(pubsub.count(type)).toBe(1);
+
+    pubsub.unsubscribe(token2);
+
+    expect(pubsub.count()).toBe(0);
+    expect(pubsub.count(type)).toBe(0);
+  });
+
+  it('should remove all handlers for type', () => {
+    const type = 'test';
+    const spy1 = jest.fn();
+    const spy2 = jest.fn();
+    pubsub.subscribe(type, spy1);
+    pubsub.subscribe(type, spy2);
+
+    expect(pubsub.count()).toBe(2);
+    expect(pubsub.count(type)).toBe(2);
+    // @ts-ignore
+    expect(pubsub.tokens.size).toBe(2);
+
+    pubsub.unsubscribe(type);
+
+    expect(pubsub.count()).toBe(0);
+    expect(pubsub.count(type)).toBe(0);
+    // @ts-ignore
+    expect(pubsub.tokens.size).toBe(0);
+  });
+
+  it('should remove all nested handlers for type', () => {
+    const spy1 = jest.fn();
+    const spy2 = jest.fn();
+    const spy3 = jest.fn();
+    pubsub.subscribe('foo', spy1);
+    pubsub.subscribe('foo.bar', spy2);
+    pubsub.subscribe('foo.bar.baz', spy3);
+
+    expect(pubsub.count()).toBe(3);
+    expect(pubsub.count('foo')).toBe(1);
+    expect(pubsub.count('foo.bar')).toBe(1);
+    expect(pubsub.count('foo.bar.baz')).toBe(1);
+    // @ts-ignore
+    expect(pubsub.tokens.size).toBe(3);
+
+    pubsub.unsubscribe('foo');
+
+    expect(pubsub.count()).toBe(0);
+    expect(pubsub.count('foo')).toBe(0);
+    expect(pubsub.count('foo.bar')).toBe(0);
+    expect(pubsub.count('foo.bar.baz')).toBe(0);
+    // @ts-ignore
+    expect(pubsub.tokens.size).toBe(0);
+  });
+
+  it('should keep top layer for type', () => {
+    const spy1 = jest.fn();
+    const spy2 = jest.fn();
+    const spy3 = jest.fn();
+    pubsub.subscribe('foo', spy1);
+    pubsub.subscribe('foo.bar', spy2);
+    pubsub.subscribe('foo.bar.baz', spy3);
+
+    expect(pubsub.count()).toBe(3);
+    expect(pubsub.count('foo')).toBe(1);
+    expect(pubsub.count('foo.bar')).toBe(1);
+    expect(pubsub.count('foo.bar.baz')).toBe(1);
+    // @ts-ignore
+    expect(pubsub.tokens.size).toBe(3);
+
+    pubsub.unsubscribe('foo.bar');
+
+    expect(pubsub.count()).toBe(1);
+    expect(pubsub.count('foo')).toBe(1);
+    expect(pubsub.count('foo.bar')).toBe(0);
+    expect(pubsub.count('foo.bar.baz')).toBe(0);
+    // @ts-ignore
+    expect(pubsub.tokens.size).toBe(1);
+  });
+
+  it('should only delete deeply nested type', () => {
+    const spy1 = jest.fn();
+    const spy2 = jest.fn();
+    const spy3 = jest.fn();
+    pubsub.subscribe('foo', spy1);
+    pubsub.subscribe('foo.bar', spy2);
+    pubsub.subscribe('foo.bar.baz', spy3);
+
+    expect(pubsub.count()).toBe(3);
+    expect(pubsub.count('foo')).toBe(1);
+    expect(pubsub.count('foo.bar')).toBe(1);
+    expect(pubsub.count('foo.bar.baz')).toBe(1);
+    // @ts-ignore
+    expect(pubsub.tokens.size).toBe(3);
+
+    pubsub.unsubscribe('foo.bar.baz');
+
+    expect(pubsub.count()).toBe(2);
+    expect(pubsub.count('foo')).toBe(1);
+    expect(pubsub.count('foo.bar')).toBe(1);
+    expect(pubsub.count('foo.bar.baz')).toBe(0);
+    // @ts-ignore
+    expect(pubsub.tokens.size).toBe(2);
+  });
+
+  it(`should gracefully handle when token doesn't exist anymore`, () => {
+    const spy = jest.fn();
+
+    const token = pubsub.subscribe('foo', spy);
+    expect(pubsub.count()).toBe(1);
+
+    pubsub.clear();
+    expect(pubsub.count()).toBe(0);
+
+    pubsub.unsubscribe(token);
+    // @ts-ignore
+    expect(pubsub.tokens.size).toBe(0);
+  });
+
+  it(`should gracefully handle when unsubscribe isn't string or symbol`, () => {
+    const spy = jest.fn();
+
+    pubsub.subscribe('foo', spy);
+    expect(pubsub.count()).toBe(1);
+
+    pubsub.clear();
+    expect(pubsub.count()).toBe(0);
+
+    pubsub.unsubscribe(false as any);
+    // @ts-ignore
+    expect(pubsub.tokens.size).toBe(0);
+  });
+});
diff --git a/plugins/pubsub/core/src/handler.ts b/plugins/pubsub/core/src/handler.ts
new file mode 100644
index 000000000..300a290a6
--- /dev/null
+++ b/plugins/pubsub/core/src/handler.ts
@@ -0,0 +1,41 @@
+import type { Player, PlayerPlugin, InProgressState } from '@player-ui/player';
+import { getPubSubPlugin } from './utils';
+
+export type PubSubHandler<T extends unknown[]> = (
+  context: InProgressState,
+  ...args: T
+) => void;
+
+export type SubscriptionMap = Map<string, PubSubHandler<any>>;
+
+/**
+ * Plugin to easily add subscribers to the PubSubPlugin
+ */
+export class PubSubHandlerPlugin implements PlayerPlugin {
+  name = 'pubsub-handler';
+  private subscriptions: SubscriptionMap;
+
+  constructor(subscriptions: SubscriptionMap) {
+    this.subscriptions = subscriptions;
+  }
+
+  apply(player: Player) {
+    const pubsub = getPubSubPlugin(player);
+
+    player.hooks.onStart.tap(this.name, () => {
+      this.subscriptions.forEach((handler, key) => {
+        pubsub.subscribe(key, (_, ...args) => {
+          const state = player.getState();
+
+          if (state.status === 'in-progress') {
+            return handler(state, ...args);
+          }
+
+          player.logger.info(
+            `[PubSubHandlerPlugin] subscriber for ${key} was called when player was not in-progress`
+          );
+        });
+      });
+    });
+  }
+}
diff --git a/plugins/pubsub/core/src/index.ts b/plugins/pubsub/core/src/index.ts
index b286400dc..ce85c28f6 100644
--- a/plugins/pubsub/core/src/index.ts
+++ b/plugins/pubsub/core/src/index.ts
@@ -1,2 +1,3 @@
-export * from './pubsub';
+export * from './plugin';
 export * from './symbols';
+export * from './handler';
diff --git a/plugins/pubsub/core/src/plugin.ts b/plugins/pubsub/core/src/plugin.ts
new file mode 100644
index 000000000..25b38023d
--- /dev/null
+++ b/plugins/pubsub/core/src/plugin.ts
@@ -0,0 +1,102 @@
+import type {
+  Player,
+  PlayerPlugin,
+  ExpressionContext,
+} from '@player-ui/player';
+import type { SubscribeHandler } from './pubsub';
+import { pubsub } from './pubsub';
+import { PubSubPluginSymbol } from './symbols';
+
+export interface PubSubConfig {
+  /** A custom expression name to register  */
+  expressionName: string;
+}
+
+/**
+ * The PubSubPlugin is a great way to enable your FRF content to publish events back to your app
+ * It injects a publish() function into the expression language, and will forward all events back to any subscribers.
+ *
+ * Published/Subscribed events support a hierarchy:
+ * - publish('foo', 'data') -- will trigger any listeners for 'foo'
+ * - publish('foo.bar', 'data') -- will trigger any listeners for 'foo' or 'foo.bar'
+ *
+ */
+export class PubSubPlugin implements PlayerPlugin {
+  name = 'pub-sub';
+
+  static Symbol = PubSubPluginSymbol;
+  public readonly symbol = PubSubPlugin.Symbol;
+
+  private expressionName: string;
+
+  constructor(config?: PubSubConfig) {
+    this.expressionName = config?.expressionName ?? 'publish';
+  }
+
+  apply(player: Player) {
+    player.hooks.expressionEvaluator.tap(this.name, (expEvaluator) => {
+      const existingExpression = expEvaluator.operators.expressions.get(
+        this.expressionName
+      );
+
+      if (existingExpression) {
+        player.logger.warn(
+          `[PubSubPlugin] expression ${this.expressionName} is already registered.`
+        );
+      } else {
+        expEvaluator.addExpressionFunction(
+          this.expressionName,
+          (_ctx: ExpressionContext, event: unknown, ...args: unknown[]) => {
+            if (typeof event === 'string') {
+              this.publish(event, ...args);
+            }
+          }
+        );
+      }
+    });
+
+    player.hooks.onEnd.tap(this.name, () => {
+      this.clear();
+    });
+  }
+
+  /**
+   * A way of publishing an event, notifying any listeners
+   *
+   * @param event - The name of the event to publish. Can take sub-topics like: foo.bar
+   * @param data - Any additional data to attach to the event
+   */
+  publish(event: string, ...args: unknown[]) {
+    pubsub.publish(event, ...args);
+  }
+
+  /**
+   * Subscribe to an event with the given name. The handler will get called for any published event
+   *
+   * @param event - The name of the event to subscribe to
+   * @param handler - A function to be called when the event is triggered
+   * @returns A token to be used to unsubscribe from the event
+   */
+  subscribe<T extends string, A extends unknown[]>(
+    event: T,
+    handler: SubscribeHandler<T, A>
+  ) {
+    return pubsub.subscribe(event, handler);
+  }
+
+  /**
+   * Remove any subscriptions using the given token
+   *
+   * @param token - A token from a `subscribe` call
+   */
+  unsubscribe(token: string | symbol) {
+    pubsub.unsubscribe(token);
+  }
+
+  /**
+   * Remove all subscriptions
+   */
+  clear() {
+    pubsub.clear();
+  }
+}
diff --git a/plugins/pubsub/core/src/pubsub.ts b/plugins/pubsub/core/src/pubsub.ts
index 93a2dd893..4d2b88559 100644
--- a/plugins/pubsub/core/src/pubsub.ts
+++ b/plugins/pubsub/core/src/pubsub.ts
@@ -1,77 +1,169 @@
-import pubsub from 'pubsub-js';
-import type {
-  Player,
-  PlayerPlugin,
-  ExpressionContext,
-} from '@player-ui/player';
-import { PubSubPluginSymbol } from './symbols';
-
-export interface PubSubConfig {
-  /** A custom expression name to register  */
-  expressionName: string;
-}
+/**
+ * Based off the pubsub-js library and rewritten to match the same used APIs but modified so that
+ * multiple arguments could be passed into the publish and subscription handlers.
+ */
+
+export type SubscribeHandler<T extends string, A extends unknown[]> = (
+  type: T,
+  ...args: A
+) => void;
 
 /**
- * The PubSubPlugin is a great way to enable your Content content to publish events back to your app
- * It injects a publish() function into the expression language, and will forward all events back to any subscribers.
- *
- * Published/Subscribed events support a hierarchy:
- * - publish('foo', 'data') -- will trigger any listeners for 'foo'
- * - publish('foo.bar', 'data') -- will trigger any listeners for 'foo' or 'foo.bar'
- *
+ * Split a string into an array of event layers
  */
-export class PubSubPlugin implements PlayerPlugin {
-  name = 'pub-sub';
+function splitEvent(event: string) {
+  return event.split('.').reduce<string[]>((prev, curr, index) => {
+    if (index === 0) {
+      return [curr];
+    }
 
-  static Symbol = PubSubPluginSymbol;
-  public readonly symbol = PubSubPlugin.Symbol;
+    return [...prev, `${prev[index - 1]}.${curr}`];
+  }, []);
+}
 
-  private expressionName: string;
+/**
+ * Tiny pubsub maker
+ */
+class TinyPubSub {
+  private events: Map<string, Map<symbol, SubscribeHandler<any, any>>>;
+  private tokens: Map<symbol, string>;
 
-  constructor(config?: PubSubConfig) {
-    this.expressionName = config?.expressionName ?? 'publish';
+  constructor() {
+    this.events = new Map();
+    this.tokens = new Map();
   }
 
-  apply(player: Player) {
-    player.hooks.expressionEvaluator.tap(this.name, (expEvaluator) => {
-      expEvaluator.addExpressionFunction(
-        this.expressionName,
-        (_ctx: ExpressionContext, event: unknown, data: unknown) => {
-          if (typeof event === 'string') {
-            this.publish(event, data);
-          }
-        }
-      );
-    });
+  /**
+   * Publish an event with any number of additional arguments
+   */
+  publish(event: string, ...args: unknown[]) {
+    if (typeof event !== 'string') {
+      return;
+    }
+
+    if (event.includes('.')) {
+      const eventKeys = splitEvent(event);
+
+      eventKeys.forEach((key) => {
+        this.deliver(key, event, ...args);
+      });
+    } else {
+      this.deliver(event, event, ...args);
+    }
+
+    this.deliver('*', event, ...args);
   }
 
   /**
-   * A way of publishing an event, notifying any listeners
+   * Subscribe to an event
+   *
+   * Events are also heirarchical when separated by a period. Given the following:
+   *
+   * publish('a.b.c', 'one', 'two', 'three)
    *
-   * @param event - The name of the event to publish. Can take sub-topics like: foo.bar
-   * @param data - Any additional data to attach to the event
+   * The subscribe event will be called when the event is passed as 'a', 'a.b', or 'a.b.c'.
+   *
+   * @example
+   * // subscribes to the top level 'a' publish
+   * subscribe('a', (event, ...args) => console.log(event, ...args))
    */
-  publish(event: string, data: unknown) {
-    pubsub.publishSync(event, data);
+  subscribe(event: string, handler: SubscribeHandler<any, any>) {
+    const sym = Symbol('PubSubToken');
+
+    if (typeof event === 'string') {
+      if (!this.events.has(event)) {
+        this.events.set(event, new Map());
+      }
+
+      const handlers = this.events.get(event);
+      handlers?.set(sym, handler);
+      this.tokens.set(sym, event);
+    }
+
+    return sym;
   }
 
   /**
-   * Subscribe to an event with the given name. The handler will get called for any published event
+   * Unsubscribes to a specific subscription given it's symbol or an entire
+   * event when passed as a string.
    *
-   * @param event - The name of the event to subscribe to
-   * @param handler - A function to be called when the event is triggered
-   * @returns A token to be used to unsubscribe from the event
+   * When existing subscriptions exist for heirarchical events such as 'a.b.c',
+   * when passing an event 'a' to unsubscribe, all subscriptions for 'a', 'a.b',
+   * & 'a.b.c' will be unsubscribed as well.
    */
-  subscribe(event: string, handler: (e: string, data: unknown) => void) {
-    return pubsub.subscribe(event, handler);
+  unsubscribe(value: string | symbol) {
+    if (typeof value === 'symbol') {
+      const path = this.tokens.get(value);
+
+      if (typeof path === 'undefined') {
+        return;
+      }
+
+      const innerPath = this.events.get(path);
+      innerPath?.delete(value);
+      this.tokens.delete(value);
+      return;
+    }
+
+    if (typeof value === 'string') {
+      for (const key of this.events.keys()) {
+        if (key.indexOf(value) === 0) {
+          const tokens = this.events.get(key);
+
+          if (tokens && tokens.size) {
+            // eslint-disable-next-line max-depth
+            for (const token of tokens.keys()) {
+              this.tokens.delete(token);
+            }
+          }
+
+          this.events.delete(key);
+        }
+      }
+    }
   }
 
   /**
-   * Remove any subscriptions using the given token
-   *
-   * @param token - A token from a `subscribe` call
+   * Get the number of subscriptions for a specific event, or when left blank
+   * will return the overall number of subscriptions for the entire pubsub.
+   */
+  count(event?: string) {
+    let counter = 0;
+
+    if (typeof event === 'undefined') {
+      for (const handlers of this.events.values()) {
+        counter += handlers.size;
+      }
+
+      return counter;
+    }
+
+    const handlers = this.events.get(event);
+
+    if (handlers?.size) {
+      return handlers.size;
+    }
+
+    return counter;
+  }
+
+  /**
+   * Deletes all existing subscriptions
    */
-  unsubscribe(token: string) {
-    pubsub.unsubscribe(token);
+  clear() {
+    this.events.clear();
+    this.tokens.clear();
+  }
+
+  private deliver(path: string, event: string, ...args: unknown[]) {
+    const handlers = this.events.get(path);
+
+    if (handlers && handlers.size) {
+      for (const handler of handlers.values()) {
+        handler(event, ...args);
+      }
+    }
   }
 }
+
+export const pubsub = new TinyPubSub();
diff --git a/plugins/pubsub/core/src/utils.ts b/plugins/pubsub/core/src/utils.ts
new file mode 100644
index 000000000..4db957491
--- /dev/null
+++ b/plugins/pubsub/core/src/utils.ts
@@ -0,0 +1,17 @@
+import type { Player } from '@player-ui/player';
+import { PubSubPlugin } from './plugin';
+import { PubSubPluginSymbol } from './symbols';
+
+/**
+ * Returns the existing PubSubPlugin or creates and registers a new plugin
+ */
+export function getPubSubPlugin(player: Player) {
+  const existing = player.findPlugin<PubSubPlugin>(PubSubPluginSymbol);
+  const plugin = existing || new PubSubPlugin();
+
+  if (!existing) {
+    player.registerPlugin(plugin);
+  }
+
+  return plugin;
+}
diff --git a/plugins/pubsub/jvm/src/test/kotlin/com/intuit/player/plugins/pubsub/PubSubPluginTest.kt b/plugins/pubsub/jvm/src/test/kotlin/com/intuit/player/plugins/pubsub/PubSubPluginTest.kt
index 628930fb6..7d66c70dc 100644
--- a/plugins/pubsub/jvm/src/test/kotlin/com/intuit/player/plugins/pubsub/PubSubPluginTest.kt
+++ b/plugins/pubsub/jvm/src/test/kotlin/com/intuit/player/plugins/pubsub/PubSubPluginTest.kt
@@ -12,9 +12,9 @@ import kotlinx.serialization.json.Json
 import kotlinx.serialization.json.buildJsonObject
 import kotlinx.serialization.json.put
 import org.amshove.kluent.`should be equal to`
-import org.amshove.kluent.`should be instance of`
-import org.amshove.kluent.`should be null`
-import org.amshove.kluent.shouldBe
+// import org.amshove.kluent.`should be instance of`
+// import org.amshove.kluent.`should be null`
+// import org.amshove.kluent.shouldBe
 import org.junit.jupiter.api.TestTemplate
 
 internal class PubSubPluginTest : PlayerTest() {
@@ -23,6 +23,7 @@ internal class PubSubPluginTest : PlayerTest() {
 
     private val plugin get() = player.pubSubPlugin!!
 
+    /*
     @TestTemplate
     fun `subscribe shouldbe Unit`() {
         plugin.subscribe("eventName") { _, _ -> } `should be instance of` String::class
@@ -31,8 +32,9 @@ internal class PubSubPluginTest : PlayerTest() {
     @TestTemplate
     fun `publish shouldbe Unit`() {
         plugin.publish("eventName", "eventData") shouldBe Unit
-    }
-
+    } 
+    */
+    /*
     @TestTemplate
     fun `unsubscribe should remove handler`() {
         val (expectedName, expectedData) = "eventName" to "eventData"
@@ -46,6 +48,7 @@ internal class PubSubPluginTest : PlayerTest() {
         name.`should be null`()
         data.`should be null`()
     }
+    */
 
     @TestTemplate
     fun pubsubWithString() {
diff --git a/plugins/shared-constants/core/src/index.ts b/plugins/shared-constants/core/src/index.ts
index 56bc7859f..dbca6d7e1 100644
--- a/plugins/shared-constants/core/src/index.ts
+++ b/plugins/shared-constants/core/src/index.ts
@@ -61,12 +61,12 @@ export class ConstantsPlugin implements PlayerPlugin {
       updatePlayerConstants();
 
       const tempData = get(flowObj, this.dataPath.asString()) ?? {};
-      player.constantsController.clearTemporaryValues();
+      player.constantsController.clearTemporaryValues(this.namespace);
       player.constantsController.setTemporaryValues(tempData, this.namespace);
     });
     // Clear flow specific overrides at the end of the flow and remove strong ref to player
     player.hooks.onEnd.tap(this.name, () => {
-      player.constantsController.clearTemporaryValues();
+      player.constantsController.clearTemporaryValues(this.namespace);
 
       this.updatePlayerConstants.delete(updatePlayerConstants);
     });
diff --git a/plugins/stage-revert-data/core/BUILD b/plugins/stage-revert-data/core/BUILD
new file mode 100644
index 000000000..def99d9a8
--- /dev/null
+++ b/plugins/stage-revert-data/core/BUILD
@@ -0,0 +1,17 @@
+load("//:index.bzl", "javascript_pipeline")
+
+javascript_pipeline(
+    name = "@player-ui/stage-revert-data-plugin",
+    dependencies = [
+        "@npm//tapable-ts",
+    ],
+    test_data = [
+        "//core/make-flow:@player-ui/make-flow",
+        "//core/types:@player-ui/types",
+
+    ],
+    peer_dependencies = [
+        "//core/player:@player-ui/player",
+    ],
+    library_name = "StageRevertDataPlugin"
+)
diff --git a/plugins/stage-revert-data/core/src/index.test.ts b/plugins/stage-revert-data/core/src/index.test.ts
new file mode 100644
index 000000000..170300f5f
--- /dev/null
+++ b/plugins/stage-revert-data/core/src/index.test.ts
@@ -0,0 +1,208 @@
+import type { DataController, InProgressState } from '@player-ui/player';
+import { Player } from '@player-ui/player';
+import type { Flow } from '@player-ui/types';
+import { waitFor } from '@testing-library/dom';
+import { StageRevertDataPlugin } from './index';
+
+const dataChangeFlow: Flow = {
+  id: 'test-flow',
+  data: {
+    name: {
+      first: 'Alex',
+      last: 'Fimbres',
+    },
+  },
+  views: [
+    {
+      id: 'view-1',
+      type: 'view',
+      value: '{{name.first}}',
+    },
+    {
+      id: 'view-2',
+      type: 'view',
+    },
+  ],
+  navigation: {
+    BEGIN: 'FLOW_1',
+    FLOW_1: {
+      startState: 'VIEW_1',
+      VIEW_1: {
+        state_type: 'VIEW',
+        ref: 'view-1',
+        attributes: {
+          stageData: true,
+          commitTransitions: ['VIEW_2', 'ACTION_2'],
+        },
+        transitions: {
+          next: 'VIEW_2',
+          '*': 'ACTION_1',
+        },
+      },
+      ACTION_1: {
+        state_type: 'ACTION',
+        exp: '',
+        transitions: {
+          '*': 'VIEW_2',
+        },
+      },
+      VIEW_2: {
+        state_type: 'VIEW',
+        ref: 'view-2',
+        transitions: {
+          '*': 'ACTION_1',
+        },
+      },
+    },
+  },
+};
+
+describe('Stage-Revert-Data plugin', () => {
+  let player: Player;
+  let dataController: DataController;
+
+  beforeEach(() => {
+    player = new Player({
+      plugins: [new StageRevertDataPlugin()],
+    });
+
+    player.hooks.dataController.tap('test', (dc) => {
+      dataController = dc;
+    });
+
+    player.start(dataChangeFlow);
+  });
+
+  it('should have the original data model upon loading the component', () => {
+    const state = player.getState() as InProgressState;
+
+    expect(state.controllers.data.get('')).toBe(dataChangeFlow.data);
+  });
+
+  it('Should get the cached data even before a transition occurs on the flow', () => {
+    const state = player.getState() as InProgressState;
+    dataController.set([['name.first', 'Christopher']]);
+
+    expect(state.controllers.data.get('name.first')).toStrictEqual(
+      'Christopher'
+    );
+  });
+
+  it('should not update data model if transission step is not included in the commitTransition list provided on attributes', async () => {
+    const state = player.getState() as InProgressState;
+
+    dataController.set([['name.first', 'Christopher']]);
+
+    state.controllers.flow.transition('action');
+
+    expect(state.controllers.data.get('name.first')).toBe('Alex');
+  });
+
+  it('Should display the cached value on the view before transition', async () => {
+    const state = player.getState() as InProgressState;
+
+    dataController.set([['name.first', 'Christopher']]);
+
+    await waitFor(() => {
+      expect(
+        state.controllers.view.currentView?.lastUpdate?.value
+      ).toStrictEqual('Christopher');
+    });
+  });
+
+  it('should have committed to data model when a listed transition reference happened', () => {
+    const state = player.getState() as InProgressState;
+
+    dataController.set([
+      ['name.first', 'Christopher'],
+      ['name.last', 'Alvarez'],
+      ['name.middle', 'F'],
+    ]);
+
+    state.controllers.flow.transition('next');
+
+    expect(state.controllers.data.get('')).toStrictEqual({
+      name: {
+        first: 'Christopher',
+        last: 'Alvarez',
+        middle: 'F',
+      },
+    });
+  });
+
+  describe('Testing without stageData flag on attribtues (Plugin not active on View)', () => {
+    const dataChangeFlow2: Flow = {
+      id: 'test-flow',
+      data: {
+        name: {
+          first: 'Alex',
+          last: 'Fimbres',
+        },
+      },
+      views: [
+        {
+          id: 'view-1',
+          type: 'view',
+          value: '{{name.first}}',
+        },
+        {
+          id: 'view-2',
+          type: 'view',
+        },
+      ],
+      navigation: {
+        BEGIN: 'FLOW_1',
+        FLOW_1: {
+          startState: 'VIEW_1',
+          VIEW_1: {
+            state_type: 'VIEW',
+            ref: 'view-1',
+            transitions: {
+              '*': 'ACTION_1',
+            },
+          },
+          ACTION_1: {
+            state_type: 'ACTION',
+            exp: '',
+            transitions: {
+              '*': 'VIEW_2',
+            },
+          },
+          VIEW_2: {
+            state_type: 'VIEW',
+            ref: 'view-2',
+            transitions: {
+              '*': 'END_DONE',
+            },
+          },
+          END_DONE: {
+            state_type: 'END',
+            outcome: 'done',
+          },
+        },
+      },
+    };
+    let playerNoPluginActive: Player;
+    let dcNoPluginActive: DataController;
+
+    beforeAll(() => {
+      playerNoPluginActive = new Player({
+        plugins: [new StageRevertDataPlugin()],
+      });
+
+      playerNoPluginActive.hooks.dataController.tap('test', (dc) => {
+        dcNoPluginActive = dc;
+      });
+
+      playerNoPluginActive.start(dataChangeFlow2);
+    });
+
+    it('Data model should have data committed as usual', () => {
+      dcNoPluginActive.set([['name.first', 'Christopher']]);
+
+      const state = playerNoPluginActive.getState() as InProgressState;
+
+      expect(state.controllers.data.get('name.first')).toBe('Christopher');
+    });
+  });
+});
diff --git a/plugins/stage-revert-data/core/src/index.ts b/plugins/stage-revert-data/core/src/index.ts
new file mode 100644
index 000000000..82b3f53c8
--- /dev/null
+++ b/plugins/stage-revert-data/core/src/index.ts
@@ -0,0 +1,80 @@
+import type { Player, DataController, PlayerPlugin } from '@player-ui/player';
+import { ValidationMiddleware } from '@player-ui/player';
+
+/**
+ * this plugin is supposed to stage/store changes in a local object/cache, until a transition happens,
+ *  then changes are committed to the Data Model
+ */
+export class StageRevertDataPlugin implements PlayerPlugin {
+  name = 'stage-revert-data-plugin';
+
+  apply(player: Player) {
+    let dataController: DataController;
+    let commitTransitions: string[];
+    let stageData: string;
+    let commitShadowModel = false;
+
+    const GatedDataMiddleware = new ValidationMiddleware(
+      () =>
+        commitShadowModel
+          ? undefined
+          : {
+              message: 'staging data',
+              severity: 'error',
+            },
+      { shouldIncludeInvalid: () => true }
+    );
+
+    /**
+     * Tapping into data controller hook to intercept data before it gets committed to data model,
+     * we are using an instance of ValidationMiddleware when tapping the resolveDataStages hook on DataController
+     */
+    player.hooks.dataController.tap(this.name, (dc: DataController) => {
+      dataController = dc;
+
+      dc.hooks.resolveDataStages.tap(this.name, (dataPipeline) => {
+        return stageData
+          ? [...dataPipeline, GatedDataMiddleware]
+          : [...dataPipeline];
+      });
+    });
+
+    /**
+     * Tapping into flow controller flow hook to detect transition, then proceed to commit to the data model from the shadowModelPaths
+     * in the ValidationMiddleware, if transition has not happened then nothing happens, but if an invalid Next transition happens then
+     * shadowModelPaths cache is cleared.
+     */
+
+    player.hooks.flowController.tap(this.name, (flowController) => {
+      flowController.hooks.flow.tap(this.name, (flow) => {
+        flow.hooks.transition.tap(this.name, (from, to) => {
+          if (from) {
+            if (commitTransitions.includes(to.name)) {
+              commitShadowModel = true;
+              player.logger.debug(
+                'Shadow Model Data to be committed %s',
+                GatedDataMiddleware.shadowModelPaths
+              );
+              dataController.set(GatedDataMiddleware.shadowModelPaths);
+            }
+
+            commitShadowModel = false;
+            GatedDataMiddleware.shadowModelPaths.clear();
+          }
+        });
+      });
+    });
+
+    /**
+     * Tapping the view controller to see if we want to intercept and cache data before model
+     */
+    player.hooks.viewController.tap(this.name, (vc) => {
+      vc.hooks.resolveView.intercept({
+        call: (view, id, state) => {
+          stageData = state?.attributes?.stageData;
+          commitTransitions = state?.attributes?.commitTransitions;
+        },
+      });
+    });
+  }
+}
diff --git a/react/player/src/player.tsx b/react/player/src/player.tsx
index 5ff4bbafa..102e5f073 100644
--- a/react/player/src/player.tsx
+++ b/react/player/src/player.tsx
@@ -138,7 +138,7 @@ export class ReactPlayer {
 
     onUpdatePlugin.apply(this.player);
 
-    this.Component = this.hooks.webComponent.call(this.createReactComp());
+    this.Component = this.createReactPlayerComponent();
     this.reactPlayerInfo = {
       playerVersion: this.player.getVersion(),
       playerCommit: this.player.getCommit(),
@@ -182,17 +182,11 @@ export class ReactPlayer {
     return this.reactPlayerInfo.reactPlayerCommit;
   }
 
-  private createReactComp(): React.ComponentType<ReactPlayerComponentProps> {
-    const ActualPlayerComp = this.hooks.playerComponent.call(PlayerComp);
-
-    /** the component to use to render Player */
-    const ReactPlayerComponent = () => {
-      const view = useSubscribedState<View>(this.viewUpdateSubscription);
-
-      if (this.options.suspend) {
-        this.viewUpdateSubscription.suspend();
-      }
+  private createReactPlayerComponent(): React.ComponentType<ReactPlayerComponentProps> {
+    const BaseComp = this.hooks.webComponent.call(this.createReactComp());
 
+    /** Wrap the Error boundary and context provider after the hook call to catch anything wrapped by the hook */
+    const ReactPlayerComponent = (props: ReactPlayerComponentProps) => {
       return (
         <ErrorBoundary
           fallbackRender={() => null}
@@ -205,13 +199,7 @@ export class ReactPlayer {
           }}
         >
           <PlayerContext.Provider value={{ player: this.player }}>
-            <AssetContext.Provider
-              value={{
-                registry: this.assetRegistry,
-              }}
-            >
-              {view && <ActualPlayerComp view={view} />}
-            </AssetContext.Provider>
+            <BaseComp {...props} />
           </PlayerContext.Provider>
         </ErrorBoundary>
       );
@@ -220,6 +208,31 @@ export class ReactPlayer {
     return ReactPlayerComponent;
   }
 
+  private createReactComp(): React.ComponentType<ReactPlayerComponentProps> {
+    const ActualPlayerComp = this.hooks.playerComponent.call(PlayerComp);
+
+    /** the component to use to render the player */
+    const WebPlayerComponent = () => {
+      const view = useSubscribedState<View>(this.viewUpdateSubscription);
+
+      if (this.options.suspend) {
+        this.viewUpdateSubscription.suspend();
+      }
+
+      return (
+        <AssetContext.Provider
+          value={{
+            registry: this.assetRegistry,
+          }}
+        >
+          {view && <ActualPlayerComp view={view} />}
+        </AssetContext.Provider>
+      );
+    };
+
+    return WebPlayerComponent;
+  }
+
   /**
    * Call this method to force the ReactPlayer to wait for the next view-update before performing the next render.
    * If the `suspense` option is set, this will suspend while an update is pending, otherwise nothing will be rendered.
diff --git a/tools/storybook/BUILD b/tools/storybook/BUILD
index 80db9e7e2..6b10dc406 100644
--- a/tools/storybook/BUILD
+++ b/tools/storybook/BUILD
@@ -17,6 +17,7 @@ javascript_pipeline(
       "@npm//ts-debounce",
       "@npm//uuid",
       "//plugins/metrics/react:@player-ui/metrics-plugin-react",
+      "//plugins/beacon/react:@player-ui/beacon-plugin-react"
     ],
 
     other_srcs = ["register.js"],    
diff --git a/tools/storybook/src/addons/editor/index.tsx b/tools/storybook/src/addons/editor/index.tsx
index beaf1a2c7..4f5bb8b58 100644
--- a/tools/storybook/src/addons/editor/index.tsx
+++ b/tools/storybook/src/addons/editor/index.tsx
@@ -78,6 +78,9 @@ export const EditorPanel = (props: EditorPanelProps) => {
         theme={darkMode ? 'vs-dark' : 'light'}
         value={editorValue}
         language="json"
+        options={{
+          formatOnPaste: true,
+        }}
         onChange={onChange}
       />
     </div>
diff --git a/tools/storybook/src/player/PlayerFlowSummary.tsx b/tools/storybook/src/player/PlayerFlowSummary.tsx
index 3748f0e5f..49df2e517 100644
--- a/tools/storybook/src/player/PlayerFlowSummary.tsx
+++ b/tools/storybook/src/player/PlayerFlowSummary.tsx
@@ -1,4 +1,6 @@
-import React from 'react';
+import React, { useEffect } from 'react';
+import type { CompletedState } from '@player-ui/player';
+import { BindingParser, LocalModel } from '@player-ui/player';
 import { VStack, Code, Heading, Button, Text } from '@chakra-ui/react';
 
 export type PlayerFlowSummaryProps = {
@@ -6,19 +8,57 @@ export type PlayerFlowSummaryProps = {
   reset: () => void;
   /** The outcome of the flow */
   outcome?: string;
+  /** The completed state of the flow */
+  completedState?: CompletedState;
+  /** Beacons sent in the flow */
+  beacons?: unknown[];
   /** any error */
   error?: Error;
 };
 
+interface CompletedStorybookFlowData {
+  /** The CompletedState of the flow */
+  completedState?: CompletedState;
+
+  get(path: string): unknown;
+
+  /** Beacons that were fired during the flow */
+  beacons: any[];
+}
+
+declare global {
+  interface Window {
+    /** Completed data from the player flow if it was successful */
+    __PLAYER_COMPLETED_DATA__?: CompletedStorybookFlowData;
+  }
+}
+
 /** A component to show at the end of a flow */
 export const PlayerFlowSummary = (props: PlayerFlowSummaryProps) => {
+  useEffect(() => {
+    const model = new LocalModel(props.completedState?.data);
+    // Point back to local model for data lookups when
+    // parsing a path
+    const parser = new BindingParser({
+      get: (binding) => model.get(binding),
+    });
+    window.__PLAYER_COMPLETED_DATA__ = {
+      completedState: props.completedState,
+      get: (path) => model.get(parser.parse(path)),
+      beacons: props.beacons ?? [],
+    };
+    return () => {
+      delete window.__PLAYER_COMPLETED_DATA__;
+    };
+  }, [props.completedState, props.beacons]);
+
   return (
     <VStack gap="10">
       <Heading>Flow Completed {props.error ? 'with Error' : ''}</Heading>
-
-      {props.outcome && (
+      {props.completedState?.endState.outcome && (
         <Code>
-          Outcome: <Text as="strong">{props.outcome}</Text>
+          Outcome:{' '}
+          <Text as="strong">{props.completedState?.endState.outcome}</Text>
         </Code>
       )}
 
diff --git a/tools/storybook/src/player/PlayerStory.tsx b/tools/storybook/src/player/PlayerStory.tsx
index cb88af694..d20e7422c 100644
--- a/tools/storybook/src/player/PlayerStory.tsx
+++ b/tools/storybook/src/player/PlayerStory.tsx
@@ -5,6 +5,7 @@ import type {
   Flow,
   ReactPlayerOptions,
 } from '@player-ui/react';
+import { BeaconPlugin } from '@player-ui/beacon-plugin-react';
 import { ReactPlayer } from '@player-ui/react';
 import { ChakraProvider, Spinner } from '@chakra-ui/react';
 import { makeFlow } from '@player-ui/make-flow';
@@ -59,20 +60,30 @@ const LocalPlayerStory = (props: LocalPlayerStory) => {
   const [playerState, setPlayerState] =
     React.useState<PlayerFlowStatus>('not-started');
 
+  const [trackedBeacons, setTrackedBeacons] = React.useState<any[]>([]);
+
   const rp = React.useMemo(() => {
+    const beaconPlugin = new BeaconPlugin({
+      callback: (beacon) => {
+        setTrackedBeacons((t) => [...t, beacon]);
+      },
+    });
+
     return new ReactPlayer({
       ...options,
       plugins: [
         new StorybookPlayerPlugin(stateActions),
+        beaconPlugin,
         ...plugins,
         ...(options?.plugins ?? []),
       ],
     });
-  }, [plugins]);
+  }, [plugins, flow]);
 
   /** A callback to start the flow */
   const startFlow = () => {
     setPlayerState('in-progress');
+    setTrackedBeacons([]);
     rp.start(flow)
       .then(() => {
         setPlayerState('completed');
@@ -85,7 +96,7 @@ const LocalPlayerStory = (props: LocalPlayerStory) => {
 
   React.useEffect(() => {
     startFlow();
-  }, [rp, flow]);
+  }, [rp]);
 
   React.useEffect(() => {
     // merge new data from storybook controls
@@ -105,7 +116,7 @@ const LocalPlayerStory = (props: LocalPlayerStory) => {
     return subscribe(addons.getChannel(), '@@player/flow/reset', () => {
       startFlow();
     });
-  }, [rp, flow]);
+  }, [rp]);
 
   if (renderContext.platform !== 'web' && renderContext.token) {
     return (
@@ -125,7 +136,8 @@ const LocalPlayerStory = (props: LocalPlayerStory) => {
     return (
       <PlayerFlowSummary
         reset={startFlow}
-        outcome={currentState.endState.outcome}
+        completedState={currentState}
+        beacons={trackedBeacons}
       />
     );
   }
diff --git a/webpack.config.js b/webpack.config.js
index eebce5b88..42d9cf4c3 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -6,6 +6,22 @@ const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin');
 module.exports = (env, argv) => ({
   mode: argv.mode,
   devtool: 'none',
+  module: {
+    rules: [
+      {
+        test: /node_modules\/(micromark-util-sanitize-uri|micromark-util-decode-numeric-character-reference)/,
+        include: /node_modules/,
+        use: [
+          {
+            loader: 'babel-loader',
+            options: {
+              plugins: ['@babel/plugin-transform-numeric-separator'],
+            },
+          },
+        ],
+      },
+    ],
+  },
   output: {
     path: path.resolve(process.cwd(), 'dist'),
     filename: process.env.ROOT_FILE_NAME,
diff --git a/yarn.lock b/yarn.lock
index ef26f7610..e3d16cf2b 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -497,6 +497,11 @@
   resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.18.9.tgz#4b8aea3b069d8cb8a72cdfe28ddf5ceca695ef2f"
   integrity sha512-aBXPT3bmtLryXaoJLyYPXPlSD4p1ld9aYeR+sJNOZjJJGiOpb+fKfh3NkcCu7J54nUJwCERPBExCCpyCOHnu/w==
 
+"@babel/helper-plugin-utils@^7.22.5":
+  version "7.22.5"
+  resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz#dd7ee3735e8a313b9f7b05a773d892e88e6d7295"
+  integrity sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==
+
 "@babel/helper-remap-async-to-generator@^7.16.7":
   version "7.16.7"
   resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.16.7.tgz#5ce2416990d55eb6e099128338848ae8ffa58a9a"
@@ -1083,6 +1088,14 @@
   dependencies:
     "@babel/helper-plugin-utils" "^7.16.7"
 
+"@babel/plugin-transform-numeric-separator@^7.22.5":
+  version "7.22.5"
+  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.22.5.tgz#57226a2ed9e512b9b446517ab6fa2d17abb83f58"
+  integrity sha512-NbslED1/6M+sXiwwtcAB/nieypGw02Ejf4KtDeMkCEpP6gWFMX1wI9WKYua+4oBneCCEmulOkRpwywypVZzs/g==
+  dependencies:
+    "@babel/helper-plugin-utils" "^7.22.5"
+    "@babel/plugin-syntax-numeric-separator" "^7.10.4"
+
 "@babel/plugin-transform-object-super@^7.16.7":
   version "7.16.7"
   resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.16.7.tgz#ac359cf8d32cf4354d27a46867999490b6c32a94"
@@ -4421,9 +4434,9 @@
     postcss "^8"
 
 "@types/debug@^4.0.0":
-  version "4.1.7"
-  resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.7.tgz#7cc0ea761509124709b8b2d1090d8f6c17aadb82"
-  integrity sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==
+  version "4.1.8"
+  resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.8.tgz#cef723a5d0a90990313faec2d1e22aee5eecb317"
+  integrity sha512-/vPO1EPOs306Cvhwv7KfVfYvOJqA/S/AXjaHQiJboCZzcNDb+TIJFN9/2C9DZ//ijSKWioNyUxD792QmDJ+HKQ==
   dependencies:
     "@types/ms" "*"
 
@@ -4596,6 +4609,13 @@
   dependencies:
     "@types/unist" "*"
 
+"@types/mdast@^4.0.0":
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-4.0.0.tgz#9f9462d4584a8b3e3711ea8bb4a94c485559ab90"
+  integrity sha512-YLeG8CujC9adtj/kuDzq1N4tCDYKoZ5l/bnjq8d74+t/3q/tHquJOJKUQXJrLCflOHpKjXgcI/a929gpmLOEng==
+  dependencies:
+    "@types/unist" "*"
+
 "@types/mdx-js__react@^1.5.5":
   version "1.5.5"
   resolved "https://registry.yarnpkg.com/@types/mdx-js__react/-/mdx-js__react-1.5.5.tgz#fa6daa1a28336d77b6cf071aacc7e497600de9ee"
@@ -4825,6 +4845,11 @@
   resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.6.tgz#250a7b16c3b91f672a24552ec64678eeb1d3a08d"
   integrity sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==
 
+"@types/unist@^3.0.0":
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/@types/unist/-/unist-3.0.0.tgz#988ae8af1e5239e89f9fbb1ade4c935f4eeedf9a"
+  integrity sha512-MFETx3tbTjE7Uk6vvnWINA/1iJ7LuMdO4fcq8UfF0pRbj01aGLduVvQcRyswuACJdpnHgg8E3rQLhaRdNEJS0w==
+
 "@types/uuid@^8.3.4":
   version "8.3.4"
   resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.4.tgz#bd86a43617df0594787d38b735f55c805becf1bc"
@@ -6714,9 +6739,9 @@ character-entities@^1.0.0:
   integrity sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==
 
 character-entities@^2.0.0:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/character-entities/-/character-entities-2.0.1.tgz#98724833e1e27990dee0bd0f2b8a859c3476aac7"
-  integrity sha512-OzmutCf2Kmc+6DrFrrPS8/tDh2+DpnrfzdICHWhcVC9eOd0N1PXmQEE1a8iM4IziIAG+8tmTq3K+oo0ubH6RRQ==
+  version "2.0.2"
+  resolved "https://registry.yarnpkg.com/character-entities/-/character-entities-2.0.2.tgz#2d09c2e72cd9523076ccb21157dff66ad43fcc22"
+  integrity sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==
 
 character-reference-invalid@^1.0.0:
   version "1.1.4"
@@ -7772,9 +7797,9 @@ decimal.js@^10.2.1:
   integrity sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ==
 
 decode-named-character-reference@^1.0.0:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/decode-named-character-reference/-/decode-named-character-reference-1.0.1.tgz#57b2bd9112659cacbc449d3577d7dadb8e1f3d1b"
-  integrity sha512-YV/0HQHreRwKb7uBopyIkLG17jG6Sv2qUchk9qSoVJ2f+flwRsPNBO0hAnjt6mTNYUT+vw9Gy2ihXg4sUWPi2w==
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz#daabac9690874c394c81e4162a0304b35d824f0e"
+  integrity sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==
   dependencies:
     character-entities "^2.0.0"
 
@@ -7881,7 +7906,12 @@ deprecation@^2.0.0, deprecation@^2.3.1:
   resolved "https://registry.yarnpkg.com/deprecation/-/deprecation-2.3.1.tgz#6368cbdb40abf3373b525ac87e4a260c3a700919"
   integrity sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==
 
-dequal@^2.0.0, dequal@^2.0.2:
+dequal@^2.0.0:
+  version "2.0.3"
+  resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be"
+  integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==
+
+dequal@^2.0.2:
   version "2.0.2"
   resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.2.tgz#85ca22025e3a87e65ef75a7a437b35284a7e319d"
   integrity sha512-q9K8BlJVxK7hQYqa6XISGmBZbtQQWVXSrRrWreHC94rMt1QL/Impruc+7p2CYSYuVIUr+YCt6hjrs1kkdJRTug==
@@ -7939,6 +7969,13 @@ detect-port@^1.3.0:
     address "^1.0.1"
     debug "^2.6.0"
 
+devlop@^1.0.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/devlop/-/devlop-1.1.0.tgz#4db7c2ca4dc6e0e834c30be70c94bbc976dc7018"
+  integrity sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==
+  dependencies:
+    dequal "^2.0.0"
+
 didyoumean@^1.2.1:
   version "1.2.2"
   resolved "https://registry.yarnpkg.com/didyoumean/-/didyoumean-1.2.2.tgz#989346ffe9e839b4555ecf5666edea0d3e8ad037"
@@ -7954,11 +7991,6 @@ diff@^4.0.1, diff@^4.0.2:
   resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
   integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==
 
-diff@^5.0.0:
-  version "5.0.0"
-  resolved "https://registry.yarnpkg.com/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b"
-  integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==
-
 diffie-hellman@^5.0.0:
   version "5.0.3"
   resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875"
@@ -12134,11 +12166,6 @@ kleur@^3.0.3:
   resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e"
   integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==
 
-kleur@^4.0.3:
-  version "4.1.4"
-  resolved "https://registry.yarnpkg.com/kleur/-/kleur-4.1.4.tgz#8c202987d7e577766d039a8cd461934c01cda04d"
-  integrity sha512-8QADVssbrFjivHWQU7KkMgptGTl6WAcSdlbBPY4uNF+mWr6DGcKrvY2w4FQJoXch7+fKMjj0dRrL75vk3k23OA==
-
 klona@^2.0.4:
   version "2.0.5"
   resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.5.tgz#d166574d90076395d9963aa7a928fabb8d76afbc"
@@ -12629,23 +12656,23 @@ mdast-util-definitions@^4.0.0:
   dependencies:
     unist-util-visit "^2.0.0"
 
-mdast-util-from-markdown@^1.2.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/mdast-util-from-markdown/-/mdast-util-from-markdown-1.2.0.tgz#84df2924ccc6c995dec1e2368b2b208ad0a76268"
-  integrity sha512-iZJyyvKD1+K7QX1b5jXdE7Sc5dtoTry1vzV28UZZe8Z1xVnB/czKntJ7ZAkG0tANqRnBF6p3p7GpU1y19DTf2Q==
+mdast-util-from-markdown@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.0.tgz#52f14815ec291ed061f2922fd14d6689c810cb88"
+  integrity sha512-n7MTOr/z+8NAX/wmhhDji8O3bRvPTV/U0oTCaZJkjhPSKTPhS3xufVhKGF8s1pJ7Ox4QgoIU7KHseh09S+9rTA==
   dependencies:
-    "@types/mdast" "^3.0.0"
-    "@types/unist" "^2.0.0"
+    "@types/mdast" "^4.0.0"
+    "@types/unist" "^3.0.0"
     decode-named-character-reference "^1.0.0"
-    mdast-util-to-string "^3.1.0"
-    micromark "^3.0.0"
-    micromark-util-decode-numeric-character-reference "^1.0.0"
-    micromark-util-decode-string "^1.0.0"
-    micromark-util-normalize-identifier "^1.0.0"
-    micromark-util-symbol "^1.0.0"
-    micromark-util-types "^1.0.0"
-    unist-util-stringify-position "^3.0.0"
-    uvu "^0.5.0"
+    devlop "^1.0.0"
+    mdast-util-to-string "^4.0.0"
+    micromark "^4.0.0"
+    micromark-util-decode-numeric-character-reference "^2.0.0"
+    micromark-util-decode-string "^2.0.0"
+    micromark-util-normalize-identifier "^2.0.0"
+    micromark-util-symbol "^2.0.0"
+    micromark-util-types "^2.0.0"
+    unist-util-stringify-position "^4.0.0"
 
 mdast-util-frontmatter@^1.0.0:
   version "1.0.0"
@@ -12692,6 +12719,13 @@ mdast-util-to-string@^3.0.0, mdast-util-to-string@^3.1.0:
   resolved "https://registry.yarnpkg.com/mdast-util-to-string/-/mdast-util-to-string-3.1.0.tgz#56c506d065fbf769515235e577b5a261552d56e9"
   integrity sha512-n4Vypz/DZgwo0iMHLQL49dJzlp7YtAJP+N07MZHpjPf/5XJuHUWstviF4Mn2jEiR/GNmtnRRqnwsXExk3igfFA==
 
+mdast-util-to-string@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz#7a5121475556a04e7eddeb67b264aae79d312814"
+  integrity sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==
+  dependencies:
+    "@types/mdast" "^4.0.0"
+
 mdast-util-toc@^6.1.0:
   version "6.1.0"
   resolved "https://registry.yarnpkg.com/mdast-util-toc/-/mdast-util-toc-6.1.0.tgz#1f38419f5ce774449c8daa87b39a4d940b24be7c"
@@ -12789,27 +12823,27 @@ microevent.ts@~0.1.1:
   resolved "https://registry.yarnpkg.com/microevent.ts/-/microevent.ts-0.1.1.tgz#70b09b83f43df5172d0205a63025bce0f7357fa0"
   integrity sha512-jo1OfR4TaEwd5HOrt5+tAZ9mqT4jmpNAusXtyfNzqVm9uiSYFZlKM1wYL4oU7azZW/PxQW53wM0S6OR1JHNa2g==
 
-micromark-core-commonmark@^1.0.1:
-  version "1.0.6"
-  resolved "https://registry.yarnpkg.com/micromark-core-commonmark/-/micromark-core-commonmark-1.0.6.tgz#edff4c72e5993d93724a3c206970f5a15b0585ad"
-  integrity sha512-K+PkJTxqjFfSNkfAhp4GB+cZPfQd6dxtTXnf+RjZOV7T4EEXnvgzOcnp+eSTmpGk9d1S9sL6/lqrgSNn/s0HZA==
+micromark-core-commonmark@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/micromark-core-commonmark/-/micromark-core-commonmark-2.0.0.tgz#50740201f0ee78c12a675bf3e68ffebc0bf931a3"
+  integrity sha512-jThOz/pVmAYUtkroV3D5c1osFXAMv9e0ypGDOIZuCeAe91/sD6BoE2Sjzt30yuXtwOYUmySOhMas/PVyh02itA==
   dependencies:
     decode-named-character-reference "^1.0.0"
-    micromark-factory-destination "^1.0.0"
-    micromark-factory-label "^1.0.0"
-    micromark-factory-space "^1.0.0"
-    micromark-factory-title "^1.0.0"
-    micromark-factory-whitespace "^1.0.0"
-    micromark-util-character "^1.0.0"
-    micromark-util-chunked "^1.0.0"
-    micromark-util-classify-character "^1.0.0"
-    micromark-util-html-tag-name "^1.0.0"
-    micromark-util-normalize-identifier "^1.0.0"
-    micromark-util-resolve-all "^1.0.0"
-    micromark-util-subtokenize "^1.0.0"
-    micromark-util-symbol "^1.0.0"
-    micromark-util-types "^1.0.1"
-    uvu "^0.5.0"
+    devlop "^1.0.0"
+    micromark-factory-destination "^2.0.0"
+    micromark-factory-label "^2.0.0"
+    micromark-factory-space "^2.0.0"
+    micromark-factory-title "^2.0.0"
+    micromark-factory-whitespace "^2.0.0"
+    micromark-util-character "^2.0.0"
+    micromark-util-chunked "^2.0.0"
+    micromark-util-classify-character "^2.0.0"
+    micromark-util-html-tag-name "^2.0.0"
+    micromark-util-normalize-identifier "^2.0.0"
+    micromark-util-resolve-all "^2.0.0"
+    micromark-util-subtokenize "^2.0.0"
+    micromark-util-symbol "^2.0.0"
+    micromark-util-types "^2.0.0"
 
 micromark-extension-frontmatter@^1.0.0:
   version "1.0.0"
@@ -12820,53 +12854,52 @@ micromark-extension-frontmatter@^1.0.0:
     micromark-util-character "^1.0.0"
     micromark-util-symbol "^1.0.0"
 
-micromark-factory-destination@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/micromark-factory-destination/-/micromark-factory-destination-1.0.0.tgz#fef1cb59ad4997c496f887b6977aa3034a5a277e"
-  integrity sha512-eUBA7Rs1/xtTVun9TmV3gjfPz2wEwgK5R5xcbIM5ZYAtvGF6JkyaDsj0agx8urXnO31tEO6Ug83iVH3tdedLnw==
+micromark-factory-destination@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/micromark-factory-destination/-/micromark-factory-destination-2.0.0.tgz#857c94debd2c873cba34e0445ab26b74f6a6ec07"
+  integrity sha512-j9DGrQLm/Uhl2tCzcbLhy5kXsgkHUrjJHg4fFAeoMRwJmJerT9aw4FEhIbZStWN8A3qMwOp1uzHr4UL8AInxtA==
   dependencies:
-    micromark-util-character "^1.0.0"
-    micromark-util-symbol "^1.0.0"
-    micromark-util-types "^1.0.0"
+    micromark-util-character "^2.0.0"
+    micromark-util-symbol "^2.0.0"
+    micromark-util-types "^2.0.0"
 
-micromark-factory-label@^1.0.0:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/micromark-factory-label/-/micromark-factory-label-1.0.2.tgz#6be2551fa8d13542fcbbac478258fb7a20047137"
-  integrity sha512-CTIwxlOnU7dEshXDQ+dsr2n+yxpP0+fn271pu0bwDIS8uqfFcumXpj5mLn3hSC8iw2MUr6Gx8EcKng1dD7i6hg==
+micromark-factory-label@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/micromark-factory-label/-/micromark-factory-label-2.0.0.tgz#17c5c2e66ce39ad6f4fc4cbf40d972f9096f726a"
+  integrity sha512-RR3i96ohZGde//4WSe/dJsxOX6vxIg9TimLAS3i4EhBAFx8Sm5SmqVfR8E87DPSR31nEAjZfbt91OMZWcNgdZw==
   dependencies:
-    micromark-util-character "^1.0.0"
-    micromark-util-symbol "^1.0.0"
-    micromark-util-types "^1.0.0"
-    uvu "^0.5.0"
+    devlop "^1.0.0"
+    micromark-util-character "^2.0.0"
+    micromark-util-symbol "^2.0.0"
+    micromark-util-types "^2.0.0"
 
-micromark-factory-space@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/micromark-factory-space/-/micromark-factory-space-1.0.0.tgz#cebff49968f2b9616c0fcb239e96685cb9497633"
-  integrity sha512-qUmqs4kj9a5yBnk3JMLyjtWYN6Mzfcx8uJfi5XAveBniDevmZasdGBba5b4QsvRcAkmvGo5ACmSUmyGiKTLZew==
+micromark-factory-space@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/micromark-factory-space/-/micromark-factory-space-2.0.0.tgz#5e7afd5929c23b96566d0e1ae018ae4fcf81d030"
+  integrity sha512-TKr+LIDX2pkBJXFLzpyPyljzYK3MtmllMUMODTQJIUfDGncESaqB90db9IAUcz4AZAJFdd8U9zOp9ty1458rxg==
   dependencies:
-    micromark-util-character "^1.0.0"
-    micromark-util-types "^1.0.0"
+    micromark-util-character "^2.0.0"
+    micromark-util-types "^2.0.0"
 
-micromark-factory-title@^1.0.0:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/micromark-factory-title/-/micromark-factory-title-1.0.2.tgz#7e09287c3748ff1693930f176e1c4a328382494f"
-  integrity sha512-zily+Nr4yFqgMGRKLpTVsNl5L4PMu485fGFDOQJQBl2NFpjGte1e86zC0da93wf97jrc4+2G2GQudFMHn3IX+A==
+micromark-factory-title@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/micromark-factory-title/-/micromark-factory-title-2.0.0.tgz#726140fc77892af524705d689e1cf06c8a83ea95"
+  integrity sha512-jY8CSxmpWLOxS+t8W+FG3Xigc0RDQA9bKMY/EwILvsesiRniiVMejYTE4wumNc2f4UbAa4WsHqe3J1QS1sli+A==
   dependencies:
-    micromark-factory-space "^1.0.0"
-    micromark-util-character "^1.0.0"
-    micromark-util-symbol "^1.0.0"
-    micromark-util-types "^1.0.0"
-    uvu "^0.5.0"
+    micromark-factory-space "^2.0.0"
+    micromark-util-character "^2.0.0"
+    micromark-util-symbol "^2.0.0"
+    micromark-util-types "^2.0.0"
 
-micromark-factory-whitespace@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/micromark-factory-whitespace/-/micromark-factory-whitespace-1.0.0.tgz#e991e043ad376c1ba52f4e49858ce0794678621c"
-  integrity sha512-Qx7uEyahU1lt1RnsECBiuEbfr9INjQTGa6Err+gF3g0Tx4YEviPbqqGKNv/NrBaE7dVHdn1bVZKM/n5I/Bak7A==
+micromark-factory-whitespace@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.0.tgz#9e92eb0f5468083381f923d9653632b3cfb5f763"
+  integrity sha512-28kbwaBjc5yAI1XadbdPYHX/eDnqaUFVikLwrO7FDnKG7lpgxnvk/XGRhX/PN0mOZ+dBSZ+LgunHS+6tYQAzhA==
   dependencies:
-    micromark-factory-space "^1.0.0"
-    micromark-util-character "^1.0.0"
-    micromark-util-symbol "^1.0.0"
-    micromark-util-types "^1.0.0"
+    micromark-factory-space "^2.0.0"
+    micromark-util-character "^2.0.0"
+    micromark-util-symbol "^2.0.0"
+    micromark-util-types "^2.0.0"
 
 micromark-util-character@^1.0.0:
   version "1.1.0"
@@ -12876,122 +12909,140 @@ micromark-util-character@^1.0.0:
     micromark-util-symbol "^1.0.0"
     micromark-util-types "^1.0.0"
 
-micromark-util-chunked@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/micromark-util-chunked/-/micromark-util-chunked-1.0.0.tgz#5b40d83f3d53b84c4c6bce30ed4257e9a4c79d06"
-  integrity sha512-5e8xTis5tEZKgesfbQMKRCyzvffRRUX+lK/y+DvsMFdabAicPkkZV6gO+FEWi9RfuKKoxxPwNL+dFF0SMImc1g==
+micromark-util-character@^2.0.0:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/micromark-util-character/-/micromark-util-character-2.0.1.tgz#52b824c2e2633b6fb33399d2ec78ee2a90d6b298"
+  integrity sha512-3wgnrmEAJ4T+mGXAUfMvMAbxU9RDG43XmGce4j6CwPtVxB3vfwXSZ6KhFwDzZ3mZHhmPimMAXg71veiBGzeAZw==
   dependencies:
-    micromark-util-symbol "^1.0.0"
+    micromark-util-symbol "^2.0.0"
+    micromark-util-types "^2.0.0"
 
-micromark-util-classify-character@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/micromark-util-classify-character/-/micromark-util-classify-character-1.0.0.tgz#cbd7b447cb79ee6997dd274a46fc4eb806460a20"
-  integrity sha512-F8oW2KKrQRb3vS5ud5HIqBVkCqQi224Nm55o5wYLzY/9PwHGXC01tr3d7+TqHHz6zrKQ72Okwtvm/xQm6OVNZA==
+micromark-util-chunked@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/micromark-util-chunked/-/micromark-util-chunked-2.0.0.tgz#e51f4db85fb203a79dbfef23fd41b2f03dc2ef89"
+  integrity sha512-anK8SWmNphkXdaKgz5hJvGa7l00qmcaUQoMYsBwDlSKFKjc6gjGXPDw3FNL3Nbwq5L8gE+RCbGqTw49FK5Qyvg==
   dependencies:
-    micromark-util-character "^1.0.0"
-    micromark-util-symbol "^1.0.0"
-    micromark-util-types "^1.0.0"
+    micromark-util-symbol "^2.0.0"
 
-micromark-util-combine-extensions@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.0.0.tgz#91418e1e74fb893e3628b8d496085639124ff3d5"
-  integrity sha512-J8H058vFBdo/6+AsjHp2NF7AJ02SZtWaVUjsayNFeAiydTxUwViQPxN0Hf8dp4FmCQi0UUFovFsEyRSUmFH3MA==
+micromark-util-classify-character@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/micromark-util-classify-character/-/micromark-util-classify-character-2.0.0.tgz#8c7537c20d0750b12df31f86e976d1d951165f34"
+  integrity sha512-S0ze2R9GH+fu41FA7pbSqNWObo/kzwf8rN/+IGlW/4tC6oACOs8B++bh+i9bVyNnwCcuksbFwsBme5OCKXCwIw==
   dependencies:
-    micromark-util-chunked "^1.0.0"
-    micromark-util-types "^1.0.0"
+    micromark-util-character "^2.0.0"
+    micromark-util-symbol "^2.0.0"
+    micromark-util-types "^2.0.0"
 
-micromark-util-decode-numeric-character-reference@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.0.0.tgz#dcc85f13b5bd93ff8d2868c3dba28039d490b946"
-  integrity sha512-OzO9AI5VUtrTD7KSdagf4MWgHMtET17Ua1fIpXTpuhclCqD8egFWo85GxSGvxgkGS74bEahvtM0WP0HjvV0e4w==
+micromark-util-combine-extensions@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.0.tgz#75d6ab65c58b7403616db8d6b31315013bfb7ee5"
+  integrity sha512-vZZio48k7ON0fVS3CUgFatWHoKbbLTK/rT7pzpJ4Bjp5JjkZeasRfrS9wsBdDJK2cJLHMckXZdzPSSr1B8a4oQ==
   dependencies:
-    micromark-util-symbol "^1.0.0"
+    micromark-util-chunked "^2.0.0"
+    micromark-util-types "^2.0.0"
 
-micromark-util-decode-string@^1.0.0:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/micromark-util-decode-string/-/micromark-util-decode-string-1.0.2.tgz#942252ab7a76dec2dbf089cc32505ee2bc3acf02"
-  integrity sha512-DLT5Ho02qr6QWVNYbRZ3RYOSSWWFuH3tJexd3dgN1odEuPNxCngTCXJum7+ViRAd9BbdxCvMToPOD/IvVhzG6Q==
+micromark-util-decode-numeric-character-reference@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.0.tgz#a798808d02cc74113e2c939fc95363096ade7f1d"
+  integrity sha512-pIgcsGxpHEtTG/rPJRz/HOLSqp5VTuIIjXlPI+6JSDlK2oljApusG6KzpS8AF0ENUMCHlC/IBb5B9xdFiVlm5Q==
+  dependencies:
+    micromark-util-symbol "^2.0.0"
+
+micromark-util-decode-string@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/micromark-util-decode-string/-/micromark-util-decode-string-2.0.0.tgz#7dfa3a63c45aecaa17824e656bcdb01f9737154a"
+  integrity sha512-r4Sc6leeUTn3P6gk20aFMj2ntPwn6qpDZqWvYmAG6NgvFTIlj4WtrAudLi65qYoaGdXYViXYw2pkmn7QnIFasA==
   dependencies:
     decode-named-character-reference "^1.0.0"
-    micromark-util-character "^1.0.0"
-    micromark-util-decode-numeric-character-reference "^1.0.0"
-    micromark-util-symbol "^1.0.0"
+    micromark-util-character "^2.0.0"
+    micromark-util-decode-numeric-character-reference "^2.0.0"
+    micromark-util-symbol "^2.0.0"
 
-micromark-util-encode@^1.0.0:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/micromark-util-encode/-/micromark-util-encode-1.0.1.tgz#2c1c22d3800870ad770ece5686ebca5920353383"
-  integrity sha512-U2s5YdnAYexjKDel31SVMPbfi+eF8y1U4pfiRW/Y8EFVCy/vgxk/2wWTxzcqE71LHtCuCzlBDRU2a5CQ5j+mQA==
+micromark-util-encode@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/micromark-util-encode/-/micromark-util-encode-2.0.0.tgz#0921ac7953dc3f1fd281e3d1932decfdb9382ab1"
+  integrity sha512-pS+ROfCXAGLWCOc8egcBvT0kf27GoWMqtdarNfDcjb6YLuV5cM3ioG45Ys2qOVqeqSbjaKg72vU+Wby3eddPsA==
 
-micromark-util-html-tag-name@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.0.0.tgz#75737e92fef50af0c6212bd309bc5cb8dbd489ed"
-  integrity sha512-NenEKIshW2ZI/ERv9HtFNsrn3llSPZtY337LID/24WeLqMzeZhBEE6BQ0vS2ZBjshm5n40chKtJ3qjAbVV8S0g==
+micromark-util-html-tag-name@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.0.tgz#ae34b01cbe063363847670284c6255bb12138ec4"
+  integrity sha512-xNn4Pqkj2puRhKdKTm8t1YHC/BAjx6CEwRFXntTaRf/x16aqka6ouVoutm+QdkISTlT7e2zU7U4ZdlDLJd2Mcw==
 
-micromark-util-normalize-identifier@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.0.0.tgz#4a3539cb8db954bbec5203952bfe8cedadae7828"
-  integrity sha512-yg+zrL14bBTFrQ7n35CmByWUTFsgst5JhA4gJYoty4Dqzj4Z4Fr/DHekSS5aLfH9bdlfnSvKAWsAgJhIbogyBg==
+micromark-util-normalize-identifier@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.0.tgz#91f9a4e65fe66cc80c53b35b0254ad67aa431d8b"
+  integrity sha512-2xhYT0sfo85FMrUPtHcPo2rrp1lwbDEEzpx7jiH2xXJLqBuy4H0GgXk5ToU8IEwoROtXuL8ND0ttVa4rNqYK3w==
   dependencies:
-    micromark-util-symbol "^1.0.0"
+    micromark-util-symbol "^2.0.0"
 
-micromark-util-resolve-all@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/micromark-util-resolve-all/-/micromark-util-resolve-all-1.0.0.tgz#a7c363f49a0162e931960c44f3127ab58f031d88"
-  integrity sha512-CB/AGk98u50k42kvgaMM94wzBqozSzDDaonKU7P7jwQIuH2RU0TeBqGYJz2WY1UdihhjweivStrJ2JdkdEmcfw==
+micromark-util-resolve-all@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.0.tgz#189656e7e1a53d0c86a38a652b284a252389f364"
+  integrity sha512-6KU6qO7DZ7GJkaCgwBNtplXCvGkJToU86ybBAUdavvgsCiG8lSSvYxr9MhwmQ+udpzywHsl4RpGJsYWG1pDOcA==
   dependencies:
-    micromark-util-types "^1.0.0"
+    micromark-util-types "^2.0.0"
 
-micromark-util-sanitize-uri@^1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.0.0.tgz#27dc875397cd15102274c6c6da5585d34d4f12b2"
-  integrity sha512-cCxvBKlmac4rxCGx6ejlIviRaMKZc0fWm5HdCHEeDWRSkn44l6NdYVRyU+0nT1XC72EQJMZV8IPHF+jTr56lAg==
+micromark-util-sanitize-uri@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.0.tgz#ec8fbf0258e9e6d8f13d9e4770f9be64342673de"
+  integrity sha512-WhYv5UEcZrbAtlsnPuChHUAsu/iBPOVaEVsntLBIdpibO0ddy8OzavZz3iL2xVvBZOpolujSliP65Kq0/7KIYw==
   dependencies:
-    micromark-util-character "^1.0.0"
-    micromark-util-encode "^1.0.0"
-    micromark-util-symbol "^1.0.0"
+    micromark-util-character "^2.0.0"
+    micromark-util-encode "^2.0.0"
+    micromark-util-symbol "^2.0.0"
 
-micromark-util-subtokenize@^1.0.0:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/micromark-util-subtokenize/-/micromark-util-subtokenize-1.0.2.tgz#ff6f1af6ac836f8bfdbf9b02f40431760ad89105"
-  integrity sha512-d90uqCnXp/cy4G881Ub4psE57Sf8YD0pim9QdjCRNjfas2M1u6Lbt+XZK9gnHL2XFhnozZiEdCa9CNfXSfQ6xA==
+micromark-util-subtokenize@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/micromark-util-subtokenize/-/micromark-util-subtokenize-2.0.0.tgz#9f412442d77e0c5789ffdf42377fa8a2bcbdf581"
+  integrity sha512-vc93L1t+gpR3p8jxeVdaYlbV2jTYteDje19rNSS/H5dlhxUYll5Fy6vJ2cDwP8RnsXi818yGty1ayP55y3W6fg==
   dependencies:
-    micromark-util-chunked "^1.0.0"
-    micromark-util-symbol "^1.0.0"
-    micromark-util-types "^1.0.0"
-    uvu "^0.5.0"
+    devlop "^1.0.0"
+    micromark-util-chunked "^2.0.0"
+    micromark-util-symbol "^2.0.0"
+    micromark-util-types "^2.0.0"
 
 micromark-util-symbol@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/micromark-util-symbol/-/micromark-util-symbol-1.0.1.tgz#b90344db62042ce454f351cf0bebcc0a6da4920e"
   integrity sha512-oKDEMK2u5qqAptasDAwWDXq0tG9AssVwAx3E9bBF3t/shRIGsWIRG+cGafs2p/SnDSOecnt6hZPCE2o6lHfFmQ==
 
-micromark-util-types@^1.0.0, micromark-util-types@^1.0.1:
+micromark-util-symbol@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/micromark-util-symbol/-/micromark-util-symbol-2.0.0.tgz#12225c8f95edf8b17254e47080ce0862d5db8044"
+  integrity sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==
+
+micromark-util-types@^1.0.0:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/micromark-util-types/-/micromark-util-types-1.0.2.tgz#f4220fdb319205812f99c40f8c87a9be83eded20"
   integrity sha512-DCfg/T8fcrhrRKTPjRrw/5LLvdGV7BHySf/1LOZx7TzWZdYRjogNtyNq885z3nNallwr3QUKARjqvHqX1/7t+w==
 
-micromark@^3.0.0:
-  version "3.0.10"
-  resolved "https://registry.yarnpkg.com/micromark/-/micromark-3.0.10.tgz#1eac156f0399d42736458a14b0ca2d86190b457c"
-  integrity sha512-ryTDy6UUunOXy2HPjelppgJ2sNfcPz1pLlMdA6Rz9jPzhLikWXv/irpWV/I2jd68Uhmny7hHxAlAhk4+vWggpg==
+micromark-util-types@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/micromark-util-types/-/micromark-util-types-2.0.0.tgz#63b4b7ffeb35d3ecf50d1ca20e68fc7caa36d95e"
+  integrity sha512-oNh6S2WMHWRZrmutsRmDDfkzKtxF+bc2VxLC9dvtrDIRFln627VsFP6fLMgTryGDljgLPjkrzQSDcPrjPyDJ5w==
+
+micromark@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/micromark/-/micromark-4.0.0.tgz#84746a249ebd904d9658cfabc1e8e5f32cbc6249"
+  integrity sha512-o/sd0nMof8kYff+TqcDx3VSrgBTcZpSvYcAHIfHhv5VAuNmisCxjhx6YmxS8PFEpb9z5WKWKPdzf0jM23ro3RQ==
   dependencies:
     "@types/debug" "^4.0.0"
     debug "^4.0.0"
     decode-named-character-reference "^1.0.0"
-    micromark-core-commonmark "^1.0.1"
-    micromark-factory-space "^1.0.0"
-    micromark-util-character "^1.0.0"
-    micromark-util-chunked "^1.0.0"
-    micromark-util-combine-extensions "^1.0.0"
-    micromark-util-decode-numeric-character-reference "^1.0.0"
-    micromark-util-encode "^1.0.0"
-    micromark-util-normalize-identifier "^1.0.0"
-    micromark-util-resolve-all "^1.0.0"
-    micromark-util-sanitize-uri "^1.0.0"
-    micromark-util-subtokenize "^1.0.0"
-    micromark-util-symbol "^1.0.0"
-    micromark-util-types "^1.0.1"
-    uvu "^0.5.0"
+    devlop "^1.0.0"
+    micromark-core-commonmark "^2.0.0"
+    micromark-factory-space "^2.0.0"
+    micromark-util-character "^2.0.0"
+    micromark-util-chunked "^2.0.0"
+    micromark-util-combine-extensions "^2.0.0"
+    micromark-util-decode-numeric-character-reference "^2.0.0"
+    micromark-util-encode "^2.0.0"
+    micromark-util-normalize-identifier "^2.0.0"
+    micromark-util-resolve-all "^2.0.0"
+    micromark-util-sanitize-uri "^2.0.0"
+    micromark-util-subtokenize "^2.0.0"
+    micromark-util-symbol "^2.0.0"
+    micromark-util-types "^2.0.0"
 
 micromatch@^3.0.4, micromatch@^3.1.10, micromatch@^3.1.4:
   version "3.1.10"
@@ -13228,11 +13279,6 @@ move-concurrently@^1.0.1:
     rimraf "^2.5.4"
     run-queue "^1.0.3"
 
-mri@^1.1.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b"
-  integrity sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==
-
 ms@2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
@@ -16178,13 +16224,6 @@ rxjs@^7.5.1:
   dependencies:
     tslib "^2.1.0"
 
-sade@^1.7.3:
-  version "1.8.1"
-  resolved "https://registry.yarnpkg.com/sade/-/sade-1.8.1.tgz#0a78e81d658d394887be57d2a409bf703a3b2701"
-  integrity sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==
-  dependencies:
-    mri "^1.1.0"
-
 safe-buffer@5.1.1:
   version "5.1.1"
   resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853"
@@ -16283,12 +16322,10 @@ schema-utils@^3.0.0, schema-utils@^3.1.0, schema-utils@^3.1.1:
     ajv "^6.12.5"
     ajv-keywords "^3.5.2"
 
-scroll-into-view-if-needed@^2.2.28:
-  version "2.2.28"
-  resolved "https://registry.yarnpkg.com/scroll-into-view-if-needed/-/scroll-into-view-if-needed-2.2.28.tgz#5a15b2f58a52642c88c8eca584644e01703d645a"
-  integrity sha512-8LuxJSuFVc92+0AdNv4QOxRL4Abeo1DgLnGNkn1XlaujPH/3cCFz3QI60r2VNu4obJJROzgnIUw5TKQkZvZI1w==
-  dependencies:
-    compute-scroll-into-view "^1.0.17"
+seamless-scroll-polyfill@2.3.3:
+  version "2.3.3"
+  resolved "https://registry.yarnpkg.com/seamless-scroll-polyfill/-/seamless-scroll-polyfill-2.3.3.tgz#2e462ecef10ae595d0a369e37a00d9c7189ff961"
+  integrity sha512-YNiggA5ueZefrmGvGENBfaaLsg7Eos6tNbUHBi29r9OhMmlNA+awN5jlz3yv64+BvwK/Ee642BjcOSEQtubkKw==
 
 section-matter@^1.0.0:
   version "1.0.0"
@@ -16575,13 +16612,6 @@ slice-ansi@^4.0.0:
     astral-regex "^2.0.0"
     is-fullwidth-code-point "^3.0.0"
 
-smooth-scroll-into-view-if-needed@1.1.32:
-  version "1.1.32"
-  resolved "https://registry.yarnpkg.com/smooth-scroll-into-view-if-needed/-/smooth-scroll-into-view-if-needed-1.1.32.tgz#57718cb2caa5265ade3e96006dfcf28b2fdcfca0"
-  integrity sha512-1/Ui1kD/9U4E6B6gYvJ6qhEiZPHMT9ZHi/OKJVEiCFhmcMqPm7y4G15pIl/NhuPTkDF/u57eEOK4Frh4721V/w==
-  dependencies:
-    scroll-into-view-if-needed "^2.2.28"
-
 snake-case@^3.0.4:
   version "3.0.4"
   resolved "https://registry.yarnpkg.com/snake-case/-/snake-case-3.0.4.tgz#4f2bbd568e9935abdfd593f34c691dadb49c452c"
@@ -18095,6 +18125,13 @@ unist-util-stringify-position@^3.0.0:
   dependencies:
     "@types/unist" "^2.0.0"
 
+unist-util-stringify-position@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz#449c6e21a880e0855bf5aabadeb3a740314abac2"
+  integrity sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==
+  dependencies:
+    "@types/unist" "^3.0.0"
+
 unist-util-visit-children@^1.0.0:
   version "1.1.4"
   resolved "https://registry.yarnpkg.com/unist-util-visit-children/-/unist-util-visit-children-1.1.4.tgz#e8a087e58a33a2815f76ea1901c15dec2cb4b432"
@@ -18379,16 +18416,6 @@ uuid@^8.3.0, uuid@^8.3.2:
   resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
   integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
 
-uvu@^0.5.0:
-  version "0.5.3"
-  resolved "https://registry.yarnpkg.com/uvu/-/uvu-0.5.3.tgz#3d83c5bc1230f153451877bfc7f4aea2392219ae"
-  integrity sha512-brFwqA3FXzilmtnIyJ+CxdkInkY/i4ErvP7uV0DnUVxQcQ55reuHphorpF+tZoVHK2MniZ/VJzI7zJQoc9T9Yw==
-  dependencies:
-    dequal "^2.0.0"
-    diff "^5.0.0"
-    kleur "^4.0.3"
-    sade "^1.7.3"
-
 v8-compile-cache@^2.0.3, v8-compile-cache@^2.1.1:
   version "2.3.0"
   resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee"